- Repurpose CircleInstance center_high.w and params.w for start_width and end_width (preserving exact 128-byte stride)
- Route wide polylines, tapered polylines with bulges, and donuts through split_mixed_polyline into GPU analytical arc instances
- Compute physical annular bands, width tapers, and round end caps in circle.wgsl using world-space distances to prevent 3D perspective foreshortening distortion
- Expand bounding quad margins dynamically with grazing-angle compensation to eliminate clipping
- Relax pick_tris check in wire classification and partition routines so wide entities retain CPU picking while drawing analytically
- Add unit and hardware GPU integration tests, including tilted 3D perspective rendering verification
Switching to a Layout re-tessellated the whole model — `memo_miss=186455`,
2034 ms — to arrive at geometry identical to what was already in the memo.
The only thing that differed was colour.
`local_wires_for` already knows this: it stores the raw colour and drops
its background argument with `let _ = bg_color;`, noting that
`Batches::finalize` applies `adapt_to_bg` last, "so the cache no longer
has to rebuild on BACKGROUND / layout-switch". The tessellation memo's
guard had not been told, and still mixed the background in, so every
entry was discarded on a switch.
`finalize` now records what its colour resolution consumed — the raw
colour, the raw glyph colours, and the three flags it branches on — as
`WireModel::bg_adapt`, and `resolve_colors_for_bg` replays it for another
background. The guard drops the background, so a memo entry serves any
layout.
The resolution recomputes from the recorded raw colour rather than from
the resolved one, which makes it idempotent and therefore safe over a set
that mixes memo hits with freshly tessellated misses. Re-adapting the
resolved colour would not be safe: `adapt_to_bg` snaps a near-white colour
to pure black on a light background, and pure black comes back pure
*white* on a dark one, losing the tint. A test pins that asymmetry so the
raw colour is not optimised away later.
The contract is tested against the real `finalize` rather than a copy of
its logic: for every background pair, building under one and resolving for
the other lands exactly where building under the other would have.
`bg_adapt` is `None` for wires built anywhere else, and the resolution
skips them, so nothing outside the block cache changes colour. It is also
`None` when the resolution would be a no-op — a preserved colour with no
canvas fill looks the same under every background — which keeps the common
case at no cost.
Ordering is unchanged: the pass runs inside the builder, before
`append_scene_markers` and `apply_refedit_fade`, both of which have their
own separate background dependency.
`wires-build` gains `colors=` so the pass has a number of its own.
Co-Authored-By: Claude Opus 5 <[email protected]>
Sync current main and companion codec changes before preserving unsupported frame rows, rendering in the entity plane, and sharing final layout metrics with placement preview.
Share source buffers across block placements and render nested content with GPU instance transforms.\n\nChunk silhouettes and compact mesh material data so large drawings stay under device buffer limits and remain responsive.
The last four truck crates are gone from the manifest. What they were doing
falls into three parts, and each now has one answer instead of two.
NURBS evaluation. truck was carrying B-spline curves and surfaces for the
SPLINE entity, hatch boundaries, BLEND's endpoint frames, the spline preview
and ACIS spline-surface faces. The kernel now has both, over one de Boor
written across however many coordinates a control point holds — so a plane
curve and a space curve cannot drift apart, and a surface is the same
algorithm applied twice. The rational and the polynomial cases stop being
separate types: weights absent means polynomial.
The entity conversion. Every LINE, ARC, ELLIPSE, SPLINE and polyline was
built into truck topology purely so it could be sampled back into points.
They are sampled through `entities::curve` now, which is where each one's
geometry is already defined once and what EXTRUDE and REVOLVE read — so a
circle drawn on screen and a circle handed to the Model tab come from the
same definition. That retires four of TruckObject's variants and the module
is renamed for what it does.
SWEEP and LOFT. Both only ever produced a mesh, so both are built from point
lists: a band of quads per span, and a lid where a profile closes. Lofting
profiles of different densities resamples them by distance rather than by
index, so a circle lofted to a square no longer twists.
Two things worth noting for anyone reading the old comments. The tolerance
globals that lived in the tessellation module were never about truck at all —
they are the per-frame chord height, and they move to `curve_tol`. And the
rule that a profile handed over as `Lines` silently broke EXTRUDE and REVOLVE
no longer holds: those read the curve directly and never look at this channel.
The two `automation` test failures are unchanged from before this and are not
caused by it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Route model, paper, viewport and nested block content through one traversal while preserving leaf renderers. Carry decoded material state into mesh shading and harden solid geometry conversion.
Preload one language-aware font resource across the interface, drawing text, and navigation labels.\n\nRender the navigation cube, ring, outlines, and labels through a 4x MSAA target.
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>