Rewrite the in-app MTEXT editor to run on acadrust's structured
MTextDocument model end-to-end: load via parse_mtext, edit a flat Cell
model, serialize via to_mtext_string. Retires the hand-rolled format-code
string handling, the parallel visible_spans parser, the composed_value
prefix, and the Edit-raw-code tab.
Selection and copy:
- double-click selects the word, triple-click selects all (click-counting)
- Ctrl+A selects all text
- Ctrl+C copies the selected text to the clipboard
Formatting:
- per-selection Bold/Italic/Underline/Overline/Strike/Case/Alignment
- Bold renders as a visibly thicker SDF pen, keyed (family, char, bold)
through the whole text pipeline; the resolved style font name is stamped
so the \f...|b1; code survives serialization
- per-selection font and color changes
- editor color dropdown reuses the Properties panel color selector
Global toolbar defaults (font/color/oblique/width/tracking) fold onto the
document as real span properties instead of a leading code prefix; a
trailing empty paragraph is preserved so Enter at the end keeps its line.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Move the editor's body edit ops (typing, Enter, backspace, delete, paste,
selection replace/delete) off the raw-string + visible_spans splice and
onto acadrust's structured MTextDocument.
The document is flattened to a Vec<Cell> (one cell per visible slot: a
char, a paragraph break, or an atomic stacking-run glyph), spliced/drained
by flat visible index, and rebuilt into paragraphs+spans — so Enter splits
a paragraph, Backspace/Delete at a boundary merges, cross-paragraph
selection delete works, and stacking fractions stay atomic. rebuild is the
single doc-sync point (re-parse + restore the trailing empty paragraph that
parse_mtext drops, else Enter at the end loses the new line).
Also: the caret on a fresh trailing line — which the layout emits no glyph
box for — is made visible by injecting a space into the preview value only
(never committed) so the layout lays that line out and the caret gets a
correctly-sized box on it.
visible_spans now serves only the formatting (\L/\Q/…) splice path, which
moves to structured properties in the next phase. Adds 9 unit tests for the
cell model + round-trip/trailing-break handling.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
First phase of moving the in-app MTEXT editor onto acadrust's structured
MTEXT model. Adds a `doc: MTextDocument` field kept in sync with the raw
content via `parse_mtext` on every preview rebuild, plus `VisSlot` and
`doc_vis_map` — the unified visible-index space that will replace the
hand-rolled `visible_spans` walker (it matches the layout's vis counting:
one caret slot per paragraph break, one per visible char, none for a tab).
Purely additive scaffolding: nothing consumes the doc yet, so editor
behaviour is unchanged. Later phases make the doc authoritative for edits,
formatting, commit and load, then delete the raw-string path.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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 <[email protected]>
On the Start page there is no drawing to act on, but ribbon panel
command icons still dispatched into the empty welcome tab. Guard
on_ribbon_tool_click: when the active tab is the Start page, close any
open flyout, show a "use New or Open first" hint, and do nothing. The
quick-access New / Open / Save buttons use a separate path and stay
available, as do the welcome page's own actions.
The icons are not yet visually greyed out (that needs an enabled flag
threaded through the ribbon render) — this stops them from running.
Refs #299
Co-Authored-By: Claude Opus 4.8 <[email protected]>
HELP was registered for autocomplete but had no dispatch arm, so it
reported an unknown command. Route it through open_url to the project's
GitHub Discussions page, matching the DONATE/WEBVERSION external-link
commands.
Closes#307
Co-Authored-By: Claude Opus 4.8 <[email protected]>
ATTMAN and BATTMAN previously produced a command-line listing of block
attributes. Route both to the attribute editor instead: extract the bare
ATTEDIT behavior into a shared open_attedit_dialog helper (edit the
selected block, or start the block-pick command) and call it from the
ATTEDIT, ATTMAN, and BATTMAN arms.
Kept as a match arm rather than an ocad.pgp alias so it works for
existing users, whose config pgp is only seeded on first run.
Replace the listing regression tests with one asserting ATTMAN and
BATTMAN behave byte-for-byte like ATTEDIT.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
External PR by Kevin Griffin (@KevinGriffin-new).
The ATTMAN/BATTMAN arm stripped the argument with
trim_start_matches("ATTMAN"), which left "BATTMAN" intact as a bogus
block-name filter, so BATTMAN always reported 'no matching block'.
Strip whichever keyword was actually typed (BATTMAN first, since ATTMAN
is a suffix of it) and match the 'BATTMAN ' prefix form. Adds two
regression tests driving the real command dispatcher.
External PR by Karim Jerbi (@KarimJerbi).
- NAVVCUBE is a command toggle so it never set active_tool; add a
show_viewcube flag to the ribbon and special-case is_active_tool so the
button highlights only while the cube is on.
- show_viewcube was absent from render_signature, so toggling a cached
view produced an identical signature and left stale cube pixels; hash
it into the signature so the toggle invalidates the cache and clears.
Replace the custom in-app file-browser save dialog with the native OS
save dialog for choosing the destination path — folder browsing and
overwrite confirmation now come from the OS. A compact in-app modal
still selects the DWG/DXF format and version (a native dialog cannot
carry the version year) and appears only for Save As. Plain Save on an
unsaved drawing defaults to DWG 2018 with no picker. The file-name field
is shown only on web, where there is no native dialog.
Remove the hand-rolled directory listing, folder navigation, and
overwrite modal.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
show_viewcube was excluded from render_signature, so toggling a static (cached) view produced an identical signature — the frame was skipped and the last GPU cube pixels stayed as stale cache. Add vp.show_viewcube to render_signature so the toggle invalidates the cache and re-renders, clearing the cube. The 2D overlay is iced UI and re-laid-out correctly, which is why only it hid.
NAVVCUBE is a command toggle, so it never set active_tool and is_active_tool never matched it. Add a show_viewcube flag to the ribbon (mirroring wireframe/ortho_mode) and special-case "NAVVCUBE" in is_active_tool to return the live show_viewcube state; ToggleViewCube now calls ribbon.set_viewcube.
Round-trip a raster preview through the DWG's embedded preview slot and
surface it to the OS file managers, so drawings show their contents in
Explorer / Finder / Nautilus instead of a generic document icon.
OCS:
* io::thumbnail — rasterize the scene to a BMP DIB on save (embedded via
acadrust's new Preview type) and read it back on open for the Start
page recent list; `--dwg-thumbnail IN OUT SIZE` CLI extracts a badged
PNG for external thumbnailers.
* io::file_association::install_thumbnailer — self-install the OS
integration on startup: Linux writes a .thumbnailer + hicolor mimetype
icons; Windows registers the IThumbnailProvider DLL under HKCU.
Shared core:
* crates/dwg-thumbnailer — lean (image-only) preview extractor + the
`badge_dwg` full-width "DWG" band, used by every platform so the
ribbon is single-sourced. Ships an rlib+staticlib.
* crates/dwg-thumbnailer-win — IThumbnailProvider COM in-proc server
(cfg(windows), CI-built as a workspace member).
* macos/ — QuickLook thumbnail extension (Swift + C-ABI bridge to the
core staticlib), assembled into a .appex.
Icons & packaging (single SVG source -> per-platform assets in CI):
* assets/mimetypes/image-vnd.{dwg,dxf}.svg — themed file icons.
* packaging: WiX ships dwg/dxf.ico + the thumbnail DLL and points the
ProgIds at them; Info.plist gets CFBundleTypeIconFile + the QuickLook
extension in Contents/PlugIns.
* release.yml — generate .ico (Windows) and .icns (macOS) from the SVGs,
build the QuickLook .appex, and bundle everything.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
BATTMAN matched the ATTMAN command arm but the argument was extracted
with `cmd.trim_start_matches("ATTMAN")`. "BATTMAN" has no leading
"ATTMAN" to strip, so the whole word survived as a bogus block-name
argument — BATTMAN always reported "ATTMAN: no matching block" instead
of listing attributes. `BATTMAN <name>` never matched at all.
Strip whichever keyword was actually typed (BATTMAN first, since ATTMAN
is a suffix of it) with `strip_prefix`, and match the `BATTMAN ` prefix
form too. BATTMAN now behaves identically to ATTMAN.
Adds headless regression tests driving the real dispatcher; they fail on
the previous code with the reported symptom and pass with the fix.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Generate a small wire-raster thumbnail when saving a DWG and embed it via
acadrust's preview API, so OCS drawings show a thumbnail in file browsers
and other CAD applications. Read the embedded preview back cheaply (file
header + preview bytes only, no full parse) to show a thumbnail per file
in the Start page's Recent Documents panel.
- io/thumbnail.rs: CPU rasterizer (current-layout wires -> BMP DIB) plus
a preview-only reader/decoder (BMP/PNG -> iced handle) for the Start
page.
- Embedded on save via stamp_thumbnail on both save paths.
- recent_thumbs cache filled on config load / recents change (never from
a view); a thumbnail is drawn beside each recent file.
Requires acadrust at 3111694 (preview read/write), already pinned in
Cargo.lock.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Two narrow-mode (e.g. phone-width) ribbon fixes; both are no-ops when the
row has room, so desktop layouts are unchanged.
- Tab row: let the inter-tab gap squeeze from 6px down to -12px, tucking
neighbouring tabs into each other's 14px side padding (labels never
overlap) so all tabs stay on one row instead of wrapping "Manage" onto a
second line. The gap only goes negative as far as needed to fit.
- Tool panels: at the tightest collapse step each panel is now a single
non-running button — the panel's first tool icon + title + a down arrow
that opens the tools flyout. No tool runs directly at this density; you
pick one from the dropdown.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Prebaked hatch fills anchored each pattern family at its authored
base point minus world_origin. On UTM-scale drawings that offset is
~1e6, which broke the fill two ways:
- the shader consumes x0/y0 as f32, so ~0.5 m quantization shredded
the pattern phase; and
- it evaluated the pattern that far from its origin, where the
multi-family aggregate patterns (AR-CONC, AR-SAND, GRAVEL, ...) —
effectively quasi-periodic — dissolve from their interlocking
stones into scattered dashes.
Boundaries were unaffected throughout; they ride the double-single
relative-to-eye path.
Fold the origin's offset from world_origin down to a small,
coherence-safe remainder on a `spacing * 64` grid, applied as one
common fold so every family keeps the relative phase that forms the
stones. Because the origin grip / Origin X/Y edit shifts every base
point by the same delta, the sub-grid remainder tracks it 1:1, so the
grip still moves the fill; a grid crossing needs a full `spacing * 64`
drag and is never hit by realistic alignment drags.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
App preferences were spread across six flat plain-text files (settings.txt,
recent.txt, recent_limit.txt, statusbar.txt, ribbon.txt, plot.txt), each with a
bespoke line parser and no grouping. Replace them with a single grouped JSON,
serialized via serde (enabling the derive feature; serde_json was already a
dependency).
- New app/config.rs: AppConfig { settings, recent, statusbar, ribbon, plot } →
<config>/settings.json (pretty JSON), load()/save().
- Make UserSettings, StatusBarConfig/StatusPill, CollapseMode and
PlotDialogState serde-derived; #[serde(skip)] marks the plot dialog's runtime
fields so only print preferences persist. Their bespoke .txt load/save is
removed.
- App gains current_config/apply_config/save_config: one AppConfig::load at
startup distributes into live state; every settings-change site
(persist_settings_if_changed, recent add/remove/limit, status-bar pill
toggle, ribbon density, plot commit) routes through save_config, which diffs
against the last write to avoid thrashing.
- Clean start: the old flat files are ignored (no migration). ocad.pgp stays a
separate hand-editable file.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Ortho and the running object-snap set are stored in the drawing header
($ORTHOMODE / $OSMODE) since they were bound to the file, so persisting them
app-globally in settings.txt too was redundant. Remove the ortho, osnap and
snap_modes keys (and the now-dead modes_from / snap_id / snap_from_id helpers)
from UserSettings, and stop reading/writing them in apply_settings /
current_settings.
No boot behavior change: Snapper::new already defaults to the same running-snap
set (Endpoint, Midpoint, Center, Node, Quadrant, Intersection, Nearest) with
the master off, and Ortho defaults to false — exactly what settings.txt used to
seed. Old settings.txt files keep working; the stale keys are simply ignored.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The Coords pill always showed live absolute X,Y,Z, ignoring the drawing's
$COORDS (coords_mode) sysvar. Drive the readout from it:
- 0 static — the last picked point; the readout freezes between picks.
- 1 live — continuous absolute X,Y,Z (previous behavior, default).
- 2 polar — distance<angle relative to the last point while a command is
prompting for a point; absolute otherwise.
The cursor and last point are reported in the active UCS, matching the rest of
the readout. Clicking the pill cycles the mode 0→1→2→0 (CycleCoordsMode), marks
the tab dirty and echoes it; SETVAR COORDS still works. $COORDS round-trips per
drawing via acadrust's coords_mode.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The status bar showed all 17 pills out of the box. Hide the informational /
niche ones by default so the bar stays uncluttered — Coordinates, Show
Lineweight, Dynamic Input, Model/Paper Space, Drawing Units, Show
Transparency, Selection Cycling and Viewport Count. The user turns any of them
back on from the customization (gear) menu, which persists to statusbar.txt.
The main drafting toggles (Ortho, Polar, Otrack, Osnap, …) stay visible.
An existing statusbar.txt (even empty = user showed everything) is
authoritative; only a fresh install with no config gets the new defaults.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Ortho and the running object-snap set were app-global (settings.txt only) and
Isolate/Hide was a session-only view state, so none survived save/reopen and
none were per-drawing. Bind all three to the file like AutoCAD:
- Ortho -> header.ortho_mode ($ORTHOMODE); running OSNAP <-> header
.object_snap_mode ($OSMODE bitmask) via a new SnapType<->bit map in settings
(osmode_from_snaps / snaps_from_osmode, round-trip tested). The header is the
per-drawing store, live state mirrors the active drawing: adopt header->live
on open and tab-switch, stamp live->header on save and when leaving a tab, a
fresh drawing inherits the current values, and SETVAR ORTHOMODE/OSMODE now
also update the live state. settings.txt stays as the boot/new-drawing
default; the per-file header overrides it on open.
- Isolate/Hide now set each entity's persisted invisible flag (DXF code 60,
which acadrust round-trips in DXF and DWG) in addition to the session set;
End Isolation clears it. The set is reseeded from invisible entities on load
and after undo/redo, the commands are undoable and mark the tab dirty. The
renderer already skipped invisible entities, so no render change was needed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
When the viewport got too narrow the top-left render-mode bar and the
top-right ViewCube overlapped. Hide them adaptively, measuring the render
bar's real width rather than guessing:
- Wrap the render bar in DensitySwap, which measures its natural width every
frame and swaps it for an empty spacer once it no longer fits the tile. Add
report_width0 so it publishes that measured width to render_bar_w.
- Compute viewcube_visible from that measured width: the cube shows only while
bar_w + gap + cube region fits. This one flag drives the ViewCube nav/UCS
widgets, the hover hit-test, and — threaded through build_viewports /
build_viewport_for_pane / viewport_data_for — the GPU cube itself, which
previously ignored the pane flag (show_viewcube was inst.active only), so
the cube stayed while its surrounding widgets vanished.
Result: the ViewCube gives way first, then the render bar once it too has no
room — no overlap, sized to the bar's real width (which varies with the
render-mode label, split buttons and tile count).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
OFFSET rejected XLINE targets: on_entity_pick only accepted
Line/Circle/Arc/LwPolyline, compute_offset had no XLine arm, and
entity_wire_pts returned no points for it (so hover/preview/through-distance
failed too).
Add offset_xline (parallel infinite line: same direction, base point shifted
perpendicular toward the pick side), a compute_offset arm, and an
entity_wire_pts case (a long segment standing in for the infinite line during
hit-test and preview; the committed XLine renders true). Also widen the pick
filter to every type compute_offset supports — this additionally reaches
Ellipse and Spline, whose offset functions already existed but weren't
selectable.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
STRETCH picks its crossing window as two corner points. After the first
corner set last_point, Ortho/Polar constrained the opposite corner to an axis
through it, collapsing the window to a line and making selection impossible.
Add CadCommand::window_corner_pick(); STRETCH returns true for its two window
steps, and the viewport skips the ortho/polar (and OTRACK) lock for that point
in both the preview and commit paths — the corners are now free.
Also render the window like a normal box selection instead of a bare green
outline: add SelectionState::preview_box (a hit-test-free marquee), draw it
with the shared draw_marquee helper (green crossing fill + stroke), and drive
it from the command's new window_first_corner(). Cleared on command end.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
seg_intersect_xy found where two segments' XY projections cross and returned
the point at z=0, ignoring height. Two lines at different Z whose plans cross
therefore fired a real Intersection snap when they only *appear* to cross in
top view — that case belongs to Apparent Intersection (view-space).
Rename to seg_intersect_3d and require the segments to be at the same height
at the crossing: compute Z on each at its crossing parameter and reject when
they differ (tolerance scaled to coordinate magnitude). A genuine crossing
now snaps at its true height instead of z=0; a plan-only crossing is left to
Apparent Intersection.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
extension_bases_screen only skipped closed curves (circle/ellipse, keyed on
the Quadrant hint) when finding which endpoints' extension rays a snapped
point lies on. An arc is an open curve with no Quadrant hint, so its
tessellation chords were scanned; one straight chord-extension grazes a
nearby line-line intersection (beyond the arc endpoint, within 4px), drawing
a spurious dashed extension guide radiating from the arc.
Skip every round curve by the Center hint (which arcs, circles and ellipses
all carry and straight lines never do) — the same signal nearest_segment
uses. The intersection snap point itself was already correct; only the stray
guide line is removed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Command aliases (L→LINE, CC→COPYCLIP) were scattered across dispatch match
arms and CommandRegistration name arrays. Move them all into a single
user-editable file:
- assets/ocad.pgp: shipped default alias table (ALIAS,*COMMAND format),
embedded and copied to config/ocad.pgp on first launch; only that user
copy is read after. AA→AREA default closes#286.
- resolve_alias() rewrites the leading command token at the top of
dispatch_command_inner, so aliases resolve for every path (families,
plugins, Repeat menu). Args after the first space are untouched.
- Autocomplete hides alias keys and, when the whole input is an alias,
surfaces its target command as the top (highlighted) suggestion so the
highlight matches what Enter runs (AA→AREA, L→LINE).
- ALIASEDIT opens a new editor modal (add/edit/delete rows) with an Apply
button; Apply persists to ocad.pgp, closing discards unapplied edits.
- Strip alias tokens from all 8 dispatch match-arm files and 71 module
CommandRegistration arrays, keeping canonical/synonym/distinct-command
names. ocad.pgp is now the sole source: deleting or remapping an alias
there takes effect. CUI stays on the keyboard-shortcut panel.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
COPYCLIP/CUTCLIP used the mean of each entity's bounding-box centre as the
paste anchor. Standard clipboard behaviour puts the lower-left corner of the
box enclosing all copied items under the cursor. Union each entity's bbox min
corner instead, and rename clipboard_centroid -> clipboard_base (COPYBASE also
stores a picked point there, so "centroid" was inaccurate).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Command previews (MOVE, COPY, ROTATE, SCALE, STRETCH, MIRROR, ARRAY,
PASTE, INSERT) build ghost WireModels via WireModel::translated/rotated/
scaled/stretched/mirrored, but those helpers moved only `points`, never
`text_verts`, and the ghosts' glyphs were never uploaded — so text
vanished (or froze at its source) during the preview, same root cause as
the grip-drag case.
Transform text_verts alongside points in all five helpers (shared
map_text_verts, reconstructing + re-splitting the double-single world
position), and gather preview_wires' glyphs into the per-frame
preview-text buffer in the render path. This covers every command
preview automatically. The grip-drag else branch no longer sets
preview_text explicitly (the render gather handles it); the grip-slide
fast path keeps its own scene.preview_text channel (empty preview_wires,
so no double-draw).
MIRROR honours MIRRTEXT: on -> true glyph mirror via mirrored(); off ->
readable relocation via translated().
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Dragging text by a grip hid it until release. The dragged entity is
hidden from the base wire set and shown as a preview overlay, but its
SDF glyph quads (text_verts) were never uploaded for the preview, so
the text vanished mid-drag.
Add a per-frame preview_text GPU buffer (sibling to preview_wires). A
rigid whole-entity move of pure text (TEXT/MTEXT, Square insertion grip)
slides the drag-start glyphs by the drag delta instead of re-shaping the
run every move; reshape grips (MTEXT width), dimensions and wire
entities re-tessellate and feed the same buffer.
Also fold preview_text into render_signature: a pure-text slide leaves
preview_wires empty, so without it the scene-render cache froze the
dragged text at its first frame (grip moved but text stayed, then
jumped to place on release).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Three defects in ACIS curved-wall (cone-surface) tessellation, all seen
in a Villa Savoye model:
- Balloon: sample_ellipse_arc ignored the edge's own sense relative to
its curve (distinct from the coedge/loop sense). A reversed edge winds
the ellipse about the opposite normal, so evaluating P(t) with the
stored normal mirrors a short boundary arc across the major axis to the
far side of the circle. That poisoned the face's angular-span estimate
and swept the whole 2pi revolution, drawing a curved wall as a full
cylinder of the surface radius. Honour edge.sense() (negate the sin
term when reversed) so the arc traces its true vertices.
- Seam: the fallback sampler's angular_range used raw min/max of atan2
angles, so an arc straddling the +/-pi seam read as a 2pi span (false
full circle). Derive the arc from the largest angular gap instead.
- Gaps: truck's rsweep/triangulation degenerates to zero triangles on
some short-arc cylinders (profile through the local origin), leaving a
see-through hole while the rest of the solid renders. Fall back to the
bespoke parametric sampler for just that face, into the same body-local
buffers. Also scale tess_cone_face's angular divisions by the arc span
so a short arc isn't tessellated as densely as a full rim.
Verified against the model: ballooned walls become wall-sized, and a
sweep of all 528 solids shows no empty faces and no triangle explosion.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
on_enter/on_escape returned CmdResult::CommitEntity, which commits the
spline but keeps the command armed with its accumulated points — the
cursor stayed in spline mode and the next click began a new spline,
trapping the user in the command loop. Return CommitAndExit so Enter/Esc
finalize the spline and return to the selection state.
The in-progress preview drew only the straight control polygon. Sample
the actual degree-3 clamped B-spline (same control points + knot vector
build() commits) so the rubber-band traces the exact final curve.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Opening a DXF whose active viewport is stored uppercase (*ACTIVE),
editing and re-saving produced a file with two full-window model-space
viewports — the file's own *ACTIVE plus a fresh *Active we add on save.
The reserved viewport name is case-insensitive, so the two didn't
collapse and other CAD apps drew them one on top of the other, which
read as the copied objects being linked / "reacting as one".
save_model_tiles_to_vports dropped existing active viewports with a
case-sensitive `name != "*Active"` filter, so an uppercase *ACTIVE
survived and sat next to the new entry. Route every *Active comparison
(save filter + camera/tile restore) through a case-insensitive
is_active_vport_name helper so the file's record is recognised and
replaced instead of duplicated.
Also bumps acadrust to #99f75fe (merged PR #1): the ASCII DXF writer now
emits shortest round-trippable floats instead of the full 16-digit
expansion, halving the round-tripped file size.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
units_btn and scale_popup_btn were byte-identical apart from the message
they emit. Fold them into a single popup_pill(label, open, msg) helper so
every popup-opening pill goes through shared chrome, matching toggle_pill
(icon toggles) and split_pill (toggle + caret dropdown).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The polar-tracking pill only right-click-cycled 15→30→45→90. Add an
AutoCAD-style angle picker opened from a caret on the pill: presets
90/45/30/22.5/18/15/10/5/1 plus a free-text field for any custom angle,
mirroring the existing units / scale status-bar popups. The chosen angle
persists like the other user settings.
Factor the shared chrome into one `split_pill(main, caret_msg, active,
open)` helper (+ `pill_text_color`) so the OSNAP and POLAR pills stop
hand-rolling the same outer border, background, caret and sizing — each
pill now only supplies its own main region. The caret is sized and
aligned to sit inside the single pill border on both.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The Continue option committed a single tangent-continuing arc and left
the command. Now each picked end point commits its arc and re-anchors at
that arc's free end, so the user keeps drawing tangent-continuing arcs
until ending the run with Enter/Esc. A degenerate (collinear) pick stays
active instead of cancelling.
Because arcs now commit via CmdResult::CommitEntity (repeating) rather
than CommitAndExit, advance the ARC_CONT continuation anchor in the
CommitEntity arm too — guarded to line/arc so non-continuable repeats
(point/ray/spline) leave it untouched. This also lets a LINE run feed a
following ARC Continue.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
TEXTFILL 1 (default) fills TrueType text; TEXTFILL 0 draws it hollow
(outline only). Settable directly or via SETVAR, 0/1 or ON/OFF. SHX/LFF
stroke fonts are unaffected.
Reuses the existing stroke-SDF path: bake_glyph treats a TTF glyph as a
stroke (outline) field instead of a signed fill field when TEXTFILL is
off. Toggling resets the glyph atlas and re-tessellates text so quads
pick up the re-baked tiles. Persisted in user settings.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Run EXTRIM, pick a cutting boundary (line/arc/circle/ellipse/spline/
polyline), then click a side: every object crossing the boundary is
trimmed on that side and objects wholly on it are erased. Side is decided
by a parity crossing count (a segment to the pick point crossing the
boundary an even number of times is on the pick side), reusing the trim
intersection machinery; line boundaries extend to infinity. Line/arc/
circle are trimmed exactly; polyline/ellipse/spline via dense sampling.
Live preview while choosing the side: boundary in yellow, surviving parts
in blue, the removed parts in red.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Space now acts like Enter in the command line so commands advance
token-by-token (CAD convention). The whole value is handed to the submit
path, which tokenises multi-token lines, so a pasted or API-fed
`LINE 0,0 10,10` runs each space as a step separator instead of stopping
at the first token. A leading `>` switches to literal-space mode (for a
text string, a path, etc.) and is stripped on submit. All command-line
entry is shown uppercase.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Run QUICKPRINT (or QP), select objects, then Enter: the selection's
bounding box becomes the plot window and a PDF is written next to the
drawing (<name>_<timestamp>.pdf) using the active page setup, with no
dialog. Model space.
Implemented as a selection-gathering CadCommand (src/modules/view/
quick_print.rs) returning CmdResult::QuickPrint(handles); the host
(on_quick_print_handles) unions the picked entities' AABBs, sets the
plot window, and reuses the tested clipped window-export path.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Restructure the plot dialog into a style-manager-style master/detail: a
left list of page setups plus a right settings editor. The list shows
<none> (defaults + PDF) and <previous> (the settings the dialog opened
with), then the drawing's layouts (*Model*, *Layout1*, …) and its
standalone named page setups.
- Named page setups live in the ACAD_PLOTSETTINGS dictionary
(src/scene/page_setup.rs: names/get/save/delete/rename, creating the
dictionary if absent); DWG round-trip verified.
- Toolbar New / Copy / Delete | Set current / Preview / Print. Selecting
a row loads it into the editor; Set current writes to the active
layout. Copy duplicates a layout or a setup into a new named setup;
Delete/Rename apply only to named setups.
- Scene::plot_settings_for(name) generalizes effective_plot_settings so
any layout's settings can be loaded; on open the settings are
snapshotted for <previous>.
- Default selection: model space → <none>; a layout → its own setup.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The batched path is the sole hatch renderer and has no family cap, yet it
was named hatch_batched_gpu while the old capped per-primitive renderer
kept the hatch_gpu / MAX_FAMILIES name — which reads as if visible hatch
fills are truncated at 16 families (they are not). Swap the names so the
active renderer is hatch_gpu.rs / hatch.wgsl and the legacy one, now used
only for wipeout masks, is wipeout_gpu.rs / wipeout.wgsl. MAX_FAMILIES now
lives only in the wipeout module. Pure rename; no behaviour change.
Refs #323.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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 <[email protected]>
Gradient Type (One/Two color), Color 1 and Color 2 editable via a generic field-keyed colour picker path. Background colour editable and stored as HATCHBACKGROUNDCOLOR extended data (group 1071 true colour), rendered in OCS as a solid backdrop drawn behind the pattern. Hide Origin X/Y for gradients (pattern-only concept) and drop the redundant gradient rows from the pattern branch.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Merge duplicate Type into pattern_type; Type/Associative/Island detection as dropdown/toggle; Angle/Scale/Spacing/Origin X-Y editable; Color via picker. Reorder Pattern group, drop redundant Color/Transparency/Layer override and the Misc section. Wire the pattern origin (per-line base_point) into the render (x0/y0 relative to world_origin) so it affects dashed/offset patterns; add a circle origin grip at the hatch centroid (new GripShape::Circle).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Single Geometry section: Start/Center/End points, Major/Minor radius (editable; minor derives ratio), Radius ratio, Start/End angle editable. Major/Minor axis vectors, parameters, Length and Normal kept read-only at the end.
Co-Authored-By: Claude Opus 4.8 <[email protected]>