Commit Graph
101 Commits
Author SHA1 Message Date
Hakan Seven 25515fa33c feat(styles): overhaul style managers
Unify editor layouts, live previews, comparisons, read-only behavior, and localized controls across all named style managers.

Refs #629
2026-08-03 02:01:47 +03:00
Hakan Seven 20c6d419e2 fix(dimensions): render custom arrow blocks
Refs #612
2026-08-02 16:06:37 +03:00
Hakan Seven e03e091b3c feat(annotation): integrate object contexts 2026-08-02 12:00:15 +03:00
Hakan Seven 5909540ebf fix(annotation): apply object context scaling 2026-08-02 03:02:11 +03:00
Hakan Seven 0c58bdd55a fix(dgn): use typed stroke patterns 2026-07-31 18:07:35 +03:00
Hakan Seven 29b15535d7 fix(render): preserve shared curve endpoints
Use stored topology vertices at tessellated curve boundaries so floating-point reevaluation cannot open gaps at extreme zoom.

Closes #561
2026-07-29 21:01:40 +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 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 bca65600cd fix: stabilize web loading and paper rendering 2026-07-25 21:34:29 +03:00
Hakan Seven 12bf126646 perf: remove large drawing stalls
Keep resident geometry and GPU resources stable across edits, MSPACE zoom, and Model/Paper switches. Add unified PERF output for tracing remaining costs.
2026-07-25 13:34:48 +03:00
Hakan Seven 7a1c0ead0d fix(xref): preserve layer-zero colors
Treat namespaced XREF layer |0 as layer zero during block inheritance so ByLayer geometry keeps its source color.\n\nCloses #437
2026-07-24 18:43:04 +03:00
Hakan SevenandClaude Opus 4.8 d54fb9e2aa fix(render): annotative scale representations + model-space linetype scale
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 <[email protected]>
2026-07-23 21:24:56 +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 <[email protected]>
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 <[email protected]>
2026-07-23 16:46:55 +03:00
Jonathan 6e92ef9a6f Fix: arc hovering 2026-07-23 13:07:51 +02:00
Hakan SevenandClaude Fable 5 fd2607452d fix(wipeout): frame draws above the mask; mask joins the draw order
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 <[email protected]>
2026-07-23 07:23:10 +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 <[email protected]>
2026-07-23 01:13:43 +03:00
Hakan SevenandClaude Fable 5 f407835543 feat(xclip): draw the clip boundary frame when XCLIPFRAME is on
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 <[email protected]>
2026-07-23 01:13:24 +03:00
Hakan SevenandClaude Fable 5 22a7c4490c fix(solid): read SAB transform Position tokens for body placement
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 <[email protected]>
2026-07-22 23:36:07 +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 <[email protected]>
2026-07-21 15:53:28 +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 <[email protected]>
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 <[email protected]>
2026-07-21 00:19:51 +03:00
Hakan SevenandClaude Opus 4.8 3b2b8ad21d feat(dwg): render section marks from their decoded style and view graph
The section mark's appearance now comes entirely from file data instead
of invented conventions:

- Arrow direction: resolved through the drawing-view graph — the
  symbol's parent AcDbViewRep, the section view (owner of the section
  definition), each view border's active viewport — by projecting the
  section camera's sight onto the parent camera's basis, using the same
  yaw/pitch/twist convention the viewport renderer does. Only when the
  chain is unavailable (DXF, older files) does a geometric fallback
  apply.
- Broken cutting line: driven by the style's plane-line / end-segment
  flags rather than hard-coded.
- Arrowhead: resolved through the shared dimension/leader arrow table
  from the style's arrow block handle; a null handle gives the standard
  ClosedFilled default.
- Label gap: the style's identifier offset.

Bumps acadrust for the style and view-graph decode.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-20 23:50:58 +03:00
Hakan SevenandClaude Opus 4.8 46e636b955 feat(dwg): render section marks on model-documentation layouts
Draw decoded section symbols (AcDbSectionSymbol) as their "A-A" cut mark:
the cut line through both endpoints extended past each end by its signed
tick, a short perpendicular barb at each tip, and the identifier glyph.
The mark now appears on the layout alongside the drawing views instead
of being silently dropped.

Bumps acadrust to pick up the section-symbol decode.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-20 22:20:58 +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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
2026-07-20 11:06:28 +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 <[email protected]>
2026-07-19 22:15:40 +03:00
Hakan SevenandClaude Opus 4.8 007d03422e fix(solid): cone silhouette uses the base radius, not the root radius
The DISPSILH silhouette for a cone anchored its `CurvedGen::Cone.base` at
the face's h_min (the base rim) but stored `radius` as the surface's h=0
root radius. The silhouette builder treats `radius` as the radius AT
`base`, so its base line landed at the wrong radius and its top line
(`radius + span·tan_a`) overshot the apex — a visibly wrong outline in
wireframe, and wrong lines drawn over the (correct) shaded fill. Store the
radius at the base instead (`radius + h_min·tan_a`). Cylinders (tan_a=0)
and root-based cones (h_min=0) are unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-19 19:14:11 +03:00
Hakan SevenandClaude Opus 4.8 e0f5b29941 fix(mesh): hide subdivision Mesh face fill in wireframe mode
The `fill_is_3d` classifier tagged only PolyfaceMesh / PolygonMesh as real
3-D surfaces (fill routed to the 3-D buffer, drawn shaded-only). The
modern subdivision Mesh was missing, so its face fill fell into the 2-D
overlay buffer that draws in every view mode — showing the shaded faces in
wireframe. Add `EntityType::Mesh` to the classifier.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-19 18:38:54 +03:00
Hakan SevenandClaude Opus 4.8 69354eca07 feat(proxy): decode shell, transformed geometry and text previews
Extends the proxy-graphics decoder past the door/wall's absolute polylines
so an ACAD_TABLE preview renders in full:
- type 29 transform: a 4x4 local->world matrix now applies to every
  following primitive (identity for the door/wall, which are absolute).
- type 9 shell: vertices + a face list -> each face as a closed boundary
  (the table's cell rectangles).
- type 32: a normal-tagged polyline (grid lines).
- type 38 text: position + direction (its length is the glyph height) +
  UTF-16 content and font, drawn as glyph strokes (simplex.shx and kin are
  single-stroke fonts, so the outline is the character).

True-colour (22) and lineweight (23) traits now reach real geometry too.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 21:34:15 +03:00
Hakan SevenandClaude Opus 4.8 1d117f514c feat(proxy): honour proxy-graphics colour traits
The preview stream sets a current colour (record type 14, an ACI) before
its geometry; track it and tag each decoded primitive. The Unknown-entity
path groups primitives by colour and draws one wire per colour, resolving a
real index (1..=255) to its RGB and letting ByLayer/ByBlock inherit the
entity colour. So a custom entity's preview now renders in its own colours
(e.g. an ACA door's blue swing) instead of a single flat colour.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 20:44:47 +03:00
Hakan SevenandClaude Opus 4.8 18d116bb33 feat(proxy): decode proxy-graphics geometry for custom entities
A custom entity without its object enabler (an AutoCAD Architecture door,
wall, ...) arrives as Unknown but ships a cached proxy-graphics preview —
the vector fallback AutoCAD itself draws. The old decoder read only a lone
type-6 polyline; generalise it to the real record stream
([total_size, record_count, {record_size, record_type, data}...]) and decode
the geometry records: polyline (6), polygon (7, closed) and circular arc
(4/5, flattened to a strip). Trait records (colour/lineweight/...) are
skipped for now, so the preview draws in the entity's own colour.

The Unknown-entity path now draws every decoded primitive in one wire, so
these objects appear (and become selectable) instead of vanishing.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 20:33:03 +03:00
Hakan SevenandClaude Opus 4.8 bf85896294 fix(mtext): treat the 0x02 background-fill flag as a mask over 0x01
MTEXT background fill flag 0x02 means "use the drawing-window colour" — a mask
that erases geometry behind the text — while 0x01 means "paint background_color".
The fill-colour pick tested 0x01 first, so text flagged 0x03 (both bits, e.g. a
dimension whose DIMTFILL is the drawing background) painted the stored
background_color (a stray grey box) instead of masking. AutoCAD shows no colour
there.

Test 0x02 first: when set, fill with the drawing-window colour (invisible on a
matching canvas) regardless of 0x01; a plain 0x01 fill still paints its colour.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 15:23:03 +03:00
Hakan SevenandClaude Opus 4.8 958c06ffe0 fix(dimension): don't draw a dimension's Defpoints markers
A dimension's definition points are baked into its *D block as POINT entities on
the Defpoints layer. AutoCAD treats them as grip markers and never draws them,
but OCS rendered them through the baked-block path as PDMODE glyphs (a vertical
tick with the drawing's PDMODE 4 / PDSIZE). Two of them sit on the measured
geometry at each extension line's foot, so the tick read as the extension line
running past the geometry — the lines looked too long.

Skip POINT sub-entities when rendering a dimension's baked block; only its lines,
arrowhead blocks and text are geometry.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 15:16:11 +03:00
Hakan SevenandClaude Opus 4.8 50f6196594 fix(render): flag 2D overlay fills so they show in every view mode
A SOLID's filled quad and a dimension/MultiLeader text background are flat 2-D
overlays that should draw in wireframe and shaded alike, but face3d_gpu
classified fills by `!fill_tris_low.is_empty()` — and a 2-D fill at UTM scale
carries a double-single low residual for precision just like a real 3-D mesh
face. So the arrowhead fill and the "5;8" cyan text background were routed to
the 3-D buffer and only appeared in shaded mode, vanishing back to nothing on
return to wireframe.

WireModel now carries an explicit `fill_is_3d`: true only for a real surface
(PolyfaceMesh / PolygonMesh face, set in the tessellator), false for every flat
overlay fill. face3d_gpu classifies on the flag, so 2-D fills stay in the
always-visible buffer while keeping their low residual (and UTM precision).

Verified on budweiser: the `_BoxFilled` arrow SOLID and the dim '5;8' MTEXT
background both tessellate with fill_is_3d = false.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 15:15:24 +03:00
Hakan SevenandClaude Opus 4.8 5e127ffa75 fix(table): place a baked *T table block at the table's insertion point
A Table whose cells are baked into a *T block (block_record_handle) was rendered
by tessellating the block's sub-entities directly. Those are laid out in
block-local space (origin at the table's top-left corner) — the Table entity
carries the world placement in insertion_point + horizontal_direction, exactly
like an INSERT — so the whole table drew at the world origin instead of its
location.

Each baked sub-entity is now cloned and placed (rotated about the local origin
by the horizontal_direction angle, then translated by insertion_point) before
tessellation, so tessellate_entity produces correctly located, relative-to-eye
geometry. Verified on budweiser handle 9378: its baked lines move from (0,0) to
the insertion point (-1200018.7, -800071.4).

Also bumps acadrust to pull the OLE2FRAME corner-decode fix, which places
OLE2FRAME entities (e.g. handle 3088) at their real corners instead of the
(1,1)/(0,0) defaults.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 13:12:08 +03:00
Hakan SevenandClaude Opus 4.8 340a358c26 fix(solid3d): keep loaded-solid mesh verts f64 until the RTE split
Loaded 3DSOLID faces rendered on a ~0.06 m grid and crawled against their own
wireframe at UTM scale. Both ACIS tessellation paths cast each vertex f64→f32 in
the samplers, and only split the double-single low half (verts_low) when the
body carried a `transform` record. But many files — budweiser among them — store
solid geometry in absolute coordinates with no body transform, so verts_low
stayed empty: the GPU got position_low = 0 and reconstructed an already
f32-quantized absolute position, while the feature edges (split unconditionally
from f64) stayed exact.

Both paths now accumulate vertices in f64 and hand them to one finalize step:

  finalize_mesh(name, verts_f64, normals, indices, color, xform)

the only place a solid vertex becomes f32 — it computes the world coordinate in
f64 (body placement applied, or identity), then splits into (verts, verts_low)
unconditionally, exactly as attach_feature_edges already does for the wireframe.
push_quad / tess_plane_face / tess_cone_face / tess_sphere_face /
tess_torus_face / tess_spline_face and the truck append_group / bespoke_face /
append_spline_faces all carry f64; apply_body_transform is gone.

Verified on budweiser (all solids, body_transform = NONE): verts_low is now
populated with the captured residual, max |low| ≈ 0.0624 m. The Model-tab
(truck_tess::polygon_to_result) and edge paths were already correct and are
unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 12:30:00 +03:00
Hakan SevenandClaude Opus 4.8 608cf8c74b refactor(solid3d): drive curved-face sampling from one chord tolerance
Curved 3-D solid faces used fixed per-LOD grid counts, so a partial arc (a
sphere cap, a torus C-arc, a short cone wall) was sampled as finely as a whole
rim, and the density knobs were scattered across four files.

Every curved face (sphere / cone / torus / spline), feature edge and isoline
now derives its segment count from a chord-height tolerance plus the arc's own
radius and span — the same model the 2-D circle/arc/ellipse wires use
(tess_util::arc_segments): a big rim samples finer than a small one, a short arc
proportionally less than a full turn, and facet error is size-independent.

All tolerances live in one block at the top of solid3d_tess: LodConfig is now a
single chord fraction per LOD (arc_segs / circle_segs derive counts), plus
EDGE_CHORD_FRAC, TRUCK_CHORD_FRAC and BOUNDARY_CHORD_FRAC. FACETRES scales the
fraction. The cone height is a straight generator (no curvature) so it meshes in
one division. Removes the scattered constants (circ_segs/grid_u/grid_v,
CURVE_REL_TOL, BOUNDARY_SEGS, per-site M / EDGE_SEGS / ring_segs).

Adaptive counts come out lower, not higher: the € solid drops 3056→1780 tris, a
full sphere stays at 1024, five-solid total is 2989.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 01:31:46 +03:00
Hakan SevenandClaude Opus 4.8 570165e77f fix(solid3d): trim torus faces to the revolution arc between end caps
An open "C" tube (the € currency symbol's ring) rendered as a full closed
donut: truck builds a torus as a full revolution and can't trim it, and the
wireframe/silhouette arc estimate was wrong.

The arc estimate failed because `collect_face_polygon` reads only the first
loop, and the "widest empty gap = tube body" heuristic breaks once the tube
carries interior holes (where the two horizontal bars punch through it): a hole
splits the body into wide hole-free stretches that masquerade as the opening.

`torus_phi_range` now walks every boundary loop and classifies each: a minor
circle sitting in a constant-φ plane (spread < ~8.6°) is an end cap; a wider
loop is an interference hole. The opening is the gap flanked on both sides by
end caps — a hole never bounds it — so the body is the rest of the turn.

Torus faces now route to the bespoke sampler (like spheres); `tess_torus_face`
clips its φ grid to that arc, so the shaded tube is an open C. `torus_isolines`
and `collect_curved_gens` use the same range for the wireframe rim and the
silhouette. Removes the now-dead truck torus path (`torus_faces`,
`Outward::Torus`, `vadd`).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 01:10:29 +03:00
Hakan SevenandClaude Opus 4.8 c0ec866fac feat(solid3d): draw the ring outline of torus faces in wireframe
A torus's ISOLINES were only its minor cross-section circles, so a ring read
as a row of disconnected circles rather than a ring — the outer and inner
profile arcs that make its outline were never drawn. They are the major
(ring-direction) isolines: a circle around the ring at each tube angle, the
outer (θ=0) and inner (θ=π) ones being the defining outline.

Both directions now draw. The revolution is swept in full rather than clipped
to the face: a tube's boundary is only its two end caps, clustered at the ends,
so it never covers the revolution arc and can't say which way round the tube
runs — the same limitation the arc range already had. Full is correct for a
closed or nearly-closed ring (this Euro symbol is a ~345° torus) and only
over-draws a short torus fillet by the small missing arc, which a later
winding-aware extent would tighten.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 00:31:27 +03:00
Hakan SevenandClaude Opus 4.8 9b7fe2c84d feat(solid3d): draw spline (intcurve) edges of ACIS solids
A curved solid's fillet/blend boundaries are `intcurve-curve` spline edges.
The edge collector handled only straight and elliptic curves, so a spline
edge fell back to a straight chord between its two vertices — or, for a
closed-loop edge whose endpoints coincide (a sphere/torus face rim), to
nothing at all. Those boundaries simply weren't drawn.

Both the feature-edge list and the face-boundary sampler now sample the
spline over the edge's own parameter span via the new `SatIntCurve`, so the
curved edges draw as real curves. On budweiser2018.dwg handle 2700 the
solid's edge-segment count rises from 110 to 392 — the previously missing
sphere/torus rim curves.

The face-boundary sampler feeding a real loop (instead of a degenerate
chord) is also the prerequisite for trimming those curved faces to their
extent, a follow-up.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-18 00:18:10 +03:00
Hakan SevenandClaude Opus 4.8 8c72a06790 feat(solid3d): ISOLINES and silhouettes on sphere and torus faces
Extends the curved-face wireframe treatment from cones to spheres and tori, so
a solid built from those faces reads the way a cylinder already did.

ISOLINES: sphere faces get meridian lines across the face's colatitude span at
evenly spaced longitudes; torus faces get minor cross-section circles at evenly
spaced revolution angles. Both clip to the face's own parametric window (from
the boundary polygon via a shared `angular_span` gap-finder), so a partial
sphere or torus arc doesn't draw lines out over the missing part.

Silhouettes (DISPSILH): `CurvedGen` becomes an enum. A sphere silhouettes as
the great circle perpendicular to the view, clipped to the face window; a torus
as the two curves where the tube turns edge-on, traced around the revolution.

The silhouette now uses the camera's view *direction* alone, not the eye
position — a single parallel direction for the whole scene — so the outline
follows the view angle, holds still under pan, and doesn't foreshorten, which
is the orthographic silhouette a CAD wireframe expects.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-17 23:30:05 +03:00
Hakan SevenandClaude Opus 4.8 ae5231427b feat(solid3d): view-dependent silhouettes (DISPSILH) on curved faces
ISOLINES show a curved solid from any angle, but its outline still cut across
whichever fixed longitudinal lines happened to face the eye. DISPSILH draws the
true silhouette — the two lines where a cylinder (or cone) turns edge-on to the
view — and it moves with the camera, so it cannot be a baked edge.

Tessellation stores a world-space generator per cone/cylinder face on the mesh
set (axis, radial frame, radius, cone taper, extent), sharing the face-extent
extraction with the ISOLINES collector. Each frame `upload_silhouettes` walks
those generators, solves `du·cosθ + dv·sinθ = -tanα·da` for the two edge-on
angles (`φ ± acos(...)`, `φ ± π/2` for a cylinder), and builds a line list in
the mesh vertex format — double-single, so it stays precise at UTM. It draws
through the existing wireframe / edge pipelines in the wireframe and
shaded-with-edges passes.

Gated on the document's DISPSILH header flag, which defaults off — so the
per-frame pass is a no-op unless the drawing asks for it. Scoped to top-level
solids: a block instance's generators aren't transformed by the INSTANCE pose
yet, so they are cleared on the block path (its isolines and feature edges
still draw).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-17 20:02:56 +03:00
Hakan SevenandClaude Opus 4.8 0ce150411a feat(solid3d): draw ISOLINES on curved faces in wireframe
A cylinder's wireframe showed only its two rim circles, so from the side it
read as two disconnected ellipses rather than a cylinder. AutoCAD draws
ISOLINES — a fixed set of longitudinal lines on each curved face — so the
surface reads from any angle.

`collect_isolines` walks the cone-surface faces (cylinder and cone), recovers
each face's height and angular extent with the same logic `tess_cone_face`
uses, and emits `header.isolines` (default 4) longitudinal segments from the
bottom rim to the top rim. They are appended to the feature-edge line list, so
they ride the existing body transform, double-single split, and top-level /
per-INSTANCE re-split for free.

Being part of `edge_verts`, they inherit the edge pass's gating: drawn in the
wireframe modes and in shaded-with-edges, hidden in plain shaded — matching
where AutoCAD shows isolines. `isolines` is threaded from the header through
the tessellation entry points alongside `facet_res`.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-17 19:45:42 +03:00
Hakan SevenandClaude Opus 4.8 26e83a90bf fix(solid3d): double-single split for shaded ACIS faces at UTM
A 3D solid's shaded faces used absolute f32 world coordinates while its
feature edges carried the double-single (high, low) pair the mesh shader
reconstructs relative to the eye. At UTM scale the faces snapped to a
~0.06 m grid, so the shaded surface crawled against its own wireframe as
the camera moved.

`apply_body_transform` is where a solid gets its world placement — the
large body translation is exactly where the coordinate stops fitting an
f32. It now computes each world vertex in f64 and splits it into the pair,
filling `verts_low` the same way the feature edges already are. Both
tessellation paths (truck B-rep and the bespoke sampler) route through it,
and the downstream re-split points (top-level offset, per-INSTANCE block
transform) already consume the pair, so they inherit the corrected
residual. Picking, which reads `verts_low` too, sharpens at UTM as well.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-17 19:15:02 +03:00