Architectural / fractional dimension text split feet / inches / fraction
*before* rounding to the fraction grid, so a value like 35.9999" (float noise
on an exact 3ft object) rounded its fraction up to 16/16, reduced to 1/1, and
leaked out as a bare "1" with no carry — printing 2'-11 1" instead of 3'-0"
(#380). The fix rounds to integer 1/denom ticks first, then decomposes, so the
carry propagates through inches into feet. Fixes#380.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each pipeline slot owned its own gpu_wires: every paper viewport and every
Model tile rebuilt an identical resident wire set into its own GPU vertex
buffers and its own handle index. With N content viewports drawing one shared
tessellation that is N copies of the (potentially huge) wire buffers in VRAM
plus N batch+index builds on every content change — the issue-131 memory
exposure multiplied by the viewport count.
A wgpu::Buffer is internally ref-counted, so sharing a WireGpu by Arc shares
its GPU allocation. Make Pipeline.gpu_wires an Arc<Vec<WireGpu>> (and
wire_handle_index an Arc<map>), turn upload_wires into build_wire_buffers(&self)
that returns those Arcs, and add MultiPipeline.wire_buffer_cache keyed by
wire_content_id. prepare() builds an entry once on a cache miss and hands Arc
clones to every slot with that id — so N viewports (and, since one MultiPipeline
backs all panes, N panes) upload the wire vertices exactly once between them.
Camera uniforms and scissor stay per-slot, so the draw still issues one call per
viewport with the right view; only the geometry is deduplicated. The shared Vec
is never mutated in place — only reassigned on a content change — so the sharing
is sound.
The cache drops entries no slot still references (Arc::strong_count == 1) once it
passes a small bound, so a live buffer (held by any pane) is never evicted.
Model multi-tile gets the same dedup for free, and two panes showing the same
model now share buffers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Model space held a full, camera-independent, un-culled wire set with a
stable content id, so pan/zoom re-drew without re-tessellating or
re-uploading and idle frames collapsed to a single blit. Paper did none of
that: the sheet was camera-keyed (full re-tess every pan/zoom), content
viewports re-baked per zoom step with their own frustum cull + LOD, and a
per-frame wire_force_nonce forced a GPU re-upload every frame AND poisoned
render_signature so paper never hit the single-blit scene cache.
Route every space through one Scene::resident_wires_for(block, anno, frozen):
the full, un-culled, LOD-free set per (block, ambient bg, anno, frozen) key,
held resident and stamped with a stable WIRE_CONTENT_GEN id. Model tiles,
BEDIT, the paper sheet (a thin border-drop + printable-area dressing over the
resident base) and paper content viewports all share it. When annotation is
inert (PSLTSCALE off and no annotative entity, cached per epoch) the per-
viewport scale is normalized away so every viewport reuses one tessellation.
Result: paper pan/zoom does zero re-tessellation and zero wire re-upload, and
paper now hits the single-blit scene cache exactly like model. Deletes
wire_force_nonce, the per-viewport height/view wire cache, and view_cull_aabb.
Tradeoff (deliberate, matches model): no cull/LOD anywhere.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 3ft object measuring 35.9999" (float noise) printed "2'-11 1"" because the
architectural and fractional formatters split feet/inches/fraction *before*
rounding. A fraction that rounded up to a full unit (16/16 -> 1/1) reduced to a
bare "1" and leaked out with no carry into the inches or feet.
Round on the fraction grid first using integer ticks, then decompose, so carries
propagate: 35.99" now reads 3'-0" instead of 2'-11 1". Adds regression tests.
Fixes#380
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up the DXF fix that reads DIMENSION group code 2, so dimensions
imported from DXF keep their baked *D block and render (their text no
longer drifts vs the DWG of the same drawing).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up the DXF over-annotative MULTILEADER fix (companion to the MTEXT
one): enable_annotation_scale now defaults false and DXF reads code 293.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up the DXF over-annotative-MTEXT fix: acadrust now defaults
MTEXT.is_annotative to false, so imported DXF MTEXT is no longer flagged
annotative and over-scaled in annotation-scaled viewports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On native the button opens the OS save dialog (folder + name + overwrite)
and on web it starts the download, so it does not itself save — "Continue"
reads truer than "Save".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Selecting a block and running BEDIT (command, ribbon button, or a
double-click on the block) opens a block-editor "space" tab, named after
the block, alongside Model/Layout in the status bar. The tab draws ONLY
the block record's own entities in block-local coordinates; edits are
live on the definition, so every INSERT updates. Save Block / Discard
buttons (side toolbar) finish the session; Discard restores the on-entry
snapshot. Space switching is locked while a block is open, and BEDIT and
REFEDIT are mutually exclusive.
Implementation: a Scene.block_edit_block override at the top of
current_layout_block_handle scopes render/pick/new-entity ownership to the
edited block while current_layout stays "Model" (so the pervasive
Model-vs-paper code keeps model behaviour). The model render/pick paths
that hardcoded model_space_block_handle (wire tile set, hatch render +
pick, block-instance meshes) are made binding-aware so the block isolation
actually takes effect and model content does not leak in. Entering fits
the camera to the block (fit_all already scopes via the binding). Renaming
the block tab renames the block itself — new Scene::rename_block re-keys
the record, updates the Block marker and repoints every INSERT reference.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WebGL2 has no vertex/fragment storage buffers, so the batched storage-buffer
hatch renderer (hatch_gpu) is disabled on wasm and real hatch fills never
rendered on the web build — the pick highlighted the boundary and the command
then appeared to cancel (issue #204).
Add a wasm-only per-hatch renderer (hatch_web_gpu + hatch_web.wgsl) that packs
the variable-length boundary / family / dash arrays into one RGBA32F data
texture read with textureLoad, instead of the fixed-size uniforms of the
wipeout path. This removes the MAX_FAMILIES (16) / MAX_HATCH_BOUNDARY_VERTS
(1024) / MAX_DASHES (128) caps, so every hatch — solid, gradient, and
arbitrarily complex line patterns — renders on the web. The fill algorithm
(in_polygon, per-family line/dash/dot, gradient, LOD, double-single vertex
transform) is identical to the desktop path; only the array ACCESS changes.
Pattern phase is anchored at world_origin with the boundary stored raw, and
scale / gradient-range clamped, to match the desktop batched renderer exactly.
Native (hatch_gpu + hatch.wgsl) and the wipeout mask renderer are untouched;
the new module and pipeline are gated on target_arch = "wasm32".
Also silence the resulting (and pre-existing) wasm-only dead-code warnings for
native-only paths so both the native and web builds compile clean.
Closes#204.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dashed/dash-dot lines could end in a gap ("empty space at the end"),
especially inside paperspace viewports. The dash pattern was only centred
on each wire, which makes the two ends symmetric but does not force them
onto a solid dash.
Implement AutoCAD "A" (aligned) linetypes: the line begins AND ends on a
solid dash, interior dashes stay nominal, and the two end dashes stretch
symmetrically to absorb the remainder (so parallel lines share an
identical interior). wire_distances computes the end-dash length D and
total length L for a dash-first pattern (D = (L - k*P + a) / 2); the GPU
in_dash walk force-lights the two end regions and phases the interior to
resume right after the start dash. Lines shorter than one pattern period
draw solid.
Only alternating dash-first patterns are aligned; gap/dot-first,
consecutive-dash, NaN-break (plinegen=false) and solid lines keep the
prior behaviour. Native and web (wasm) shaders kept in sync.
Refs #13.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make objects genuinely annotative — carrying real per-scale
representations that interoperate with other CAD apps — rather than just
a native flag. `create_annotation_context` synthesizes the extension-dict
chain (AcDbContextDataManager -> ACDB_ANNOTATIONSCALES -> per-scale leaf)
from the entity's placement; the editable Annotative toggle now covers
Text and block references as well as MText/MLeader; and a new
"Annotation Object Scale" dialog (OBJECTSCALE) adds/removes an object's
per-scale memberships. Requires the acadrust ObjectContextData encoder
(Cargo.lock bump).
Also fixes a render bug where objects carrying an *empty*
ACDB_ANNOTATIONSCALES (a single-representation marker with no per-scale
reps) were treated as annotative and (mis)scaled by the annotation factor
in scaled paper viewports, ballooning the text: an object is now
annotative-by-context only when its scale collection is non-empty, so
such objects render at their base geometry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pipeline slots are addressed by list index, but the renderer drops
off-canvas viewports from the list, so a slot can be reused by a
different viewport across frames — e.g. when the first paper viewport
scrolls off the canvas the second slides into its slot and inherits its
stale, differently frustum-culled GPU buffers. The text upload is gated
on a geometry-epoch change (unaffected by the index shift), so the
survivor rendered the gone viewport's text and its own vanished.
Give each viewport a stable identity, remember which one last used a
slot, and reset all cache keys when the occupant changes so wires, text,
hatches, meshes and Face3D re-upload for the new viewport. No cost in the
steady state — the reset fires only when a viewport enters/leaves view.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to #374. resolve_hatch_rings flagged every outline nested in
the fill ring as a hole, so with 3+ nesting levels a click in an outer
band added grandchild islands too — even-odd fill then re-filled the
innermost shape. Skip a candidate when another outline sits strictly
between it and the fill ring, so only the immediate next level holes out.
Closes#377.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HATCH/GRADIENT/BOUNDARY pick-inside now resolves the click against every
enclosing outline, fills the smallest-area one, and emits inner outlines as
NaN-separated holes. DXF export writes one boundary path per ring.
Co-authored-by: KarimJerbi <KarimJerbi@users.noreply.github.com>
The "Dim line color" row on Leader and Dimension entities was
read-only: an earlier attempt wired it to Leader.override_color, which
acadrust never serialises, so the pick was lost on save. Store it
instead as a standard ACAD_DSTYLE per-object dimension-style override
(DXF code 176, an ACI index) through the existing dim_override codec,
so it round-trips through both DWG and DXF like the other dim
overrides. RGB picks collapse to the nearest ACI, matching the rest of
the dim-colour stack (dimension styles are index-only through the file
layer). The renderer prefers the override over the style's DIMCLRD.
The write branch is guarded to leaders and dimensions so a mixed
selection cannot stamp the override onto other entity types.
Bumps acadrust to c9fe982, which carries parsed XDATA onto dimensions
on DXF read (it was dropping common.extended_data), so the dimension
override persists on DXF save as well as DWG.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Some DWGs written by other programs leave the root named-objects
dictionary unrecoverable: the header pointer names a non-dictionary and
the real NOD child dicts are owned by a table-control handle, so nothing
has owner==NULL. Navigating the root then silently no-ops, and
registering a new named-object entry (page setup, annotation scale, CTAB)
against the missing root orphans it — it never round-trips.
Add shared annotative::root_named_dict_handle(): resolve the header
pointer, else adopt the richest owner==NULL dict, else synthesise a fresh
root and write it back. Route ensure_plotsettings_dict, add_scale's
scalelist-create branch, and set_saved_active_layout through it. Resolve
the plot-settings dict via the PlotSettings owner when the header pointer
is stale, mirroring scalelist_dict_handle.
Verified page setups survive a DWG save/reload on the repro file with
layouts intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Double-click rename did nothing and ratio edits were lost when switching
scales — and on drawings saved by other programs nothing could be edited
at all.
- Fallback scales were virtual (no Scale objects), so edit/rename no-oped;
materialize the standard set into real staged objects when the manager
opens, reverted on close unless applied.
- On third-party DWGs the mutators walked the root named-objects dict ->
ACAD_SCALELIST, which can be unreachable, and matched dictionary keys
that are internal ids (*A1, A0) rather than scale names. Locate the
scale-list dict robustly and match scales by object name + handle.
- Rename rows inline via double-click; repair a dangling current-scale
header pointer on staged rollback.
- Stage text/mleader style-manager edits on switch so they persist like
the scale manager.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The scale manager now runs as a transaction, matching the style managers'
staging: New starts a blank editor draft (nothing is added to the drawing
until Apply), Delete removes immediately but staged, and closing the window
without Apply restores the snapshot taken on open — so a scale created or
deleted mid-session is discarded on cancel. Apply commits the staged list
changes as a single undo entry. This replaces the earlier behaviour where
New copied the selected scale and edits landed immediately.
A `changed` flag guards the commit so a no-op / failed Apply (blank or
invalid fields, or an edit that didn't apply) no longer dirties the drawing
or pushes a phantom undo entry. Set Current is deliberately outside the
transaction — it takes effect immediately like the scale pill (and syncs
CANNOSCALEVALUE), so it is never rolled back on close. Renaming the current
scale follows the pointer; editing one of the built-in fallback scales (shown
when the drawing has no scale list) materialises it as a real scale; the
snapshot restore touches only the ACAD_SCALELIST root entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the scale popup's inline delete/add affordances with a single
"Manage scales..." row that opens a proper manager, matching the style
managers: it reuses the shared style-manager palette and chrome (toolbar +
list + editor frame), so it looks and behaves like the Text / Dim / MLeader
managers.
The manager lists the drawing's annotation scales (name + ratio, the current
one checked) and edits them through a Name / Paper units / Drawing units
editor: New adds a scale from the fields, Apply renames / re-ratios the
selected one, Delete removes it (never the current scale), and Set Current
makes the selected scale the model-space annotation scale. Backed by
Scene::add_scale / remove_scale / edit_scale on ACAD_SCALELIST.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The drawing's annotation-scale list (ACAD_SCALELIST) is now editable, so the
set of scales offered by the status-bar scale pill can be grown or trimmed:
- Scene::add_scale / remove_scale create or drop a Scale object and its
SCALELIST dictionary entry (creating the SCALELIST dictionary, and
registering it in the root, for drawings that carry none).
- SCALELISTEDIT gains ADD <ratio> / DELETE <name> alongside the existing
listing (e.g. `SCALELISTEDIT ADD 1:50`).
- The scale popup (model space) grows a delete affordance on each non-active
scale and a "+ Add scale..." row that primes the command line. The current
annotation scale is protected from deletion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Annotative row becomes an editable toggle for the two entity types that
carry a native per-object annotative flag — MTEXT (is_annotative) and
MULTILEADER (enable_annotation_scale). Turning it on flags the object
annotative (it then scales with the current annotation scale in model space);
turning it off clears the flag AND removes the per-object annotation context
(the AcDbContextDataManager -> ACDB_ANNOTATIONSCALES leaf subtree) plus the
legacy annotative XDATA markers, via scene::annotative::set_entity_annotative /
clear_annotation_context, so the object stops resolving annotative instead of
staying on through a stale context. The shared SCALE objects are left intact.
Style-derived types (text/dimension/leader/table/insert) keep a read-only
Annotative row — their state comes from the assigned style, and making a single
object annotative there needs a per-object context object (a dedicated
acadrust ObjectContextData writer) which is out of scope here. Verified an
existing annotative DWG round-trips its 134 per-object contexts through a
save/reload unchanged, so the far more common open-and-save path is unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When writing a hatched region with holes to DXF/DWG, every NaN-separated ring was created with BoundaryPath::external(), so consumers treated inner loops as outer islands instead of holes. Only the first (outer) ring should carry the external / outermost flags; hole rings are left unflagged. A picked big-minus-small hatch now persists with the small rectangle as a real hole.
Adds a regression test asserting only the first boundary path is flagged external.
The HATCH/GRADIENT/BOUNDARY "pick inside" mode returned the first outline containing the click point, so the result depended on draw order and any nested boundary was ignored. For a small rectangle inside a big one, clicking inside the small shape would hatch the whole big rectangle (or the small one, depending on which was drawn first), and clicking in the gap between them always hatched the full big rectangle with no hole.
Resolve the click against every enclosing outline and choose the smallest-area one as the fill. Any other outline fully contained by that ring but not containing the click point becomes a hole. The boundary is emitted as NaN-separated rings, which the GPU and CPU rasteriser already fill via even-odd, so pattern, solid, and gradient hatches all render the correct region (the small shape, or the big-minus-small ring) regardless of draw order. DXF export now writes one boundary path per ring so nested hatches persist with real holes.
The Columns row becomes a No columns / Static / Dynamic picker, with
editable column count / width / gutter rows that are live only when
columns are on. Switching a plain MText to columns seeds a consistent
count / width / gutter (dynamic defaults to auto-height) so the stored
layout isn't half-defined; the rows then refine it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pulls the acadrust fix that emits XDATA for every entity on DXF save (not
just hatches), so the leader dimension overrides and hyperlinks now
round-trip through DXF as well as DWG.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A legacy leader's Lines & Arrows / Text / Fit rows are now editable
per-object overrides of its dimension style, stored in the standard
ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow
block, arrow size, dim-line lineweight, text offset, vertical text
position and overall scale each prefer an override over the style; the
renderer honours arrow size / block / overall scale and the dim-line
lineweight, so an edited leader redraws at its new arrow and weight.
XDATA edits go through a new dispatch::set_entity_xdata, which registers
the application in the APPID table (the DWG writer drops records for an
unregistered app) and drops that app's stale verbatim EED block (which
otherwise wins over the structured record on a DWG save), so edits and
clears round-trip. The hyperlink editor moves onto this path too.
Dim-line colour stays read-only: it would live in the leader's
override_color, which the file layer doesn't serialise, so making it
editable would silently lose the pick on save. Picking "Closed filled"
writes an explicit null-handle arrow override rather than clearing, so
it sticks even when the style's arrow differs; an unparseable numeric
entry is ignored instead of wiping the stored override.
Note: the DXF entity writer only emits XDATA for hatches, so these
overrides (and hyperlinks) currently persist on DWG save but not DXF —
an acadrust-side gap to close separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The General-group Hyperlink row becomes an editable text field; committing a
value writes the standard PE_URL XDATA record (an empty value clears it). Adds
scene::view::dispatch::set_common_xdata, which rebuilds an entity's XDATA to
replace or remove a single application record — ExtendedData only appends, so a
plain add_record would leave a stale duplicate the reader would pick first.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Render: annotative-ness is resolved centrally in scene::annotative::is_annotative
(per-object context dictionary, legacy XDATA, or annotative style) so the bake and
the properties panel agree. Text, dimensions, tables and blocks now display at the
current annotation scale in model space. An annotative block scales as one uniform
unit about its insertion point — its internal geometry and attributes are carried by
that scale instead of being scaled individually, fixing the block-attribute size
regression. Tables scale their column/row/margin geometry.
Properties: the handle- and flag-backed rows are now editable pickers — MLEADER
multileader style / text style / arrowhead / leader linetype, General Material, and
Plot style (named plot-style mode only; the color-dependent mode stays read-only,
which is correct). A picked name is resolved back to its handle in the update loop,
where the document is in scope. Annotative Yes/No and the annotation-scale row are
shown per object.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The UCS Icon, Properties, File Tabs, and Layout Tabs buttons were highlighted unconditionally because their ids fell through to an active_tool equality check in is_active_tool, which activate_tool sets on every click and never clears. Each id is now mapped to its corresponding visibility flag (show_ucs_icon, show_properties, show_file_tabs, show_layout_tabs), and those flags are kept in sync from their toggle handlers so the highlight reflects live state. The per-toggle booleans previously threaded through the render path are consolidated into a single ToggleState struct read from one place, so introducing a further toggle no longer requires changing every render-function signature and call site.
Picks up the DWG reader reflecting CANNOSCALE from the AcDbVariableDictionary
into the header, so the status-bar annotation-scale pill shows the drawing's
real model-space scale (e.g. 1:70) instead of the "1:1" default.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve, per selected entity, whether it is annotative (from its text /
dimension / multileader style, or its own flag) and — when annotative — the
name(s) of the annotation scale(s) applied to it, shown as a read-only
"Annotative scale" row (Text / MText / Leader / MLeader).
The applied scale is read from the entity's per-scale object-context leaves
via the new acadrust CadDocument.context_scales map (context leaf ->
AcDbScale handle), walked from the entity's extension dictionary
(AcDbContextDataManager -> ACDB_ANNOTATIONSCALES). Bumps acadrust to pick up
that parsing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Match the entity Properties panels to the standard CAD layout and
web-verified behaviour:
- Text: combined Justify (horizontal x vertical) dropdown; Position and
Text-alignment rows become editable/grayed by justification; drop the
Annotative-scale row.
- MText: combined Justify; editable Direction, Line space distance
(back-solves the factor) and Line space style; Defined width/height
gated on column mode.
- MLeader: regrouped to match the palette - drop the Leader Structure
group (fold landing rows into Leaders), gate Text vs Block on content
type, full 9-value attachment label set, resolve handle-backed rows
(style, text style, arrowhead, linetype) to names.
- Leader: Lines & Arrows / Text / Fit are resolved from the dimension
style (arrowhead, arrow size, dim-line lineweight/colour, text gap,
vertical text pos, overall scale); Current Vertex navigator; combined
Type; Geometry group after General; no 3D-Visualisation group.
- General: rename "LT Scale" to "Linetype scale"; show ByLayer
transparency by name. Add shared num_prop helper for conditionally
editable numeric rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Render MultiLeader labels from the stored context fields instead of
re-deriving them:
- Draw each leader line with the line's own path type (code 170) rather
than the MultiLeader's overall/style type. A spline-style leader can
carry straight lines; splining a straight two-point run through
Catmull-Rom bowed it, so the leader looked wavy with a dirty elbow.
- Drop the stray landing segment that ran from the dogleg end up to the
text's top-left insertion, leaving just the horizontal dogleg.
- Anchor the horizontal-leader text so its bottom line sits at the
connection point (the box then spans connection.y → text_location.y,
matching the writer's content_base_point + landing_gap geometry).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pulls acadrust 4ac3d68: entities with proxy/preview vector graphics
(MULTILEADER, WIPEOUT) omit the R2013+ has_ds_data bit, so reading it
desynced the record and produced garbage MultiLeader text_location
(~2e39 → NaN glyphs). The labels now parse with finite locations and
render through the existing MText SDF path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Text and dimensions vanished on a text-heavy, multi-font DWG. The global
SDF glyph atlas was a fixed 1024² page shared across drawings; once full,
pack() returned None and every further (font, char, bold) glyph was
silently skipped — so late-baked glyphs (dimension text especially) never
rendered.
Initialise the atlas at 2048² (covers real multi-font/CJK drawings with
no growth, so no stale UVs) and add a grow_height() fallback that doubles
height, appends zeroed rows, rescales cached entries' V/solid_uv and marks
the texture dirty (up to MAX_ATLAS_PX = 8192) rather than dropping glyphs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Saving from a non-first paper layout still reopened on the first paper
tab: set_saved_active_layout only updated an existing CTAB entry, but
documents authored here never carried one, so CTAB was never written and
the reader fell back to $TILEMODE (which only records model-vs-paper) →
the first paper layout.
set_saved_active_layout now creates the CTAB DICTIONARYVAR under the root
named-object dictionary when it is absent (updating in place otherwise).
The root dictionary is taken from the header handle, or found by scanning
for the dictionary that holds ACAD_LAYOUT when a from-scratch document
has not populated the header handle yet. The writers persist it (they
serialize the document's objects and root-dictionary entries; the
root-dict rebuild in CadDocument::build() is not run on save).
Adds tests/active_space_roundtrip.rs, including a full DXF save→reload
round-trip asserting both $TILEMODE and CTAB survive.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files saved while a paper layout was active reopened in Model space
because the loader hardcoded the current layout to "Model" and never
consulted the space recorded in the file.
The active space is a standard part of the format and acadrust already
round-trips it: $TILEMODE (header.show_model_space) for model-vs-paper,
and the CTAB current-tab variable for the exact layout name.
- Read: on open, pick the current layout from CTAB (when it names a
layout that exists), else $TILEMODE (Model, or the first paper layout).
- Write: Scene::sync_active_space_to_document() mirrors the active layout
back into the document (show_model_space + CTAB). It runs from
set_current_layout, so every active-layout change — the status-bar tab,
the Layout Manager's Set Current, create / rename / delete, and
undo/redo — keeps the document in sync for the next save.
$TILEMODE is the guaranteed mechanism (read and written for both DXF and
DWG); CTAB refines the exact tab when the variable is present.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
manage/mod.rs never declared `mod mview`, so this file was never
compiled. It also carried a stale f32 `on_point` signature predating
the DVec3 command trait. The live MVIEW command is layout/mview.rs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the f64 migration on the CPU data path: the only f32 left now
is the GPU/render/screen layer, which is architecturally required (wgpu
vertex buffers and iced are f32; f64 vertices would double memory).
- EntityTransform::Rotate.angle_rad and Scale.factor are now f64 (the
transform was already applied in f64 around the DVec3 center, so this
only preserves the typed angle/factor). apply_standard_transform and
apply_standard_scale take f64.
- CmdResult::ExtrudeEntity.height and AlignSelected.{angle_rad,scale}
are f64; the EXTRUDE height parses as f64.
- defaults.rs (last-used construction values: circle radius/diameter,
rotate angle, scale factor, offset/fillet/chamfer distance, array
spacing/counts/angle) moved from Cell<f32> to Cell<f64>. Array counts
round-trip through u32 (exact below 2^53).
- Consumers in circle/array/fillet/offset/rotate/scale/align/solid3d and
dimension.rs rotate_point/scale_point follow. `as f32` is kept only
where a value feeds a WireModel rubber-band/ghost (the preview path is
screen-space f32).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three commit helpers narrowed a typed/picked point back to f32 before it
reached the persisted entity, so the coordinate was quantized to the f32
grid (worse at large/UTM extents):
- Scene::create_block_from_entities took the base point as glam::Vec3, so
the block's local-space translation and the replacement INSERT's
insert point were f32. Param is now DVec3 (matching the sibling
define_block_from_owned_entities); the command_driver handler no longer
downcasts with as_vec3().
- The MTEXT and TEXT in-place editors stored `pos` as Vec3 and built the
committed insertion_point from it, quantizing a new label's position
and re-quantizing an existing one on edit. Both `pos` fields are now
DVec3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The HATCH/GRADIENT/BOUNDARY commands built the persisted hatch through
the render-side boundary (f32 offsets from world_origin), and with a
zero world_origin that meant absolute f32 - so a typed boundary vertex
of 2000.8 was stored as 2000.8020, and at UTM scale the fill landed far
from its boundary.
HatchModel.boundary stays f32 (the GPU render contract). Added
HatchModel.boundary_wcs: Option<Arc<Vec<[f64;2]>>>, an exact absolute-WCS
boundary set only by the draw commands. Scene::add_hatch persists from
boundary_wcs when present (exact f64 -> DxfHatch), else falls back to the
f32 + world_origin reconstruction used by DXF-rebuilt hatches. manual_pts
is now Vec<DVec3>; make_hatch takes the f64 boundary, stores the exact
copy and derives the RTE render rep. The committed render model is
rebuilt from the DXF entity, so nothing renders the command model's f32
boundary. This also fixes command hatches mis-placing at large
coordinates (world_origin was [0,0]).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewrite the in-app MTEXT editor to run on acadrust's structured
MTextDocument model end-to-end: load via parse_mtext, edit a flat Cell
model, serialize via to_mtext_string. Retires the hand-rolled format-code
string handling, the parallel visible_spans parser, the composed_value
prefix, and the Edit-raw-code tab.
Selection and copy:
- double-click selects the word, triple-click selects all (click-counting)
- Ctrl+A selects all text
- Ctrl+C copies the selected text to the clipboard
Formatting:
- per-selection Bold/Italic/Underline/Overline/Strike/Case/Alignment
- Bold renders as a visibly thicker SDF pen, keyed (family, char, bold)
through the whole text pipeline; the resolved style font name is stamped
so the \f...|b1; code survives serialization
- per-selection font and color changes
- editor color dropdown reuses the Properties panel color selector
Global toolbar defaults (font/color/oblique/width/tracking) fold onto the
document as real span properties instead of a leading code prefix; a
trailing empty paragraph is preserved so Enter at the end keeps its line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the editor's body edit ops (typing, Enter, backspace, delete, paste,
selection replace/delete) off the raw-string + visible_spans splice and
onto acadrust's structured MTextDocument.
The document is flattened to a Vec<Cell> (one cell per visible slot: a
char, a paragraph break, or an atomic stacking-run glyph), spliced/drained
by flat visible index, and rebuilt into paragraphs+spans — so Enter splits
a paragraph, Backspace/Delete at a boundary merges, cross-paragraph
selection delete works, and stacking fractions stay atomic. rebuild is the
single doc-sync point (re-parse + restore the trailing empty paragraph that
parse_mtext drops, else Enter at the end loses the new line).
Also: the caret on a fresh trailing line — which the layout emits no glyph
box for — is made visible by injecting a space into the preview value only
(never committed) so the layout lays that line out and the caret gets a
correctly-sized box on it.
visible_spans now serves only the formatting (\L/\Q/…) splice path, which
moves to structured properties in the next phase. Adds 9 unit tests for the
cell model + round-trip/trailing-break handling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First phase of moving the in-app MTEXT editor onto acadrust's structured
MTEXT model. Adds a `doc: MTextDocument` field kept in sync with the raw
content via `parse_mtext` on every preview rebuild, plus `VisSlot` and
`doc_vis_map` — the unified visible-index space that will replace the
hand-rolled `visible_spans` walker (it matches the layout's vis counting:
one caret slot per paragraph break, one per visible char, none for a tab).
Purely additive scaffolding: nothing consumes the doc yet, so editor
behaviour is unchanged. Later phases make the doc authoritative for edits,
formatting, commit and load, then delete the raw-string path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two defects in the in-app MTEXT editor, both surfaced once the preview
text became visible:
1. Invisible preview text. After the SDF text migration, tessellate emits
glyphs as SDF quads on `text_verts` and leaves the polyline `points`
empty, but the editor preview draws on a 2D canvas that can't run the
SDF shader — so only the selection box showed. Add a `force_text_strokes`
parameter to tessellate: when set, TEXT/MTEXT run-groups also emit their
glyph outline strokes as points. The preview passes true (real stroke
font, aligned with glyph_boxes); every other caller passes false and
keeps SDF-only text.
2. Enter did not start a new line. mtext_type inserted a literal newline,
but the MTEXT layout only breaks on the `\P` code (a raw 0x0A is a break
only to the caret logic). Normalise every literal newline — from Enter or
a multi-line paste — to `\P`, so the line breaks and the saved value
stays standard.
Closes#308
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>