Commit Graph
542 Commits
Author SHA1 Message Date
Dylan M. Taylorandskylartaylor 2dbc7699a0 security: budget the rendering pipeline against DoS geometry (CWE-400/407/789) (#1849)
* security: budget the rendering pipeline against DoS geometry (CWE-400/407/789)

Several render paths let crafted drawing data spin the rendering thread or
allocate without bound:

- Hatch dash sweeping reset its per-span step counter and used emitted geometry
  as its only limit, so gap-only families (which emit nothing) could multiply
  CPU work. Skip families that cannot draw and charge every dash step against a
  shared per-hatch work budget (row 10).
- Hatch polygon deduplication compared loops with an O(n²) cyclic match and
  every loop against every kept one. Charge vertex comparisons against a shared
  budget and stop deduplicating when spent (row 12).
- Table coverage marked every merge rectangle, so overlapping merges re-marked
  covered cells quadratically. Bound marking to a small multiple of the cell
  count (row 11).
- Broken-table part layout expanded sparse rows to full width before checking
  whether detailed layout is allowed. Only expand when within the cell limit;
  otherwise leave cells sparse and let layout draw an outline (row 17).
- Dimension text substitution copied the base for every `<>` placeholder with no
  size bound. Estimate the expansion and cap it at a fixed text budget (row 32).
- Linetype range generation cycled all-gap patterns forever (nothing emitted, so
  the emit limit never tripped). Return continuous for all-gap patterns and
  bound visited pattern elements (row 38).
- Viewport contents built with a fresh expansion counter, so a layout could far
  exceed the documented block-expansion limit. Share one budget across the whole
  build, seeded with what the enclosing list already spent (row 22).

New tests for the all-gap linetype path; all render tests pass, clippy clean.

Addresses findings rows 10, 11, 12, 17, 22, 32, 38.

* fix: preserve bounded render fallbacks and text output

---------

Co-authored-by: skylartaylor <26588250+skylartaylor@users.noreply.github.com>
2026-10-22 17:08:25 -04:00
Dylan M. Taylorandskylartaylor 7c72ec45a9 security: bound drawing document-model work and allocation (CWE-400/407) (#1850)
* security: bound drawing document-model work and allocation (CWE-400/407)

Several document-model operations let crafted drawing data cause quadratic or
exponential work, or multiply retained allocations:

- Polyline grip generation rebuilt the whole segment vector inside the loop over
  every vertex, so enumerating one large polyline's grips was quadratic. Compute
  the segments once before the loop (row 35).
- Associative-array name sync searched the whole replacement vector for every
  placement. Index replacements by placement coordinate once (last entry wins),
  turning the per-item lookup O(1) (row 44).
- Dimension-extent bounds recursively expanded the referenced block without the
  shared bounds cache the Insert path uses, so a branching or self-referencing
  dimension block cost one pass per reference. Route it through the same
  `(name, depth)` cache (row 37).
- Dynamic-block parsing parsed every graph-member occurrence into its own owned
  parameter, so a graph repeating a member multiplied one record into gigabytes.
  Deduplicate member handles before resolving them (row 7).
- Stretch-action application linearly searched the vertex list and cloned the
  index vector for every selection occurrence. Index vertices by handle and skip
  handles already handled in the action's selection (row 30).

New tests for polyline grips; all doc tests pass, clippy clean.

Addresses findings rows 7, 30, 35, 37, 44.

* fix: retain first dynamic stretch vertex selection

---------

Co-authored-by: skylartaylor <26588250+skylartaylor@users.noreply.github.com>
2026-10-22 17:08:02 -04:00
Dylan M. Taylorandskylartaylor 0158712bfd security: bound geometry-kernel degree and work (CWE-407) (#1848)
* security: bound geometry-kernel degree and work (CWE-407)

Several geometry routines let a crafted spline or region cause quadratic or
worse CPU work:

- B-spline basis evaluation is O(degree²) per sample and tessellation takes
  O(degree) samples, so a HATCH/DXF spline edge declaring a very high degree
  (DXF allows up to 100 000) turned a tiny spline into billions of iterations.
  Add `MAX_DEGREE = 64` and treat higher-degree splines as invalid, so
  `eval`/`tessellate` fall back to the control polygon. The render clip boundary
  also rejects such degrees up front (rows 18, 28).
- `Spline::derivative` re-ran the O(knots) validity scan on every call, and
  arc-length integration calls it once per sample, making a valid spline's
  measurement quadratic. Split off `derivative_unchecked` and have integration
  (validated once by its public entry points) use it (row 24).
- `infer_knot_param` matched each fit parameter against the whole knot vector
  from the start. Both are ascending, so replace it with a linear merge (row 16).
- Fillet seed collection did not stop the enclosing run-pair loops when the
  per-call candidate cap was hit, and refined duplicate candidates. Bound total
  seeds across pairs and deduplicate before Newton refinement (row 36).
- Region grid intersection counted only distinct pairs against its budget, so a
  long segment replicated across many cells re-visited the same pair unbounded.
  Charge every candidate-pair visit against a budget (row 41).

New spline degree tests added; all geom and render tests pass, clippy clean.

Addresses findings rows 16, 18, 24, 28, 36, 41.

* fix: preserve spline knot and fillet search behavior

---------

Co-authored-by: skylartaylor <26588250+skylartaylor@users.noreply.github.com>
2026-10-22 17:05:16 -04:00
Dylan M. Taylorandskylartaylor 345fb27a69 security: write files atomically through an exclusive temp file (CWE-59) (#1840)
* security: write files atomically through an exclusive temp file (CWE-59, CWE-377)

Save, export, plot, autosave and the audit log each wrote their output by
putting bytes in a predictable sibling temporary path (`<path>.cadcraft-tmp`,
`<recovery>.tmp`, or the companion `.adt`) and renaming it over the real file.
Opening that pre-existing temporary path for a truncating write follows a
symlink planted there by another local user, letting them redirect the write
onto any file the victim can write before the rename.

Add a shared `atomic_write` helper that creates a fresh temporary file with
`create_new` (O_CREAT|O_EXCL) in the destination directory, writes through the
retained handle, flushes/syncs, and atomically renames it into place. An
existing entry (symlink or not) makes the exclusive create fail instead of
being followed. Route autosave, save, export, plot and the audit log through it.

The macOS CI cert importer likewise wrote the decoded PKCS#12 container to a
fixed name in a possibly shared temp directory; it now creates a private
per-run directory with `mktemp -d` and creates the file with noclobber.

Addresses findings rows 1, 25, 40 (CWE-59) and 19 (CWE-377).

* fix: preserve permissions during atomic replacement

* fix: restrict temporary permissions during exclusive creation

---------

Co-authored-by: skylartaylor <26588250+skylartaylor@users.noreply.github.com>
2026-10-22 17:03:46 -04:00
Dylan M. Taylor acd109d97c Use chrono instead of hand-rolled UTC conversion (#1789)
* Use chrono for UTC/calendar conversion instead of hand-rolled copies

Replaces the three civil_from_days copies (logging timestamp, Drawing
Recovery time, parity checklist date) with chrono as a workspace
dependency. Output formats are unchanged and pinned by the existing tests.
AGENTS.md now says time goes through chrono.

Fixes #1785

* Never panic on a clock outside chrono's range; keep 5-digit years unprefixed

chrono's From<SystemTime> unwraps, so a clock past year 262143 panicked
inside the logger. timestamp and xtask's date now use the fallible
DateTime::from_timestamp and read such a clock as the epoch, as the old
saturating code did. xtask's date also clamps a pre-epoch clock to
1970-01-01 again. Years past 9999 are written with {:04} like before,
because %Y would add a '+'. Tests cover all three cases.

* Don't build an unrepresentable SystemTime in the far-future test

UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.

* logging/autosave/parity: keep extended-year dates, no epoch fallback

Applies the stricter no-epoch-fallback rule (from wordcraft#3427 / filmcraft#2662,
superseding the earlier epoch-or-last-valid suggestion) so all sibling PRs agree:
a timestamp the old code converted past chrono's year 262143 range must keep its real
proleptic-Gregorian date, not collapse to 1970-01-01. Each site uses the 400-year
Gregorian cycle (146_097 days) with chrono as the only calendar logic: chrono converts
the day within one cycle, whole cycles carry in the year (i128, saturated into i64).
This also removes the panic FredApps flagged — `DateTime::<Utc>::from(SystemTime)` could
panic past chrono's range; `from_num_days_from_ce_opt` within one cycle never does.
Verified the cycle result equals the old formula across a wide range and at the extremes.

- apps/cadcraft/src/logging.rs `timestamp`: cycle `civil_from_days(u64)`; pre-1970 epoch
  clamp and ms/time-of-day format kept. Pins: +10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z
  (was 1970-...); checked_add u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch).
- crates/ui-egui/src/autosave.rs `utc`: cycle converter. Pins: utc(10_000_000_000_000)
  = "318857-05-20 17:46"; utc(u64::MAX) = "584554051223-11-09 07:00" (was "1970-01-01 00:00").
- xtask/src/parity.rs `date`: cycle converter; pre-1970 epoch clamp kept. Pins:
  day(115_740_740) = "318857-05-20"; checked_add u64::MAX/2 s -> "292277026596-12-04" (was epoch).

* Don't build a year-318857 SystemTime unchecked in the logging test

Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.

* Name the 400-year range extension as the one exception to the time rule

The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.

* Say the library computes month and day; time-of-day is plain arithmetic

Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
2026-10-22 14:09:16 -04:00
skylar taylor-barrick e4214f7180 Merge pull request #1677 from MarekSpodBiedry/issue-1644
Write the ACAD_TABLE *T block relative to its insertion point (#1644)
2026-10-22 07:51:40 -06:00
skylar taylor-barrick 35934718ea Merge pull request #1683 from MarekSpodBiedry/issue-1667
Keep a layout's plot window corners when saving DXF (#1667)
2026-10-22 07:51:33 -06:00
skylar taylor-barrick 632967aa78 Merge pull request #1336 from midasmatterhorn/fix/r4-dxf-table-record-caps
io: the four symbol tables honour the same record cap as their peers
2026-10-22 07:51:19 -06:00
skylar taylor-barrick f15be2305c Merge pull request #1680 from MarekSpodBiedry/issue-1650
DWG: write the current text style's own handle, so AutoCAD opens with it current (#1650)
2026-10-22 07:51:12 -06:00
skylar taylor-barrick f2dadec23a Merge pull request #1678 from MarekSpodBiedry/issue-1648
Multileader sizes no longer multiplied by DIMSCALE (#1648)
2026-10-22 07:51:06 -06:00
skylar taylor-barrick 00acec952e Merge pull request #1774 from philipp-tch/fix/foreign-handles-above-2-48
DXF: keep foreign objects whose handles are above 2^48 (fixes #1773)
2026-10-22 07:51:00 -06:00
skylar taylor-barrick 78254b4d3f Merge pull request #1480 from Silvertree2010/fix/h2426-polyline-segments-with-a-tiny-no
Treat polyline bulges with a sub-EPS sweep as straight segments
2026-10-22 07:50:53 -06:00
skylar taylor-barrick 8094d7746f Merge pull request #1635 from Hamza0101/entsel-pickbox
AutoLISP: an entsel prompt shows the pick box whatever its message says
2026-10-22 07:50:47 -06:00
Philipp ThomannandClaude Opus 5.5 82424e47c9 DXF: keep foreign objects whose handles are above 2^48
AutoCAD writes handles above 2^48 (whole drawings whose handles start at 0xDAD1...).
dxf_raw dropped every foreign object with such a handle, so these drawings lost their
dynamic blocks and all other unmodelled objects on read, DXF to DXF as well. DXF handles
are up to 16 hex digits; the whole 64-bit range is now believed (null still is not).
Modelled entities and table records already accepted such handles.

Test: the DOOR fixture with every handle moved above 2^48 keeps its dynamic block
through a save (red before the change).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 07:23:16 +02:00
MarekSpodBiedry a7bb28d722 DWG: write the current text style's own handle, so AutoCAD opens with it current (#1650) 2026-10-19 22:33:02 +02:00
MarekSpodBiedry 27bff9cc30 Write the ACAD_TABLE *T block relative to its insertion point (#1644) 2026-10-19 22:29:01 +02:00
MarekSpodBiedry d600a80354 Multileader sizes no longer multiplied by DIMSCALE (#1648) 2026-10-19 22:27:34 +02:00
MarekSpodBiedry 81f0aa53ff Keep a layout's plot window corners when saving DXF (#1667) 2026-10-19 22:27:04 +02:00
Hamza 27861ad0ed Read a prompt's wording only where the engine wrote it
The pick-box heuristic read the message for a leading "Select", and
this branch had widened it past leading whitespace so that a routine's
own message would be read the same way. That makes a plain point prompt
look like a pick: (getpoint "\nSelect a point: ") showed the pick box
and lost its point modifiers, since pointmod refuses them wherever
picks_objects holds.

A routine writes all of its own message (raw), and says through
picks_one when it picks an object, so the wording needs reading only
where the engine wrote it. Covered for both spellings a routine might
use, with and without the leading newline.
2026-10-20 00:31:05 +05:00
Hamza b72680cfb7 AutoLISP: an entsel prompt shows the pick box whatever its message says
Whether the cursor shows the pick box came from `Prompt::picks_objects()`,
which read the prompt's wording: a selection-set prompt, or a point prompt whose
message starts with "Select". An `entsel`/`nentsel` prompt takes a point and is
not a selection set, so it fell to the wording -- and an AutoLISP routine writes
its own message, conventionally starting with a newline:

  (nentsel)                                       "Select object: "   pick box
  (nentsel "\nSelect second line or polyline: ")   starts with "\n"   none
  (entsel  "\nPick a line: ")                     says no such word  none

A prompt now says that it picks one object rather than being read for it, and
`GetRequest::prompt()` sets that for `GetKind::EntSel`, so all three show the
pick box. The wording test is read past leading whitespace as well, so a command
machine's prompt beginning with a newline is not caught by the same thing.

Point modifiers (`.x`, `.y`…) are offered at a point prompt that is not a pick,
so they no longer appear at an `entsel` prompt with a message -- as they never
did at one without.

Fix: #1598
2026-10-19 19:13:23 +05:00
Hamza d9adce038c AutoLISP: = compares the symbols type returns
`(= (type x) 'LIST)` is how an AutoLISP routine tests a type, because `type`
returns a symbol. `=` coerced both of its arguments to numbers, so the symbol
failed the coercion and the routine stopped at its first type test:

  _$ (= (type 1) 'INT)
  bad argument type: numberp: INT

`=` and `/=` now compare numbers and strings as before, and any other atom -- a
symbol, `nil`, an entity name, a selection set, a file handle -- by what it is,
the way `eq` does. Two consequences, both closer to AutoLISP than raising was:
a mismatch of kinds answers `nil` rather than failing, so `(= 1 "1")` and
`(= '(1) '(1))` answer (comparing list structure is `equal`'s job), and
`(= nil nil)` is `T`.

The ordering comparisons still take numbers and strings only, as AutoLISP has
them: `(< 'a 'b)` remains `bad argument type: numberp: A`.

Fix: #1597
2026-10-19 17:26:39 +05:00
Brandon ThomasandClaude Opus 5.5 0b32c2e6a1 i18n(hu): the ViewCube pill's key is now WCS (after #1558)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 03:02:53 -04:00
323f4629eb Add Greek (el) interface localization (#1307)
Co-authored-by: Camthalion77 <2944375+Camthalion77@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 03:02:49 -04:00
c02027abc8 Bound CAL unary minus/plus and ^ chain recursion (#1586)
Found while testing crates/engine/src/cmd/utility/calc.rs:124-140 (Parser::unary recursion, MAX_DEPTH only counted in expr() and '[')

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 02:57:52 -04:00
8c249aa722 Menu bar switches to the hovered menu once one is open (fixes #1567) (#1571)
With a menu of the in-window bar open, pointing at another title now
closes the open menu and opens that one. With no menu open, pointing
still opens nothing. Clicking the open title, clicking elsewhere, Esc
or choosing an item close the menu as before.

The bar now draws its titles first, then opens the hovered title's menu
if another bar menu is open, then draws the menus. egui 0.36's MenuBar
has no hover switching of its own.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:57:40 -04:00
a0aa0863bf Turkish interface (Türkçe) (#1529)
Add a Turkish catalog (tr.tsv, 1,330 rows): the union of the existing
catalogs, four menu headings (Ölçülendirme for the Dimension menu, Ölçü for
the object) and single-form counts, since a Turkish noun stays singular after
a number. No marked UI literal falls back to English. Prompts read "… girin"
for Enter and "… belirtin" for Specify; clickable keywords (Top, Zero, Bottom,
_tan to, nX/nXP) keep the spelling the engine accepts.

Register the language (Preference::Turkish, `tr`, Türkçe) and add locale,
preference, count, menu-heading, live-switch, dialog and font tests. Docs,
README and ATTRIBUTION list it; the translation is AI-assisted, the running
app was reviewed by a native speaker, and line-by-line proofreading is still
to do.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:57:28 -04:00
b4509d2368 DWG: leave out orphaned DGN line style components, so DGN-bloated drawings open (fixes #1482) (#1501)
* DWG: leave out orphaned DGN line style components, so DGN-bloated drawings open (fixes #1482)

A DWG that has been through MicroStation can carry hundreds of thousands of
DGN line style components no line style definition uses. In an 82.8 MB site
plan they were 888,483 of 889,053 and made the DXF rendition 1.2 GB, over the
1 GB limit, so the file didn't open. After the failsafe read, components no
LSDEFINITION reaches (through compound and point components) are left out with
their dictionary entries, and a read note says how many.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* contributors: add my real name for the About window credits

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>
2026-10-19 02:57:16 -04:00
13a3ebc0d1 AREA: an ellipse's perimeter is its exact length, not a closed-form estimate (#1471)
AREA's Object option reported a full ellipse's perimeter with
Ramanujan's closed-form estimate, which falls short as the ellipse
flattens: 46.0256 for a 10 x 4 ellipse (exactly 46.0262), 39.8741 for a
10 x 0.1 one (exactly 40.0110, 0.34% short). It now integrates the
perimeter with the geometry crate's ellipse arc length, the one LENGTHEN
measures with, so both report the same length for the same ellipse.

Ellipse::arc_length_from halves a panel where it needs it (the same
adaptive Gauss-Legendre as spline arc lengths): with fixed panels a
100:1 ellipse came out 4e-8 long, as it turns sharply at its ends.

Tests check four ellipses (round to 100:1) against the
arithmetic-geometric mean to 1e-9, and the printed AREA line.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:57:04 -04:00
41e0860725 ALIASEDIT opens the Command Aliases dialog (#1381) (#1391)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:56:52 -04:00
0d23cc3cb3 MOCORO: move, copy, rotate and scale a selection from one base point until eXit (#1388)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:56:40 -04:00
81acc9b8e4 Print signed two-digit exponent for scientific units (LUNITS 1) (#1386)
Found while testing crates/engine/src/units.rs:13

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 02:56:28 -04:00
397b8e156a MLEDIT Merged Tee: the first multiline's inner elements run on to the second's (#1375)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:56:16 -04:00
12567f14f2 BURST: explode block references with their attribute values as text (#1345)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:56:04 -04:00
95e86caea5 TXT2MTXT: selected TEXT combined into one MTEXT top-down, or one MTEXT per TEXT, with SEttings (#1343)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:55:53 -04:00
7adf8d89cd BREAKLINE: a polyline with a break symbol between two points, with Block, Size and Extension (#1340)
Tools > Express Tools > Break-line Symbol draws one polyline from the first point to the second with a zigzag break symbol at a picked location (the midpoint on Enter), as AutoCAD's Express Tool does. Size scales the symbol (times DIMSCALE), Extension runs the line past both points, and Block takes a block of the drawing whose two points on the Defpoints layer mark where the line meets the symbol. The built-in symbol is our own geometry. One undo step; the options are remembered for the session.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:55:41 -04:00
006f71f650 Keep Z and elevation of the pieces left by BREAK (#1270)
Found while testing crates/engine/src/cmd/modify.rs:1457 (break_entity; helpers `line()`/`arc()`/`lwpoly()` in crates/engine/src/cmd/helpers.rs:10-23)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 02:55:28 -04:00
b0ed57f1c7 i18n(sv): translate the 95 strings added since the Swedish catalog (#1258)
Covers the Hatch and Gradient dialog, the Block Editor, REFEDIT, multileader
editing, the Options selection settings, the MTEXT editor and recent files.
The Swedish catalog now has every key of the Ukrainian reference catalog.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 02:55:14 -04:00
30db7296c4 선택 객체 조회 필터와 MCP 연동 추가 (#1253)
## 변경 내용
- entities 명령에 selected 불리언 필터를 추가해 현재 공간의 선택 객체만 조회
- 기존 종류·레이어·영역 필터와 결합하고 필터 적용 후 페이지 처리
- MCP query_entities 스키마와 명령·제어 프로토콜 문서에 옵션 공개
- 빈 선택, 중복·오래된 핸들, 다른 공간 객체, 잘못된 입력 및 조회 전후 상태 보존 회귀 테스트 추가

## 검증
- cargo xtask ci 6단계 통과: fmt, clippy, 테스트 2,012개, assets, layers, wasm
- 별도 .NET 클라이언트와 실제 헤드리스 엔진의 localhost 통합 검증 통과
- 실제 데스크톱 UI 조작 및 고객 DWG 왕복 검증은 수행하지 않음

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:55:02 -04:00
a76775bf2b Holding Shift overrides ortho while it is held, as in AutoCAD (#1198)
At a point prompt with a base point, holding Shift turns ortho on when
ORTHOMODE is off and off when it is on; releasing it restores the setting.
ORTHOMODE itself is never changed. While the override turns ortho on, polar
tracking is off, as with ortho on.

The UI already passed the Shift state to Session::snap_cursor (for POLARMODE
bit 8's Shift to acquire), so the change is in the engine only: snap_cursor
applies the override to its copy of the settings. Window corners (ZOOM
Window) and prompts without a base point are unaffected.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 02:54:51 -04:00
6576cac1a3 JOIN with a single arc or elliptical arc offers cLose: the arc becomes its circle, the elliptical arc its full ellipse (fixes #1175) (#1197)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:54:39 -04:00
4125b351cd Add HATCHEDIT boundary add and remove (#1156)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:54:28 -04:00
6d15a40f43 Selection cycling: a click over overlapping objects lists them, Shift+Space steps through them (#1149)
* Selection cycling: a click over overlapping objects lists them, Shift+Space steps through them (fixes #1143, fixes #1145)

* canvas: keep viewport_content's doc comment on it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:54:16 -04:00
5f390e6e6a Constraints: the settings checkboxes show or hide all, dropping per-object exceptions (part of #1134) (#1144)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:54:03 -04:00
00f521858a Dynamic blocks: a grip drag previews the reference in its own colours (#1124)
* Dynamic blocks: a grip drag previews the reference in its own colours

The preview of a dynamic grip drag is the reference re-evaluated at the
cursor. It was drawn in the preview colour, fills left out, so on a filled
label the text was white on white. Commands can now give preview objects
with their own properties (`Interactive::preview_objects`, drawn under the
rubber band); the grip drag gives the block's objects with the properties
they draw with in the reference (layer 0 on its layer, ByBlock its
colour, linetype, lineweight and transparency) and its attributes with their
definitions', fills included in the block's order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Preview: the rubber band is grip blue over objects previewed in their own colours

Over a block previewed in its own colours the rubber band and tick marks,
in the current layer's colour, could vanish on its fill (white on white).
They are drawn in the grip blue there.

Co-Authored-By: Claude Opus 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>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 02:53:50 -04:00
bcd0b3745e fix(render): stabilize dashed highlights of filled objects (#1046)
* fix(render): keep filled highlight dash anchors stable

* outline test: as_chunks for clippy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:53:38 -04:00
ba27f36b58 Inquiry: SHAPES reports closed-shape geometry facts (#1507)
* Inquiry: SHAPES reports closed-shape geometry facts

`shapes` is a read-only command that reports, for every closed shape in the
current space (closed polylines, circles, full ellipses, closed splines),
its perimeter, area, centroid, winding, first self-intersection and
coincident vertices, exact with arcs and bulges. It filters and pages like
`entities` and counts other matches under `skipped` with the reason.

`Polyline::centroid` and `polygon_centroid` give the area centroid.

Fixes #1436.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shapes: cap the quadratic self-intersection test at 4000 edges

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>
2026-10-19 02:53:10 -04:00
9f7a2422ea feat(engine): support AUDITCTL system variable and .adt log generation (gaps F4) (#1504)
* feat(engine): support AUDITCTL system variable and .adt log generation (gaps F4)

* audit: write the .adt log only beside a saved drawing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:53:04 -04:00
148091aa93 Properties: a dimension's text height, arrow size, overall scale and precision (#1259) (#1473)
* Properties: a dimension's text height, arrow size, overall scale and precision (#1259)

The Properties palette showed only a dimension's text override and style,
so the only way to change its text height was a new dimension style.
It now shows Text height (DIMTXT), Arrow size (DIMASZ), Dim scale overall
(DIMSCALE) and Precision (DIMDEC) with the values the dimension draws
with. Editing one sets a per-dimension override (DIMOVERRIDE) on the
selected dimensions, as AutoCAD's Properties palette does; the style is
left alone. properties.set takes textHeight, arrowSize, dimScale and
precision for dimensions and refuses bad values before changing anything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* props: keep DIMOVERRIDE's doc comment on dim_override

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>
2026-10-19 02:52:59 -04:00
c82d6228ff i18n:Add missing translations for the print dialog and page settings. (#1289)
* i18n:Add missing translations for the print dialog and page settings.

* i18n:Add missing translations for the print dialog and page settings.

* i18n(zh): drop rows that repeat existing keys and the unused 'Page setup of'

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:52:53 -04:00
786c8c46e4 Wrap format_angle to zero when rounding reaches a full turn (#1256)
* Wrap format_angle to zero when rounding reaches a full turn

Found while testing crates/engine/src/units.rs:63-66,87-88,102

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* units: wrap a full turn by the formatted value, so ties round as {:.*} does

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 02:52:48 -04:00