Production drawings show AutoCAD stacking two equal limits for a DIMLIM
style with zero offsets; the plain-value shortcut matched one drawing from
another authoring tool and broke twenty-five that AutoCAD wrote.
AutoCAD prints 0.4255 as 0.426; Rust's formatter rounds the binary value,
which sits a hair below the decimal tie, and printed 0.425. Decimal
dimension text, tolerances and limits now settle the tie the way the
drawings do.
Alternate units multiplied the raw measurement instead of the displayed
DIMLFAC-scaled value, so a paper-space dimension's bracketed value was off
by the viewport factor, and alternate limits were written high/low with a
slash where AutoCAD stacks them like the primary limits.
A limits style whose upper and lower offsets are both zero prints the plain
value in AutoCAD, not a stack of two equal numbers. A three-point angular
dimension whose extension point sits on its vertex has no measurable angle;
AutoCAD keeps showing the angle it stored, and the text now does too instead
of reading 0 degrees. Both cases came out of comparing saved picture blocks
with regenerated text across a set of production drawings.
Dimensions with DIMLIM on were regenerated as the nominal value plus a
separate stacked pair, and the pair was built from the unscaled
measurement. AutoCAD prints only the upper limit over the lower one, with
the offsets applied to the DIMLFAC-scaled, DIMRND-rounded value, and keeps
both halves at the same number of decimals under DIMTZIN. Drawings with a
limits-only style looked right on load, from their saved picture blocks,
and gained an extra number as soon as an associative refresh dropped the
picture.
Fit and fill estimates now count visible cells, so a stack is as wide as
its wider half at DIMTFAC, and the text-to-line offset uses the stacked
height.
Refresh associations after complete SpaceMouse camera updates and recognize imported paper-space owners without Layout objects. Use kernel containment, intersection, perpendicular and spline tangent solving; pin cadkernel 7ce27f2 and add regressions.
DXF gives DIMLFAC a sign convention: a positive value scales every
dimension, a negative one applies |DIMLFAC| only to dimensions that live in
a paper-space layout (where it compensates a viewport's scale). The linear
formatter multiplied by the raw signed value, so a drawing with negative
DIMLFAC (e.g. 115448.dxf) rendered negative measurements.
Resolve the sign once, in `resolved_dimension_style`, the single place an
effective style is derived, so dimension text, alternate units, exploded and
baked text and the properties panel all agree. `extended_data` keeps the raw
signed value, so DXF/DWG export is unchanged. The properties "Measurement"
row now reports the value the text shows rather than the raw distance, while
`actual_measurement` (group 42) stays raw as the format requires.
Also adds the PR2 seams the dimension work builds on:
* MeasurementScale::user_lfac_for_space / viewport_dimlfac_override —
the persisted representation of viewport compensation.
* Scene::viewport_frame and paper-point viewport resolution.
* Model-geometry hit testing through a content viewport, including
descent into block instances with the instance transform baked in.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A linear or aligned dimension whose normal is not world Z was drawn and
exploded as if it lay in world XY. The builders read the offset and the
extension directions in XY only, so a dimension turned into a working plane
such as XZ lost its offset (now along Z): the dimension line fell onto the
measured points and the extension lines stuck out along world Y.
Move such a dimension into its own OCS before building it and map the result
back onto its plane, as arc length and large radial dimensions already do,
for both tessellation and EXPLODE.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With a working plane other than world XY, ROTATE and MIRROR handed a
non-ordinate dimension to acadrust's Entity::apply_transform, which only
translates those dimension types by where the transform sends the origin.
The definition points, plane and angles stayed unturned, so the dimension
jumped back once its block was baked again.
Send both through the existing Affine branch, which ALIGN already uses and
which transforms points, normal and stored angles together. The world-Z
paths are unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ROTATE and MIRROR in the drawing plane moved a linear dimension's points
but left its stored rotation (the axis it measures along) unchanged. A
horizontal dimension rotated 90 degrees then measured its now-vertical
points along X and its value dropped to 0; mirroring about a slanted line
did the same. Polar ARRAY goes through the same path.
Turn the axis by the rotation angle, and reflect it about the mirror line.
A zero-length mirror line still leaves the dimension untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DIMTMOVE=1 ("moves the text and adds a leader") draws the leader from the midpoint
of the dimension line to the text, so a text dragged outside the dimension line gets
a diagonal that starts in the middle of the line and crosses the extension lines.
Mechanical-drawing practice (GB/T 4458.4, and what AutoCAD itself draws) extends the
dimension line along its own axis so that it runs underneath the text. Linear and
aligned dimensions now do that whenever the text is outside the dimension-line extent;
when the text is still inside it (only moved up/down) the previous connect-to-text
leader is kept. Every other dimension type is unchanged, and only dimensions with
DIMTMOVE=1 are affected at all (the shipped default is 0).
Adds four unit tests covering the placements (outside right, outside left, inside,
radius draws none) plus an integration render smoke test driven through the real
Scene::entity_wires() -> export_pdf() pipeline, gated by OCS_DIM_LEADER_PDF so CI
stays side-effect free.
Switching to a Layout re-tessellated the whole model — `memo_miss=186455`,
2034 ms — to arrive at geometry identical to what was already in the memo.
The only thing that differed was colour.
`local_wires_for` already knows this: it stores the raw colour and drops
its background argument with `let _ = bg_color;`, noting that
`Batches::finalize` applies `adapt_to_bg` last, "so the cache no longer
has to rebuild on BACKGROUND / layout-switch". The tessellation memo's
guard had not been told, and still mixed the background in, so every
entry was discarded on a switch.
`finalize` now records what its colour resolution consumed — the raw
colour, the raw glyph colours, and the three flags it branches on — as
`WireModel::bg_adapt`, and `resolve_colors_for_bg` replays it for another
background. The guard drops the background, so a memo entry serves any
layout.
The resolution recomputes from the recorded raw colour rather than from
the resolved one, which makes it idempotent and therefore safe over a set
that mixes memo hits with freshly tessellated misses. Re-adapting the
resolved colour would not be safe: `adapt_to_bg` snaps a near-white colour
to pure black on a light background, and pure black comes back pure
*white* on a dark one, losing the tint. A test pins that asymmetry so the
raw colour is not optimised away later.
The contract is tested against the real `finalize` rather than a copy of
its logic: for every background pair, building under one and resolving for
the other lands exactly where building under the other would have.
`bg_adapt` is `None` for wires built anywhere else, and the resolution
skips them, so nothing outside the block cache changes colour. It is also
`None` when the resolution would be a no-op — a preserved colour with no
canvas fill looks the same under every background — which keeps the common
case at no cost.
Ordering is unchanged: the pass runs inside the builder, before
`append_scene_markers` and `apply_refedit_fade`, both of which have their
own separate background dependency.
`wires-build` gains `colors=` so the pass has a number of its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DIMAUNIT 4 is Surveyor's units, but format_angular_value matched only
0-3 and let 4 fall through the catch-all to decimal degrees. An angular
dimension in a drawing authored with surveyor units therefore read as
plain degrees, while AUNITS 4 in the same drawing writes a bearing for
the coordinate readout — the two disagreed on one angle.
Format the bearing from the dimension's own DMS path so it carries
DIMADEC, DIMAZIN and DIMDSEP, quantise before picking the quadrant so a
value just short of a cardinal does not quote a quarter turn, and offer
the option in the dimension-style dialog.
The Properties panel reads and writes ACAD_DSTYLE overrides for all three
dimension colours (DIMCLRD 176, DIMCLRE 177, DIMCLRT 178), but the
renderer consulted the override only for DIMCLRD and took DIMCLRE/DIMCLRT
straight from the dimension style. Editing 'Text color' on a dimension
therefore showed the new colour in the panel while the drawing kept
rendering the inherited style colour — which for the common
DIMCLRT = ByLayer default is the layer colour. (#898)
Route all three through one helper so the renderer and the panel read the
same value.
format_engineering split feet from inches before rounding to DIMDEC, so a
remainder that rounds up to a full foot printed out of range: 35.999" at
two decimals read 2'-12.00" instead of 3'-0.00". A magnitude that rounded
away to zero also kept a lone minus sign.
Round on the decimal grid first and split afterwards, matching the carry
handling already used by format_architectural and format_fractional.