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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
A wide polyline draws a solid band, but hit-testing only ever saw the
centreline 8 px out, so a donut could only be selected by clicking the
phantom circle inside the disc it paints. `self.hatches` — the map the
hatch pick channel walks — is filled from HATCH and SOLID alone, so the
band `wide_fills` produces was drawn (via `synced_hatch_models`) and never
picked.
The band rides in on `pick_tris`, the same channel a thickness wall uses:
`wide_fills` already computes it, so it costs one tessellation and inherits
the block transform. It ear clips rather than fans — an arc band is an
annular sector, concave on its inner edge — reusing `triangulate_planar`.
Wiring `pick_tris` through `Lines` alone was not enough: a plain wide
polyline leaves `to_truck` as `Contour`, and a PLINEGEN=0 one as
`SegmentedLines`. Both now carry it.
This needs the wire's cullable box to cover the band, and it did not.
`bounding_box()` boxes a polyline's stored vertices, knowing nothing of the
width painted around them: a donut's two vertices sit on one horizontal
line, so the box had zero height and rejected every click on the disc
before any triangle was tested. Every site that stamps the entity box onto
a wire now goes through `set_wire_aabb`, which widens it over `pick_tris`
(a no-op for the entities that have none, which is nearly all), and the
block cache chains `pick_tris` into its own box for the same reason.
That box was already too small for the centreline itself — an arc leaves
the vertex box — so a donut was unselectable at its top and bottom at
enough zoom. Widening fixes that too.
Verified end-to-end: tessellate a width-0.5 donut on a 0.05 centreline
radius, then `click_hit` five points 20 px clear of the centreline — all
land, and a click outside the disc still misses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Selection tested a flat 8 px radius around each wire's centreline and
nothing else, so two things the user can see were not pickable.
A wire renders as a band `line_weight_px` wide, so pick now widens to that
rendered half-width via `pick_tolerance_px`, mirroring the wire shader's
`select(0.5, half_width, lwdisplay_enable)`. Behaviour is unchanged for
every standard weight (the widest, 2.11 mm, renders 7.97 px half-width —
just under the 8 px threshold); this keeps the two sides from drifting
apart if the display boost in `lineweight_to_px` ever changes.
Entities extruded by a DXF thickness draw their wall as four edges with
nothing in between, so the interior was a hole the cursor fell through.
WireModel gains `pick_tris`: triangles hit-testing treats as solid and the
renderer never uploads. Kept apart from `fill_tris` because that channel
reaches the GPU, and a wall that rendered shaded would change how every
thickness drawing looks. Ranked below `fill_tris` — those are drawn, these
are not, so where they overlap the drawn face is what the user means.
Walls are dropped when a wire is projected into a floating viewport:
`points` are reprojected and clipped into paper coords there, so cloned
model-space triangles would hit-test at a model-scale offset. Extruded
entities stay selectable by their edges, which do get projected.
The wall loop runs on every hover that misses everything else, so it
rejects on the wire's AABB first — an extruded circle is ~128 triangles
and a drawing full of thickness would otherwise project every wall on
each mouse move.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An entity from an application we have no reader for — e.g. an Autodesk Raster
Design embedded raster image — now arrives as `Unknown` (acadrust 769741d) and
rendered as nothing at all, so the drawing silently lost it with no hint that
anything was there.
Such entities almost always ship proxy graphics: the vector preview their author
cached for precisely this case, and what AutoCAD itself falls back to when the
object enabler is absent. Decode it and draw it, so the entity holds its real
place and size.
The record grammar has no reference implementation to copy — LibreDWG keeps the
blob raw ("see par 29") and ACadSharp stores the bytes without parsing them — so
rather than infer a general decoder from one sample, `proxy_graphics` accepts a
single polyline record and only when the blob matches it byte-for-byte: sizes
must agree, the points must consume every remaining byte, and coordinates must
be finite. Anything else returns None and draws nothing, exactly as before, so
a file this does not model cannot regress or sprout invented geometry.
Verified on a Raster Design drawing: its 140-byte preview decodes with 0 bytes
left over to the image frame (1192.149 x 877.824, closed), matching what ODA
reports for the same entity. Unit-tested against that real blob.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MLINE dashed elements were fully CPU-expanded (apply_along) — one wire per
dash — to keep the parallel lines in phase. Move pure-dash elements onto the
GPU dash shader and end-align every element to the multiline's centre-line so
they share one interior phase.
- wire_distances now accumulates arc-length from the double-single (high+low)
points, so the dash phase stays precise at UTM coordinates. f32-high-only
deltas quantised ~0.1 there and drifted the parallel lines apart — the
original reason MLINE dashes were CPU-side.
- New WireModel.dash_align_end carries a shared begin/end dash length. The
shader uses it as align_end for every parallel element (the interior phase
depends on align_end, not the wire's own length) while align_total stays the
element's own length, so each still ends on a dash at its own endpoint.
- MLINE tessellation derives that length once from the centre-line, routes
pure dash/space/dot elements to the GPU pattern and text-bearing elements to
apply_along.
- apply_along gained an "A"-type mode (shared reference length): a solid
begin/end dash and a phased interior, so its dashes — and embedded text —
line up with the GPU-dashed sibling elements instead of tiling from start.
Pure-dash MLINEs now cost one WireModel + pattern per element instead of N
segments; single (non-MLINE) entities keep the from-start tiling unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DET pipe walls now carry the stroke pattern (dash_from_start), which
tiles from each wall's own start vertex and skips the A-type end
alignment that was force-lighting a solid dash under the end symbol.
Two rendering fixes on top:
- Dedup the host polyline's vertices before offsetting the walls. Some
DGN pipe polylines carry trailing duplicate end vertices; a zero-length
segment gave offset_xy a degenerate normal, leaving that vertex
un-offset and folding the wall down to the centre line. The two walls'
fold segments met at the centre and drew a spurious line linking them.
- Re-key the symbol (end circle / half-ellipse) wires to the host handle
so the whole pipe picks and selects as one entity instead of the
symbols acting as separate phantom entities.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DGN line-style linetypes (empty standard LTYPE) drew only end symbols on
a single centre line. Render the host polyline as two parallel walls
offset by ±(symbol radius = block extent / scale) so each wall sits
tangent to the end circles, and dash them with the line style's stroke
pattern.
The stroke dash lengths live in the component leaf (byte-aligned
big-endian f64 after the shared class GUID). `wall_dashes` reads the
native dash of the root compound's dash stroke ([5.25, 7.125] for DET);
`stroke_dashes` decodes the leaf. The native lengths scale to drawing
units by f = radius / symbol-scale (0.1875) — the same factor that maps
the compound's native offset (2.0) onto the measured wall offset
(0.375), which also lands the dash at ~1.0 drawing unit.
Walls dash through the normal WireModel pattern (shader rebuilds each
segment relative-to-eye, so it stays precise at UTM coordinates).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Linetypes converted from MicroStation DGN keep their pattern in DGN line-style
objects (empty standard LTYPE), so entities using them drew as a plain line and
lost the symbols (e.g. a pipe's end circles). Walk the line-style tree
(resolved by name to its LSDefinition), collect the symbol components' anonymous
blocks, and draw each at the host polyline's endpoints, scaled by the symbol's
decoded scale (geometry / scale) so it matches the drawing (verified: a 0.75
ellipse renders at the file's intended 0.375 radius).
Bumps acadrust to 8e18c6b (DGN line-style symbol-scale decode). First pass: the
stroke dash pattern and exact per-vertex placement need more of the line-style
leaf decoded; the base line still draws solid.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Multilines were drawn with a fixed ±scale/2 two-line guess in the entity
colour, ignoring the MLINESTYLE entirely, so a drawing's custom style (per-
element offsets, colours and linetypes) was lost — a red/yellow multiline
rendered as a white box.
- mline.rs: resolve the MLINESTYLE (by style_handle, then name) and build one
parallel line per element from its offset x scale + justification shift.
- tessellate.rs: special-case MLINE (like Leader) to emit one WireModel per
element in the element's own colour, CPU-dashing every element through
apply_along so the parallel lines stay in phase.
- linetypes.rs: render a drawing's OWN complex linetypes (embedded text/shape),
document definition first and the bundled .lin catalog only as a fallback, so
a file-defined text linetype like PLYN shows its text.
- Run the dash/glyph layout in a local frame (f64 origin subtracted, re-split to
double-single after) so spacing stays precise at UTM coordinates, and pass
tracking = 1.0 so embedded text keeps its letter spacing.
Cargo.lock pins acadrust to the MLINE->MLINESTYLE handle-link reader fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Render: annotative-ness is resolved centrally in scene::annotative::is_annotative
(per-object context dictionary, legacy XDATA, or annotative style) so the bake and
the properties panel agree. Text, dimensions, tables and blocks now display at the
current annotation scale in model space. An annotative block scales as one uniform
unit about its insertion point — its internal geometry and attributes are carried by
that scale instead of being scaled individually, fixing the block-attribute size
regression. Tables scale their column/row/margin geometry.
Properties: the handle- and flag-backed rows are now editable pickers — MLEADER
multileader style / text style / arrowhead / leader linetype, General Material, and
Plot style (named plot-style mode only; the color-dependent mode stays read-only,
which is correct). A picked name is resolved back to its handle in the update loop,
where the document is in scope. Annotative Yes/No and the annotation-scale row are
shown per object.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two defects in the in-app MTEXT editor, both surfaced once the preview
text became visible:
1. Invisible preview text. After the SDF text migration, tessellate emits
glyphs as SDF quads on `text_verts` and leaves the polyline `points`
empty, but the editor preview draws on a 2D canvas that can't run the
SDF shader — so only the selection box showed. Add a `force_text_strokes`
parameter to tessellate: when set, TEXT/MTEXT run-groups also emit their
glyph outline strokes as points. The preview passes true (real stroke
font, aligned with glyph_boxes); every other caller passes false and
keeps SDF-only text.
2. Enter did not start a new line. mtext_type inserted a literal newline,
but the MTEXT layout only breaks on the `\P` code (a raw 0x0A is a break
only to the caret logic). Normalise every literal newline — from Enter or
a multi-line paste — to `\P`, so the line breaks and the saved value
stays standard.
Closes#308
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Snap and hover hit-testing scanned the whole model wire set every cursor move; a heavily block-instanced drawing explodes to millions of wires, so this stalled for over a second per move. Add a cached spatial grid (wire_grid) keyed on the geometry epoch and query only the cursor neighbourhood via Scene::hit_test_wires_near. Gate the O(in-range segments squared) intersection passes on the in-range POINT count, not the wire count — a handful of curved wires, each hundreds of tessellated points, blew the quadratic up while the wire count looked modest. Give zero-size entities away from the origin (POINTs) a real cullable AABB, and fall back to a block sub-wire's own world points when its entity bounding box is degenerate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SDF is the only text renderer now — the stroke path and its LOD ladder are
gone, and the remaining text kinds render as SDF glyph quads too.
- ATTDEF / ATTRIB: carry a GlyphRun so single-line attribute text renders as
SDF (multi-line already did via layout_mtext). Tolerance / complex-linetype
text already SDF.
- Remove `sdf_text_enabled()` (the OCS_TEXT_SDF flag / kill switch) and take
the SDF path unconditionally everywhere: tessellate Text arm (per-group by
GlyphRun presence), hit_test fallbacks, render gather, complex_lt, table,
mleader.
- Delete the text LOD ladder: tess.rs baseline/greek block and the
block_cache greek/baseline path (emit_greeked_text / emit_text_baseline),
plus the now-dead helpers text_baseline_points / text_greek_obb_tris /
text_obb_corners_native / mtext_line_count and the LocalWire
text_height_local / text_obb_local fields. All text is crisp at every zoom;
text entities are exempt from the sub-pixel stub cull.
- Fix a latent bug: block-internal SDF text was culled because its LocalWire
aabb_local was [0,0,0,0] (no points) — fold the glyph-quad extent into
aabb_local and exempt text_verts-carrying wires from the sub-pixel cull.
- Clear the pre-existing dead-code warnings (TextPlacement.elevation, TabKind,
MTextRunKind::Tab).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An in-app-created table (no baked *T### block) synthesised its cell text via
layout_mtext as stroke polylines — the last text path still rendering strokes
while SDF is the default (imported/baked tables already render SDF via the
recursive Text arm).
- tessellate_table: when SDF is on, build each cell's glyph quads from the
run-carrying layout and collect them into one text-carrying wire (per-vertex
cell colour, glyph-bounds AABB); the stroke/fill accumulation is the SDF-off
fallback. Mirrors the MLEADER SDF branch.
- tess.rs synthesized-table path: guard the whole-table AABB clobber on
(!points.is_empty() || !fill_tris.is_empty()) — matching the dim / mleader /
baked-block paths — so the empty SDF-text wire keeps its tight glyph-box AABB
and the pick box hugs the text instead of the whole table. Found by the
adversarial review pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Render the MTEXT background (never drawn before — the flags were only shown
read-only in the panel) and make it editable.
- Text arm emits an opaque fill_tris rect behind the glyphs: flag 0x01 uses
the entity background colour, 0x02 uses the canvas colour (a wipeout-style
mask). Box = glyph bounds padded by (background_scale - 1) × text height.
- Text frame (flag 0x10) draws a closed rectangle in the text colour around
the same box.
- Threaded `bg_color` into `tessellate()` (+ all callers and the
MultiLeaderTess trait) so the mask can use the drawing-window colour.
- Properties: "Background mask" is a Off / Fill / Mask dropdown, "Background
color" is its own colour picker (independent of the entity colour — new
PropBgColor* messages; picking a colour switches to Fill mode), and "Text
frame" is a checkbox toggle. Edits re-tessellate so the change shows live.
Renders via the face3d fill path, so the fill shows with FILLMODE on (same as
MLEADER background fill); the frame is a stroke and always shows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rearchitect SDF text from a document-wide collector to per-entity
production during tessellation, so each entity owns its glyph quads and
composite/nested text is covered for free (no re-explosion, no per-frame
document walk).
- WireModel gains `text_verts`: glyph quads ride on the wire, so they are
cached by the tess memo, cloned on hit, and transformed by the block
expand loop exactly like `points`.
- Text arm (tessellate.rs) builds the quads when SDF is on and suppresses
the strokes; render gathers text_verts from the viewport wire set
(`gather_text_verts`, cached on wire content id) — the old collector is
removed. Covers model, dimension, table, block-internal and paper-sheet
text uniformly.
- MLEADER text (lays out via layout_mtext, not the Text arm) gets its own
SDF branch driven by the per-run GlyphRun.
- Pick box comes from the rendered glyph quads (true text extent), not
entity_aabb which mis-places the box for MTEXT; composite paths no longer
clobber the empty text wire's tight AABB (dim pick box hugs the text).
- Selection / hover highlight via a text-highlight overlay (upload_text_
highlight, keyed on selection_generation) mirroring the selected-wire
xray overlay; base glyphs stay neutral so a deselect leaves no stale tint.
- Debug: env OCS_TEXT_BOX draws each run's glyph-bounds rectangle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With OCS_TEXT_SDF on, top-level text took the full path only at ≥5 px and
still drew greeked OBB rects / baseline lines when zoomed out — doubling
with the SDF glyphs. Force the text LOD height to None while SDF is on so
text always falls through to the full path, which is itself suppressed to
an empty snap-only wire; SDF then renders it crisply at every zoom.
(Block-internal text is unaffected — it is not collected for SDF, so its
wire/greek path stays.)
Verified in-app: zoomed-out SDF text stays crisp with no greek/baseline
double draw. Flag off: unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Locked-layer objects were fully editable. Now they stay visible and
snappable but are otherwise protected:
- Not selectable: the five interactive pick paths (box, lasso, single
click, cycling, box-release) plus SELECTALL and QSELECT skip locked
entities.
- Not modifiable (defense-in-depth even if a handle slips through):
transform_entities, erase_entities, apply_grip and the Properties
commit all skip locked entities.
- Faded: entities on a locked layer render dimmed toward the background
(render_style + wire tessellation); toggling lock re-tessellates.
- Snap still works: locked entities are intentionally kept in the wire
set (visibility_ok untouched), so object snap keeps finding them.
- Feedback: a small padlock is drawn by the crosshair when hovering a
locked object, and clicking one prints "Object is on locked layer …".
New Scene::is_layer_locked / locked_layer_name helpers centralise the
check (mirroring layer_hidden).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entities inside a block definition that sit on layer "0" must take on the
properties of the layer the block reference is inserted on; every other
layer is "sticky" and keeps its own identity. OpenCADStudio resolved a
block child's ByLayer color/linetype/lineweight against the child's own
layer in all cases, so layer-0 content stayed layer-0 (white) instead of
inheriting the instance layer.
Resolution now follows the standard rule for block children — explicit
wins, then ByBlock → the insert's own style, then layer-0 + ByLayer →
the insert's *layer* style, else the child's own layer. The insert's
layer style threads through the whole block-expansion chain (and chains
correctly through nested inserts), with the child's own transparency
preserved.
Covered across every block-render path:
- wires (line/arc/circle/polyline/text) via the block cache + the
explode fallback + insert attributes
- hatch / solid fills (also fixes the PDF/print export path)
- 3D solid block children (per-instance mesh recolor)
- Dimension / Table baked-block sub-entities
New shared helpers in scene::view::render: InheritStyle, layer_render_style,
lineweight_to_px; render_style_for_block_sub gained the layer-0 branch.
Adds unit tests covering the layer-0, sticky-layer, ByBlock, explicit and
transparency cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scene/tess.rs held the free parallel-tessellation functions
(tessellate_entity, lod_stub_wire, entity_aabb, …) that the giant-file
split had left at the scene root. They belong with the rest of the
tessellation code, so move the file to scene/convert/tess.rs alongside
tessellate.rs / tess_util.rs and re-point the re-export.
Leaves the scene/ root as a consistent set of `impl Scene` method
groups; the free-function outlier is gone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>