Commit Graph
1912 Commits
Author SHA1 Message Date
Karim Jerbi d0bfa48037 fix(units): clamp precision vars and their formatters
Bug:
SETVAR LUPREC and SETVAR AUPREC stored any parseable i16 verbatim, and
the header value flows through UnitContext into the per-call
formatters, which applied only a lower bound (.max(0)) with no upper
bound: format_length and format_angle rendered {:.*} with the raw
precision. SETVAR LUPREC 30000 therefore made format_length emit
30003-character strings on every properties and annotation formatting
call - a ~30 KB allocation per formatted value - while format_area in
the same file had long since capped itself with .min(15) and the units
dialog only ever offers 0-8 places. Negative values were also stored
verbatim.

Reproduce:
1. Command line: SETVAR LUPREC 30000 (or AUPREC 30000), then select
   something so the properties palette formats lengths/angles.
2. Before: the header stores 30000 and each formatted value is ~30 KB
   (the test measured 30003 chars for one length); -3 is stored as-is.
3. After: the setter stores 8 (0-8 range, echoed truthfully), -3
   stores 0, 5 stores 5 - and even a file carrying LUPREC 30000 in its
   header formats short output, because the formatters cap themselves.

Fix:
Two layers. The formatters get the cap format_area already uses:
format_signed_length and format_signed_angle now clamp with
.max(0).min(15), so every source - SETVAR, the units dialog, and
values carried in DWG/DXF files - produces bounded output on the hot
paths (15 matches the point past which f64 decimal output is
meaningless, which is why format_area was written that way). The
setters store x.clamp(0, 8) with the clamped value echoed back, the
same stay-inside-the-app's-own-UI-range rule SETVAR ISOLINES follows
against the Options slider. Architectural and fractional formats
ignore precision (fixed 1/64-inch resolution) and are unaffected.
Regression tests mirror the existing patterns: one forces hostile
precision through the formatters while saving and restoring the
thread-local unit context, one drives SETVAR through the command line
and checks the stored header values.
2026-09-30 19:15:50 +01:00
Karim Jerbi b1f4047f9c fix(plugins): degrade ensure_plugin_state instead of panicking
Bug:
The typed ensure_plugin_state helper and the object-safe trait method
it wraps turned two recoverable conditions into process deaths. On the
in-process host, the helper downcast the stored state with
.expect("plugin state type mismatch for plugin_id"), so registering
state under one plugin_id as one type and later asking for another
(e.g. a reloaded plugin version changes the type while the per-tab
state map survives) panicked the host. On out-of-process hosts
(ipc/client.rs, ipc/v4/client.rs) and on NullHost, the trait method
itself was implemented as a bare panic!("ensure_plugin_state is not
supported for out-of-process plugins..."), so any out-of-process
plugin calling the documented helper crashed its own runner process -
even though the sibling plugin_state_any and plugin_state_any_mut
methods on those same implementations already degrade to None
gracefully. The helper also used .expect("init called once") on the
initialiser closure.

Reproduce:
1. Out-of-process: a plugin whose runner calls ensure_plugin_state
   (&mut host, "my.id", ...).
2. Type mismatch: in an in-process plugin, ensure state under one id
   as u32, then ensure a different type under the same id.
3. Before: step 1 kills the plugin runner at the trait panic; step 2
   kills the host at the downcast expect. After: both get None, keep
   running, and existing state is untouched.

Fix:
Give the trait method a failure channel: ensure_plugin_state_any now
returns Option<&mut (dyn Any + Send + Sync)>, mirroring the sibling
plugin_state_any_mut on the same implementations, and the typed helper
returns Option<&mut T> (any?.downcast_mut). The panicking
implementations (ipc v3, ipc v4, NullHost, and the three never-called
test dummies) return None; the in-process app host wraps its real
storage in Some with unchanged insert-or-return semantics. The init
closure is now guarded by take() with a unit-Any fallback, so even an
implementation invoking it twice fails the typed downcast instead of
panicking. Object safety, argument types and vtable slot are
unchanged; external plugin sources get a compile error at the helper
and adopt Option handling on rebuild. Regression tests use catch_unwind
so they compile against both signatures: one per panic site (out-of-
process, NullHost, type mismatch), each additionally asserting the
None result and that pre-existing state survived.
2026-09-30 19:15:50 +01:00
Karim Jerbi b19253374c fix(start): saturate the recent-limit plus step
Bug:
The keep-recent-files footer on the Start page builds its [-] [count]
[+] buttons eagerly while constructing the widget, and the [+] step was
written as plain shown + STEP while the [-] button next to it already
used saturating_sub. shown comes from the input box, whose handler
keeps every ASCII digit with no length cap - so typing
18446744073709551615 (2^64-1) parses to usize::MAX and the addition
overflowed during view(), before the button was ever clicked. Debug
builds panic with "attempt to add with overflow"; release builds (no
overflow checks) wrap to a small number and request a nonsense limit.
The clamp in set_recent_limit runs only after the value is computed.

Reproduce:
1. Start page -> "keep recent files" box -> type
   18446744073709551615.
2. Before: the next frame panics at widget construction (on render, no
   click needed); release builds wrap the step silently.
3. After: no panic; pressing Enter or [+] applies the clamped limit and
   the box resyncs to it.

Fix:
Mirror the [-] button: shown.saturating_add(STEP). At usize::MAX the
message carries usize::MAX, which the handler's existing
clamp-and-resync treats exactly like any other over-max entry, so both
step buttons are now saturating and the release-build wrap is closed as
well. on_submit has no arithmetic and needs no change. The regression
test calls start_page_content directly with the hostile string - it
fails at the same line instead of requiring a GUI session.
2026-09-30 19:15:49 +01:00
Karim Jerbi e15d801011 fix(tess): clamp isoline counts before they reach the kernel
Bug:
$ISOLINES (header i16) and Surface.u/v_isolines (file-controlled i16)
were only ever floored at 0, never capped, and were handed to the
tessellation kernel as the per-face isolate count - which draws
0..count isolines per face with no ceiling of its own. SETVAR ISOLINES
32767 (or a DWG carrying that header, or a Surface with u_isolines =
32767) turned every rebuild and every per-cursor preview of
PRESSPULL/REVOLVE/SWEEP/LOFT into a multi-million-vertex computation:
measured 4,259,966 edge vertices for one 12-unit cylinder preview.

Reproduce:
1. SETVAR ISOLINES 32767 (or open a DWG whose $ISOLINES is large, or
   edit a Surface whose u_isolines is 32767).
2. Draw a circle and start REVOLVE (or PRESSPULL/SWEEP/LOFT), or shade
   any 3D solid.
3. Before: the app allocates without bound - abort on memory failure or
   a permanent freeze; previews recompute on every mouse move.
4. After: previews stay interactive; a hostile header renders exactly
   like the largest value the Options slider can request.

Fix:
Cap each value at the ceiling this product already agrees on, where the
type changes from i16 to the kernel count: header $ISOLINES <= 64 (the
Options slider's own "Isolines per surface" range), Surface
u/v_isolines <= 200 (the plugin editor's writable range). One helper
(clamp_header_isolines) replaces all 11 copies of .max(0) as usize
across the entity/scene/control/display feed sites,
surface_isoline_counts clamps both axes, and SETVAR ISOLINES stores
x.clamp(0, 64) so the echo, slider and re-saved files agree with what
renders; file-sourced headers keep their raw stored value and are
clamped at consumption. Clamping is density-only: geometry, bounds and
saves are untouched. Three regression tests cover hostile surface
counts, the SETVAR entry, and a file-sourced header measured through
display tessellation (hostile renders like the slider ceiling, negative
renders like zero).
2026-09-30 19:15:49 +01:00
Karim Jerbi 438a46b80f fix(serve): drop idle socket clients so the accept loop keeps serving
Bug:
--serve in socket mode serves one client at a time: the accept loop
blocks in the per-client line read before it reaches the next
connection, and this path never called set_read_timeout - unlike
rest.rs, http_bridge.rs and control::transport, which all use 15
seconds. A client that connects and sends nothing held the read open
forever, wedging the port: every later client hung waiting for the
ready banner (the banner is written before the read loop, so the server
looked healthy while it was stuck).

Reproduce:
1. Start OpenCADStudio --serve --port 8765.
2. From any shell, connect and send nothing:
   python -c "import socket,time;socket.create_connection(('127.0.0.1',8765));time.sleep(3600)"
3. From a second shell, connect and wait for the banner.
4. Before: the second client hangs forever; the port never serves
   another connection.
5. After: the idle first client is disconnected 15 seconds after its
   last read, and the second client receives the ready banner.

Fix:
The one timeout this transport was missing: SERVE_IDLE_TIMEOUT = 15 s
(same value as the other transports), set on each accepted socket via
serve_socket_with_idle - a seam that also lets the regression test run
with 300 ms. A timed-out read surfaces as an error from lines_capped,
which the loop's existing let Ok(line) ... else break already turns
into "drop this client and accept the next one"; a partially read line
is discarded, which is correct because it was never dispatched. The
timeout only runs while a read is pending, so long server-side
operations do not spend the idle budget, and the document session
survives the reconnect. Regression test: an idle client must see EOF
and the following client must still get its banner.
2026-09-30 19:15:49 +01:00
Karim Jerbi a0308dfa88 fix(data-link): clamp cell-range column window to the data width
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.
2026-09-30 19:15:49 +01:00
Karim Jerbi c86746726b fix(config): parse colour hex digits as chars, not byte offsets
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.
2026-09-30 19:15:49 +01:00
Karim Jerbi ee188a6e3c fix(control): reject unknown entities_transform actions instead of panicking
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.
2026-09-30 19:15:49 +01:00
Karim Jerbi be8a7b95c4 fix(io): cap line reads on the serve and MCP transports
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.
2026-09-30 19:15:49 +01:00
Karim Jerbi 3345b6453e fix(background): keep the app alive when a background worker panics
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().
2026-09-30 19:15:49 +01:00
Karim Jerbi 2d6f4fcfdf fix(tabs): close by stable document id instead of vector index
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().
2026-09-30 19:15:49 +01:00
Karim Jerbi 6951cc2a7a feat(mcp): declarative op table and per-variant request validation
- 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
2026-09-30 16:54:45 +01:00
Hakan SevenandClaude Opus 5.5 ab0682153d fix(spellcheck): Linux wordlist flags unknown words; only hunspell counts as CLI
- 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 <[email protected]>
2026-09-29 23:25:25 +03:00
Hakan Seven 8ac04533c2 Merge pull request #1534 from yztink/fix/arc-cont-tangent
fix: resolve tangent snap when continuing an arc
2026-09-29 23:05:54 +03:00
Karim Jerbi 77fbb79917 feat(text): native OS spell-checker for text_audit (Windows/macOS/Linux)
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.
2026-09-29 13:48:27 +01:00
Karim Jerbi 97ea84d99a feat(text): add text_audit spell-check and dry-run replacement simulation
- 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
2026-09-29 13:01:31 +01:00
Karim Jerbi 88169c1d7a feat(mcp): add text_search and text_replace with multiline MText and accent-insensitive matching
- 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
2026-09-29 12:42:00 +01:00
Hakan SevenandClaude Opus 5.5 07f25f4bca fix(grips): a grip lands on the snap marker it shows
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 <[email protected]>
2026-09-29 09:12:31 +03:00
yztink 596558f80a fix: resolve tangent snap when continuing an arc 2026-09-28 22:52:21 -07:00
Hakan SevenandClaude Opus 5.5 0a3dab12e3 fix(parametric): constraint point picks snap to endpoints and midpoints again
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 <[email protected]>
2026-09-28 23:25:13 +03:00
Hakan SevenandClaude Opus 5.5 f8db262577 fix(blocks): palette insertion starts like a typed INSERT
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 <[email protected]>
2026-09-28 23:02:39 +03:00
Karim Jerbi fdbbcc419e perf(flatten): pre-sized collect/plan and batched entity publish
flatten_plan 2,289 -> 1,098-1,144 us (-50..-52%); flatten_scene_apply 1,213 -> 789-813 us (-33..-35% vs baseline; -28.5% same-session O2-vs-O1, drift-corrected).

Pre-sizes collect/plan vectors and adds Scene::update_entities so FLATTEN issues one geometry bump per batch instead of per entity; single update_entity keeps its exact bump-per-call semantics.
2026-09-28 20:29:40 +01:00
Karim Jerbi 91900c4e73 bench(flatten): add FLATTEN metrics with extracted helpers
Moves the flatten projection fns and command helpers into modules/draw/modify/flatten.rs (behavior unchanged) and adds flatten_plan / flatten_scene_apply to the COMMANDS suite.
2026-09-28 20:29:39 +01:00
Hakan SevenandClaude Opus 5.5 3754426361 fix(paste): pasted associative hatches follow the pasted boundary
Paste (and WBLOCK, which shares the kernel) kept the source boundary
handles, so the pasted hatch stayed tied to the original outline or to
unrelated handles in another drawing. The COPY remap now lives at the
document level and runs for both.

Refs #1370

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 20:33:54 +03:00
Hakan SevenandClaude Opus 5.5 c45f22652d fix(dimension): grip menu Above Dim Line places text by the style, not a 1-unit nudge
It moved the text a fixed drawing unit off the line and then another unit
up, wrong at any real scale. It now sets DIMTAD = Above on the dimension
and lets the automatic layout place the text.

Fixes #543

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 19:44:09 +03:00
Hakan SevenandClaude Opus 5.5 97a5bc1d39 fix(offset): the offset just created can be hovered and offset again
A running command only knew the entities that existed when it started;
committed results are now handed back to it, and OFFSET adds them to its
pick set.

Fixes #673

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 19:35:19 +03:00
Hakan SevenandClaude Opus 5.5 d70cc46712 fix(web): STEP export no longer panics; STEP/STL exports download in the browser
The STEP header timestamp used SystemTime, which panics on wasm, and the
exports wrote through std::fs, which the browser does not have.

Fixes #761

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 19:18:53 +03:00
Hakan SevenandClaude Opus 5.5 38fe2b0b08 fix(plot): text and leader background masks plot paper-white, not black
The mask is filled with the canvas colour; plotting carried that dark
colour (or the light-to-black rule) onto the sheet.

Fixes #1072

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 19:01:56 +03:00
Hakan SevenandClaude Opus 5.5 c8c2238d4a fix(wblock): exported drawings carry their block definitions and styles
WBLOCK copied only the entities and their layers, so INSERTs pointed at
missing blocks and the file opened almost empty or needed recovery. It
now goes through the cross-drawing paste kernels, which bring layers,
linetypes, styles, block definitions and baked dimension blocks along.

Fixes #1111

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 18:56:08 +03:00
Hakan SevenandClaude Opus 5.5 a94d1cfd17 fix(input): AltGr and macOS Option characters type into the command line
Windows reports AltGr as Ctrl+Alt, so '@' and similar layout glyphs were
treated as shortcuts and dropped; polar input like @10<45 failed.

Fixes #1236

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 18:47:17 +03:00
Hakan SevenandClaude Opus 5.5 c964e365c3 fix(snap): parallel snap keeps its reference, base and lock in f64
The reference direction, base point and locked point went through f32,
so the drawn line drifted off true parallel.

Fixes #1387

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 18:32:50 +03:00
Hakan SevenandClaude Opus 5.5 e97314cdef fix(pick): overlapping entities resolve to the front-most in draw order
Fixes #1439

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 18:01:38 +03:00
Hakan SevenandClaude Opus 5.5 bb9e447c64 fix(grip): drop the polar/ortho tracking guide when a grip edit ends
Fixes #1456

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 17:59:07 +03:00
Hakan Seven 1a7e372f24 Merge pull request #1517 from mechpanos/fix/windows-printer-properties-freeze
fix(print): open the driver's printing preferences without blocking the event loop
2026-09-28 17:27:32 +03:00
Hakan Seven d9899be35e Merge pull request #1514 from ramox81/codex/dwf-dgn-underlays
Attach Underlay ribbon menu, DWF/DGN attach dialog, DGN true colours
2026-09-28 17:17:17 +03:00
ramox81andClaude Opus 5.5 5b6cbff5b2 Attach Underlay ribbon menu, DWF/DGN attach dialog, DGN true colours
- Insert > Reference: the PDF Attach button becomes an Attach Underlay menu
  (PDF / DWF / DGN); its face runs the last one chosen.
- DWFATTACH, DGNATTACH and ATTACH on a .dwf/.dwfx/.dgn open the Attach
  dialog for that kind: sheets or models with thumbnails (one chosen),
  placement, path type, file details with the sheet / model size, and for
  DGN the conversion units (sub units offer 1 / sub units per master as the
  scale).
- DGN V8 true colours come from the ExtendedColors record packed in the
  non-model attributes stream; the colour word's high part numbers them.
- A DGN model's extent includes each text's box (its length along the
  baseline and 1.5 x its height), as the reference sizes a model.
- DWFx text uses the family named in its embedded (deobfuscated) font.
- DWF contrast pivots at 0.73 once colours are turned over for a dark
  background.
- Hairline strokes are drawn solid one pixel wide, and underlay rasters are
  made at or below their size on screen so those lines stay unbroken.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 17:08:06 +03:00
Hakan SevenandClaude Opus 5.5 267ba1ed48 chore(start): remove the CAD Editor mobile app promotion
Drops the Google Play banner and its image from the Start page sponsors; the OpenAEC sponsor stays.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 17:07:10 +03:00
ramox81 fecbcd8138 Merge remote-tracking branch 'origin/main' into codex/dwf-dgn-underlays
# Conflicts:
#	Cargo.lock
2026-09-28 16:13:14 +03:00
ramox81andClaude Opus 5.5 56ecf786f1 DWF/DGN underlays: clipping, layers, text, 3D DGN, colours and zoom extents
- PDFOSNAP, DWFOSNAP and DGNOSNAP are separate; UOSNAP sets them all and
  reads 2 while they differ. Enable Snap switches the selected kind's.
- DWFCLIP / DGNCLIP clip their own kind with the kind's prompts and message;
  CLIP routes DWF and DGN underlays there; Enter at a corner asks again.
- The right-edge underlay tools show for DWF and DGN too (import is PDF
  only).
- Underlay layers: DGN levels (V8 level table, "Default" listed as "0"; V7
  used levels named from the level name table), hidden through the layer
  override data with the DGN placeholder string first; a plotted DWF has
  none.
- V8 colour read from its colour word (by level, index, or a true colour
  drawn as its nearest index); 3D V8 elements drawn in plan (lines, line
  strings, ellipses and arcs by their orientation, text); a model's extent
  includes its text.
- DWF 6 TrueType text (font, height, rotation from the font opcode) and DWFx
  glyph runs are drawn; the default W2D colour map is the measured one.
- Contrast pulls lightness towards a per-kind pivot as measured (PDF 0.5,
  DWF 0.243, DGN 0.65).
- Zoom extents includes raster images and underlays whose frames are hidden.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 16:10:01 +03:00
mechpanosandClaude Opus 5 9ba2493027 fix(print): open the driver's printing preferences without blocking the event loop
Properties… in the Plot dialog froze the application on Windows.

`on_printer_properties` called `DocumentPropertiesW` with `DM_IN_PROMPT`
from inside the closure handed to `iced::window::run`, and iced runs that
closure synchronously on the winit event-loop thread. The driver's
property sheet wants to run its own modal message loop on that same
thread, so the two loops collide: the sheet is never created at all, the
window stops painting, and the thread spins at 100% of one core with no
way back.

Measured on Windows 11 x86-64: the pinned thread is the window's own UI
thread, and compstui.dll plus the driver's PrintConfig.dll are loaded
into the process, so execution does reach the driver's property-sheet
code and never leaves it. It reproduces with a physical printer as
readily as with Microsoft Print to PDF, so the mechanism was broken for
every printer, not only the virtual one named in the report.

The system's own printing-preferences UI is now started as a separate
process, which pumps its own messages and leaves ours free, so the
freeze is impossible by construction rather than merely unlikely. The
command that does it already existed for the case where no printer is
selected; it now serves a named printer too, and the in-process modal
path is gone. Preferences still reach the job: the sheet writes the
per-user DEVMODE that the plot path reads back through
`DocumentPropertiesW`, which was confirmed by changing the orientation
in the sheet and watching the value the plot path reads change with it.

The new test pins down that the printer name must reach `printui` as a
single argument: split across several it matches nothing and the process
exits 0 without drawing anything, which would be a failure with nothing
to show the user.

Fixes #1474

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-28 15:40:39 +03:00
Hakan SevenandClaude Opus 5.5 eac19348d5 refactor(draw): TRIM geometry helpers come from the kernel; drop a probe test
#1511 added an orthogonal ellipse projection and a nearest-end
arc_parameter to draw/modify/geom.rs, the latter shadowing the kernel
function of the same name. Both now live in opencadkernel 237827e and
are re-exported here, so every kernel caller gets them too.

test_coincident_line_and_arc_grip_move only printed values and asserted
nothing; removed.

Pins opencadkernel 237827e, opencadgraph fe36957.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 14:08:36 +03:00
Hakan Seven 66ac8c4ec0 Merge pull request #1511 from yztink/feat/fluid-tangent-arc
feat(draw): add fluid tangent arc, circle, and line construction and snap trim cuts to boundary contacts
2026-09-28 14:02:11 +03:00
Hakan Seven 7694c506aa Merge pull request #1512 from ramox81/codex/frame-parity
Frames: follow the drawing state, DWF/DGN frames, image frame profile, frame-only picking

The codec [patch] to ramox81/cadcodec@ea5edaa is dropped: that commit landed as opencadcodec#63 and main already pins ad16215, which contains it.
2026-09-28 13:52:06 +03:00
ramox81andClaude Opus 5.5 1cbe89ef62 Underlays are rasterised for the view's scale; DGN V7 strokes one pixel wide
A page shown small was drawn from its full-size raster and thin lines broke
up into dots. Once a zoom settles on another quarter-octave step, PDF, DWF
and DGN underlays are rasterised again near their size on screen (never
above the full-size raster), frames are requested until that happens, and
the adjusted-pixel memo is kept per raster size. V7 element weights no longer
widen strokes, matching V8 models.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 13:49:33 +03:00
ramox81andClaude Opus 5.5 cb7860e50e DWF and DGN underlays: attach, display, snaps, frames and references
DWFATTACH / -DWFATTACH and DGNATTACH / -DGNATTACH attach a DWF / DWFx sheet
or a DGN (V7 or V8) model with the reference's prompts; ATTACH routes the
file types to them. The sheet or model is read into vector paths, drawn as a
transparent underlay raster (contrast, fade, monochrome and background
adjustment apply) and offers its geometry to object snaps. Definitions are
kept in ACAD_DWFDEFINITIONS / ACAD_DGNDEFINITIONS, the External References
palette lists and reloads them, and image textures get line-keeping mip
levels so thin underlay lines stay visible when zoomed out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 12:53:25 +03:00
yztink 92fa661065 feat(draw): add fluid tangent arc, circle, and line construction with ellipse support 2026-09-28 01:56:03 -07:00
ramox81andClaude Opus 5.5 5ca7becd97 Raster images: transparency off draws pixels opaque; TRANSPARENCY command
An image's transparency flag decides whether transparent pixels show what
is behind the image or draw in their stored colour: the image pass and PDF
plots now follow it (it was ignored, so every PNG alpha showed through).
TRANSPARENCY selects images and asks for the mode, defaulting to the first
image's current one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 11:23:19 +03:00
ramox81andClaude Opus 5.5 7f8f8457c7 Frames: state-following ribbon list, DWF/DGN frames, image frame encoding and profile, frame-only picking
- The Frames list shows the drawing's frame state (hide / display and plot /
  display but not plot / frames vary), follows commands, undo and document
  switches, and lists the mixed state as a disabled item.
- DWFFRAME and DGNFRAME are frame variables of their own, kept in the header,
  and FRAME sets them with the others.
- Out-of-range frame values say "Requires an integer between 0 and 2." and
  ask again; a set value reports "Regenerating model.".
- IMAGEFRAME is stored in the image variables as bits (2 is written as 3) and
  a drawing without them uses the profile value, which setting IMAGEFRAME
  updates and the settings file keeps.
- Raster images, PDF underlays and wipeouts are picked on their frame only;
  underlay snap geometry is never picked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 10:53:53 +03:00
Karim Jerbi 7978d1eb2e bench: add COMMANDS suite with EXPLODE metrics
Shares the benchmark harness between both suites, extracts the plan/batch/apply helpers out of the EXPLODE command arm, and adds the command_benchmarks target.
2026-09-28 06:58:47 +01:00
Hakan SevenandClaude Opus 5.5 1f1fba31b2 refactor(commands): plugin dispatch ends through finish_dispatch
The branch added by #1498 repeated finish_dispatch's focus and dynamic-input sync; call it instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 06:47:21 +03:00