The orthographic depth half-range was frozen at fit time and measured along
the fit-time eye direction. A 3-D drawing fitted straight-down (top view) got
a shallow depth span; orbiting only rotated the camera, never resized the
span, so the drawing's width loaded onto the eye axis and overran near/far —
clipping the model the moment it was tilted off top (#473).
Cache the model AABB on the camera and recompute the near/far depth from it
for the live eye direction every frame. This tracks orbit, viewcube snaps,
roll and restored views uniformly, and keeps the same tight, zoom-independent
depth precision (coincident solids / meshes / wires keep their draw order).
Falls back to the scalar half-range / distance-scaled span when no AABB is set.
Closes#473
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Companion to the acadrust DXF-fidelity sweep (bump to 379ce12):
- Legacy LEADER hookline: underline the annotation using the leader's own
stored text width (DXF code 41) when the MTEXT has no computed extents,
so the landing spans the text instead of a short stub.
- SHAPE: resolve the glyph by name when the shape number is 0 (DXF SHAPE
carries only the name), and search shape-file text styles when the entity
has no direct style reference.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Selecting a proxy object we render from its cached graphics (an AEC
Wall/Door/Window and the like) showed "Unknown" as its type. The entity
now carries its real DXF class name from the CLASSES section (acadrust
bump), so surface it: ui_name_or_class title-cases the class ("AEC_WALL"
→ "Aec Wall") for the properties Type row and the status-bar label, and
still says "Unknown" only when the class truly can't be named.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Even collected at the right place, a block-internal wipeout still let
geometry show through: the wipeout pipeline's depth bias was positive,
pushing the mask behind geometry at the same depth (a block's wipeout and
its shapes are coincident at Z=0), so LessEqual rejected it. Bias toward
the camera (constant -8, slope -1) so the mask wins against coincident
2-D geometry, small enough that genuinely nearer geometry still occludes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A wipeout defined inside a block never masked the geometry once the block
was inserted — wipeout_models only scanned top-level entities, so the
wipeout was invisible unless you entered block-edit (where it is
top-level). It also drew a stray mask at its raw block-local coordinates
because the top-level scan lacked the block-defn reject the hatch path
uses.
Descend visible inserts and place each block-internal wipeout in world
space, applying the accumulated insert transform via apply /
apply_rotation. Add the belongs_to_visible_block filter so block-defn
wipeouts only appear through the descent, in world space.
Bumps acadrust for the matching transform_wipeout fix: its u/v basis was
double-scaled (apply_rotation then another scale-factor multiply), so a
scaled insert grew the mask; the descent sidesteps that path but the
explode/copy commands need the crate fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two rendering defects surfaced on the budweiser benchmark drawings, both
tied to a drawing's annotation scale.
Annotative scale representations. A drawing may materialise each annotation
scale as its own object (here a block insert per scale, on a "0 @ <scale>"
layer, each carrying a per-object AcDbContextData bound to one scale).
Nothing filtered them, so every scale drew at once — a 1× copy stacked
under a 10× one. Add annotative::annotative_offscale and apply it in the
three paths that expand block content: the wire block-defn cache, the
resident/top-level visibility gate (model space only — paper viewports use
their per-viewport frozen scale layers), and the exploded-insert hatch/fill
walk (filtered before normalize strips the handle). An object is shown when
a representation matches the current scale; the off-scale ones are hidden.
When the current scale is unsupported by any representation, fall back to
the base "1:1" copy instead of hiding everything (the object must not
vanish) or showing everything (the copies must not stack).
Model-space linetype scale. PSLTSCALE (a paper-space flag) was applied to
model-space linetypes via the annotation scale, so a drawing saved at e.g.
CANNOSCALE 10:1 drew its linetypes 10× off. Thread a `paper_space` flag
through tessellate_entity and gate the PSLTSCALE factor on it, so it only
scales linetypes for content shown inside a paper-space viewport; model
space keeps LTSCALE × CELTSCALE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A circle with a complex linetype (embedded shapes/text, e.g. IZOLACE) drew
its dashes and symbols scattered far off the entity at UTM-scale
coordinates. Two defects compounded, both catastrophic-cancellation at a
~1.2M-unit origin:
- The single-entity complex-linetype path fed apply_along the wire's HIGH
double-single half only (`base.points`), an absolute f32 quantised to
~0.1 at 1.2M. Walking the pattern along those points blew the shape /
dash positions up to ~1e308. Reconstruct the absolute f64 path from
high+low, re-origin it at the first vertex, walk apply_along in that
local frame, then lift each result wire back to world with
shift_wire_to_world (now pub(crate)) — the same local-frame trick the
MLINE complex-linetype path already uses. Fixes every entity, not just
circles.
- The circle was tessellated with truck's circle_arc (arc-through-three-
points), whose fit cancels catastrophically at 1.2M and returned a ~3%
radius wobble with uneven segments. Tessellate directly as a cos/sin
polyline (adaptive segment count from the curve tolerance); adding a
small ±r term to the centre stays precise, and the Lines path RTE-splits
it. Arc and polyline bulges share the circle_arc root and remain to fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three defects made a leader's arrowhead render as one stray line far
longer than the leader itself, at UTM coordinates:
- The arrowhead was drawn even when it is larger than the first leader
segment it sits on. A DIMASZ sized for a bigger plot (here 6 units on a
~1-4 unit pointer) painted a stroke longer than the whole leader.
AutoCAD omits the arrowhead when it does not fit the first segment, so
gate the arrow on `arrow_size <= first_segment_len`.
- Leader geometry was built as absolute f32. At UTM coordinates (Y ~ 4.5M,
f32 ulp ~ 0.5) the oblique arrow tick's small cross-slope quantised
away, collapsing the 45-degree tick into a flat horizontal line. Build
the path and arrow in absolute f64 — the arrow around a local origin so
append_arrow's f32 math stays precise — then RTE-split into the double
single (points_low / fill_tris_low) at the end.
- The `_Oblique` / `_Archtick` arrowhead block mapped to ArrowKind::Tick
with the full DIMASZ, but the tick emitter draws `size` to either side
(half-length semantics, matching DIMTSZ). Pass half so an oblique
arrowhead sizes to DIMASZ like every other arrowhead, not double.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR #446 kept a fully-copied group grouped for the in-drawing COPY/ARRAY
path, but clipboard paste (Ctrl+C / Ctrl+V, PASTECLIP) runs through
finalize_paste, a separate route that never touched groups — so pasted
copies came out ungrouped, and a cross-drawing paste had no group data at
all.
Unify all copy routes on one shared Scene::recreate_groups(sources,
handle_map): copy_complete_groups now gathers the live source groups and
delegates to it, while paste snapshots the complete groups into the
clipboard at copy time (ClipboardDeps.groups) and recreates them over the
pasted handles. A group whose whole membership is copied now stays grouped
whether the copy lands in the same drawing, via the clipboard, or in a
different file; partially-selected groups still stay ungrouped.
Closes#440
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`case_insensitive_ttf_resolution` matched `Face::resolve` against Ttf and
Lff only; the SHX shape-font variant added later left the match
non-exhaustive, which failed the whole lib-test build. Add the arm.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The embedded DWG preview was the whole drawing fit to a canvas, encoded
as a 24-bit uncompressed DIB (~180 KB at 256²) — the bulk of the gap
between an OCS-saved file and AutoCAD's. Three changes:
- Frame it like the screen. `from_scene` now projects every wire vertex
through the live camera into a viewport-aspect canvas, so the thumbnail
is the current pan / zoom / rotation and only the visible region, not
the full extent. Off-screen / clipped segments don't draw (a `None`
projection or NaN separator breaks the polyline run). Works for any
view. `stamp_thumbnail` passes the model pane's pixel size and the
target version.
- PNG for R2013+ (AC1027+). A line drawing on a flat background is almost
one colour, so PNG collapses to a few KB. PNG previews are only valid
from R2013, so older targets keep a BMP.
- BMP is now 8-bit palettised, BI_RLE8-compressed (was 24-bit raw). The
long single-colour runs of a preview compress to a handful of KB; a
view with >256 colours (rare) falls back to the 24-bit DIB. A full
256-entry palette is written so the reader's `(1<<bpp)*4` pixel-data
offset lands correctly.
Net: the preview drops from ~120 KB to ~2-5 KB on every version.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PURGE left behind three classes of dead data that AutoCAD removes,
so an OCS-purged drawing stayed far larger than AutoCAD's (a 23 MB test
file purged to 3.5 MB in OCS vs 0.58 MB in AutoCAD):
- Anonymous blocks (`*U`/`*D`/`*T`/dynamic-block variants) were skipped
wholesale. Leftover unreferenced ones now purge like AutoCAD's, held
live only when actually referenced.
- Block liveness now comes from REACHABILITY: a block is kept only if it
is reached from a real layout container by following block references
(INSERT names, dimension *D, table *T / cell blocks, MultiLeader
content). The old flat "named by some INSERT" scan could not free a
mutually-referencing dead subgraph — e.g. the dependent blocks a
detached xref leaves behind, whose head carried a DANGLING layout
handle that `is_layout()` mistook for a real layout and shielded. That
subgraph (here 467k lines) is now purged; live xrefs, reached from the
layout that inserts them, stay. The used-set also gains the by-handle
block references (table/MultiLeader/dimstyle-arrow) so purging
anonymous blocks can't delete an in-use block no INSERT names.
- SortEntitiesTable objects (per-block draw order) were never removed, so
each purged block's table lingered still listing the handle of every
deleted entity — 341k stale handles / ~2.7 MB of dead weight the save
re-emitted. Drop every table whose owning block is gone.
Net on the test file: 3.5 MB -> 0.81 MB (AutoCAD 0.58 MB).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Save-As dialog hard-coded the format dropdown to the latest
(DWG/DXF 2018) regardless of what was opened, so a round-trip silently
re-targeted the version. Default it to the loaded document's own version
and DWG-vs-DXF kind instead (from `document.version` + the file
extension), which always matches one of the dropdown options. A new,
unsaved drawing keeps its document default. QSAVE on an existing file
already preserved the version; only Save-As was wrong.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- image_model.rs: drop unnecessary parentheses around a map's array
literal return.
- text_support.rs: remove dead row_in_column / col_rows computation and
its unused `li` binding (the only consumer).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A dimension bakes its geometry into a per-instance *D block, offset into
the owning block by the dimension insertion_point. When that dimension
sits inside a block (INSERT → block → Dimension → *D), only its top-level
form drew; nested it rendered nothing, and once the lines/text/arrow
outlines were routed through the block cache the arrowhead FILLS were
still missing:
- 2D SOLID arrowheads: block_cache's Dimension arm now expands the *D
block, translating each sub by insertion_point (WCS → block-local) and
routing Insert subs through build_nested_ref, others through
tessellate_sub_local (which emits the solid fill_tris).
- Insert(hatch) arrowheads (custom arrow blocks = LwPolyline + Hatch):
the hatch renders via the separate GPU explosion path, which only
descends INSERT→INSERT→Hatch trees and so never reached a hatch behind
a Dimension. block_has_hatch now recurses through a Dimension into its
*D block, and the fill explosion's new explode_including_dims lifts
each dimension child's *D content into the owning block's frame
(translate insertion_point, then apply the insert transform) so its
arrow inserts / hatches ride the normal walk.
Table *T baked blocks get the mirror treatment in block_cache.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
entity_wires() returns the memoized Arc instead of a per-call deep copy;
MESH entities over 50k vertices ship no per-vertex snap/key tables
(matching PolyfaceMesh/PolygonMesh); the Face3D/other render split
caches a None marker and uses the base set directly when there is no
Face3D wire; test fix for the depth-map BlockCache::build signature.
Reviewed: correctness sound — wire AABB comes from entity_aabb (not
key_vertices) so large meshes stay selectable; snap consumers are
empty-guarded; the Arc/strong_count reasoning for try_resident_patch
holds. One consistent pre-existing quirk noted (auto_fit_viewport
centroid excludes tableless meshes, as Poly*Mesh already do).
The horizontal attachment point was read from the entity-level field,
which only exists from R2010 on; older records (R2007 and earlier) left
it at the default and centre/right-attached leaders shifted. The
annotation context carries the same value in every version, so anchor
from there for both the render and the width-grip geometry.
acadrust bump also brings: the R2007 MULTILEADER tail gating (leaders no
longer render upside-down), the R2004-R2006 inline SAB payload read, the
v1-SAT wireframe point_present decode (3D solids/regions land in place
on R2000/R2004), and the XCLIPFRAME default of 2 so clip-frame borders
draw on files whose header omits the variable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Document linetypes referencing ltypeshp.shx glyphs (ZIGZAG, BATTING,
TRACKS) skipped their shape elements whenever the shape file wasn't on
disk — the usual case for traded drawings. The standard ltypeshp
numbers now bridge to the bundled LFF substitute shapes (130 TRACK1,
131 ZIG, 132 BOX, 133 CIRC1, 134 BAT); a resolvable file still wins
with the real SHX glyph.
The element rotation also converted twice: DWG stores radians, the
segment field carries degrees (the .lin unit), and the walker calls
to_radians() — a 180°-flipped second copy rendered at ~3°, folding the
below-baseline half of ZIGZAG/BATTING up onto the line. Convert at the
document boundary.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wipeout pass ran after the wire pass with no draw-order bias, so
the mask erased its own boundary frame (and everything else at its
plane regardless of draw order). The mask uniform now carries the
entity's draw-order depth (uniform grows 80 → 96 bytes) and biases its
clip-z like every other pipeline, and the frame wire rides a
+half-rank override — the later mask pass fails the depth test at the
frame's pixels, so the boundary stays visible, while entities drawn
above the wipeout are no longer erased.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SPHERICAL/HEMISPHERICAL gradients painted colour 1 at the centre;
AutoCAD's convention is colour 1 at the rim and colour 2 at the centre
(the INV variants swap them back). Both the native and the WebGL2
shader now mix the radial branch outside-in; linear/cylinder/curved
are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Element lines were re-derived from the style's flat offsets along the
vertex miters, but the file already stores the real numbers: each
vertex's per-element parameter list starts with the offset measured
ALONG THE MITER — corner lengthening (1/cos of the half-angle),
justification and scale baked in — followed by draw/gap toggle
distances along the segment, which is how crossing multilines cut
each other. Re-deriving pinched the channel at every corner (the ends
looked flared) and drew full uncut lines through crossings, stacking
intersecting multilines on top of each other.
mline_lines now consumes the stored parameters directly (runs chain
across vertices when continuous; an odd toggle count leaves the last
run open to the segment end) and computes end caps from the actual
per-vertex offsets. Files without parameters keep the style-offset
fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The SHX reader now serves all three consumers instead of just SHAPE
entities:
- Complex-linetype shape elements stopped being skipped: a document
linetype's Shape{number} resolves its style's shape file (shared path
fallbacks) and draws the real glyph; `.lin` catalog references keep
the file token and look the shape up by name. The converted-LFF
substitute set remains the fallback for anything unresolved.
- Face::Shx renders resolvable on-disk .SHX text FONTS with their real
stroke glyphs: character code → shape lookup, cap height normalised
to the 9-unit space every stroke-font consumer expects, advance from
the interpreter's final pen position, line spacing from the shape #0
above/below header, TTF fallback per missing glyph.
resolve_text_style resolves a style's .shx next to the drawing and
passes the absolute path through; unresolvable names keep their LFF
substitutes. Shapes-only files (no #0 header) are refused as fonts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SHAPE entities only drew a diamond placeholder — .SHX files were never
parsed. A new reader (scene/text/shx.rs) decodes the classic
`AutoCAD-86 shapes 1.0/1.1` container and interprets the full shape
bytecode — pen up/down, scale divide/multiply, position push/pop,
subshapes, signed XY runs, octant/fractional/bulge arcs, the 16
direction vectors and the vertical-only skip — into unit-space
polylines, cached per file and per shape number.
The entity resolves its STYLE by handle (name fallback), the style's
shape file with the shared path fallbacks (as stored → relative to the
drawing → basename next to it; traded files carry foreign absolute
paths), then places the glyph with size × relative X scale, oblique
shear and rotation through the OCS mapping. load_file now records
doc.source_path so render-time resolution knows the drawing's folder.
A missing or unreadable shape file keeps the diamond placeholder.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Image textures were created Rgba8UnormSrgb: sampling decoded sRGB to
linear, but the surface isn't sRGB, so every raster (images, OLE, PDF
underlays) rendered visibly dark. Rgba8Unorm passes the texels through
in the same space as every other colour in the app.
The frame wire also always carried the X diagonals — that's the
broken-reference placeholder, and it scribbled over resolvable images.
The diagonals now draw only when the reference doesn't resolve.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PDF underlays only drew their clip outline / insertion cross; the page
itself never appeared. The chain now runs: the definition's file path
resolves with the raster-image fallbacks (as stored, relative to the
drawing, basename next to the drawing — attach-time relative paths go
stale), the page rasterises through the system pdftoppm (poppler) at
150 dpi — no new crate; missing tool degrades to the outline — cached
per (path, page) including negative results, and the pixels feed the
existing image pipeline as an ImageModel: 1 underlay unit = 1 PDF inch
(px/dpi × entity scale), rotated about the insertion, fade as opacity,
the clip polygon (non-inverted) as the textured region. Draw-order,
layer visibility and per-viewport freezes ride the generic image path;
in-place edits rebuild through the shared image seed.
When the page renders, the wire path draws its frame with an interior
pick surface instead of the lone cross, so selection covers the visible
page. wasm keeps the outline placeholder (no external process).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The annotation context is AutoCAD's baked display state; the renderer
now trusts it instead of re-deriving:
- text_attachment_point (L/C/R) is the stored horizontal anchor — the
leader-side guess shifted centre/right-attached texts by their width.
- ctx.text_direction/text_rotation apply as stored; forcing the
Horizontal angle type to 0 un-rotated angled leaders. The conn.y
baseline snap now applies only to unrotated text.
- The dogleg runs along the leader root's stored direction (angled
leaders drew it world-horizontal), and vertically-attached leaders
draw no dogleg at all.
- Underline attachments (BottomLine / BottomOfTopLineUnderline…) draw
the leader-coloured rule under the attached line(s), full text width.
- Vertical text flow (ctx.text_flow_direction) feeds the shared
vertical layout; frame/background boxes wrap the laid-out glyph
bounds padded by ctx.landing_gap, and run-less decoration strokes +
inline \C colours reach the output instead of being dropped.
- The wrap-width grip rides the shared flow_grip_axis() (rotation, flow
and attachment side), and its drag projects on the same axis.
acadrust bump: every MULTILEADER decodes (has_ds peek), MTEXT
top-to-bottom direction reads correctly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Vertical (top-to-bottom) text used to reuse the horizontal line stack —
every line landed on the same baseline and overlapped. The layout now
stacks each character in its own 1.4h cell down the column, one column
per paragraph (1.2h apart), with word-boundary column wrap against the
defined width (which measures COLUMN LENGTH for vertical flow — the
horizontal pre-wrap is disabled there). Justification anchors place the
column block from wrap-accurate metrics: the pre-pass simulates the
render's word wrap exactly, so right/centre/bottom-attached blocks stay
put as columns grow. Layouts also report pre-rotation local_bounds so
frames and fills can box what was actually drawn.
Width grips route through one shared helper, flow_grip_axis(): the flow
axis (rotated baseline, or down the column) plus an attachment-side
factor — a right-attached text forms LEFT of its insertion, so its grip
sits there too; centre attachments use ±half. MTEXT's grip and drag
projection both use it, covering all 9 attachments × both flows × any
rotation from a single table.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The embedded solid display-cache wires are body-local (unplaced), and
R2013+ AcDs-backed solids used to misparse them into garbage points near
the origin — a clipped region rendered a stray fragment at (0,0,0). When
the ACIS payload parses, the mesh pipeline (fill + feature edges +
isolines, all body-placed) is the authoritative render, so the wires now
only serve as a last resort for unreadable ACIS data.
acadrust bump brings the decoded wireframe section and real reference
points: solids/regions report the file's stored bounding-box centre (or
a geometry-derived one) instead of (0,0,0), which feeds OCS grips,
Position properties and insertion snaps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fill triangles (DIMTFILL backgrounds, MLEADER masks, hatches) carry the
entity's draw-order rank, but SDF glyph quads were pushed at neutral 0.0
— any entity in the front half of the draw order had its own background
fill painted over its text. gather_text_verts now adds the host wire's
wire_draw_depth to each glyph: a text background lands at the same
biased depth (glyphs draw after fills and win the LessEqual test), and
text respects draw order against other entities' fills too.
The #403 glyph-reuse fast path gains a rank-stability gate: adding or
removing an entity shifts every sibling's bias (the rank denominator
changes), so reuse is only safe across all-Modified edits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clipped INSERTs cut their content correctly but never showed the
boundary itself. When the header XCLIPFRAME is 1 or 2 (visible), a
closed ring over the world-space clip polygon now rides the insert's
resolved colour and line weight. The frame is deliberately not clipped —
it is the boundary — and carries no snap points.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A constant ATTDEF renders as block content, but with an empty default
value there is nothing to draw — the ATTDEF tessellator would fall back
to its tag-placeholder preview (meant for standalone definitions), so
blocks showed the literal tag text where AutoCAD shows nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DWG stores no "hookline exists" flag (only the direction bit), so every
DWG-loaded leader kept hookline_enabled=false and drew no hook. Derive
it: a text-annotated leader hooks when its final segment leaves the
horizontal by more than 15°, shared by the render and snap/grip paths.
Hook length comes from the dim style like the arrowhead (DIMASZ ×
DIMSCALE), and with DIMTAD "text above" it extends under the annotation
to the far edge of the MTEXT's laid-out extents — newly exposed by
acadrust (lock bumped); the old text-height heuristic stays as the
no-style fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fit_spline_polyline bailed to a straight chord below three fit points,
ignoring the stored begin/end tangents — a short tangent-clamped spline
rendered as a line. The C² solver handles n=2 as-is (both end rows are
clamp or natural conditions), so only the guard drops to n<2; tangentless
two-point splines still solve to the straight chord.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The SAB reader tokenizes an ACIS transform record's matrix rows and
translation as Position triplets, while SAT text yields 13 individual
floats. body_transform() collected only float tokens, so every binary-SAB
solid/region (R2013+ AcDs payloads) lost its placement and rendered at
the body-local origin. Flatten Position tokens into the numeric run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-epoch Face3D/other split cloned every wire into two owned lists
even when the face3d list came out empty — for pure 2-D drawings and
mesh imports (#358) the 'other' list was a wire-for-wire copy of the
entire base set, held resident in split_cache alongside the original.
Cache None as an 'everything is other' marker and hand the render the
base set itself for that frame. The base Arc is deliberately not stored
in the cache: try_resident_patch can only apply an incremental patch
while the resident Arc is uniquely held, so pinning it there would
silently degrade every edit to a full rebuild.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The subdivision MESH converter emitted one snap candidate and one key
vertex per mesh vertex — ~56 B/vertex retained in every copy of the
wire set. On NWD-scale imports (#358: tens of millions of vertices)
that is GBs spent on snap targets far too dense to distinguish on
screen. PolyfaceMesh and PolygonMesh already ship empty tables at any
size.
Above 50,000 vertices, ship none. Smaller meshes (including the #358
repro sample at 34.6k vertices per mesh) keep vertex snap unchanged.
Measured (mem_probe_meshent, 1M-triangle MESH): 646 -> 589 B/face
resident; wire content 210 -> 183 MB per 1M triangles.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
entity_wires() cloned the entire wire set — every point, fill, and snap
buffer of every entity — on each call, although the set is already
memoized behind an Arc and none of the 18 call sites mutates it. On
large mesh imports (#358: 42M faces, ~210 MB of wire content per 1M
triangles) that is a multi-GB allocation + memcpy on every pick, snap,
selection, or plot pass.
Return the Arc instead. Call sites iterate as before; the two
window-plot paths that build a culled owned subset now clone only the
wires inside the plot window, and the print pipeline moves the Arc into
its async task.
Measured (tests/mem_probe_meshent.rs, 1M-triangle MESH, release):
resident after entity_wires() drops 758 -> 547 MB (866 -> 646 B/face).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hatch/wipeout boundary sub-loops were separated by NaN vertices that the
fill shaders detected with `p.x == p.x`. Intel Mesa folds that NaN
self-compare to `true` under fast math, so separators read as real
vertices, the ray-cast crossing count broke, and fills bled outside
their boundary — only on Intel iGPUs. GPU uploads now rewrite the
separators to a finite 1e30 sentinel (GPU_BOUNDARY_SEP) and the shaders
test `abs(x) < 1e29`, which every driver evaluates identically. The
CPU-side NaN representation is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PICKADD 0 = click/box/lasso replaces the selection (Shift toggles),
empty click clears; PICKADD 1 (default) keeps today's accumulate.
PICKDRAG 1 = press-drag spans a rectangle marquee through the box
machinery; PICKDRAG 0 (default) keeps the freeform lasso. Both persist
in settings.json. Also re-points the start-page gate test at the
start_allowed fn it moved into (#388/389 refactor left it broken).
Closes#226
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MATCHPROP copied only the general properties (and text style/height),
so matching dimension-to-dimension or hatch-to-hatch looked like it did
nothing — the visible differences live in type-specific style fields.
General set gains transparency. match_special_props transfers every
STYLE-affecting field the destination supports — never content or
placement:
- Text: style, height, width factor, oblique (style+height cross
TEXT <-> MTEXT); MText adds line spacing, drawing direction and the
background fill block.
- Dimension / Leader / Tolerance: the dim style name crosses all
three, and the ACAD_DSTYLE override record is replicated through
set_entity_xdata (cleared on the destination when the source carries
none, so the match is exact). Restyled dimensions drop their baked
*D block and repaint immediately.
- Hatch: pattern definition, type, angle, scale, solid flag and
gradient — the boundary stays.
- LwPolyline: constant width (vertex widths included) and linetype
generation.
- MultiLeader: style plus all style overrides (path type, line
color/type/weight, landing/dogleg, arrowhead, text style/color/
frame/height, overall scale, override flags); leader geometry, text
and block content stay.
Closes#281
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PEDIT reworked around a mode machine:
- Picking a line/arc asks "Object is not a polyline. Turn it into one?
[Yes/No] <Y>" — Yes converts in place (arc keeps its bulge) and
editing continues on the result. Pickfirst: an already-selected
polyline skips the select step, a selected line/arc goes straight to
the convert prompt.
- Join gathers with the normal selection system (single picks AND
window/crossing boxes) and merges through the JOIN machinery.
- Fit replaces every segment with a classic equal-parameter BIARC: two
arcs per segment meeting tangentially at an inserted knee, both
segments at a vertex sharing that vertex's tangent — the whole run is
tangent-continuous (verified: zero tangent jump on L/closed/zigzag/S
shapes). Spline resamples the vertex frame as a uniform cubic
B-spline (8 samples per span); Decurve straightens every bulge.
- Earlier hardening kept: undo snapshot before the mutation, immediate
repaint, polyline-only pick validation.
- Vertex editing lives on grips, so no Edit-vertex submode; instead the
vertex grip hover menu gains Break — a closed polyline opens at the
vertex, an open one splits into two (interior vertices only),
handled driver-side as an entity replacement.
JOIN itself: segs_of now emits one segment per span of an OPEN
polyline (bulges kept), so polylines merge with their neighbours —
PEDIT Join's set always contains the target polyline and used to abort
every time; plain JOIN had the same gap. JOIN also honours pickfirst:
launched with two or more objects selected it joins them immediately.
Closes#263
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PLAN acted like a ViewCube TOP click the moment it ran — already on Top
meant an immediate flip to Bottom, and the options never appeared. It
now prompts "[Current ucs / Ucs / World] <Current>:" right on
activation and only changes the view once an option runs: Current =
plan of the active UCS, World = world XY, Ucs = prompts for a named UCS
(case-insensitive lookup). The new plan_snap goes straight to the plan
view with no flip-to-opposite cube behaviour.
KeywordCommand gains with_default(): a bare Enter at the verb step
dispatches the default verb (PLAN's <Current>) instead of cancelling.
Closes#326
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every angle row went through the linear formatter, so switching LUNITS
to Architectural showed a block's Rotation as feet-and-inches — and the
string wouldn't parse back, wedging the field until it was cleared and
retyped. Only the decimal/fractional LUNITS survived by accident.
New edit_angle row helper formats angles with format_angle
(AUNITS/AUPREC: decimal degrees, DMS, grads or radians), applied to all
17 angle rows (rotations, obliquing, arc start/end, hatch pattern
angle, multileader text rotation). parse_angle_deg converts every
AUNITS display form back to degrees ("30°"/"30d", DMS 30°15'20",
33.33g, 0.52r) and parse_f64 falls back to it, so the displayed value
round-trips through an edit.
Closes#297
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Holding Shift while a command asks for a point captures the nearest
polar increment (or ortho axis, UCS-aware) from the rubber-band base
toward the cursor and pins the pick to that ray. Every candidate —
osnap hits included — projects onto the locked direction, so a snap far
off-axis contributes only its along-axis component; the click commits
exactly what the preview shows. The direction stays fixed while Shift
is held and drops the moment it releases. Grip drags get the same lock
from the grip origin.
Guards: engages only when the active command wants a point — never
during entity picks (Shift there is the TRIM/EXTEND swap), tangent
picks, selection gathering or window-corner picks.
Closes#312
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The playlist page lists oldest-first; reverse before persisting so the
cached listing and offline launches share the order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The TTT solver's circle-pair difference equation loses its cx/cy terms
when the two circles are CONCENTRIC (a ring + a line through it, the
issue's drawing): lx = ly = 0, the equation only pins r, and the 2x2
center solve hit a zero determinant and returned no candidates.
The degenerate branch now recovers the solution directly: r comes from
the pinning equation (for a ring exactly (R_outer - R_inner) / 2), and
the centers from intersecting the remaining equation — a line in cx/cy
once r is known — with the first circle's distance constraint
|C - P| = |r + eps*cr|. Concentric pairs inside three-circle picks ride
the same branch. Candidate selection is unchanged, so the circle
nearest the three pick points wins.
Closes#318
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>