Commit Graph
1354 Commits
Author SHA1 Message Date
Hakan Seven 0e368934ea chore: release v0.7.7 v0.7.7 2026-07-08 00:33:34 +03:00
Hakan SevenandClaude Opus 4.8 b446786d7d feat(supporters): merge hand-maintained supporters, rank by amount
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]>
2026-07-08 00:32:12 +03:00
Hakan SevenandClaude Opus 4.8 31ac5372d6 feat(command): clickable option buttons in the command line (#304)
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]>
2026-07-08 00:16:59 +03:00
Hakan Seven efa4bdd5e1 Merge PR #306: surface About command in Manage ribbon (#302) 2026-07-07 23:36:43 +03:00
Kevin GriffinandClaude Fable 5 8693857cfb feat(manage): surface About in the ribbon — Application group (#302)
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]>
2026-07-07 13:27:31 -07:00
0964e97cfa fix(text): keep inline \C colours through block colour inheritance (SDF)
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]>
2026-07-07 22:17:27 +03:00
Hakan SevenandClaude Opus 4.8 8498b08abf Merge PR #303: paste copies dimensions correctly (#290)
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]>
2026-07-07 21:57:51 +03:00
Hakan SevenandClaude Opus 4.8 fe00bc30ba Merge PR #300: tessellate hatch fills so navigation no longer flickers (#258)
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]>
2026-07-07 21:45:02 +03:00
Hakan SevenandClaude Opus 4.8 fa322991c6 fix(text): clip SDF text to xclip boundary; stop debug-assert crash on block fills
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]>
2026-07-07 21:32:17 +03:00
Hakan SevenandClaude Opus 4.8 5c15091e74 refactor(text): SDF for all text kinds; remove the legacy stroke text path
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]>
2026-07-07 21:07:00 +03:00
Hakan SevenandClaude Opus 4.8 67ce45d40b feat(text): SDF for synthesized-table cell text + guard table pick box
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]>
2026-07-07 18:37:17 +03:00
Hakan SevenandClaude Opus 4.8 d9a2c8245c feat(text): SDF for tolerance + complex-linetype text; per-group model; pick cycle
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]>
2026-07-07 18:22:08 +03:00
Hakan SevenandClaude Opus 4.8 96181563fb feat(text): make SDF text the default renderer
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]>
2026-07-07 17:47:06 +03:00
Hakan SevenandClaude Opus 4.8 cd7306f530 feat(text): MTEXT background fill / mask + text frame, editable in Properties
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]>
2026-07-07 17:41:16 +03:00
Hakan SevenandClaude Opus 4.8 110683f419 refactor(text): per-entity SDF text — all text kinds, selection/hover highlight
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]>
2026-07-07 17:01:36 +03:00
sLuCHa c20f277e54 fix(clipboard): paste copies dimensions correctly (#290)
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.
2026-07-07 15:53:21 +02:00
sLuCHa b4152ae1fe perf(hatch): tessellate fills so navigation no longer flickers (#258)
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.
2026-07-07 15:23:54 +02:00
Hakan SevenandClaude Opus 4.8 fd0b8fc1d4 perf(text): cache SDF text vertices across frames
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]>
2026-07-07 08:29:18 +03:00
Hakan SevenandClaude Opus 4.8 75ddab6da7 feat(text): bypass the greek/baseline LOD ladder for SDF text
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]>
2026-07-07 08:24:11 +03:00
Hakan SevenandClaude Opus 4.8 9f60470067 feat(text): suppress glyph strokes when SDF text is on
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]>
2026-07-07 08:18:08 +03:00
Hakan SevenandClaude Opus 4.8 1709d41e4f refactor(text): parse single-line ATTRIB text via acadrust
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]>
2026-07-07 01:15:36 +03:00
Hakan SevenandClaude Opus 4.8 a561b088d2 feat(text): draw SDF text decorations (underline/overline/strike)
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]>
2026-07-07 01:11:05 +03:00
Hakan SevenandClaude Opus 4.8 d528a3e2a5 feat(text): parse TEXT via acadrust + extend SDF text to MTEXT
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]>
2026-07-07 00:53:19 +03:00
Hakan SevenandClaude Opus 4.8 b88e5e4ad5 refactor(mtext): parse MTEXT via acadrust, drop OCS's own parser
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]>
2026-07-07 00:04:11 +03:00
Hakan SevenandClaude Opus 4.8 896b6c385c feat(text): render TEXT as SDF quads behind OCS_TEXT_SDF flag
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]>
2026-07-06 23:02:34 +03:00
Hakan SevenandClaude Opus 4.8 374cf7e89f feat(text): SDF glyph-atlas foundation for shader text rendering
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]>
2026-07-06 22:03:36 +03:00
Hakan SevenandClaude Opus 4.8 9490c85b32 revert: drop PLOTWINDOW forced endpoint snap (#293)
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]>
2026-07-06 20:35:50 +03:00
Hakan SevenandClaude Opus 4.8 cb41910f12 fix: address review findings on merged #293/#292 hatch+snap
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]>
2026-07-06 20:23:59 +03:00
Hakan Seven dcef60c6b0 Merge PR #292: correct hatch rendering in PDF / plot export 2026-07-06 20:15:03 +03:00
Hakan Seven 1e8097d944 Merge PR #293: snap plot-window corners to geometry endpoints 2026-07-06 20:15:03 +03:00
sLuCHa 6cd422e5b9 feat(plot): snap plot-window corners to geometry endpoints
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.
2026-07-06 13:38:56 +02:00
sLuCHa 1a45f6bd97 fix(plot): correct hatch rendering in PDF / plot export
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.
2026-07-06 12:24:10 +02:00
Hakan SevenandClaude Opus 4.8 b50921923f chore: post-merge cleanup for #287/#289
- 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]>
2026-07-06 01:22:10 +03:00
Hakan Seven dd4f678c41 Merge PR #287: plot a model-space window to PDF 2026-07-06 01:17:03 +03:00
Hakan Seven 72233cd2fa Merge PR #289: exclude XLine/Ray display segments from ZOOM Extents (#284) 2026-07-06 01:16:52 +03:00
Hakan SevenandClaude Opus 4.8 f4ed263425 feat(ui): ribbon collapse-mode selector + start-page recent controls
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]>
2026-07-06 01:06:01 +03:00
sLuCHa 363593b09f feat: plot a model-space window to PDF
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).
2026-07-05 23:13:43 +02:00
Kevin GriffinandClaude Fable 5 a3107d95e4 fix(view): exclude XLine/Ray display segments from ZOOM Extents (#284)
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]>
2026-07-05 14:08:43 -07:00
Hakan SevenandClaude Opus 4.8 8e6146fd91 feat(ribbon): tighten + squeeze collapsed panels, content-driven height
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]>
2026-07-05 23:58:18 +03:00
Hakan Seven ced42daff5 chore(release): v0.7.6 v0.7.6 2026-07-05 21:05:51 +03:00
Hakan SevenandClaude Opus 4.8 19a7343c80 feat(properties): collapse the panel to a vertical bar on a narrow window
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]>
2026-07-05 21:02:05 +03:00
Hakan SevenandClaude Opus 4.8 e2a56033d4 feat(start): three-part start page (Recent · Welcome · Supporters) that tabs when narrow
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]>
2026-07-05 20:53:11 +03:00
Hakan SevenandClaude Opus 4.8 6a5402ccae fix(ribbon): Properties panel compact/collapse, undo-redo anchoring, restore panel dividers
- 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]>
2026-07-05 19:23:48 +03:00
Hakan SevenandClaude Opus 4.8 5164dc1387 feat(ribbon): degrade tool panels per-panel from the right instead of all at once
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]>
2026-07-05 19:04:47 +03:00
Hakan SevenandClaude Opus 4.8 39043e6e56 feat(ui): anchor every ribbon dropdown and status-bar popup to its widget, flipping at window edges
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]>
2026-07-05 18:51:54 +03:00
Hakan SevenandClaude Opus 4.8 56e48b8d0f feat(ribbon): collapse overflowing panels to flyout buttons, anchor dropdowns to their widget
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]>
2026-07-05 18:04:55 +03:00
Hakan SevenandClaude Opus 4.8 ff36a1bdac feat(ribbon): wrap compact panels onto extra rows when still too narrow
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]>
2026-07-05 16:51:26 +03:00
Hakan SevenandClaude Opus 4.8 65dfd4e462 feat(ribbon): shrink large tools to compact icons when the ribbon is narrow
Add a DensitySwap widget that renders the widest variant of the tool
area that fits the current width. The ribbon builds two variants via the
new build_tool_row(compact) helper: full-size panels, and a compact one
where LargeTool/LargeDropdown collapse to small icon columns (the
layer/properties/style combos always stay full). render_small now also
matches the Large* variants so they can be drawn small.

First stage of the adaptive tool area (shrink -> wrap -> collapse).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 16:44:44 +03:00
Hakan SevenandClaude Opus 4.8 a065dc939f feat(ui): flex-wrap the document tab bar when tabs overflow
The Start tab, drawing tabs and the "+" button now live in a single
WrapFlow instead of a fixed-height Row, so they spill onto lower rows
when there are more tabs than the width can hold. Drops the fixed 30px
height and the trailing Fill spacer.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 16:44:44 +03:00
Hakan SevenandClaude Opus 4.8 9e1b997631 feat(ui): flex-wrap the ribbon top bar and status bar when narrow
Add shared adaptive-bar widgets in src/ui/wrap_bar.rs:
- WrapBar: two-block bar (lead + optional Fill middle + trail). Fits on
  one row, otherwise the trailing block drops below; with no middle both
  blocks flex-wrap in their own row bands and stack so a wrapped lead item
  never shares a row with a trailing item.
- WrapFlow: CSS-style flex-wrap row that spreads its items across as many
  rows as the width needs.

Ribbon tab row: quick-access buttons and undo/redo are merged into one
WrapFlow; the tabs are a second WrapFlow. Dropdown overlays anchor to the
measured (possibly taller) bar height instead of a hardcoded 28px.

Status bar: the hamburger menu plus the Model/layout tabs form one
WrapFlow; the status pills form another. The layout-tab scroll arrows and
horizontal scrollable are removed in favour of wrapping.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 16:17:44 +03:00