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.
A DIMENSION references the block "that contains the entities that make up
the dimension picture", and that block is the picture: AutoCAD requires
it and draws it, BricsCAD draws it when present and only renders from the
dimension variables when it is missing. OCS re-derived the picture from
DIMVARS every time and ignored the block entirely.
That works while a style agrees with what the drawing actually drew. A
Revit-exported schedule here does not: its style stores DIMTXT already in
drawing units beside a DIMSCALE meant to scale it, so the text came out
three hundred times too tall, and each dimension carries a DIMFXL
override that is likewise already in drawing units, so the extension
lines ran nearly a million units past the points they measure.
Neither is guesswork about units now. The block holds the geometry the
drawing was authored with, so it is drawn: the contents are already in
world space and the block's base point is the origin, and the identity
instance that carries them exists only to reuse the scene graph's
ByBlock / layer-0 colour resolution. Every wire answers to the dimension,
so a pick still selects the dimension rather than one of its pieces.
Re-deriving stays as the fallback for a dimension with no block.
Annotative dimensions keep the old path: their several representations
are separate blocks and choosing between them is what the annotation
machinery already does, while the doctrine above assumes one picture.
A picture is only good while its inputs hold, so it is dropped when they
change: editing a dimension's properties, and saving the dimension style
it is drawn with. Entity edits already dropped 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.
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>