Bug:
Custom lineweight table entries in a CTB are keyed by a number parsed
from the file and stored without an upper bound, but the only reader
(resolve_lineweight) indexes through entry.lineweight: u8. A key beyond
u8::MAX (e.g. 18446744073709551615 in a crafted file) caused an
out-of-bounds-style failure or a multi-GB table allocation when the
style was applied.
Reproduce:
1. Craft a CTB whose custom lineweight table uses a huge key
(usize::MAX).
2. Apply the style or open plot settings.
3. Before: overflow panic (or a giant allocation).
4. After: the key is out of range, the line is ignored like any other
malformed CTB line, and the style loads.
Fix:
Keys are bounded to u8::MAX when parsed - exactly the range the
u8-typed reader can ever use. In-range keys behave as before; keys 256
and above are dropped. Tests: a usize::MAX key does not overflow, an
in-range key is kept and applied, 256 is ignored.
Bug:
CTB plot-style tables are zlib-deflated, and decompress_ctb inflated
them with a bare read_to_end - no output budget. A few hundred KB of
crafted deflate input expands to gigabytes (A/space runs compress about
1000:1), and the Vec grows until the allocator aborts; an abort is not a
catchable panic. Both the zlib path and the legacy raw-deflate fallback
were unbounded.
Reproduce:
1. Craft a CTB whose payload deflates from 128 MiB of spaces (tiny on
disk), with a valid PIAFILEVERSION header or as legacy raw deflate.
2. Select it as the plot style (open/plot).
3. Before: RSS climbs past the limit, then "memory allocation failed".
4. After: the read stops at the 64 MiB limit and the load fails with
"... inflated data exceeds the 67108864-byte limit".
5. Boundary: a payload inflating to exactly 64 MiB loads (the limit
means "exceeds"); one byte more is refused.
Fix:
inflate_capped reads through take(MAX_INFLATED_BYTES + 1) and refuses
when the count is greater than 64 MiB. The extra byte is what makes
"exactly at the limit" distinguishable from "at least the limit": a bare
take(cap) stops at the cap, so read == cap is ambiguous, and a >= cap
comparison would wrongly refuse a legitimate boundary payload. All three
inflation sites (zlib, raw-deflate fallback, legacy decoder) route
through the helper; error strings are preserved. 64 MiB is far above any
real plot-style table. Four tests: zlib bomb refused, legacy bomb
refused, exactly-at-cap accepted, cap-plus-one refused.
Bug:
crop_range bounded the row axis with skip/take over the file's real rows
but left the column axis unbounded: c0..=c1 collects one String per
column, and a range like A1:ZZZZZZ1 parses to roughly 308 million
columns, causing a capacity overflow while allocating a row.
Reproduce:
1. Data Link -> pick any CSV/xlsx, set Cell Range to A1:ZZZZZZ1 (or any
range whose parsed column index is enormous).
2. Before: "capacity overflow" abort while building the preview.
3. After: the window is cropped to the file's widest row; columns past
the data yield nothing beyond the data width.
Fix:
c1 is clamped to widest.saturating_sub(1) before the range is collected,
so the span is provably at most the file's own data - allocation is
O(file). Rows outside the file still come back empty (in-range blank
padding inside the data width is preserved), and c0 > c1 naturally
yields an empty window. Regression tests cover the 13-Z capacity-overflow
input and a 6-Z range that now crops.
Bug:
parse_hex validated the colour string by byte length and then sliced at
fixed byte offsets. A multi-byte UTF-8 character inside the field makes
the byte offsets land mid-character, so committing the Options colour
keystroke field (or set_hex) panics at the char-boundary slice.
Reproduce:
1. Options -> any colour field; enter a value that passes the byte
length check but contains a multi-byte character.
2. Commit the field.
3. Before: panic at the slice.
4. After: the value parses as far as valid hex digits allow or is
rejected as invalid input - no panic.
Fix:
The parser walks chars, consuming exactly two hex digits per channel via
to_digit(16) and requiring the exact total length - byte offsets no
longer exist in the function. Covers all five colour fields and set_hex.
A regression test covers multi-byte inputs.
Bug:
evaluate_table_formula located "(" with find (forward) and ")" with
rfind (backward), then sliced body[open+1..close]. For malformed input
like =)( the delimiters come out reversed and the slice panics ("slice
index starts at 2 but ends at 0") during tessellation - on the render
path, from a single pasted cell value.
Reproduce:
1. Insert a table and double-click a cell.
2. Type =)( (or =a)b(c ) and press Enter.
3. Before: the next repaint panics.
4. After: the formula does not evaluate; the literal text renders.
Fix:
body.get(open + 1..close)? - reversed delimiters yield None and the
caller's existing fallback shows the literal text. Well-formed formulas
are untouched: regression tests keep =SUM(A1:B2), =AVERAGE(A1:B2) and
=A1 evaluating, and reject =)(, =a)b(c and =).
Bug:
control_entities_transform validated the action field only inside the
transform closure - and invoked that closure from three of the match's
arms. Any other action fell into unreachable!("validated by
transform()") and aborted the process. The claim in the panic message
was false: for unknown actions the validating code never ran. The handler
is reachable from every remote surface (REST, MCP, --serve) because they
share this dispatch. Two extra triggers: "move " (trailing space, no
trim) and an all-whitespace action.
Reproduce:
1. Start OpenCADStudio with --http 8765.
2. POST /api/v1/entities (a LINE), take handles[0].
3. POST /api/v1/entities/transform with
{"handles":["<handle>"],"action":"explode"}.
4. Before: "internal error: entered unreachable code" and the process
exits.
5. After: {"ok":false,"code":"invalid_action",...} and the server keeps
serving. "action":"move " is now accepted (trimmed); " " is
rejected.
Fix:
The action is trimmed, lowercased and checked against the six-value
whitelist after require_existing (error precedence unchanged: a bad
handle still reports first) but before push_undo_snapshot, so an invalid
request no longer dirties the undo stack. The match's fallback arm
returns the same invalid_action error instead of unreachable!, so a
future mismatch between the whitelist and the match fails closed.
Bug:
The --serve automation loop (stdin/socket) and the MCP stdio loop read
input with unbounded read_line/lines. A single line over 64 MiB - a
huge paste or a hostile client - grows the buffer until the allocator
aborts, the same class of failure as the HTTP path, on the non-HTTP
transports.
Reproduce:
1. Start --serve on a socket (or the MCP server on stdio).
2. Send one 65 MiB line with no terminating newline anywhere in it.
3. Before: RSS grows to the full line, then "memory allocation failed"
(or a multi-second stall while the line is copied).
4. After: the read fails at the cap and the existing EOF arm closes the
connection/stdio promptly.
Fix:
New shared src/io/line_read.rs module: lines_capped(reader, 64 MiB)
yields Err for an over-cap line, which the serve and MCP loops already
treat as EOF/close. rest.rs was refactored onto the same helpers so the
cap logic lives in one place. A regression test streams a 65 MiB line
at the serve socket and asserts the connection ends without a response.
Bug:
read_request used bare BufRead::read_line for the request line and every
header. A client that connects and streams bytes without ever sending
CRLF grows a single String until the allocator fails and the process
aborts. The 15-second idle timeout never fires while bytes keep
arriving, so one socket is enough.
Reproduce:
1. Start OpenCADStudio with --http 8765.
2. Connect (e.g. nc 127.0.0.1 8765) and write "GET /" followed by
gigabytes with no newline.
3. Before: memory climbs until "memory allocation failed" aborts the
process.
4. After: the line hits the 8 KiB cap, a 431 response is written and
the connection closes; the server keeps serving.
Fix:
The request line and headers are read through capped helpers - 8 KiB per
line, 64 KiB for the whole header block. An over-limit request gets the
standard 431 (Request Header Fields Too Large) written inside
read_request before anything else parses it. The GUI bridge path shares
read_request, so it is covered too. Regression tests stream an oversize
request line and an oversize header block and assert the 431.
Bug:
background_task joined the worker thread with .expect(...). Any panic
inside a background closure (decoding a malformed file, an unexpected
None) was re-raised on the main thread and took the whole application
down, even though the worker's failure was already contained.
Reproduce:
1. Run an operation that executes in a background worker, e.g. loading
or converting a deliberately corrupted file whose decoder panics.
2. Before: the process dies with the worker's panic message.
3. After: the command line shows "background task failed: <label> -
<panic>" and the application keeps running.
Fix:
The worker closure is wrapped in catch_unwind; a panic is converted to
Message::BackgroundTaskFailed(context, message) delivered back to the
update loop and surfaced as a command-line error. Each call site passes
a context label naming the operation. A failed thread join (thread
aborted) is handled the same way instead of expect().
Bug:
Message::TabClose carried a vector index captured when the view message
was built. Closing several tabs in one batch (Close All / Close Others
with unsaved-changes dialogs, or a queue of continuation updates) let an
earlier close shift the vector before a later TabClose(idx) was handled:
self.tabs.remove(idx) either panicked (index past the end) or closed a
different tab than the one the user confirmed.
Reproduce:
1. Open several drawings and modify each so they all prompt on close.
2. Use Close All, or close two tabs in quick succession so both messages
are handled in one batch.
3. Before: panic in remove(idx), or the wrong tab closes while the tab
the user chose stays open (or its work is lost).
4. After: the chosen tab closes; an id already closed by an earlier
message is a no-op.
Fix:
TabClose now carries the tab's stable u64 id, resolved to a vector
position when the message is handled, not when it is built;
PendingClose::Tab stores ids the same way. All construction sites pass
tab.id (a raw usize index no longer type-checks), and
DocTabCloseAll/DocTabCloseOthers build their queues from live ids at
handling time. A missing id means the tab is already gone, so the
handler returns Task::none().
The #1538 drawing's solids now draw every face: extrusion, sum and
offset surfaces and summary-only intcurves are read, vertex blends are
filled over their boundary, and the mesher closes boundaries fitted to a
few thousandths, notched cylinders and torus bands.
Fixes#1538
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Define Ty, Param, Rule, and OpDef in src/mcp_ops.rs for all 39 mutations
- Generate Draft 2020-12 schema using per-op anyOf variants with op enum discriminators
- Disambiguate colliding properties (replace, center, name) across mutation variants
- Add pre-dispatch validator enforcing required fields, rules, and parameter types
- Emit unknown parameter warnings into response warnings
- Introduce target_document_id for entities_copy_to with deprecated document_id fallback
- Update unit tests and automation smoke assertions
Brings the #1538 NURBS face fixes: reversed intcurves, period-folded
explicit pcurves, and pcurves cut to their edge instead of dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sets debug = 1 on dev and test profiles to emit line tables only, reducing debug executable sizes by ~50% (~700 MB per binary) and substantially cutting linker memory overhead and disk I/O.
Sets opt-level = 2 on external dependencies in dev mode ([profile.dev.package.*]) so cached third-party libraries run optimized without affecting developer code compilation speeds, improving test execution speed by ~4.6x.
Release mode ([profile.release]) remains completely untouched with full -O3 optimizations.
Disables Cargo's automatic test target discovery (autotests = false) and gathers all 24 integration test suites under a single integration test target in tests/integration.rs.
This reduces the number of separate integration test binaries from 24 to 1 across all platforms, eliminating redundant linker invocations and peak memory pressure during cargo test while significantly improving build and test times.
Two causes behind the load stalling at the scene-cache step:
- Solids were fitted to the ACIS header's first tolerance-line value,
which is the millimetres-per-unit scale (25.4 in this inch drawing,
1 in most), not a tolerance. It is now resabs, the second value.
- The kernel (37b6902) drops pcurves that miss their edge and caps the pcurve
walk, so a solid carrying a bad pcurve no longer refines an edge
hundreds of thousands of times. Graph b741bd6.
The attached drawing now meshes all 76 solids in 3.4 s (release) and
opens in 19 s in a debug build.
Fixes#1538
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Commit 98b56ef6 ('Complete Hatch host model coverage') added the hatch_geometry_and_boundaries_are_validated test without the #[cfg(feature = host)] guard, and commit f537aff7 imported codec directly instead of through crate::host::codec. This broke cargo test --workspace with 20 compilation errors because codec and the canvas validation helpers are only compiled when the host feature is active.
Fixes regression from commit 98b56ef673.
Since 2026.39 reads `unifont 1.0` files (isocp.shx and its family among
them) instead of falling back to a substitute, a glyph built from a
subshape went wrong: code 7 is followed by a two-byte code point in a
unifont, but only big fonts read two bytes, so the low byte ran on as a
drawing opcode. ISOCP's `9` came out at x-height as a small bowl.
Fixes#1556
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
角质 means keratin; both Simplified and Traditional menus used it for the
Angular dimension entry.
Fixes#1552
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two variants put their description string inside `properties`,
where every value must be a schema, so strict clients (Claude Code, and
by the look of discussion #1541 Codex too) rejected ocs_execute and only
the read tools loaded. The whole tools/list schema now walks clean.
Fixes#1555
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loader added every catalog linetype to the drawing with its [shape]
and ["text"] elements stripped, and the renderer reads the drawing's
own definition first — so BATTING, ZIGZAG, PLANT, POINT1/2, DRAINAGE,
FENCELINE, TRACKS, GAS_LINE, HOT_WATER_SUPPLY drew as plain dashes (a
near-solid line), and a saved file carried no shapes either.
- populate_document_from_source now stores each shape / text on the
element it follows, with a shape-file STYLE (empty name, one per file)
and the named text style; values keep their written decimals.
- One table maps shape names to numbers per shape file: the standard
ltypeshp 130–134, the bundled catalog's own beyond them. The renderer
resolves stored numbers through it instead of a hard-coded list that
ignored which file a number belonged to.
- A drawing's copy stripped to exactly the catalog's dashes (as this
loader saved them) gets its shapes and text back on open.
- Opening completes the linetypes before the geometry is prepared, so
the first view already draws them.
Fixes#665
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- check_word returned "valid" for a word missing from a loaded wordlist
(!words.is_empty() && !contains, with contains already false), so the
wordlist backend never flagged anything.
- aspell on PATH counted as a CLI speller, but only hunspell is invoked,
so on a machine with aspell and no hunspell every CLI check silently
failed.
- A bare wordlist offers no suggestions; the test no longer demands
them from the linux_system backend. Unused PathBuf import dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Process ids are reused: report_for_pid now takes the sentinel's arm
time and ignores a report written before it, so an old crash of an
unrelated run with the same pid no longer costs a backend. The test
plants such a stale report.
- is_device_failure drops "buffer" and "surface": drawing code and the
modelling kernel panic with those words too. It keeps wgpu, out of
memory and device lost, and adds wgpu's "validation error".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The byte count at 110 was read with direct indexing; a truncated element
now skips like every other field read does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PR's principal-axis math now lives in the codec's transform_ellipse
(8464e0a), which explode, INSERT and every other codec path share.
transform_ellipse_geometry delegates to it for the block-cache tangent,
and the entity goes back through apply_standard_entity_transform, with
MIRROR delegated to the codec's apply_mirror: the old closure reflected
only the centre and major axis, leaving mirrored arcs on the wrong side,
and the PR's version turned the normal to -Z. The mirror test now checks
the kept +Z normal and the negated parameters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add cross-platform SystemSpeller abstraction with three backends:
- Windows: direct COM vtable calls to ISpellChecker (Win32 Spell API),
no external crate — uses the same hand-rolled FFI pattern as
file_association.rs. Handles ALL-CAPS words by also testing lowercase.
- macOS: NSSpellChecker via objc2/objc2-app-kit (already in deps).
- Linux: Hunspell .dic file reader + hunspell CLI pipe fallback.
text_audit gains three new parameters:
system_spellcheck: true — activate OS spell-checker
language: en-US — BCP-47 language tag (default: OS default)
suggest: true — include spelling suggestions
The agent dictionary and system speller work as a layered pipeline:
words matching the agent dictionary are accepted immediately; only
remaining words are forwarded to the OS checker.
Response includes a system_speller object (enabled, available, backend,
language) and an unrecognized_words array with handle, position, and
optional suggestions.
Integration test: text_audit_system_spellcheck_flags_misspelled_words
(20/20 p1_tests pass). Release build verified.
- Adds ext_audit operation under ocs_read, control bridge, and headless server
- Implements pairs / dry_run_pairs simulation returning diffs, coordinates, and unmatched queries
- Implements check_terms suspect matching and dictionary vocabulary verification
- Adds dry_run: true to ext_replace to preview modifications without mutating entities or pushing undo
- Supports bounds, layers, handles, and scope filtering
- Updates MCP tool definitions, schemas, and automation test suites
- Add text_search (read) and text_replace (mutating) operations to control bridge, automation, and MCP
- Support single and batch find/replace pairs under atomic undo transactions
- Safely parse and preserve MText formatting tags, inline fonts, colors, and margins
- Add flexible multiline MText matching and replacement across paragraph breaks (\n <-> \P)
- Add accent/diacritic-insensitive matching across Unicode and ASCII character sets
- Support Windows-1252 / ISO-8859-1 code decoding (%%nnn), DXF Unicode escapes (\U+XXXX), and special character codes
- Expose ignore_accents parameter across ocs_read and ocs_execute MCP tool schemas
Layer, text-style, dimension-style and object-style invalidation bumped
every dependent entity as Modified, which also re-resolved associative
dimensions, centre marks and hatches and re-ran the parametric solve.
A dimension whose association disagrees with its own points (as older
saves can) jumped its dimension line on LAYON. A restyle moves no
geometry, so it now refreshes display only.
Fixes#1536
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inside a block, text-bearing and wide wires got their child rank as a
depth override, but fill-only wires stayed at the bare insert level. A
dimension's baked MTEXT with a DIMTFILL background therefore drew its
box in front of its own text whenever the text's rank fell below zero:
cyan boxes with no numbers. Fill-only wires now take the rank as well,
so a background ties with its text and the text pass wins.
Refs #774
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A DWF sheet or DGN model is drawn as world-space wires (one line wire per
colour and width, one fill wire per colour) instead of a raster rescaled
to the view, so it stays sharp while zooming instead of showing pixels
until the raster is made again. Colours go through the colour table
mapping, contrast, fade and monochrome; clip boundaries clip the wires.
The wires are drawn and plotted but neither picked nor snapped; the
underlay's own snap geometry still serves object snaps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One-pixel hairlines keep horizontal and vertical pieces solid, but
slanted pieces and curves are anti-aliased: their pixel steps broke up
into dashes once the raster was scaled to the screen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1519 stopped the sentinel blaming a backend for a run that had already
drawn for SENTINEL_PROOF_DELAY, on the reasoning that a backend which
kept the application alive that long had proved itself. That reasoning
does not hold for the failure this repository sees most on integrated
hardware.
Measured here on an Intel Iris Xe: with `--backend dx12` a large drawing
aborts the process 110 s in, long past the proof delay, so the sentinel
carried `survived=1` and the next launch went straight back to dx12 and
crashed again. The same binary on `--backend vulkan` opens the same
drawings with no out-of-memory report and no uncaptured errors at all,
so the backend really is the difference and moving off it really is the
cure. D3D12 is fine while the interface draws and only fails once the
whole scene uploads, which is why the clock is the wrong question.
The crash report from #1521 answers the right one. It records the
backend that was live and what the panic was, and it is named for the
process that wrote it, so it joins exactly to the `pid` the sentinel
already stores — no guessing from whichever report is newest, and no
new state to keep. A sentinel marked survived is now excused only when
that run left no report, or left one that has nothing to do with the
graphics device.
The test that matters is the pair, and both were run end to end:
killed from Task Manager after 40 s, no report -> dx12 kept
device crash after 110 s, report names wgpu -> dx12 skipped, Vulkan
`is_device_failure` is deliberately narrow: an indexing bug in drawing
code that happens to run on a frame must not cost the user a working
backend.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- V8 models are placed relative to their global origin (model header).
- A V8 model's extent is the union of its elements' stored ranges; a V7
model's text box is one text height tall.
- Files without a colour table use the standard DGN default table; colour
255 is read from the table's background entry.
- Colours from a file's colour table follow the background: near-white
colours swap with it, and over non-black backgrounds some shades drop a
step.
- DGN contrast pulls towards a pivot taken from the model's own colours,
after the fade, which is blended into the background first.
- Shared cells are read through their full 3D placement, so tilted
instances and 3D definitions are drawn in plan.
- Deleted V8 elements are skipped; lines along the model's extent stay in
the raster.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a grip drags, the edited entity is hidden from snapping and its
drag-start shape got a reference-only pass: the marker appeared on the
old edge's midpoint or corner, but the grip stayed at the raw cursor,
and the pass ran only with object snap tracking on. The reference pass
now always runs, still limited to discrete points (never nearest-style,
so the point cannot be pulled back onto the old shape) and skipping the
grip's own start point, and when no snap on the rest of the drawing wins
it places the grip too.
Fixes#1528
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the wire shaders read transparency_enable; hatch (solid, gradient
and pattern), text and 3D/fill-triangle fragments always drew the
entity's alpha, so turning Show Transparency off left them see-through.
Fixes#1529
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Booleans now cut a sphere wherever the other solid crosses it — box
edges and corners, planes through its centre, another sphere, a
cylinder on its axis — instead of refusing or keeping the wrong piece.
Reported in discussion #1499.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every near-white colour turned black on a light background (and near-black
white on a dark one), including true colours. A 255,255,255 solid used as a
white mask became a black block. Now only colour 7 swaps: true colours and
ACI 255 resolve a hair off pure white/black and keep their value on screen
and in plots.
Discussion #1500
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows the drawing was held with an exclusive byte lock, which also
refuses reads, so AutoCAD / BricsCAD could not open it at all. The lease
now reads through a handle that shares reads but refuses write opens, with
a shared lock that refuses writes; the sidecar still keeps a second OCS
session out. Unix keeps its advisory lock, which never blocked readers.
Discussion #1416, issue #739
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add transform_ellipse_geometry helper in scene::view::transform to compute exact principal semi-axes, normal, ratio, and shifted arc parameters under any affine transform (negative scales, reflections, negative rotations, non-uniform scaling, and translations).
- Use transform_ellipse_geometry in block_cache::transform_tangent for TangentGeom::PlanarEllipse so GPU analytical instances and curve endpoints match true WCS positions.
- Update entities::ellipse::apply_transform to use transform_ellipse_geometry for complete scaling, rotation, mirroring, and affine transformations.
- Clean up tangent parameters in to_render for Ellipse so start_param and end_param are consistently unwrapped.
- Add comprehensive unit tests in transform.rs, block_cache.rs, and ellipse.rs verifying negative rotations, negative scales, and distant origins.
Entity picks switch object snap off, but COINCIDENT (and the other
constraints that take a point) only recognise a point when the pick lands
exactly on it. Without snap there were no point markers and every pick
became the whole curve. Point-accepting entity picks now keep object snap
and hand the snapped point to the command.
Discussion #1526
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The palette installed the insert command directly and kept the previous
command's last point, so ORTHO locked the insertion point to an axis
through it. The command-start reset is now a shared helper that the
palette uses too.
Fixes#1525
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>