Once the first point of a segment is placed, OTRACK now offers an
alignment along the line joining that base point and an acquired
tracking point — the direction *between* them, not only the ortho/polar
axes or the corner's own edges. Acquire an existing corner while drawing
and the cursor locks onto the base→corner line, with typed-distance
measured from the base point.
Adds a base→corner ray per acquired point in otrack_snap, grouped with
that corner so it never self-intersects with the corner's other rays.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
OCS renders a dimension live from its definition points but exports a
baked *D block that other applications draw. An in-place edit (DIMTEDIT,
text override, DIMSPACE, any ReplaceEntity/ReplaceMany edit) changed the
dimension while leaving the old block, so the export kept the pre-edit
graphics — BricsCAD/ODA showed the stale dimension while OCS showed the
edit.
Add explode::invalidate_dim_block, which drops a dimension's baked *D
block (record + owned sub-entities, so no orphan blocks accumulate) and
clears block_name; bake_dimension_blocks then rebuilds a fresh block on
the next save. Call it from the in-place edit paths. The transform path
(MOVE/COPY/PASTE) keeps its own block in sync and is left untouched, so
dimensions loaded from other applications and not edited are preserved.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A dimension's *D block is baked in absolute WCS, so the DWG group-12
insertion point must be the origin. OCS's dimension commands seeded it
with the text anchor, so a reader that positions the block by that point
(BricsCAD / ODA) drew the dimension shifted by the offset, while OCS —
which renders the block in place — showed it correctly.
Reset base.insertion_point to the origin in bake_dimension_blocks, where
the absolute-WCS block is created. Only dimensions whose block OCS bakes
are touched; blocks preserved from other applications (baseline/continue
dimensions with a legitimate non-zero clone insertion) are left alone.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add a MANUAL_SUPPORTERS list in patreon.rs for donations that arrive
outside Patreon; both the native and web boot paths merge it with the
fetched patrons and sort the combined list by amount (highest first,
then alphabetically). This also sorts the web supporters list, which
previously arrived unordered, and keeps hand-maintained supporters
visible when the Patreon fetch returns nothing (offline / no token).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Commands now expose their step options as clickable buttons rendered
inline beside the command-line prompt, so options need not be typed.
Adds a CmdOption/options() API to CadCommand: each command returns the
options for its current step; the command line renders them as
uppercase buttons and a click feeds the keyword to the command exactly
as if typed (an empty keyword submits like Enter).
Construction commands become "central" — CIRCLE offers [3P/2P/TTR/TTT/
DIAMETER], ELLIPSE [ARC/AXIS], ARC [SCE/SCA/SEA/SER/CSA/3P], RECTANGLE
[ROTATION/CENTER], POLYGON [CIRCUMSCRIBED/EDGE] — each keyword hands off
to the existing dedicated variant command via CmdResult::Dispatch,
instead of the variants only being reachable as isolated commands.
Migrates the existing bracketed-option prompts (PLINE, PEDIT, SPLINE,
SCALE, FILLET, CHAMFER, HATCH, GRADIENT, SOLID, POINT, RAY, XLINE, TRIM,
EXTEND) to the same button API and strips the option text from the
prompt strings, keeping value/coordinate hints. PEDIT's Width option
now enters a value sub-step so the button acts on click.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The ABOUT command and its version-reporting window already exist, but
the ToolDef sat dead-code in the draw module with no ribbon entry, so
the only way in was typing ABOUT. Move it to the manage module and give
it an Application group on the Manage tab, as a discoverable fallback
for version diagnostics.
Co-Authored-By: Claude Fable 5 <[email protected]>
Ports PR #301 (by Kevin Griffin) to the SDF text renderer. Block-nested MTEXT
with inline colour codes (\C1;, \c…) collapsed to a single colour: the block
cache applies ByBlock / layer-0 colour inheritance per entity, so at emit time
every segment's colour was replaced with the inherited colour — including
segments carrying an explicit inline override.
- block_cache tessellate_sub_local: gate ByBlock / layer-0 colour inheritance
on `wire_on_base_color` (a wire whose colour differs from the entity base
carries an explicit override and keeps it) — #301's fix, still correct for
the remaining stroke wires.
- block_cache emit_wire: the per-vertex analogue for SDF text — a glyph whose
colour equals the wire's base inherits the resolved colour; a glyph with an
inline \C / \c override keeps it. (Previously every block glyph was recoloured
with final_color, collapsing colour-split MTEXT.)
- tests/text_font_rendering.rs: restore the block colour-split regression test
(it had stopped compiling on main when expand_insert gained the InheritStyle
param) and adapt it — and drawable_point_count — to SDF text_verts, since
block MTEXT now renders as glyph quads, not colour-split stroke wires.
\C (ACI) / \c (true-colour) are parsed by acadrust (mtext_format
SpanProperties.color) → OCS InlineColor → the glyph's per-vertex colour.
Co-Authored-By: Kevin Griffin <[email protected]>
Co-Authored-By: Claude Opus 4.8 <[email protected]>
External contribution by sLuCHa. A dimension draws from its baked *D block
(baked in absolute WCS); the in-drawing COPY re-points block_name to a
transformed copy (#161) but clipboard PASTE did not, so a pasted dimension
rendered at the source location (same drawing) or was missing (cross drawing).
Copy now snapshots each copied dimension's *D block into the clipboard
(dim_blocks); paste builds a fresh copy transformed by the paste offset and
re-points block_name — same- and cross-drawing alike, no orphan blocks.
Refactors out shared define_transformed_block / snapshot_block helpers
(behaviour-preserving).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
External contribution by sLuCHa. Triangulates each hatch boundary once
(lyon even-odd, cached with the batch build) and rasterizes the triangles
instead of the per-fragment point-in-polygon test, removing the screen-fill
overdraw + boundary raycast that forced the flicker-causing interaction LOD
(now default off behind OCS_HATCH_LOD). Precision preserved (triangles in the
same world_origin-relative frame + RTE path); untessellatable boundaries fall
back to the old AABB-quad + in_polygon path (poly_test=1).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Two fixes for opening drawings with XCLIP'd blocks (crashed / mis-rendered
since SDF text became the default).
- block_cache emit_wire: the fill_tris/fill_tris_low length debug_assert
panicked in debug builds on a legitimate case — a Leader/dimension arrowhead
fill nested in a block carries fill_tris with an empty fill_tris_low (the
WireModel contract's "all-zero low" form, which the code already handles via
get().unwrap_or). Relax the assert to permit the empty case; a *partial* low
half is still caught.
- xclip clip_wires: also clip the SDF glyph quads. Block-internal text was
bleeding past the clip boundary (mixed batch) or vanishing (the tail retain
dropped empty-points wires). Now each glyph quad is kept/dropped by its
centroid vs the boundary (whole-glyph granularity), text_verts-carrying wires
survive the retain, and the pick-box AABB is recomputed from the surviving
glyphs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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 <[email protected]>
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 <[email protected]>
Extend SDF text to the remaining text kinds and make the Text arm decide SDF
per group instead of per entity type.
- Text arm: a stroke group renders as SDF glyph quads when it carries a
GlyphRun (strokes suppressed); a run-less group keeps its strokes. TEXT /
MTEXT / dim / block / mleader are unchanged (all run-groups). A composite
Text object can now mix geometry + text: its run-less groups stay strokes and
its run-groups become SDF. The glyph-bounds AABB is hoisted so both the
bins-empty and bins-non-empty paths stamp it on the text wire.
- Tolerance: refactored to emit the feature-control frame (border, separators,
dividers) as run-less stroke groups and each cell's text as a run-group, so
the frame stays crisp geometry while the values/symbols render as SDF text.
- Complex linetypes: embedded LtSeg::Text renders as SDF glyph quads on an
extra text wire (glyph-bounds AABB) instead of stroke polylines.
- click_hits_all: include SDF text boxes so selection cycling steps through
text too, ranked after real geometry.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Flip `sdf_text_enabled()` to default ON: text renders as SDF glyph quads
(crisp at every zoom, no LOD, box-based snap/pick) for everyone. Set
`OCS_TEXT_SDF=0` to fall back to the legacy stroke path — a kill switch kept
in case a drawing or environment needs the old renderer. Off-screen text is
still view-culled per entity, so dropping the text LOD ladder stays bounded.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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 <[email protected]>
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 <[email protected]>
Copy-paste (CTRL+C / CTRL+V) dropped dimensions. A dimension draws from
its baked *D block, whose geometry is in absolute WCS coordinates, and
the clipboard paste path added the dimension entity but never gave it its
own transformed block — unlike the in-drawing COPY, which does (#161). So
the pasted dimension kept pointing at the source block and rendered at
the original location instead of the paste point (same drawing), or had
no block at all (cross-drawing).
Capture each copied dimension's *D block into the clipboard, and on paste
build a fresh transformed copy of it and re-point the dimension's
block_name — for same- and cross-drawing pastes alike. Factored the
block-building out of clone_transformed_block into a shared
define_transformed_block, and the block snapshotting out of
capture_blocks into snapshot_block.
The batched hatch pass drew a screen-covering AABB quad per hatch and,
for every fragment, ran a point-in-polygon ray-cast over the whole
boundary plus the pattern test, with a discard that disabled early-Z.
Zoomed into a hatch the quad covers the screen, so the shader re-ran
per pixel every frame — the cost the PR #273 interaction-LOD worked
around by suppressing hatches during navigation, which made fills
flicker on and off while zooming.
Triangulate each boundary once (lyon, even-odd — same coverage as the
old in_polygon, islands/holes included) and rasterize those triangles
instead: no screen-filling overdraw, no per-fragment boundary test. The
fragment shader keeps the boundary test only on a per-instance fallback
path (poly_test == 1) for boundaries that fail to tessellate.
Tessellation is cached with the batch build (geometry_epoch) in local
space, so the double-single relative-to-eye precision is preserved.
With the fast path the interaction-LOD is no longer needed; it is gated
behind OCS_HATCH_LOD (default off) as a safety net for now.
The SDF text collector re-laid out every glyph of every text entity on
each frame (pan/zoom included). Cache the vertex list on the Scene keyed
by (geometry_epoch, annotation-scale bits) — it only changes when geometry
or the scale changes — so pan/zoom reuses it. Glyph baking happens on the
cache-miss path only; the atlas is a persistent global and the texture
upload is already gated on the geometry epoch, so cache hits skip both the
layout and any re-upload. No behaviour change.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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 <[email protected]>
With OCS_TEXT_SDF set, text now renders only as SDF quads: the wire path
skips building glyph-stroke geometry (bins stays empty, so the existing
empty-wire branch still emits the insertion snap point). This removes the
double draw (strokes + SDF) and, as a side effect, fixes the long-standing
snap pollution (#276) — snapping near text no longer catches individual
glyph-stroke endpoints, only the insertion point.
Shared sdf_text_enabled() (cached env read) gates both the collector and
the stroke suppression so they stay in lock-step. Flag off: unchanged.
Verified in-app: TEXT/MTEXT/decorations render clean as single SDF, and
snap no longer hits glyph strokes. Click-pick on text is limited while the
flag is on (no stroke geometry) pending a text OBB pick box.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Route single-line attribute text through the same acad_text_encode path
TEXT uses (acadrust parse_plain_text, re-encoded for the stroke
tessellator). Multi-line attributes already flow through layout_mtext
(acadrust). No visible change — attribute text with %% codes rendered
correctly before via the local tokenizer; this shares the one parser.
TOLERANCE cells and complex-linetype text still use the local tokenizer
(specialized paths that rarely carry %% codes).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The SDF glyph path skipped decoration toggles, so underline / overline /
strikethrough lines were missing from OCS_TEXT_SDF text. Reserve a small
solid (all-255) cell in the glyph atlas and expose solid_uv(); layout_glyph_quads
now tracks the decoration runs (mirroring the stroke tessellator's
UNDER/OVER/STRIKE positions) and emits a thin quad per run whose corners
all sample the solid cell — the SDF shader fills it solid in the run
colour, so decorations render through the same pass as glyphs, no extra
pipeline. Verified in-app; SDF text now covers glyphs, %% specials, and
decorations for both TEXT and MTEXT.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Two related steps for the text pipeline:
- TEXT parsing now goes through acadrust's parse_plain_text (like MTEXT
uses parse_mtext). text::acad_text_encode parses the value's %% codes —
including %%u/%%o underline/overline and %%nnn — and re-encodes to the
stroke tessellator's \L/\O + resolved-Unicode grammar, so OCS no longer
tokenizes TEXT %% codes itself. (ATTRIB/TOLERANCE/DIMENSION still use the
local tokenizer for now.) Bumps the acadrust pin to the commit that
completes the TEXT/MTEXT control-code set.
- SDF text (OCS_TEXT_SDF) now covers MTEXT and every text-producing
entity, not just top-level TEXT. TextStroke carries a GlyphRun (layout
params); the collector walks convert()'s TruckObject::Text runs and
applies annotation scale exactly as tessellate does for strokes.
layout_glyph_quads shares lff::tokenize_run so %% specials resolve and
\L/\O decorations are skipped (decoration LINES in SDF are still TODO).
Verified: stroke rendering unchanged with the flag off; SDF glyphs render
correctly for TEXT and MTEXT with OCS_TEXT_SDF=1. 154 lib tests green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
OCS maintained a second MTEXT inline-code parser that duplicated
acadrust's `mtext_format::parse_mtext`. Replace it with an adapter
(`adapt_mtext_paragraphs`) that maps acadrust's structured MTextDocument
onto the existing layout types (`MTextLine`/`RunState`); the layout
engine (`layout_mtext`, `mtext_line_count`, and their callers) is
unchanged. Removed the hand-rolled `parse_mtext_paragraphs*` and its
helpers.
Validated during migration by a differential test (adapter vs the old
parser matched byte-for-byte over a 20-case corpus) and confirmed
rendering identically in the app; the differential test is replaced with
adapter-only unit tests. Relies on acadrust's new `MTextScalar` to tell
absolute `\H` from relative `\Hx` (bumped the acadrust pin).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Wires the SDF glyph atlas into the render loop end-to-end for top-level
TEXT entities, gated by the OCS_TEXT_SDF env var so the default path is
unchanged. Verified rendering on a real drawing: glyph quads land exactly
over the existing stroke text (position, size, rotation), confirming the
atlas → shader → pipeline → screen path and the RTE / UV math.
- text::text_run_placement — extracted from to_truck (behaviour-preserved)
so the stroke and SDF-quad paths compute an identical run placement.
- sdf_atlas::text_atlas — process-wide GlyphAtlas (Mutex/OnceLock), shared
by the collector (bakes glyphs) and the GPU upload (reads texels).
- Scene::sdf_text_vertices — collects per-glyph TextVertex for the frame's
TEXT entities via layout_glyph_quads + push_glyph_vertices. Empty unless
OCS_TEXT_SDF is set.
- ViewportData.text_verts + Pipeline text_pipeline/atlas/vbuf, upload_text
(rebuilds the R8 atlas texture when it grows), and a text render pass
drawn over the wires.
Still additive over the stroke text; only top-level TEXT is wired (MTEXT,
attributes, and block-internal text are next), and the collector re-runs
per frame (vertex caching by geometry epoch is a follow-up).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Groundwork to render CAD text as per-glyph SDF quads (box-quad snap,
perf, no LOD) instead of stroke-polyline wires. CPU side fully tested;
GPU side compiles but is not yet wired into the render loop.
- scene::text::sdf_atlas — GlyphAtlas: shelf packer + R8 SDF tiles baked
per (font, char). Unified field so one shader draws both kinds: TrueType
= signed distance to the filled outline, LFF stroke = pen band. Uses only
crates already in the tree (ttf-parser/lyon via the existing Glyph).
- scene::text::glyph_quads — layout_glyph_quads: lays a run out as per-glyph
quads using the exact tessellate_text_run transform, so quads land where
the strokes would. Per-char advance (TTF shaping is a follow-up).
- shaders/text.wgsl — SDF fragment shader (smoothstep over fwidth), RTE
vertex path mirroring image.wgsl, per-vertex colour + draw-order bias.
- scene::pipeline::text_gpu — TextVertex + double-single vertex builder,
R8 atlas texture upload, bind group, and the image-cloned pipeline.
13 unit tests across the three CPU stages, all green. Modules carry
#![allow(dead_code)] until the render-loop integration lands.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Remove the forces_endpoint_snap mechanism merged in #293:
CadCommand::forces_endpoint_snap, Snapper::snap_forced_corners /
forced_corner_modes, the PlotWindowCommand override, the on_tick +
click-commit wiring, and the associated tests. PLOTWINDOW now uses the
normal snapper like every other command.
Rationale: forcing Endpoint snap on regardless of the global OSNAP master
overrides an explicit user setting and is surprising — a plot window is a
coarse print region that usually wants a margin, not a corner lock, and
the forced set only caught Endpoint (not center/mid/intersection) so it
was a partial, opinionated helper. Parking it pending a design discussion
with the PR author; can be reinstated behind a clearer UX (e.g. respecting
OSNAP, or an opt-in modifier) if justified.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Three defects found reviewing the just-merged PRs:
- PLOTWINDOW forced corner snap (#293) only ran in the on_tick preview
path, so the marker highlighted an endpoint but the click committed a
plain snap — with the global OSNAP master (or Endpoint mode) off the
corner landed at the raw cursor, defeating the feature. Apply the same
snap_forced_corners branch at the click-commit recompute.
- build_dxf_pattern (#292) wrote the pattern line's LOCAL step into the
world-frame HatchPatternLine.offset. The new prebaked reader
inverse-rotates offset assuming world frame, so app-created hatches
(HATCH command) collapsed their spacing by cos(angle) — ANSI31 at 45deg
rendered 2.245 instead of 3.175 on both viewport and PDF/plot export.
Rotate the local step into world here so it round-trips and the stored
offset is format-correct for other CAD apps.
- far_from_origin_pattern_hatch_still_fills placed its offset ALONG the
45deg lines (projects to k~0), so it never exercised the span-cap fix and
passed even with the old absolute-index clamp restored. Move the offset
perpendicular to the lines so |k| >> the cap. Add
app_created_hatch_roundtrips_catalog_spacing to guard the offset-frame
fix (verified: fails at 2.245 without it, passes at 3.175 with it).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
PLOTWINDOW / PW now forces Endpoint object-snap while picking its two
corners, independent of the global OSNAP toggle, so the plot area lands
exactly on frame corners. The snapper's master toggle and configured
modes are saved and restored around the pick, so the user's OSNAP
settings are unchanged afterward.
- CadCommand::forces_endpoint_snap() opt-in (default false; PlotWindowCommand true).
- Snapper::snap_forced_corners() forces Endpoint only, then restores state.
Intersection is deliberately excluded: it is O(n^2) over the hit-test wire
set and froze the UI on large drawings when a plot window is picked
zoomed-out. Frame corners are endpoints, so Endpoint alone suffices.
- the cursor-move handler routes the plot-window pick through the forced path.
Works in model and paper space (shared pick path). Tests: opt-in flag;
forced mode set is endpoint-only; snapper state is restored after the call.
Hatched content — especially fills nested inside a block INSERT, such as a
title-block logo — plotted incorrectly: missing, mis-spaced, or with phantom
bars. Five related fixes make the PDF/plot output match AutoCAD.
1. Export block-internal hatch fills.
The export collected only hatches owned directly by the layout block, so a
hatch nested in a block INSERT was dropped and printed as bare monochrome
outlines. The insert-explosion the viewport already does is extracted into a
shared `exploded_insert_hatch_models()` and called from both the viewport
(`synced_hatch_models`) and the export (`paper_canvas_hatches`), so a plot
draws block-internal hatches identically to the screen.
2. Honour the hatch's own stored pattern-line spacing.
`hatch_model_from_dxf` re-derived pattern spacing from the name-matched
catalog entry x pattern_scale, ignoring the resolved line geometry the DWG
stores on the hatch. When a drawing was authored against a different base
spacing (imperial 0.125 vs the catalog's metric 3.175 for ANSI31), lines
came out up to ~25x too coarse and a dense fill collapsed to a few stray
lines. When the hatch carries its own line geometry it is now used directly
(identity scale/angle); the catalog path remains the fallback.
3. Fill far-from-origin pattern hatches.
`pattern_segments` clamped the ABSOLUTE scan-line index to
+/-MAX_LINES_PER_FAMILY. A fine-spaced hatch far from the pattern origin has
large-magnitude indices at both ends but a small span, so the clamp inverted
the range and emitted nothing — silently dropping the fill. Cap the line
count (span) instead of the absolute index.
4. Skip TEXTBOX boundary paths.
TEXTBOX boundary paths (flag bit 3) are text bounding-boxes AutoCAD derives
for island detection; they are never drawn or filled. Treating one as a fill
boundary painted its rectangle solid — a phantom bar. It is now skipped when
building the fill boundary.
5. Print white/ACI-7 hatch fills black on paper.
Hatch fills arrive adapted to the dark screen background, so a white/ACI-7
fill would vanish white-on-white on the sheet. Mirror the wire pass:
near-white/near-yellow -> black, near-cyan -> dark blue, matching AutoCAD's
colour-7-on-white plotting. Wipeouts keep their paper-white mask.
Adds `tests/block_hatch_export.rs` covering: block-internal hatch reaches the
export set, stored-line spacing is honoured, far-from-origin fills are not
dropped, and TEXTBOX paths are not filled.
- Reword the two new explanatory comments to describe the behaviour on its
own instead of citing another product.
- window_to_sheet: drop the unused window_min parameter (the caller already
derives its coordinate offset from the returned min-corner position) and fix
the doc comment to match what the function returns.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Ribbon:
- A ▾ selector at the right of the tab row picks the tool-panel density —
Auto (size to window), Full, Compact, or Collapsed. The choice is persisted.
It hides itself when the tool row is already tight, giving the cramped tab
row its space back.
Start page:
- The Recent Documents card is restyled to match the Supporters card
(rounded corners, 20px padding, matching title + colours).
- Its scrollbar no longer sits on top of the row ✕ remove buttons (a gutter
is reserved).
- A footer control sets how many recent files to keep: [-] an editable count
[+] with the max shown. Typing applies on Enter; an over-max entry snaps to
the max. The limit is persisted and enforced on load and on every open.
- The "Support on Patreon" heart is now the SVG icon instead of a font glyph.
Config:
- Add a shared `config::config_dir()` helper; the recent-files and status-bar
stores now go through it instead of each re-deriving the path. New minus.svg
icon for the stepper.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Pick a rectangular window in model space with the existing PLOTWINDOW command (now enabled in model space, previously paper-space only) and export it to a PDF at a chosen ISO paper size (A4-A0), orientation and scale (fit or ratio). The window is a hard clip boundary. Adds paper-size presets, a window-to-sheet transform, scale + clip support in the PDF exporter, and Page Setup controls (format/orientation/pick window).
XLine/Ray tessellate as +/-1e6 display segments (entities/ray.rs), and
both fit_all outlier defenses miss them: the IQR reject passes because a
construction line through the drawing has its centroid at its base
point, inside the consensus cluster; and the per-point lim filter passes
because local_extent_max is computed once at document load and stays at
the 1e9 default for drawings created fresh in-app. The far endpoints
then poison the bounds and the view fits +/-1e6, shrinking real
geometry to a dot.
Exclude XLine/Ray wires from the extents up front (AutoCAD likewise
ignores infinite lines in ZOOM Extents), falling back to fitting their
base points when the drawing holds nothing else.
Regression tests verified to fail without the fix (camera distance
3,000,000) and pass with it.
Co-Authored-By: Claude Fable 5 <[email protected]>
Adaptive tool-panel row now keeps degrading past the collapsed state instead
of overflowing:
- Panels still shrink from the right one at a time (full -> compact ->
collapsed). Once every panel is a collapsed button and the row still
overflows, each button drops its large icon to a small one (tight) together,
then the buttons are pulled together up to 8px per gap (reclaiming edge
padding) before anything is clipped.
- Tool-area height now tracks the tallest shown panel, so the ribbon shrinks
as its panels collapse; the measured height is reported for dropdown
anchoring.
- Tabs squeeze their inter-tab gaps shut before wrapping to a new row
(WrapFlow min_spacing_x).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
When the window is too narrow for the full properties panel it now
collapses to a slim vertical "Properties" bar (its label drawn rotated 90°
on a canvas), freeing width for the viewport. Clicking the bar expands the
panel back out alongside the bar; clicking it again re-collapses
(props_expanded state + TogglePropertiesBar). Reuses the rotated-label bar
helper originally built for the Start page.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Rebuild the Start page as three parts — Recent Files (left), Welcome
(center) and Supporters (right) — that sit side by side while they fit.
When the page is too narrow for all three it falls back to a tab bar
(Recent Files / Welcome / Supporters) showing one section at a time, with
Welcome open by default (new StartSection state + StartSectionSelect).
The Recent Documents list moves off the left properties slot and into the
page itself. The Welcome column's button rows now flex-wrap, and the two
intro-video cards sit side by side, wrap to a stacked column when the
width gets tight, then shrink to fit the remaining height so they never
spill over the status bar. Trimmed the headline to just "Open CAD Studio"
(dropped "Welcome to" + the subtitle) and the "Release Notes" button, and
tightened the page's side padding.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- The Properties panel's Match button now shrinks to a small icon when the
panel is compacted (render_large gained a `compact` flag), so the panel
actually gets narrower and joins the from-the-right degradation instead
of jumping straight to collapsed.
- A collapsed Properties panel now shows its Match button as the
representative face: item_id recognises PropertiesGroup and
collapse_button renders its match_prop as a large tool.
- The undo/redo history controls are wrapped in PosReport, so their
dropdowns anchor directly below the button (and flip at the edge) like
every other dropdown, instead of falling back to the corner.
- Panel dividers are back: CollapsePanels draws a 1px line between adjacent
panels, skipped only between two collapsed panels.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The tool area no longer swaps every panel between a full and a compact
row together. CollapsePanels is now the whole tool area: it measures each
panel at full, compact and collapsed widths and degrades from the right —
first shrinking a panel's large buttons to compact icon columns one panel
at a time until the row fits, then (only if all-compact still overflows)
collapsing the rightmost panels to their ▾ flyout buttons. Left panels
stay full as long as possible.
Removes the global DensitySwap full/compact/collapse variant selection and
the now-unused build_tool_groups / tool_divider helpers.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
All ribbon dropdowns (tool dropdowns, undo/redo history, and the layer /
colour / linetype / lineweight / style combos) and all status-bar popups
(scale, units, OSNAP, isolate, filter, customization menu, layout list)
now open directly at their widget instead of at a layout-estimated
position, and flip to the other side when the panel would cross a window
edge.
Each dropdown/pill widget is wrapped in a transparent PosReport that
records its screen bounds into a thread-local map on draw. The overlays
read those bounds:
- Ribbon: Ribbon::dd_anchor(id, panel_w, win_w) returns
(align_right, h_pad, top); position_ribbon_dropdown places the panel
below the widget, flipping right-aligned near the right edge.
- Status bar: popup builders take (pill_bounds, win_size) and call
popup::position_statusbar_popup / popup_anchor, opening just above the
pill and flipping left/right at the window edge.
The window size is now tracked (win_size, set on WindowResized). The old
layout estimators compute_dropdown_left / compute_layer_combo_left /
compute_prop_combo_left / compute_history_dropdown_left and
osnap_popup_right_offset are deleted.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Third stage of the adaptive tool area (shrink -> compact -> collapse).
When even the compact panels don't fit, DensitySwap picks a new
CollapsePanels widget that keeps a left prefix of panels inline and
collapses the rest to buttons; clicking a button opens the full panel as
an overlay flyout anchored below it (closes on outside press, mirroring
color_select's Floating).
A collapsed panel's button shows a live representative tool — the panel's
last-used tool (tracked per panel in Ribbon::note_panel_tool on every
tool click), defaulting to its first — rendered as a large icon. To
support this, render_small now also renders Large* items small and
render_large now renders plain Tool/Dropdown items large. Large dropdowns
render as icon + label + a bottom arrow strip via a shared
render_large_dropdown helper, used in both the full ribbon and collapsed
panels.
Ribbon dropdowns (tool dropdowns + undo/redo history) now anchor their
overlay directly below their widget at any size: a transparent PosReport
wrapper records each dropdown button's screen bounds into a thread-local
map on draw, and the overlays read it via Ribbon::dd_anchor instead of the
layout-estimating compute_*_left helpers.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
DensitySwap now takes a third, narrowest variant: the compact panels
flex-wrapped (via WrapFlow) across as many rows as the width needs. The
tool area therefore degrades full -> compact -> wrapped as the window
shrinks. build_tool_row becomes build_tool_groups, returning fixed-height
panel elements so they lay out identically in a row or a wrap flow.
The tool bar is no longer a fixed TOOL_BAR_H; DensitySwap reports its
measured height into a cell and the ribbon dropdown overlays anchor at
tab_bar_height() + tool_bar_height() so they track the taller bar.
Second stage of the adaptive tool area (shrink -> wrap -> collapse).
Co-Authored-By: Claude Opus 4.8 <[email protected]>