Commit Graph
257 Commits
Author SHA1 Message Date
Hakan Seven 2c64888048 fix(blocks): preserve UCS transformations
Bake BEDIT frames into block-local geometry while keeping model and viewport UCS isolated. Normalize legacy nonzero block origins during load.
2026-07-30 19:50:33 +03:00
Hakan Seven b8889ccd20 feat(mview): add viewport creation options
Support polygonal and object viewports through native clip-boundary handles, including clipped rendering and selection.

Closes #525
2026-07-27 19:28:51 +03:00
Hakan Seven 4a30928f5a fix(ui): refine empty drawing state
Show drawing defaults when nothing is selected and disable layout controls on Start.

Refs #514
Refs #515
2026-07-27 17:37:17 +03:00
Hakan Seven 759f2316aa fix(hatch): apply pattern property edits
Closes #476
2026-07-27 16:44:31 +03:00
Hakan Seven 6fb68d1c7c feat(scene): integrate DWG object data 2026-07-26 23:38:38 +03:00
Hakan Seven eb23715605 feat(scene): integrate standard DWG data 2026-07-26 22:30:14 +03:00
Hakan Seven 731df4a725 feat(cad): integrate standard codec coverage
Render, edit, query, select, and explode newly decoded standard entity variants. Pin acadifc de8d6b5 and preserve None color semantics.
2026-07-26 19:30:43 +03:00
Hakan Seven 34d0c377e2 fix(3d): complete ACIS solid rendering
- track per-face coverage and retain safe display fallbacks
- tessellate shared NURBS, pcurve trims and periodic surfaces
- pin the matching acadifc decoder revision
2026-07-26 01:35:16 +03:00
Hakan Seven ece5598372 fix: correct viewport stretch and SHX plots
Anchor the opposite viewport corner during grip and STRETCH resizing.
Match plotted stroke-font pen widths to the SDF atlas unless CTB overrides.

Closes #435
2026-07-24 19:26:10 +03:00
Hakan Seven ceddb287f5 fix(renderer): match leader and dash styles
Read canonical ACAD/DSTYLE overrides for leader color and arrow size. Scale block-local linetype patterns through INSERT transforms.

Closes #436
2026-07-24 19:04:57 +03:00
Hakan SevenandClaude Opus 4.8 ce49f4da0a fix(render): DXF leader hookline width + SHAPE name fallback; bump acadrust
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>
2026-07-24 11:30:03 +03:00
Hakan SevenandClaude Opus 4.8 0e61836e91 fix(properties): name AEC / proxy objects instead of "Unknown"
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>
2026-07-23 23:59:27 +03:00
Hakan SevenandClaude Opus 4.8 5e2b67c642 fix(linetype): keep complex linetypes precise at large coordinates
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>
2026-07-23 17:16:57 +03:00
Hakan SevenandClaude Opus 4.8 2d3f33de10 fix(leader): suppress oversized arrowheads, keep them precise at UTM
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>
2026-07-23 16:46:55 +03:00
Hakan SevenandClaude Fable 5 0d4bd274b4 style: clear two build warnings
- 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>
2026-07-23 11:39:23 +03:00
Hakan Seven 1803f10264 Merge PR #442: cut mesh-import wire-set memory ~35% (#358)
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).
2026-07-23 10:25:27 +03:00
Hakan SevenandClaude Fable 5 faa135a7a1 fix(mleader): read the attachment anchor from the annotation context
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>
2026-07-23 08:15:51 +03:00
Hakan SevenandClaude Fable 5 bfcc71678a fix(mline): drive geometry from the stored vertex parameters
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>
2026-07-23 07:00:35 +03:00
Hakan SevenandClaude Fable 5 bcd1fc15ea feat(shx): real SHX glyphs for text fonts and linetype shapes
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>
2026-07-23 06:44:48 +03:00
Hakan SevenandClaude Fable 5 1de4344280 feat(shape): render SHAPE entities from their compiled SHX shape file
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>
2026-07-23 03:53:41 +03:00
Hakan SevenandClaude Fable 5 f4f8c0687c fix(image): stop darkening rasters; X only for broken references
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>
2026-07-23 03:41:52 +03:00
Hakan SevenandClaude Fable 5 1e913e9e78 feat(underlay): render PDF underlays end-to-end
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>
2026-07-23 03:41:41 +03:00
Hakan SevenandClaude Fable 5 ac0a6ac150 fix(mleader): honour the baked context — anchors, rotation, dogleg, frames
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>
2026-07-23 02:58:17 +03:00
Hakan SevenandClaude Fable 5 6e225e068c feat(text): real top-to-bottom layout; shared flow-aware width grips
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>
2026-07-23 02:58:02 +03:00
Hakan SevenandClaude Fable 5 f1c494b81e fix(solid): skip display-cache wires when ACIS parses; real centres
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>
2026-07-23 01:13:43 +03:00
Hakan SevenandClaude Fable 5 bf06a8e330 fix(leader): derive hookline for DWG leaders; size it from the dim style
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>
2026-07-22 23:36:25 +03:00
Hakan SevenandClaude Fable 5 48e8988528 fix(spline): honour end tangents on a two-fit-point spline
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>
2026-07-22 23:36:15 +03:00
Kevin GriffinandClaude Fable 5 5aecdff12e perf(mesh): skip per-vertex snap tables above 50k vertices
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>
2026-07-22 12:59:07 -07:00
Hakan SevenandClaude Fable 5 e7eb84b514 feat(pedit): convert prompt, join, fit/spline/decurve; join fixes (#263)
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>
2026-07-22 20:06:04 +03:00
Hakan SevenandClaude Fable 5 b998082c82 fix(props): angle rows format via AUNITS, not LUNITS (#297)
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>
2026-07-22 16:42:14 +03:00
Hakan SevenandClaude Fable 5 d2be0c9bea fix(pline): Convert to Arc follows the cursor and seats on click (#339)
Three fixes to the polyline arc-segment grip:

- bulge_from_midpoint keyed the sign off which chord side the midpoint
  lies on — the opposite convention of BulgeArc::from_bulge, so every
  grip-drag frame flipped the arc to the other side and the radius
  jumped wildly ("snaps to a restricted range of radii"). It now
  derives sign and sweep from the p0 -> mid -> p1 traversal
  orientation, verified as the exact inverse of BulgeArc's midpoint.

- The arc segment's mid grip dragged in Translate mode, feeding
  per-frame deltas through the recomputed arc midpoint — the drag
  deadlocked the moment the cursor crossed the chord. Arc grips now
  drag in Absolute mode: the arc re-fits through the cursor itself.
  Straight-segment grips keep the Translate stretch.

- Picking Convert to Arc in the grip menu now engages that segment
  grip (the Add Leader placement mechanic), so the new arc follows the
  mouse immediately and the next click seats it.

Closes #339

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 12:15:36 +03:00
Hakan SevenandClaude Fable 5 87049967a2 fix(render): close remaining OCS/WCS gaps for mirrored entities
Follow-up to the LINE/POINT WCS fix - the same bug class elsewhere:

- 2D SOLID rendered its OCS corners raw in both the wire path and the
  shared fill builder (solid_hatch_model); SHAPE placed its marker at
  the raw OCS insertion. Both now map through the arbitrary axis.
- Grips: central wrap in view/dispatch.rs shows planar entities' grips
  at their world position (OCS→WCS) and converts drags back (WCS→OCS
  via the new wcs_point_to_ocs) - per-entity grip code stays in raw
  field space, INSERT keeps handling its own OCS.
- Pulls acadrust 81ebfee: OCS-aware bounding boxes, translate,
  transform and mirror for the ten planar entity types, so MOVE /
  ROTATE / MIRROR and culling work on mirrored entities too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 17:58:09 +03:00
Hakan SevenandClaude Fable 5 eb0073781f fix(render): LINE/POINT endpoints are WCS, not OCS
DXF/DWG store LINE endpoints and POINT locations in world coordinates -
the extrusion normal only orients the thickness sweep / glyph. Running
them through the arbitrary-axis OCS mirrored every line and point
carried over from a MIRROR (normal 0,0,-1) onto the wrong side of the
drawing: bolt shafts of a mirrored detail block landed in the adjacent
panel while its nut arcs (correctly OCS-mapped) stayed put.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 17:27:51 +03:00
Hakan SevenandClaude Fable 5 e177f92ff5 fix(xref): blocks respect their internal draw order
Xref merge now inserts entities in the source block's chain order and
carries SortEntitiesTable objects across (remapped to merged handles).
draw_depth_map values become [depth, half] so exploded block/xref fills
and wide-polyline band wires both compose child depth as
insert_depth + in-block rank x half, recursing into nested inserts with
the same divisor - bands and fills from one block interleave by the
block's internal order instead of stacking at the insert's single depth.
Band batches split per rank via WireModel.depth_override; thin wires
stay merged. The wire arena falls back to a full rebuild when a slab
carries mixed depths so refreshes don't flatten band overrides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:53:28 +03:00
Hakan SevenandClaude Fable 5 1546c1dc30 fix(polyline): spline-fit 2D polylines draw the curve, not the frame
A curve-/spline-fit heavy polyline stores both the fitted curve points
(vertex flag 8 / 1) and the spline-frame control points (flag 16),
which only SPLFRAME is meant to show. Chaining every stored vertex
drew spokes from the curve to the control frame — stray points on the
curve and, on wide polylines, a blobbed band (#408). drawn_vertices2d
now drops the control points whenever fit data is present (falling
back to the stored vertices when it is not) and feeds the wire, the
wide-band fills and the tapered band alike, so snapping no longer
lands on frame points either.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:12:52 +03:00
Hakan SevenandClaude Fable 5 930d8c51ea feat(gradient): selectable gradient types + working creation
GRADIENT silently degraded to a plain hatch: add_hatch rebuilt the
render model from the DXF entity but never encoded the gradient on it,
so the committed fill lost its gradient entirely. The entity now
carries the full gradient record (name, angle, colour stops,
solid-fill flag), and the standard shape set became selectable:
Linear / Cylinder / Spherical / Hemispherical / Curved plus an Invert
toggle, offered as prompt buttons on the GRADIENT command and as a
Gradient type dropdown + Invert box in the properties panel. All nine
standard DXF names round-trip (inverted linear persists by swapping
the colour stops), and the shaders shape each kind distinctly —
mirrored cylinder, eased curve, dome-profiled hemisphere — on both the
native and web pipelines without changing the GPU struct sizes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 12:51:08 +03:00
Hakan SevenandClaude Fable 5 9bfcad18b9 fix(hatch): pattern-scale edits take effect + background on/off
Property edits on a hatch were invisible: fills render from the
prebuilt model cache, which move/copy refreshed but the properties
path never did — so a pattern-scale change (which must also rescale
the stored final pattern lines by the ratio) changed nothing on
screen (#415). invalidate_property_targets now rebuilds the edited
fill's cached model via Scene::refresh_fill_model.

The background row gains an on/off box: off removes the
HATCHBACKGROUNDCOLOR record entirely (the colour could never be
reverted before), on seeds ByLayer. The codec covers the full
colour-method range (ByLayer / ByBlock / RGB / ACI) and the backdrop
resolves ByLayer / ByBlock through the style chain. "More Colors…" on
the background picker now opens the palette window targeting the
background colour — it used to just close the picker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 12:22:15 +03:00
Hakan SevenandClaude Fable 5 411f5396bc feat(pline): snap onto the start point closes the polyline
A PLINE point landing back on the first vertex (endpoint snap) now
closes the polyline instead of stacking a duplicate vertex there —
including an arc closing segment, whose bulge is already computed for
the last-to-first span (#421). Line mode requires two real segments
first so a doubled-back line isn't treated as closed. Setting
Closed: Yes in the properties panel on a polyline whose last vertex
already sits on the first drops the duplicate control point for the
same reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 11:25:33 +03:00
Hakan SevenandClaude Fable 5 8821bb7131 feat(props): uniform-scale checkbox for block references
A selected INSERT's Geometry section gains a "Uniform scale" box
(#427): while the three factors are equal it is checked and the panel
shows a single Scale row that writes all three axes; unchecking
expands the familiar Scale X/Y/Z rows without touching the values
(remembered per entity), and re-checking collapses Y/Z onto X as an
undoable CHPROP step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 10:27:50 +03:00
Hakan SevenandClaude Fable 5 e1a4ddef5e feat(pick): select frame-like entities by clicking inside them
Images, OLE frames, wipeouts, underlays, paper-space viewports and
drawing-view borders were selectable only on their outline. Each now
carries an interior pick surface (pick_tris over its rectangle, or the
triangulated clip polygon for a clipped underlay), ranked below edge
and fill hits so content drawn inside still wins the click. The
layout's own sheet ("overall") viewport is excluded — identified by
the Layout object's viewport link, since DWG files carry viewport
id = 0 and some center the sheet viewport off-origin — or it would
swallow every click over the real viewports beneath it. Drawing-view
borders draw nothing normally; they emit an invisible pick-only wire
whose rect outline appears while selected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 09:28:07 +03:00
Hakan SevenandClaude Opus 4.8 1cfb856224 refactor(dwg): use the first-class SectionSymbol and ViewBorder entities
Section marks and drawing-view borders are now real acadrust entity
types instead of Unknown-with-extras, so the renderer dispatches on the
variant directly, the arrow-direction resolver reads the active viewport
off the ViewBorder entity, and selection / properties show "Section
Symbol" and "View Border" instead of "Unknown". View borders are
non-plotting and tessellate to nothing; both count toward the lossy
Save-As warning since they round-trip via their raw records.

Bumps acadrust for the entity promotion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 00:19:51 +03:00
Hakan SevenandClaude Opus 4.8 fd33810057 fix(attribute): blank INSERT attribute renders nothing, not "[]"
An INSERT attribute left with an empty value drew a "[]" placeholder: the
SingleLine tessellation fell back to `[value]` whenever every line was
empty, but that branch only fires once the value is already empty, so it
always produced bare brackets (and never carried the tag it was meant to
preview). On a heavily scaled block instance the unscaled brackets ballooned
across the sheet.

Render nothing for an empty value instead, matching AutoCAD, where a blank
attribute shows no text. An attribute definition previews its tag as a
non-empty value, so it never reached this branch and is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 18:04:41 +03:00
Hakan SevenandClaude Opus 4.8 b3016ca892 feat(polyline): smooth per-segment taper for varying-width wide polylines
A wide polyline with per-vertex varying widths rendered at its widest edge
(uniform-width ones were already exact). Draw the true taper instead.

`WireModel.taper_widths` carries a per-point full band width. The wire
instance (native slim + web fat) gains per-endpoint `world_hw_a/b`, and both
wire shaders interpolate `mix(hw_a, hw_b, which_end)` so each segment's band
narrows/widens linearly — depth-tested, and the linetype dash still applies.

Alignment (widths must match the tessellated points) is solved by building
both together: a new `TruckObject::TaperedLines(points, widths)`, filled by
`common::tapered_band_points` (straight = endpoints, arc = 16 samples with the
width lerped along it). to_truck routes a varying-width polyline here —
LwPolyline before the PLINEGEN split, Polyline2D at its return — while a
uniform-width one keeps the constant-band Contour path unchanged. `world_width`
stays the widest edge as the PDF-stroke + zoom-floor fallback.

Blocks fall back to the constant-widest band (`LocalWire` carries no
taper_widths).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 16:36:06 +03:00
Hakan SevenandClaude Opus 4.8 a8aece1909 feat(polyline): Polyline2D width band + tube, and block-instance shader bands
Extends the wide-polyline shader band to the two cases the first pass left on
the hatch pipeline.

Polyline2D:
- A flat wide Polyline2D now draws its band from the centre-line wire
  (`world_width`), same as LwPolyline (`polyline_band_width` handles both).
- A thickened wide Polyline2D extrudes a solid 3-D tube. `thick_wide_band`'s
  body moves to a shared `common::thick_band_tube(origin, fills, thickness,
  normal, to_wcs)`; LwPolyline delegates to it and Polyline2D calls it from
  its thickness path. Tube edges read black for both kinds.
- The model-space wide-band hatch loop is deleted (both kinds now wire/tube).

Blocks:
- Block-internal wide polylines draw the shader band too. `LocalWire` carries
  `world_width`; `emit_wire` scales it by the insert transform (average of the
  X/Y axis image lengths) so the band tracks a scaled insert, and the scaled
  width joins the batch `style_key`. `push_block_wide_fills` (the block hatch
  band) is removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 16:04:03 +03:00
Hakan SevenandClaude Opus 4.8 bd1a5affd7 feat(lwpolyline): render the wide-polyline band via the wire shader
A wide polyline's flat band was drawn by the hatch pipeline: a solid fill
with only draw-order depth. It ignored the entity linetype (the width
"crushed" the dash pattern) and showed through 3-D geometry in a tilted view.

Draw the band from the polyline's own centre-line wire instead. A new
`WireModel.world_width` (drawing units) makes the wire vertex shader expand
the quad by `world_half_width / world_per_pixel` (clamped to a hairline), so
the band is sized in world units and tracks zoom. Because it is now a wire:

- the GPU dash shader dashes the band by arc-length — the linetype applies;
- it is depth-tested at the polyline elevation — no draw-order see-through;
- no CPU band triangles or per-dash geometry are generated.

`world_half_width` rides in `WireConst` (native, replacing a pad) and the web
`WireInstance` (loc 14); both wire shaders branch on it. The tessellator sets
`world_width` from the LwPolyline width on the Contour / SegmentedLines arms,
and the model-space fill loop no longer emits a hatch band for any LwPolyline.
Pick still uses the band polygon (`pick_tris`).

Blocks keep the hatch band: `LocalWire` carries no `world_width` (finalize
zeroes it), so a scaled insert still scales its band correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 14:50:27 +03:00
Hakan SevenandClaude Opus 4.8 3d37e1f8d8 fix(thickness): edit persists + wide polyline extrudes as a solid tube
Entering a thickness (DXF 39 extrusion) on a wide polyline now works end
to end:

- The properties Thickness field is a General-group common prop, so it is
  routed to apply_common_prop (set_entity_thickness) instead of the
  geometry setter, which ignored it — the value was silently dropped and
  reverted to 0.
- A wide polyline extrudes its whole band into a 3-D tube (outer + inner
  walls with top and bottom caps), not just its centreline.
- The tube is real 3-D geometry, not a flat draw-order hatch: it renders
  solid in every view mode with true depth, so nothing shows through the
  walls. The flat 2-D band fill is skipped for a thickened polyline.
- A fill whose triangles span depth keeps its real depth (no draw-order
  bias), so the extrusion's own edge wires stay in front of the surface
  and remain visible instead of only the silhouette.
- The corner and cap edge lines read black, framing the coloured tube.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 13:40:12 +03:00
Hakan SevenandClaude Opus 4.8 626e8091f7 refactor(viewport): unify content-viewport clipping on the stencil path
Rectangular paper viewports now go through the same GPU stencil clip as
non-rectangular ones: viewport_clip_boundary_ndc returns the four rect
corners (a full-target mask) instead of nothing, so the render path has
no rect/non-rect special case — every content viewport carries a clip
boundary and draws with stencil reference 0xFF.

With a single clip path, the old per-wire / -image / -wipeout scissor
mechanism is fully dead and removed: compute_*_scissors, the
*_pixel_scissors buffers, the project_scissor helper, the per-pass
set_scissor_rect blocks, the vp_scissor field threaded through
WireModel / ImageModel / HatchModel and their GPU mirrors (WireGpu /
ImageGpu / WipeoutGpu / HatchWebGpu), from_run's scissor parameter, the
scissor grouping in build_wire_buffers, and the is_arena_eligible gate.
Net -245 lines across 54 files.

Rectangular viewports still clip exactly via their own render rectangle;
the stencil mask is a redundant full-target no-op there, kept only so
the clip code path stays uniform. XCLIP keeps its CPU wire clip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:06:28 +03:00
Hakan SevenandClaude Opus 4.8 1b6a3a6f5b feat(mtext): evaluate dynamic text fields at render time
Re-evaluate AcDbField-driven MTEXT — date/login/document-property fields, table
formulas and object properties — against the current system instead of showing
the frozen last-save cache. The evaluation engine lives in `acadrust::fields`;
OCS supplies only the environment (system clock, OS login, env vars) through a
FieldContext implementation, and hooks the resolved text into MTEXT rendering,
falling back to the cached value for anything it can't evaluate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 00:06:58 +03:00
Hakan SevenandClaude Opus 4.8 b30f9b9741 feat(scene): draw nonprint light, camera and geo-location glyphs
Lights, camera views and the geographic-location marker had no on-screen
representation. Add display glyphs for all three:

- Lights render through a new EntityType::Light: a cone/flashlight for spot
  lights, a wire sphere for point lights, a sun for distant lights, sized in
  screen space (fixed-world fallback).
- Camera views (perspective VIEW records) and the GEODATA "daisy" marker are
  synthesized into the model-space resident wire set from the document, since
  they are not entities.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 22:15:40 +03:00
Hakan SevenandClaude Opus 4.8 ef19f898b0 fix(mtext): size the empty-line caret slot to the text, not the entity
The paragraph-start caret slot used the raw entity height, so a blank
line's caret stood entity-tall — many times the visible text when `\H…x;`
runs shrink it (5× on budweiser2018.dwg handle 50019). Size it to the
line's own text height instead (an empty line inherits the previous line's
height, the same rule `per_line_h` already uses for spacing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:24:37 +03:00