Commit graph cad-editor/src/app/view.rs
Author SHA1 Message Date
Hakan Seven
9db206f310 feat(select): Select Similar + Quick Select panel (#28)
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>
2026-05-29 20:30:56 +03:00
Hakan Seven
cf925be861 fix(ctx-menu): anchor right-click menu under cursor, share overlay infra
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>
2026-05-29 15:58:23 +03:00
Hakan Seven
86ca1b7033 fix(grip): popup sizes to widest label + full-row highlight
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.
2026-05-29 10:00:30 +03:00
Hakan Seven
391aa3a76f feat(grip): keyboard nav + click-outside dismiss + single-widget popup
- 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.
2026-05-29 09:51:12 +03:00
Hakan Seven
595ee3e245 fix(grip): open hover popup without requiring mouse motion
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.
2026-05-29 09:38:37 +03:00
Hakan Seven
6c1462932d feat(grip): multi-functional hover popup menu
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.
2026-05-29 09:30:54 +03:00
Hakan Seven
5755c2e130 refactor(grip): square-only marker set + oriented stretch handles
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.
2026-05-29 02:07:34 +03:00
Hakan Seven
f952fc9bb7 fix(dyn-input): support cartesian coords via "," (#35)
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>
2026-05-29 00:46:18 +03:00
Hakan Seven
39eab9a4c4 feat(viewport): draggable Model-tile dividers
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.
2026-05-28 18:42:39 +03:00
Hakan Seven
8d64426087 refactor(viewport): unify paper into one shader, transparent clear
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>
2026-05-28 10:03:01 +03:00
Hakan Seven
417a708275 feat(viewport): tile-relative input + overlays in unified model
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>
2026-05-28 09:45:19 +03:00
Hakan Seven
c45146bbda feat(viewport): model-space tiled split (▤ / ▥ buttons)
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>
2026-05-27 22:33:36 +03:00
Hakan Seven
eeb7df5fec fix(paper): make active-viewport picker & ViewCube clickable
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>
2026-05-27 18:17:10 +03:00
Hakan Seven
a41a05baa1 feat(paper): render-mode picker + ViewCube on active viewport
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>
2026-05-27 18:05:09 +03:00
Hakan Seven
fda0f2dbb9 refactor(ui): remove the viewport navigation bar
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>
2026-05-27 17:49:23 +03:00
Hakan Seven
6483566c5d fix(dyn-input): capture numpad 8/2 as digits, not history nav
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>
2026-05-27 17:36:06 +03:00
Hakan Seven
4abde6001a feat(dyn-input): TAB-editable distance/angle/coordinate fields
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>
2026-05-27 14:08:57 +03:00
Hakan Seven
1fc73ba8b0 fix(dyn-input): correct Y coordinate, label by prompt type — #27
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>
2026-05-27 13:41:54 +03:00
Hakan Seven
132efe6574 feat(render): Flat/Gouraud shading, WithEdges overlay, paper-viewport mode
Faz 3 — FlatShaded vs GouraudShaded
- Uniforms.flat_shade (f32, 0/1) + mesh shader fragment derivative
  bazlı per-triangle face normal hesabı; GouraudShaded ise interpolate
  edilen vertex normalini kullanıyor.

Faz 4 — *WithEdges varyantları
- Pipeline::render.show_3d_edges parametresi:
  - FlatShaded / GouraudShaded → 3DFACE edge pass'i atlanıyor
  - *WithEdges varyantları → shaded fill'in üstüne mesh_wireframe
    pipeline ile triangle-edge overlay (LessEqual depth test ile)
  - face3d_edges pass'i show_3d_edges ile gate'lendi

Faz 5 — Paper-space viewport entity round-trip
- build_viewport_primitive / build_active_viewport_primitive artık
  render_mode parametresi almıyor; viewport entity'sinin kendi
  render_mode alanından okuyor. PaperViewportPane'dan render_mode
  field'i kaldırıldı. Model layout'taki tab.render_mode hala
  pick_list ile yönetiliyor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 05:57:19 +03:00
Hakan Seven
2145ab8a66 feat(render): phase 0 — render-mode pick_list + 3-flag pipeline plumbing
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>
2026-05-27 00:08:37 +03:00
Hakan Seven
05ea31aa2a feat(commandline): autocomplete suggestions + inventory-based registry
Type-as-you-go suggestion popup above the input that filters known
command names by substring (so `LEADER` surfaces both LEADER and
MLEADER). Up / Down navigates the list, Enter dispatches the
highlighted entry, clicking a row dispatches directly. Esc on a
non-empty input clears the buffer instead of escalating to
command-cancel.

The name corpus comes from `inventory::submit!` blocks placed next
to each `CadCommand` impl rather than a hand-maintained static list.
Adding a new interactive command means dropping one `inventory::submit!`
line in its own module file — the autocomplete picks it up at the
next compile. 98 submissions seeded across 60 module files;
`command::all_registered_command_names()` flattens them at runtime.

One-shot dispatch arms in `app/commands.rs` (NEW, OPEN, SAVE, UNDO,
…) don't have a `CadCommand` impl and are therefore absent from
autocomplete by design.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 22:45:54 +03:00
Hakan Seven
5a8c79deb5 feat(commandline): fade history entries off the overlay after 3 s
The overlay used to keep the last four history lines on screen
indefinitely, leaving stale command output cluttering the viewport.
Stamp each `HistoryEntry` with its `Instant` of insertion and filter
the rows shown on the overlay to entries younger than 3 s.

`CommandLine::has_visible_history()` gates a per-frame tick
subscription so the view re-renders and the entry disappears
on time. The subscription auto-stops once no entry is fresh enough,
keeping the idle CPU draw at zero (#18).

Full history stays in the backing `Vec` — the upcoming dropdown
button will surface it on demand.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:28:09 +03:00
Hakan Seven
862e5aed31 feat(commandline): float as bottom-centre overlay over the viewport
The command-line panel used to consume a full-width row at the bottom
of the main column, taking vertical space away from the drawing area
even when no command was active. Move it onto the viewport stack as a
fixed-width 720 px pill aligned to the bottom-centre, with rounded
corners so it reads as a floating element. The status bar still owns
the very bottom of the window.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:23:19 +03:00
Hakan Seven
39150d617f feat(commandline): always-on keyboard capture + space submits like enter
Two behaviour changes that bring the command line in line with
established CAD workflow:

1. **Always-on keyboard capture.** The input was blurred whenever no
   command was running, so typed letters went nowhere until the user
   clicked into the field. Replace the blur with a re-focus on every
   command lifecycle transition (cmd_result.rs) and on viewport / tab
   / ribbon clicks (update.rs), focus the input at startup, and add
   a global key subscription that catches any unhandled character key
   or backspace and routes it into the buffer via two new messages
   (CommandAppendChar / CommandBackspace) — so the command line keeps
   accumulating keystrokes even if focus parks somewhere not
   focusable. Removes the unused blur_cmd_input helper.

2. **Space submits.** Typing a space at the end of the input now
   submits the buffer the same way Enter does. Only exception: an
   active command that's currently collecting free-form prose
   (TEXT / MTEXT / DDEDIT body, ATTDEF prompt / default value,
   INSERT attribute value, ATTEDIT cell). Those advertise the new
   `CadCommand::wants_text_with_spaces()` flag so the Space character
   reaches the buffer as a literal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:59:51 +03:00
Hakan Seven
ef1f9a08d6 chore: rename H7CAD to Open CAD Studio
Joining the OpenAEC Foundation (open-aec.com). Crate/struct become
OpenCADStudio, user-facing strings use the spaced "Open CAD Studio"
form, repo URLs point to HakanSeven12/open-cad-studio. Logo (window
icon + assets/logo.svg) redrawn with OCS initials. Drop examples/
binaries — unused.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 17:16:15 +03:00
Hakan Seven
fb3dd58d09 fix(polyline): correct wide-polyline fill band arc center + add LWDISPLAY toggle
Wide LWPolyline/Polyline2D fill bands now sit on the centerline:
- polyline_segment_fill arc center matched explode.rs / acadrust convention
  (signed d_perp = r·(1-b²)/(1+b²), handles major arcs correctly)
- per-vertex hw0/hw1 taper applied along arc; tess density 24/circle
- synced_hatch_models subtracts hatch_offset from band boundary so it lives
  in the same offset-rel space as the wire tessellation

LWDISPLAY:
- new command `LWDISPLAY [ON|OFF]` toggles header.lineweight_display
- LWT status-bar pill (between ORTHO and POLAR) drives the same flag
- bump_geometry() on change so the viewport re-tess immediately

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 07:15:26 +03:00
Hakan
2357b39a1d feat(update): show release notes + version-card layout in update notice
The update notice dialog now mirrors what GitHub publishes: side-by-side
Installed → Latest version cards (the new one accent-tinted to draw the
eye) and a scrollable release-notes panel under them. Light markdown
rendering covers `##` headings, bullet lists, and inline bold/code
markers — enough for the GitHub release body format we ship.

- update_check::check_for_update() returns UpdateInfo { version, body };
  body is parsed from the releases API JSON with a tiny custom
  string-field extractor (\n / \" / \u handling) so we keep the
  serde_json-free dependency footprint.
- Window grows from 480x240 to 560x460 and becomes resizable to make
  room for the notes scroller.
- Layout tightens: header bottom padding drops 14 → 6, notes heading
  10, and the notes block is wrapped in a Fill-height container so it
  claims every leftover pixel — no more dead space between heading and
  footer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 12:55:37 +03:00
Hakan Seven
dbed40effd remove unused BRAND_TINT_BG color constant 2026-05-22 06:11:21 +03:00
Hakan
35acdb2dc8 feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.

- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
  Start tab. Recents persist to disk
  (%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
  on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
  to 20 entries, saved on every open and rehydrated at boot.
  Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
  retired; the same actions surface on the welcome page instead. The
  Donate button is the primary call-to-action — solid #B03020 brand
  fill with a soft brand-tinted ambient glow halo behind the action row
  (Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
  selection markers) are suppressed on the Start tab so its clicks
  don't trigger CAD commands. OpenFile's "current_is_empty" check
  refuses to replace the Start tab, and TabClose ignores requests on
  it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
Hakan Seven
0db2d16232 perf(app): only subscribe to window::frames while animating
H7CAD was subscribing to window::frames() unconditionally, so iced kept
firing Message::Tick at the display refresh rate even when nothing on
screen was changing. That re-ran update() and the view-tree builder ~60
times a second and showed up as 2-3% idle CPU on Windows.

Gate the subscription on `self.opening.is_some()` (the only thing that
actually needs per-frame redraws today — the file-open progress
indicator). Camera-change detection moves with the events that drive it
since user input already wakes iced.

Closes #18

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 13:42:03 +03:00
Hakan Seven
86f5ba7b62 feat: in-app update notice — checks GitHub releases on startup
Adds a one-shot update check that runs on app boot and pops up a small
"Update Available" window when the GitHub releases API reports a newer
tag than this build's `CARGO_PKG_VERSION`.

- `src/update_check.rs` — async wrapper around a blocking `ureq` GET to
  `api.github.com/repos/HakanSeven12/H7CAD/releases/latest`, returns
  `Some(version)` when a newer release exists, `None` otherwise (silent
  on network / parse failure so a missing connection never blocks
  launch).
- `H7CAD::boot` chains a `Task::perform` next to the main-window open
  so the check runs in parallel with first-paint.
- New window plumbed like the other floating panels (page setup, about,
  etc.): `update_notice_window`/`update_notice_version` state, title
  entry, `OsWindowClosed` cleanup, and a `ui::update_notice::view_window`
  modal that shows installed vs. latest version plus "Later" /
  "Open Release Page" buttons. Open routes through `open::that` to the
  releases URL and dismisses the modal.

Dependency: adds `ureq = { version = "2", default-features = false,
features = ["tls"] }` — the smallest blocking HTTP client + TLS combo
that doesn't drag in an async runtime.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 19:55:57 +03:00
Hakan
586df77b2c feat(io): show loading overlay while opening CAD files
Big DWG/DXF files block the UI for seconds during parse + cache build,
leaving the user wondering whether the app froze. Split the file-open
flow into pick → load and show a modal overlay during the load: file
name, size, phase label (Reading / Parsing / Building caches /
Finalizing), animated indeterminate bar, and Cancel button.

Phase is tracked via a shared AtomicU8 written by the loader thread
and read by the overlay each frame. Cancel hides the overlay and
discards the result; the parser thread runs to completion in the
background since acadrust has no interrupt hook.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 09:59:07 +03:00
Hakan
ca07268c7c cargo fmt 2026-05-12 10:36:15 +03:00
Hakan
0e5dec0948 feat: annotation scale system (CANNOSCALE, PSLTSCALE, MultiLeader, Leader)
- Scale popup dropdown replaces cycle behavior; paper-space viewport scale
  editable when a viewport is active/selected
- CANNOSCALE persisted via DXF $USERR1 (read on open, write on save)
- PSLTSCALE: linetype dash lengths scaled by viewport anno_scale so dashes
  appear uniform physical size in paper space
- MultiLeader: arrow size, dogleg length, and text height scale with
  anno_scale; leader line paths (world coords) stay fixed
- Leader: arrow size and hookline length scale with anno_scale; also
  corrects world_offset subtraction that was previously deferred to the
  TruckObject::Lines path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 17:50:46 +03:00
Hakan Seven
1b4c5e13e5 fix: keep open drawing when creating new file 2026-05-03 19:55:38 +03:00
Hakan Seven
7e8fc8ef88 feat: replace rfd Save As with native iced file chooser
Implements a full in-app file browser window for Save As, replacing the
rfd dialog that could not expose the selected filter index on Linux.
The new chooser includes directory navigation, a scrollable file list,
filename input, and a format drop-down (DWG/DXF × 2018/2013/2010/2007/2004/2000/R14).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 16:20:38 +03:00
Hakan Seven
9cd8eff77f fix: replace broken version filters with custom Save As dialog
rfd does not expose the selected filter index, so the old DWG/DXF version
filters had no effect — every file was always saved as AC1032 (R2018).

Replace with a single custom iced Save As window that shows:
- File name text input
- Save-in folder input + Browse button (rfd folder picker)
- Format dropdown: DWG/DXF × 2018/2013/2010/2007/2004/2000/R14
- Save / Cancel buttons

The selected format is passed to save_as_version() which temporarily sets
doc.version before writing, so DwgWriter/DxfWriter emit the correct file
format. Also works in the unsaved-changes Save flow.

Closes #9

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 16:12:32 +03:00
Hakan Seven
01943c5b8d fix: correct paper-space grip projection and MTEXT attachment point rendering
- Grip points in paper space now use the 2-D linear to_px transform
  (tx/ty/ortho_size) instead of the 3-D view_proj matrix, matching the
  coordinate system used by PaperCanvas
- world_offset is no longer subtracted from paper-space entity grip
  positions — paper-space coordinates are already unoffset (same rule
  as wire tessellation in wires_for_block)
- MTEXT v_offset corrected for all attachment points: CXF glyphs sit
  on the baseline and extend upward, so Top attachment needs v_offset=-h,
  Bottom needs (n_lines-1)*line_h, Middle is the midpoint of the two

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 12:16:46 +03:00
Hakan Seven
fc0aa8698b feat: show Save/Discard/Cancel dialog before closing unsaved tabs or quitting
Opens a floating OS window when the user tries to close a dirty tab or
quit with unsaved changes. The main window stays alive until the dialog
is resolved — Cancel aborts the close entirely. For quit with multiple
dirty tabs the dialog loops through each one individually.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 11:31:11 +03:00
Hakan Seven
5f3182fa9a fix: NAVVCUBE and UCSICON commands now properly toggle visibility
NAVVCUBE: propagate show_viewcube flag through ViewportPane and the wgpu
Primitive so the ViewCube pipeline skips upload/render when hidden.
UCSICON: bare command (no subcommand) now toggles current state instead
of printing usage info.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 01:31:52 +03:00
Hakan Seven
1ec53a4730 feat: view tab widget toggle commands and unified SVG icon color scheme
Integrate NAVVCUBE, NAVBAR, PROPERTIES, FILETAB, LAYOUTTAB, TOOLPALETTES
and SHEETSET commands to toggle UI widgets at runtime. Add 15 new SVG icons
for view module tools previously using glyph fallbacks. Restyle all SVG
icons to use the established two-color scheme: #e0e0e0 for passive geometry,
#4cc9f0 for active/accent elements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 01:23:06 +03:00
Hakan Seven
a6592cc9bc fix: ViewCube hover, glow, centroids, corner geometry and snap angles
- Hide OS crosshair and drawn crosshair when cursor is over ViewCube area
- Enhanced hover glow in WGSL shader (blue tint + brightness boost)
- Fixed region_centroids() Y-axis sign for all FRONT/BACK-related regions
- Swapped corner geometry assignments (TPF↔TBK, BTF↔BBK) to match
  intended FRONT/BACK semantics (sy=-1 = FRONT, sy=+1 = BACK)
- Corrected all snap_angles: yaw_pitch_to_quat(0,0) places eye at -Y
  (FRONT view), so FRONT=0 and BACK=PI — cascaded through edges/corners

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 19:11:19 +03:00
Hakan Seven
0a291c877b feat: A4 landscape default paper, white paper area on dark desk
- paper_limits() falls back to A4 landscape (297×210mm) even when no
  Layout object exists in the document
- paper_bg_color default changed to white [1,1,1,1] (paper surface color)
- PaperCanvas::draw() fills canvas with dark desk color first, then draws
  the paper area rectangle in paper_bg_color so the sheet is clearly
  visible against the desk background
- view.rs container background uses the fixed desk color instead of
  paper_bg_color so it no longer conflicts with the paper-surface color
- BACKGROUND RESET for paper space now resets to white instead of dark gray
- Removed unused PaperSheet variant, paper_sheet() constructor,
  build_paper_sheet_primitive() and associated match arms (dead code
  after 2D canvas replacement)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 14:05:07 +03:00
Hakan Seven
fcc29de469 feat: inline MSPACE viewport overlay on 2D paper canvas
- viewport_screen_rect now mirrors PaperCanvas::draw()'s camera-based
  to_px transform instead of using a fixed paper-limits scale, so the
  3D overlay lands exactly over the drawn viewport border at any zoom/pan

- blue border frame (stack layer above the shader) makes the active
  viewport boundary always visible even when it fills the canvas

- space-mode pill in MSPACE now dispatches LayoutSwitch("Model") so
  clicking it jumps to full Model Space instead of just exiting to PSPACE

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:57:34 +03:00
Hakan Seven
459b525ffe feat: replace paper-space shader with 2D Iced canvas widget
Paper space is now rendered as a 2D vector canvas (iced::widget::canvas)
instead of a 3D shader widget.  This allows direct interaction with
paper-space entities (title blocks, viewport borders, annotations)
without needing to enter MSPACE first.

Changes:
- src/scene/paper_canvas.rs: new PaperCanvas<'a> canvas::Program that
  renders paper wires (with linetype dashes), solid/gradient hatch fills,
  and wipeout backgrounds using the paper camera's orthographic transform.
- Scene: three new public helpers (paper_canvas_wires, paper_canvas_hatches,
  paper_canvas_wipeouts) feed the canvas with the same data the old shader
  used, including inactive viewport projections and interim/preview wires.
- view.rs: paper_canvas_view() now wraps PaperCanvas instead of
  shader(ViewportPane::paper_sheet(…)).  The PaperViewportPane 3D shader
  overlay for the active MSPACE viewport is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 09:48:53 +03:00
Hakan Seven
c2df63c4cc feat: add clickable space-mode button to status bar
Replaces the static MODEL/LAYOUT pill with an interactive button that
shows and toggles the current editing context:

  Model tab     → "MODEL"  (non-clickable, informational)
  Layout PSPACE → "PAPER"  (click → MspaceCommand: enter MSPACE)
  Layout MSPACE → "MODEL"  (click → ExitViewport: return to PSPACE)

Highlights in blue when in MSPACE to signal active model editing.
Mirrors the AutoCAD status-bar convention for MODEL/PAPER switching.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 09:18:01 +03:00
Hakan Seven
97dbd80c93 feat: render active MSPACE viewport as a true 3D widget in paper space
When a paper-space viewport is activated (double-click / MSPACE command),
it is now rendered through its own dedicated PaperViewportPipeline rather
than the CPU-projection approach used for inactive viewports.

Architecture:
- PaperSheet widget (full-canvas): renders paper entities + CPU-projected
  content of all viewports *except* the active one.
- PaperViewportPane widget (Fixed w×h, positioned at viewport screen rect):
  renders the active viewport with a true 3D camera derived from the
  viewport's view_direction/view_target/view_height.

PaperViewportPipeline is a newtype of Pipeline.  Having a distinct type
gives it its own Iced storage entry (keyed by TypeId), preventing the
shared prepare() overwrite that broke the earlier per-viewport approach.

Changes:
- render.rs: PaperViewportPipeline, PaperViewportPrimitive newtypes;
  build_active_viewport_primitive(); build_paper_sheet_primitive() now
  excludes active_viewport from CPU projection.
- viewport_pane.rs: PaperViewportPane<'a> with shader::Program impl.
- mod.rs: viewport_content_wires() gains exclude_vp parameter;
  viewport_screen_rect() is no longer dead code.
- view.rs: paper_canvas_view() overlays PaperViewportPane when MSPACE
  is active.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 03:19:15 +03:00
Hakan Seven
d1d4bb3d02 fix: restore paper-space viewport rendering as single full-canvas widget
Iced 0.14 batches all shader prepare() calls before any render() calls.
Multiple ViewportPane widgets sharing the same Pipeline type overwrite
each other's GPU buffers, causing the PaperSheet layer (full-canvas) to
render the last viewport's model content — appearing full-screen.

Fix: paper_canvas_view() now renders a single full-canvas PaperSheet
widget. build_paper_sheet_primitive() is updated to include model-space
content projected through each viewport's view matrix (via
viewport_content_wires()), restoring the original working behaviour.

ViewportPane::Paper mode and viewport_screen_rect() are kept with
#[allow(dead_code)] — a per-viewport wgpu sub-renderer that accumulates
data across frames could revive them in the future.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 03:02:01 +03:00
Hakan Seven
da9fafc0f8 feat: introduce ViewportPane widget for unified model/paper rendering
- Remove Scene: shader::Program<Msg> impl; all GPU rendering now goes
  through ViewportPane which supports three modes:
    Model      — full model-space view (unchanged behaviour)
    PaperSheet — paper-space entities only, using the paper camera
    Paper      — model content through a specific viewport's own camera

- Add Scene helper methods (render.rs):
    build_primitive / build_paper_sheet_primitive / build_viewport_primitive
    update_viewcube_state / viewcube_mouse_interaction

- Add Scene helpers (mod.rs):
    paper_sheet_wires()        — paper entities without viewport projection
    camera_for_viewport()      — derive Camera from Viewport entity data
    model_wires_for_viewport() — model wires filtered by per-viewport frozen layers
    viewport_screen_rect()     — paper-space → pixel rect mapping

- Add paper_canvas_view() in view.rs:
    Stack of PaperSheet + one Paper widget per Viewport entity,
    positioned with Space offsets from viewport_screen_rect()

- Update VIEWPORT_WIDGET_PLAN.md: mark Steps 1–4 done, document remaining work

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 02:18:41 +03:00
Hakan Seven
070ad1f9c5 feat: add Report/About/Changelog buttons to Support ribbon group
- REPORT opens GitHub new-issue page
- ABOUT opens a dialog window with version, OS and arch info + Copy Info button
- CHANGELOG opens GitHub releases page
- Fix Windows build: add windows-sys as target-specific dependency for PRINT command

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 22:22:33 +03:00