Route every text-bearing entity to an in-place editor instead of the
command line: a plain text box for single-line text (Text, attributes,
dimension override, tolerance) and the rich MText editor for MText /
MultiLeader. A Leader resolves to the entity it annotates.
- TEXT places a plain text box at the click; double-click edits in place.
- DDEDIT and double-click on any text entity open the matching editor.
- MLEADER places the leader then opens the MText editor on its content.
- LEADER follows the classic flow: points, Enter, then a linked empty
MText annotation opens in the editor; the leader's annotation_handle
links the pair so they edit/erase as a unit.
Adds shared field read/write helpers and a begin_text_edit router, plus
CommitAndEditText / CommitManyAndEditText results that commit an entity
and open its editor (commit_entity now returns the new handle).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Type directly into the rendered MText preview: click to position the
caret, insert/delete, Enter for a new line, Space for a literal space,
and arrow keys to move. The caret blinks when idle.
Keep the trailing empty paragraph in the editor so the caret appears on a
fresh line immediately after Enter, before any typing. parse_mtext_paragraphs
split into a _ex variant; layout_mtext skips the blank-edge trim when
emitting glyph boxes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MTextType/MTextBackspace/MTextDelete were never emitted — preview typing,
backspace and delete route through the existing CommandAppendChar /
CommandBackspace / DeleteSelected handlers, which call the methods directly.
Removes the dead variants and their arms (fixes a never-constructed warning).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The rendered MText preview is now an editable surface: it shows a text caret,
click to position it between glyphs, and type/Backspace/Delete/Enter to edit.
Left/Right arrows move the caret; drag-selection + toolbar formatting still
work and typing replaces a selection.
Keystrokes are routed by mapping visible-character offsets to byte spans in
the value (raw newlines count as paragraph breaks, matching the layout). While
the preview is open the command line releases its on_input so typed glyphs
reach the preview instead, and the caret starts at the end of the text so
typing works without a click first.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The rendered MText preview is now interactive: drag across glyphs to select
a character range and apply toolbar formatting (bold, italic, underline,
overline, strike, case, alignment) to exactly those characters.
The text layout gains an opt-in pass that emits one world-space box per
visible character carrying its reading-order index; the editor maps those
boxes into the preview frame, hit-tests pointer drags against them, and
highlights the selection. Formatting splices the matching inline codes into
the raw value at the selected byte range (mapped via a code-aware visible-
character scanner). With no preview selection the Edit-box path is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The editor opens in preview by default and switches body views with a
segmented Edit | Preview control placed between the toolbar and the body
(active tab highlighted). All toolbar properties feed the rendered preview.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
Picking Stretch from the grip popup just closed the menu and returned, so
the grip never became hot and the follow-up click did nothing (issue #48).
It now engages the picked grip — tracking the cursor until the placement
click — the same as picking the grip directly in the viewport.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Grip editing now engages on a click and places on the next click, with the
entity tracking the cursor in between — no need to hold the button down.
Legacy press-drag still commits on release; the two are told apart by
whether the grip actually moved. Escape ends a hot grip at its last preview
position. Lingering press state from the engaging click is cleared so later
moves don't read as an in-progress drag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single-click picking cleared the selection first (exclusive), so clicking a
second object dropped the first (issue #47). Plain clicks now add to the
selection and Shift+click removes the picked entity; empty-space click and
Esc still clear. Shift state is tracked from keyboard ModifiersChanged
events since mouse click messages carry no modifiers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The viewport context-menu item dispatched a bare "ZOOM" string, which no
command branch handled, so it did nothing. Send "ZOOM EXTENTS" instead.
fit_all() also fed on the frustum-culled wire set, so it only framed the
entities already on screen — each invocation revealed a little more and the
view converged on the true extent only after several uses (issue #51). It
now builds an un-culled wire set at a fixed tessellation tolerance, so the
bounds are zoom-independent and the first call fits the whole drawing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cross-platform prerequisite for OS-level file associations: the boot
task now consumes `std::env::args_os().nth(1)` as a path and dispatches
`Message::OpenRecent` for it. Flag-style args (starting with `-`) are
ignored, and `OpenRecent` already does the file-existence check, so a
bogus path lands as a clean command-line error instead of a panic.
Linux: the AppImage's desktop entry adds `image/vnd.dwg` alongside the
existing `image/vnd.dxf` MIME type and grows a `%f` placeholder in
`Exec=` so the launcher forwards the picked file path. Once the
AppImage is integrated with the system (xdg-mime / AppImageLauncher),
double-clicking a .dwg / .dxf opens it in Open CAD Studio.
Windows: new `packaging/windows/main.wxs` defines an MSI installer —
per-machine install to `Program Files\Open CAD Studio`, Start Menu
shortcut, `MajorUpgrade` so newer MSIs replace older ones, and a
single ProgID `OpenCADStudio.Drawing` that owns both `.dwg` and
`.dxf` and launches the exe with the file as `argv[1]`. The icon is
pulled from the SVG logo, converted to multi-resolution ICO at build
time with the runner's pre-installed ImageMagick. CI now also runs
`candle` / `light` (WiX Toolset 3, pre-installed on `windows-latest`)
and uploads both the bare `.exe` (portable) and `.msi` (installer)
to the release.
macOS Info.plist already declares the DWG / DXF UTIs; combined with
the argv-handling change above, double-clicking a drawing in Finder
will open it in Open CAD Studio without further packaging changes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The eight layer-state commands (LAYOFF, LAYFRZ, LAYLCK, LAYULK, LAYON,
LAYTHW, LAYISO, LAYUNISO) mutated `document.layers` directly through
`dl.turn_off()` / `dl.freeze()` / `dl.lock()` / … and then called
`refresh_layer_panel()` — but never bumped the scene's geometry epoch.
The 3D viewport caches its uploaded geometry keyed by that epoch, so
visibility / freeze changes only took effect once something else
triggered a re-render. Pressing Esc was the workaround because the Esc
handler calls `deselect_all()`, which internally bumps geometry.
The Layer Properties panel toggles already did this (their handlers in
`update.rs` call `scene.bump_geometry()` after each freeze / visibility
toggle), which is why the panel worked and only the command path
exhibited the lag.
Add `scene.bump_geometry()` after the layer mutation loop in each of
the eight command branches so the viewport repaints immediately. Lock /
unlock don't strictly affect rendering today but are bumped for
consistency — selection / grip code may grow lock-aware later.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two ways to extend the current selection by entity type + layer:
* `Select Similar` (right-click → Select Similar, or `SELECTSIMILAR` /
`SELSIM`): for every (type, layer) pair in the current selection, add
every entity in the active layout that matches the pair. The seed
selection stays selected; matches already selected are not
double-counted.
* `Quick Select` (right-click → Quick Select…, or `QSELECT` / `QS`):
opens a centred floating panel with Object-type and Layer pick_lists
(populated only with values that actually exist in the active layout)
and an "Append to current selection" checkbox. `(Any)` in either slot
skips that filter. Apply replaces the selection (or extends it with
Append on) and reports the match count on the command line. Cancel,
Esc, and outside-click all dismiss without applying. When opened with
a selection present, the panel pre-fills both filters from the first
selected entity so QSelect doubles as an editable Select Similar.
Scoped to entities owned by the active layout's block-record so a paper
layout's QSELECT never reaches into model-space entities (and vice
versa). Entity-type names live in a single `entity_type_name` helper in
`entities::traits` to keep the UI strings and the filter keys aligned.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The right-click context menu was being placed via window-relative
coordinates while the cursor position it anchored to was canvas-relative,
so the menu drifted away from the cursor by the size of the surrounding
ribbon / sidebar / status bar.
Move the menu inside `viewport_stack` so its anchor resolves in the same
coordinate space the cursor was captured in. Introduce
`position_canvas_overlay(anchor, panel)` — wraps `panel` in a column+row
of fixed-width spacers and `iced::widget::opaque(panel)` so events on the
menu itself don't fall through to the viewport mouse area underneath.
The multi-functional grip popup is rewritten to use the same helper.
Outside-click dismiss now mirrors the grip popup: `ViewportLeftPress`
checks `selection.context_menu.take()` at the top and returns early
when it was set, so a press that reaches the viewport mouse area
(i.e. outside the opaque panel) closes the menu without running the
rest of the left-click handler. The redundant `context_menu = None`
later in the handler and the now-unused `Message::ViewportContextMenuClose`
are removed.
Two related issues fixed in the same pass:
* The right-drag threshold for orbit was 3 px squared, small enough that
normal hand jitter between a right-button press and release was
promoting a click to a drag and suppressing the menu on release.
Bumped to 8 px squared.
* `Message::ViewportExit` was clearing `context_menu`. With the menu now
living above the viewport mouse area, the opaque panel capturing the
cursor fires the underlying area's exit handler — which closed the
menu the instant it opened. Stop clearing the menu there; outside-click
dismiss is enough.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lengthen / Radius / Arc Length need a number after the pick. Stash
`grip_pending` on the picked action, push prompt to command line,
route next `CommandSubmit` parse into `apply_grip_menu_value`.
Escape cancels.
- `Grippable::grip_menu_value_prompt` returns label when action
wants value; `apply_grip_menu_value` runs the edit. Dispatched
via `EntityTypeOps`.
- Line: Lengthen extends either endpoint along line direction.
- Arc: Radius sets radius; Arc Length redrives end angle from
`value / radius`; Lengthen extends start / end angle the same
way.
The previous single-widget refactor left the column at
`Length::Shrink` and the buttons at `Length::Fill`, which iced
resolves to a zero-width column — the popup degenerated into a
1-pixel vertical line. Set the column to a fixed pixel width
derived from the longest item's label (and restore `width(Fill)`
on the buttons) so the selection highlight covers the full row
instead of just the text glyphs.
Also silences a `dead_code` warning on `GripMenuAction` variants
that the Phase 2 / 3 wiring will consume next.
- Popup items now render inside one bordered container with
borderless buttons so the menu reads as a single widget instead
of stacked tiles.
- Arrow Up / Down (and Tab) walk `grip_popup.selected` while the
popup is open; Enter commits the highlighted item by re-emitting
`GripMenuPick`; Escape dismisses without acting; the underlying
handlers (`CommandHistoryPrev/Next`, `CommandFinalize`,
`CommandEscape`, `DynTabNext`) keep their normal behaviour when
no popup is open.
- A press inside the viewport that reaches the mouse_area (i.e. not
on a popup item) closes the popup and consumes the click — the
user's next press then starts a normal selection / drag.
Dwell timing only advanced when `ViewportMove` fired; a perfectly
still cursor never triggered the popup. Subscribe to animation
frames while `grip_hover` is set and dispatch `GripDwellTick` from
them so `update_grip_hover` re-checks the elapsed dwell each frame.
Subscription auto-stops as soon as the hover clears or the popup
opens.
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
Phase 1 of bringing the grip vocabulary in line with the standard
CAD convention: drop the per-position-type shape encoding (Diamond
for translate, Circle for parameter) and use a plain square for
every endpoint / vertex / centre grip. The behavioural midpoint vs
vertex distinction stays — it lives on `GripDef::is_midpoint` and
drives `Translate` vs `Absolute` in the grip-edit dispatcher — only
the marker shape collapses.
- `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond /
Circle are gone.
- `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction
vector consumed by `Rectangle` to orient the box along its
segment.
- `entities::common` exposes `square_grip`, `center_grip` (same
square marker, flagged as translate), `rectangle_grip(id, world,
dir)` for oriented mid-segment handles, and a kept-for-Phase-2
`triangle_grip`.
- `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir`
through to the canvas painter; the Rectangle path rotates the
box around the grip centre using the world-XY direction (flipping
the sin for screen-Y).
Every existing `diamond_grip(id, w)` call becomes `center_grip(id,
w)` — the visual change is square-replacing-diamond at line
midpoints / arc midpoints / circle centres / ellipse centres /
viewport / hatch loop centroids / dimension grips / image corners /
multileader / ray / underlay / ole2frame / solid3d.
`LwPolyline` grows a stretch handle on every segment (straight or
arc), drawn as the new oriented rectangle. The grip-edit path
translates both segment endpoints for straight segments and
adjusts the arc bulge from the new midpoint for arc segments.
LAYOFF / LAYFRZ / LAYLCK / LAYON / LAYTHW / LAYULK / LAYISO /
LAYUNISO mutated the document's layer table (\`turn_off\` /
\`freeze\` / \`lock\` / …) and then called \`sync_ribbon_layers\`,
which feeds the ribbon dropdown from \`self.tabs[i].layers.layers\`
— the \`LayerPanel\` cache. That cache was never refreshed from
the document for these commands, so the dropdown stayed wrong and
the Layers Properties Manager kept showing the old visibility /
lock / freeze icons.
Add \`OpenCADStudio::refresh_layer_panel\` that runs
\`LayerPanel::sync_with_viewports\` from the live document before
calling \`sync_ribbon_layers\`. Replace every direct
\`sync_ribbon_layers\` in \`commands.rs\` (all of them sit inside
LAY* arms) with the new helper.
Closes#39
A ribbon tool that opens a popup window (LAYERS, PAGESETUP,
ABOUT, the style managers, …) marks itself blue via
\`activate_tool\` when the user clicks it, but nothing un-marked
the tool after the window was dismissed — \`OsWindowClosed\`
cleared the matching \`_window\` slot, the active-tool string
stayed put, and the button kept rendering as if the panel were
still open.
Add \`Ribbon::deactivate_tool_if(id)\` (clears only when the
active tool matches) and call it for every popup-window slot in
\`OsWindowClosed\`. Also clear inside the \`ToggleLayers\` close
branch — that path \`take()\`s \`layer_window\` to \`None\` before
\`OsWindowClosed\` fires, so the conditional in the
\`OsWindowClosed\` handler would miss the close otherwise.
Mapped IDs match the matching ribbon \`ToolDef.id\`s (LAYERS,
PAGESETUP, ABOUT) plus command names for the popups normally
opened via the command line (TEXTSTYLE, TABLESTYLE, MLSTYLE,
LAYOUTMANAGER / LAYOUTPANEL, PLOTSTYLE / STYLESMANAGER, DIMSTYLE,
SHORTCUTS / KEYBOARD) so a future ribbon tool with one of those
ids gets the same correct deactivate.
Closes#40
Every secondary window (save-as, unsaved-changes, layout manager,
layer properties, ribbon dialogs, …) opened at the default
\`Level::Normal\`, so flicking focus to the main window let the popup
disappear behind it — easy to lose, especially the modal-flavoured
save-as / unsaved-changes prompts where the user has no clear way
back. Set \`level: Level::AlwaysOnTop\` on every \`window::open\`
call in \`update.rs\` so each popup floats above the main window
until it's closed. Main window's \`Settings\` in \`mod.rs::boot\`
keeps the default level on purpose.
Refs #38
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`sync_dyn_fields` only ran during `ViewportMove`, so a typed-coord
or click-driven point commit left the previous iteration's field
shape in place until the user nudged the mouse. After the first
LINE click that meant the stale `[X, Y]` (built when there was no
base) kept serving the next point's input as cartesian even though
the command-default for a "Point with base" is now polar
`[Distance, Angle]`. The original #35 fix preserved a
`,`-reshaped cartesian set on purpose, but the same machinery now
also held onto a never-explicitly-chosen `[X, Y]` carried over from
"no base" → "has base".
Track that intent explicitly with `dyn_user_reshaped` (set in
`dyn_comma_advance`, cleared on point commit and command start)
and only treat a non-matching shape as acceptable while that flag
is on. Re-run `sync_dyn_fields` from every point-commit path
(command-line typed coord, mouse click, dynamic-input locked
commit) so the freshly updated `has_base` state immediately picks
the right default and the user sees polar fields without having
to move the mouse first.
Typing \`,\` while DYN was on appended the character to the focused
field's buffer (treated as a European decimal point), so users
couldn't switch to cartesian coordinates mid-command. The dynamic
input was stuck in polar \`[Distance, Angle]\` whenever a base point
existed.
\`,\` now acts as the coordinate separator (matching the AutoCAD
convention): pressing it locks the current field's buffer, then
either advances within the existing cartesian set or reshapes a
polar configuration into cartesian:
- \`[Distance, Angle]\` + \`,\` on the first field → \`[X(buf), Y]\`
- \`[X, Y]\` + \`,\` on Y → \`[X, Y, Z]\`
- otherwise advances to the next field (Tab-equivalent)
\`sync_dyn_fields\` accepts both polar and cartesian shapes as valid
\`Point\` configurations so a mouse-move after the reshape doesn't
revert to polar. \`dyn_resolve_point\` and the overlay's live-value
formatter treat cartesian inputs as RELATIVE to the base point when
one exists — matching the DYN-on relative-coords convention from
#26. \`DynComponent::Z\` plus its label / value paths fill out the
3-D variant.
Closes#35
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Typing a coordinate (e.g. \`25\`\\TAB\`45\` for polar input) and pressing
Enter committed the point through \`on_point\` but never re-ran the
preview hook. The mouse-move pipeline that normally publishes the
rubber-band segment from the latest \`last_point\` didn't fire — no
mouse motion occurred — so the active blue segment kept dangling
from the *previous* point until the user nudged the mouse.
Add \`refresh_active_cmd_preview\` and call it after the keyboard
point commits (both the command-line path and the dynamic-input
locked-field path) so the next segment starts from the
just-committed point immediately.
Closes#32
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The OCS main menu's "Exit Open CAD Studio" button dispatched
`Message::Command("EXIT")` but `dispatch_command` had no arm for it,
so the click was swallowed. Route `EXIT` and `QUIT` through
`Message::WindowCloseRequested` so the unsaved-changes dialog runs
first and the app then exits via the same path as the OS-window close.
Closes#41
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`adapt_to_bg` (pure black → white on dark bg, pure white → black on
light bg) was only running through `render_style`, which the wire and
per-frame hatch paths already use. The mesh population paths and the
file-load hatch path went straight through `aci_to_rgba`, so:
- ACIS solids / regions / bodies appeared in their raw ACI colour
even when that colour was pure white on a white paper bg.
- The initial hatch upload used the unadapted colour for the first
frame after load (`synced_hatch_models` then re-ran adaptation per
frame, so the issue cleared after one redraw — but visible).
Now every mesh / hatch creation site funnels through
`render_style_for` + `adapt_to_bg`:
- `build_derived_caches` (load): uses the default model bg as the
adaptation target.
- `populate_meshes_from_document`: uses `self.render_style` so the
per-frame paper / model bg selection applies.
- New `Scene::recolor_meshes` rewrites every cached mesh's colour
field through `render_style`; called from `BACKGROUND` (after the
user changes bg) and `set_current_layout` (Model ↔ paper bg
switch) so ACIS mesh colour tracks the live bg without re-
tessellating ACIS geometry.
Wipeout fills stay bg-coloured by design (that's their job). Raster
images skip adaptation — pixel data is not entity colour.
Tiled Model layouts now show the inner edges between panes as 2-px
dividers that act as resize grips: cursor on a divider → resize cue,
press + drag moves the edge (with every tile sharing that edge
updating together), release runs a collapse pass that absorbs any
tile that fell below the viewcube-comfort minimum into the neighbour
with the longest shared contact edge — drag a divider all the way
across to remove a pane.
- Scene grows `model_tile_edges`, `hit_model_tile_edge`,
`move_model_tile_edge`, `collapse_small_model_tiles`. Edges are
derived from the existing `model_tiles` rects (no extra storage).
- App carries a `tile_drag` Option; `ViewportLeftPress` starts it
ahead of tile activation / picking, `ViewportMove` drives the
edge, `ViewportLeftRelease` collapses + clears.
- `SelectionCanvas` draws the dividers and switches the cursor to
`Resizing{Horizontally, Vertically}` over them.
`model_wires_for_viewport` was tessellating with `self.bg_color` (the
model bg), so model content rendered inside a paper-layout viewport
adapted to a dark bg even though the transparent shader now reveals
the PaperCanvas sheet behind it — white wires stayed white on a light
sheet. Pass the paper bg in a paper layout.
Wire cache also needed an explicit invalidation when the bg or the
active layout changes; otherwise the cached, already-adapted wires
were returned against the new bg unchanged.
- BACKGROUND command bumps `geometry_epoch` after both branches
- new `Scene::set_current_layout` setter bumps on actual change; layout
switch + history restore use it
Collapse the paper-space rendering path into the same shader widget the
model layout uses. The PaperCanvas widget draws the paper sheet, paper
entities and viewport borders underneath; the unified shader paints all
content viewports on top, scissored to each viewport's rect.
- Drop PaperViewportPane / PaperViewportPipeline newtype /
PaperViewportPrimitive, ViewportPaneMode::Paper, ViewportPane::paper,
build_primitive and build_viewport_primitive. Scene exposes only
build_viewports.
- view.rs: the paper branch stacks PaperCanvas + the unified shader
instead of the old paper_canvas_view function. The active paper
viewport's blue border, render-mode picker and ViewCube hit-area are
layered in the same viewport_stack as the model layout.
- Transparent shader clear (alpha=0) + alpha-blended blit pipeline so
the underlying widget (container bg in model, PaperCanvas sheet in
paper) shows through everywhere the shader has not painted geometry —
including inside content viewport rectangles.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Route every per-frame interaction through the active model tile's screen
rectangle so a tiled VPORTS layout behaves like independent panes while
still sharing one shader widget:
- pick / draw / snap / box+lasso: cursor mapped to tile-local space; the
hover-activates-tile rule on ViewportMove keeps the active tile under
the cursor, so the picked camera matches what the user sees.
- pan: switched to Camera::pan_screen(dx, dy, viewport_height) so the
world-per-pixel rate matches the active tile's height instead of the
full canvas.
- grid: clipped via Frame::with_clip to the active tile rect; world→screen
adds the tile origin so the lines align with the panel.
- UCS icon: origin offset by the tile rect so the gizmo sits in the
active tile's bottom-left corner.
- render-mode picker (+ split buttons) and ViewCube hit-area: positioned
via active_model_tile_bounds with leading Spaces, mirroring the
floating-viewport pattern.
- ViewCube hover: driven by the CursorMoved/ViewportMove handlers into a
new Scene::viewcube_hover cell, so the highlight survives the cube
hit-area overlay masking events from shader::Program::update.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Model space'de VPORTS artık iki aşamalı: bare VPORTS bir yapılandırma
istiyor (SIngle/2H/2V/4), sonraki komut-satırı girdisi düzeni kuruyor
(boş giriş = SINGLE). set_model_tile_layout normalized rect listesiyle
tiled düzeni değiştiriyor:
- SINGLE: tek tam-ekran
- 2H: üst/alt
- 2V: sol/sağ
- 4: 2x2
awaiting_vports app flag'i prompt sonrası girdiyi yakalıyor. Paper-space
VPORTS davranışı değişmedi.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tiled Model layout'ta aktif olmayan bir tile'a tıklamak onu aktive edip
kamerasını canlı scene.camera'ya yüklüyor (set_active_model_tile_at).
Böylece her panel bağımsız bir bakış tutuyor — bir panelde orbit/zoom
yaparken diğerleri sabit kalıyor. Aktif tile'a tıklama normal
selection/draw akışına devam ediyor.
Pick/pan/viewcube'un tam tile-relative koordinat yönlendirmesi sıradaki
adımda.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Model layout artık tiled viewport destekliyor. Scene.model_tiles
normalized ekran rect'i + kamera tutan tile listesi; active_viewports
model layout'ta her tile için bir ViewportInstance üretiyor (aktif tile
canlı scene.camera'yı, inaktif tile'lar saklı snapshot'ı kullanıyor).
Render-mode picker'ın yanına iki buton eklendi: ▤ aktif tile'ı yatay
(üst/alt), ▥ dikey (sol/sağ) ikiye böler. split_active_model_tile
bölmeyi yapıyor; her iki yarı aktif kameranın kopyasını alıyor.
set_active_model_tile_at (cursor → aktif tile, kamera swap) ve
reset_model_tiles eklendi ama henüz bağlanmadı — aktif tile cursor
seçimi + pick yönlendirme sonraki adımda.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Picker ve ViewCube paper_canvas içinde gömülü olduğundan tümüyle
viewport mouse_area'nın altında kalıyor ve tıklamaları o yakalıyordu.
İkisi de ana view stack'ine, mouse_area'nın üstüne taşındı; aktif
viewport'un ekran dikdörtgeninin köşelerine konumlandırılıyor.
- model picker artık yalnızca model layout'ta gösteriliyor (paper'da
viewport kendi picker'ını alıyor)
- ViewCube hit alanı aktif viewport rect'inin sağ üst köşesinde
- CursorMoved ve ViewportClick, ViewCube hit-test'ini full canvas yerine
aktif viewport rect'ine göre eşliyor; böylece paper'da gizmo ile
tıklama hizalı
- picker/viewcube dışı tıklamalar alttaki mouse_area'ya geçtiği için
viewport içi seçim/çizim etkilenmiyor
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Paper-space'de bir viewport'a çift tıklanıp aktif hale gelince, model
space'deki gibi sol üst köşede render-mode dropdown'ı ve sağ üst köşede
ViewCube gösteriliyor.
- render_mode_picker helper'a çıkarıldı; model space ve aktif paper
viewport ortak kullanıyor (kod tekrarı yok)
- SetRenderMode artık viewport-aware: aktif viewport varsa o viewport
entity'sinin render_mode'unu, yoksa model-layout tab stilini günceller
(Scene::active_viewport_render_mode getter/setter)
- build_viewport_primitive'e show_viewcube parametresi; aktif viewport
primitive'i ViewCube gizmo'sunu çiziyor
- PaperViewportPane shader::Program artık Message'a özelleşti ve sol
tık ViewCube hit-test'ini bounds-relative yapıp ViewCubeSnap üretiyor
Bilinen sorun: picker/viewcube doğru konumda görünüyor ancak tıklamalar
üstteki viewport_mouse mouse_area katmanına gidiyor; katman sırası bir
sonraki adımda düzeltilecek.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sağ kenardaki dikey orbit/pan/zoom/zoom-extents araç çubuğu tümüyle
kaldırıldı: viewport render'ı, overlay::nav_toolbar fonksiyonu,
show_navbar state + ToggleNavbar mesajı/handler'ı, NAVBAR komutu, View
şeridindeki "Navigation Bar" aracı ve navbar modülü/ikonu. Aynı
gezinme işlemleri komutlar (3DORBIT, PAN, ZOOM*) ve ViewCube üzerinden
erişilebilir durumda.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
NumLock açıkken Numpad8 / Numpad2 logical key olarak ArrowUp / ArrowDown
geliyor ama text "8" / "2" taşıyor. Klavye aboneliğinde ok/geçmiş
arm'ları bu tuşları yakaladığı için dinamik girişte 8 ve 2 yazılamıyordu.
Printable glyph üreten her tuş artık navigation arm'larından önce, text
alanı üzerinden yakalanıyor; whitespace/control üreten tuşlar (Space,
Enter, Tab) named handler'lara düşmeye devam ediyor. Gerçek ok tuşları
(text üretmez) geçmiş gezinmesini korur.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DYN overlay artık ekran-yukarı için world Y'yi (w.y) gösteriyor; eskiden
w.z kullanıldığı için Z-up sahnede top/3D görünümde Y hep 0 görünüyordu
(#27).
Ayrıca overlay etiketi aktif komutun o anki sorusuna göre biçimleniyor:
yeni DynField (Point / Distance / Angle) trait metodu eklendi. CIRCLE
yarıçap adımında 'd=...', ROTATE açı adımında '<...°', diğer nokta
adımlarında koordinat / polar (d + açı) gösteriliyor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Koordinat girişi artık göreli/mutlak ayrımını destekliyor:
- `@x,y[,z]` → son girilen noktaya göreli (UCS eksenlerinde döndürülür)
- `#x,y[,z]` → her zaman mutlak (DYN durumundan bağımsız)
- ön ek yok → DYN açıksa göreli, kapalıysa mutlak
İlk noktada referans olmadığından göreli giriş mutlağa düşer. Çözülen
WCS noktası last_point olarak saklanıyor, böylece ardışık göreli
girişler zincirleniyor.
parse_coord artık (Vec3, CoordKind) döndürüyor; relative offset için
ucs_rotate_vec eklendi (origin translation olmadan UCS rotasyonu).
UCS ORIGIN komutu prefix'i yok sayıp mutlak kalıyor.
Ayrıca ViewCube snap refactor'undan kalan kullanılmayan kod temizlendi:
angle_close, active_viewport_yaw_pitch, CubeRegion::snap_angles.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ViewCube tıklaması artık sabit (yaw, pitch) tablosu yerine bir yön
vektörüne (eye direction) snap ediyor. Up vektörü şöyle seçiliyor:
1. Mevcut up alın.
2. ±X / ±Y / ±Z arasından (yeni gaze ile (anti-)paralel olmayanlar)
mevcut up'a en yüksek dot-product veren cardinal axis'i seç.
3. Bu axis'i yeni gaze yönüne dik plane'e projekte et ve up olarak
kullan.
Sonuç:
- Küçük tilt'ler ilgili canonical axis'e snap olarak temizleniyor.
- TOP yukarıda iken TOP-LEFT edge tıklayınca TOP yukarıda kalıyor;
cube edge'e dönüyor ama kullanıcının "yukarı" hissi korunuyor.
- Upside-down orbit edilmişse -Z up olarak seçilir, snap baş aşağı
yapılandırmayı bozmuyor.
CubeRegion::snap_angles ve Camera::snap_to_angles yerini
snap_direction / snap_to_direction aldı.
snap_active_viewport_to_direction viewport'un view_direction'ını
günceller, twist_angle'a dokunmaz.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the binary Wireframe/Solid toggle with a 7-mode dropdown
that maps onto acadrust's `ViewportRenderMode` enum: Wireframe 2D /
Wireframe 3D / Hidden Line / Flat Shaded / Gouraud Shaded / Flat
Shaded + Edges / Gouraud Shaded + Edges. Per-tab state lives on
`DocumentTab::render_mode`; the old `SetWireframe` message forwards
to `SetRenderMode` so the ribbon + WIREFRAME / SOLID command line
keep working.
`scene::render::render_mode_flags()` translates the mode into three
pipeline-facing booleans (`face3d_fill`, `mesh_fill`,
`show_3d_edges`); `mesh_fill` / `show_3d_edges` are stubs that the
upcoming phases will consume. `Primitive` carries the derived
`view_wireframe` plus the two new fields through to `prepare()`.
`Pipeline::upload_face3d` takes a `wireframe_only` flag that drops
3DFACE quad fills (built from `key_vertices`) while keeping the
`all_wires.fill_tris` channel — that channel carries text-LOD greek
rectangles and PolyfaceMesh face triangles which must stay visible
in wireframe mode. 3DFACE edges stay on regardless of mode.
Solid3D mesh wireframe rendering, Hidden Line z-prepass, and Flat /
Gouraud shader uniforms come in phases 1–4.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>