Commit Graph
210 Commits
Author SHA1 Message Date
Hakan Seven 89fcb21b46 fix(layer): propagate LAY* command changes to panel + dropdown (#39)
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
2026-05-29 01:30:02 +03:00
Hakan Seven 8f9fe5226b fix(ribbon): clear blue tool state when its popup window closes (#40)
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
2026-05-29 01:23:25 +03:00
Hakan SevenandClaude Opus 4.7 e9bc923677 fix(window): keep popup windows above the main window (#38)
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 <[email protected]>
2026-05-29 01:07:36 +03:00
Hakan Seven 8f02e1ca07 fix(dyn-input): re-sync fields immediately after point commits (#35)
`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.
2026-05-29 00:57:37 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-29 00:46:18 +03:00
Hakan SevenandClaude Opus 4.7 4c60dce81a fix(cmd): refresh rubber-band preview after typed point (#32)
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 <[email protected]>
2026-05-28 22:59:48 +03:00
Hakan SevenandClaude Opus 4.7 e27ab79b08 fix(menu): wire EXIT / QUIT commands to the close path (#41)
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 <[email protected]>
2026-05-28 22:55:08 +03:00
Hakan Seven 8a510c563c fix(render): adapt mesh + hatch load colours to background
`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.
2026-05-28 21:15:28 +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 e3c962bdd1 fix(viewport): adapt entity colours to visible bg
`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
2026-05-28 18:28:18 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-28 10:03:01 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-28 09:45:19 +03:00
Hakan SevenandClaude Opus 4.7 6fe25e4b5f feat(viewport): interactive VPORTS model tiled presets
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 <[email protected]>
2026-05-27 22:52:55 +03:00
Hakan SevenandClaude Opus 4.7 41c75dbb9b feat(viewport): click activates tile, swaps in its camera
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 <[email protected]>
2026-05-27 22:38:45 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-27 22:33:36 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-27 18:17:10 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-27 18:05:09 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-27 17:49:23 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-27 17:36:06 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-27 14:08:57 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-27 13:41:54 +03:00
Hakan SevenandClaude Opus 4.7 874226bd43 feat(input): relative coordinate entry (@/#) and DYN default — #26
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 <[email protected]>
2026-05-27 11:40:28 +03:00
Hakan SevenandClaude Opus 4.7 451239c4f9 fix(viewcube): snap to direction with nearest-canonical up axis
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 <[email protected]>
2026-05-27 08:09:38 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-27 05:57:19 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-27 00:08:37 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-26 22:45:54 +03:00
Hakan SevenandClaude Opus 4.7 4124cb2689 feat(commandline): full-history dropdown next to the input
Add a ▸/▾ toggle button on the right edge of the input row. Tapping
it pops up the full backlog of everything the command line has pushed
since startup — useful once the on-overlay rows have faded out.

The dropdown is a fixed-height scrollable panel rendered above the
input pill. `anchor_bottom` keeps the newest entry in view on open
and `opaque` blocks wheel events from bubbling through to the
viewport shader behind it (without it, scrolling the history zoomed
the drawing). Submitting a command closes the dropdown so the new
prompt is immediately visible.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 22:13:15 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-26 19:28:09 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-26 19:23:19 +03:00
Hakan SevenandClaude Opus 4.7 979ca28632 fix(commandline): empty Space/Enter repeats the last command
Two follow-ups to the always-on keyboard work:

1. **Repeat last command on empty Submit.** Pressing Space (or Enter)
   after a command ended did nothing — the trailing-space submit path
   reached `CommandSubmit`, found no active command and no buffered
   text, and bailed. `CommandFinalize` already had the repeat-last
   shortcut via `tab.last_cmd`; mirror it inside `CommandSubmit` so
   the space-as-submit path picks it up too.

2. **Drop the per-event focus_cmd_input calls** added in the previous
   commit for `ViewportLeftPress`, `ViewportClick`, `TabNew`,
   `TabSwitch`, `RibbonSelectTab` and `RibbonToolClick`. The global
   keyboard subscription routes unhandled character / backspace
   events into the buffer regardless of focus, so the only places
   that still need an explicit refocus are `cmd_result.rs` (command
   lifecycle), `boot()` (initial caret), and the per-char append
   handlers (caret feedback while typing).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 19:10:01 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-26 18:59:51 +03:00
Hakan SevenandClaude Opus 4.7 5da32ff202 refactor(entities): collocate ui_name + dxf_name into entities/names.rs
Two near-identical match-on-EntityType tables lived far apart:
scene/properties.rs::entity_type_name returned mixed-case UI labels;
app/commands.rs::entity_type_name returned uppercase DXF record names.
Both enumerated ~30 variants; adding a new entity meant remembering to
touch two files in two crates' worth of source layout.

Move both into a single entities/names.rs as `ui_name()` and
`dxf_name()`. Adjacent so a new variant gets both labels added at once;
function names make the intended audience explicit at the callsite.

scene/properties.rs and app/commands.rs each lose their local match
plus one re-export, gaining a `crate::entities::names::*` call.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 17:18:47 +03:00
Hakan SevenandClaude Opus 4.7 e78321190a refactor(commands): per-entity MassProps + TextContent traits
Two free helpers in app/commands.rs lived as big match-on-EntityType
blocks: massprop_entity (Circle/Arc/Line/LwPolyline/Ellipse area +
perimeter + centroid, ~140 lines) and entity_text_content /
replace_entity_text (Text/MText/AttributeDefinition/AttributeEntity).

Move each to its own trait in entities/traits.rs and implement
per-entity:

  MassPropsCalc  — Circle, Arc, Line, LwPolyline, Ellipse
  TextContent    — Text, MText, AttributeDefinition, AttributeEntity

EntityTypeOps gets dispatch shims that route to the trait via the
existing macro, so callsites just call entity.mass_props() /
entity.text_content() / entity.replace_text(search, rep).

The remaining free helpers in commands.rs (entity_list_details,
entity_extra_info, entity_type_name, build_data_extraction_csv) are
single-command formatting helpers tightly coupled to LIST / EXTRACT /
text-format output. Distributing them as per-entity methods would
inflate every entity file with command-side formatting concerns
without making the command-side any cleaner — left in place.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-23 23:30:34 +03:00
Hakan SevenandClaude Opus 4.7 ecdb0cd0a0 chore(release): bump to 0.4.0
Fix repo URLs to canonical HakanSeven12/OpenCADStudio casing.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-23 17:32:29 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-23 17:16:15 +03:00
HakanandClaude Opus 4.7 e9488f99dd perf(wire): move LWDISPLAY toggle from CPU rebake to wire shader uniform
LWDISPLAY (Show Lineweight) flipping the toggle no longer triggers a full
retessellate. `render_style_for` always bakes the entity's resolved (layer-
inherited) lineweight, and the wire shader reads `Uniforms.lwdisplay_enable`
to either honour the per-vertex half_width or collapse the quad to a 1 px line.

Side effect: PDF export now uses the real lineweight regardless of LWDISPLAY,
matching AutoCAD's plot semantics (LWDISPLAY only affects screen render).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-23 09:45:24 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-23 07:15:26 +03:00
HakanandClaude Opus 4.7 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 <[email protected]>
2026-05-22 12:55:37 +03:00
Hakan SevenandClaude Opus 4.7 c2f3844cb6 feat(io): export hatch + wipeout fills to PDF
The PDF / print pipeline only walked `WireModel.points` (lines), so every
hatched region disappeared the moment a sheet left H7CAD. Route the same
`paper_canvas_hatches()` / `paper_canvas_wipeouts()` lists that the
on-screen canvas consumes into `pdf_export::export_pdf` and
`print_to_printer::print_wires`, and emit each one as an
`Op::DrawPolygon` with the EvenOdd winding rule so multi-ring boundaries
(islands / holes) render correctly:

- Solid       → fill the boundary polygon in the hatch colour.
- Pattern     → stroke the outline (PAT line families stay procedural;
                follow-up work will rasterise them into segments).
- Gradient    → fill with the averaged colour, mirroring paper_canvas.

Wipeouts emit first, then hatches, then wires — same z-order as the
on-screen canvas, so wires draw on top of the fills they belong to.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-22 06:45:36 +03:00
Hakan Seven dbed40effd remove unused BRAND_TINT_BG color constant 2026-05-22 06:11:21 +03:00
HakanandClaude Opus 4.7 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 <[email protected]>
2026-05-21 16:19:31 +03:00
Hakan SevenandClaude Opus 4.7 50281c6a05 fix(ui): per-tab Layer/Color/Linetype/Lineweight defaults
The Home-ribbon Color / Linetype / Lineweight chips live on the
global `self.ribbon` and were never re-seeded from the active tab's
header, so opening a fresh file, creating a new tab, or switching
between tabs kept showing whatever the prior tab had picked. Layer
worked already because `self.tabs[i].active_layer` is per-tab.

Two halves of the fix:

1. When the user picks a new value from the ribbon with no entity
   selection, persist it into that tab's `document.header`
   (CLAYER + handle, CECOLOR, CELTYPE + handle, CELWEIGHT) and
   mark the tab dirty. Switching back later reads the saved value
   back out.

2. After every event that changes the active tab — `FileOpened`,
   `TabNew`, `TabSwitch`, `TabClose` (including the
   close-last-and-reset path) and `UnsavedDialogDiscard` — call
   `sync_ribbon_from_selection()` so the chips track the new
   tab's defaults (or its current selection).

Closes #21

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-20 21:00:13 +03:00
Hakan SevenandClaude Opus 4.7 4cca6363ef feat(app): surface round-trip HeaderVariables via DWGPROPS + USERI/USERR
The "round-trip metadata" fields (timestamps, GUIDs, file identity,
user-script scalars) were parsed and re-emitted on save but never
exposed anywhere, so users couldn't confirm what the file actually
carried. Add two command-line commands that read / edit them:

- DWGPROPS / DWGPROP dumps the header to the command-line output:
  create / update / total-edit / user-elapsed timestamps,
  fingerprint_guid, version_guid, code_page, menu_name, hyperlink_base,
  project_name, stylesheet, required_versions, measurement (with a
  human label), proxy_graphics, tree_depth, plus the five USERI / USERR
  slots and the user_timer flag.
- USERI <1-5> <int> / USERR <1-5> <real> writes one of the five
  document-scoped scalar slots and marks the tab dirty so the value
  rides the next save. Lets users park drawing-scoped numbers in the
  same place AutoCAD scripts have always used them, even though we
  don't ship a LISP / DIESEL runtime.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-20 05:48:07 +03:00
Hakan SevenandClaude Opus 4.7 b3e117a969 feat: honour LUNITS / LUPREC unit formatting + INSUNITS scaling on insert
Property-panel coordinate display and block-insert scaling were both
hard-coded, ignoring the document's stored unit configuration.

- LUNITS / LUPREC: edit_prop() now formats values through a thread-local
  UnitContext seeded by refresh_properties(). Decimal / Scientific /
  Engineering / Architectural / Fractional all render correctly without
  threading the document handle through every entity properties builder.
- AUNITS / AUPREC: format_angle() helper available (decimal degrees, DMS,
  grad, rad). Callers that already format angular values via radians can
  switch over without touching the helper signatures.
- INSUNITS + MEASUREMENT: xref's source INSUNITS is carried onto the
  host BlockRecord.units during merge. commit_entity() then scales new
  INSERTs so 1 source-unit maps to the host's INSUNITS length. When
  either side is unitless (0) MEASUREMENT acts as the fallback
  (0=Imperial / inches, 1=Metric / mm), matching AutoCAD's rule.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-20 05:43:55 +03:00
Hakan SevenandClaude Opus 4.7 0db21568c1 feat(app): seed ribbon defaults from CECOLOR/CELTYPE/CELWEIGHT/CLAYER + apply CELTSCALE
The Home ribbon's creation-default chips were hard-coded to ByLayer
whenever the selection went empty. New entities therefore ignored the
file's saved CECOLOR / CELTYPE / CELWEIGHT and started fresh at ByLayer.

Pick those defaults up from header instead:

- sync_ribbon_from_selection() reads current_layer_name, current_entity_color,
  current_linetype_{name|handle} and current_line_weight when nothing is
  selected, mapping the i16 weight code through LineWeight::from_value.
- commit_entity() stamps header.current_entity_linetype_scale onto the
  fresh entity's linetype_scale when it differs from 1.0.

ELEVATION / PAPER_ELEVATION / CMLSCALE / CMLJUST will follow with the
commands that consume them; PLINEGEN already routes through the per-
polyline bit at render time.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-20 04:47:15 +03:00
Hakan SevenandClaude Opus 4.7 0f12c289c8 feat(ui): ribbon dropdowns edit the current selection
Picking a layer / color / linetype / lineweight from the Home ribbon was
only updating the creation default — selected entities stayed unchanged.
DWG editors uniformly treat these dropdowns as edit controls when an
entity is selected.

Route the ribbon Layer / Color / Linetype / Lineweight handlers through
the same property_target_handles + apply_common_prop / apply_color /
apply_line_weight path the Properties panel uses. With no selection the
old behaviour (change creation default) is preserved.

Follow-up to #17.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-19 20:13:03 +03:00
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-19 13:42:03 +03:00
Hakan SevenandClaude Opus 4.7 4fa01b3a1a fix(ui): mirror entity selection into the Home-ribbon dropdowns
Selecting an entity refreshed the Properties panel but the Layer / Color /
Linetype / Lineweight dropdowns kept showing the creation-time defaults
— so they couldn't be used to inspect or edit a single object's settings.

refresh_properties() now also calls sync_ribbon_from_selection(), which
walks the current selection and mirrors common values into ribbon.active_*
fields. Mixed selections leave the prior value in place (no "*Varies*"
chip yet); an empty selection restores per-tab creation defaults.

Closes #17

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-19 13:40:16 +03:00
Hakan SevenandClaude Opus 4.7 4adbb20c5d feat(scene): honour the last DimStyle fields end-to-end
Tessellation and DIMBASELINE pick up the few DimStyle fields that were
still hard-coded or ignored:

- DIMTMOVE = 1 draws a connector segment from the dim-line anchor to the
  saved text_middle_point when the text has been moved
- Leader arrows are resolved via the leader's DIMSTYLE → DIMLDRBLK; the
  shape (closed-filled / open / dot / tick / …) is drawn through the same
  arrow emitter the dim path uses, including fill_tris for filled blocks
- DIMBASELINE picks up DIMDLI from the active DimStyle instead of the
  hard-coded 1.5 increment
- DIMTOFL / DIMTIX / DIMATFIT / DIMUPT / DIMTXTDIRECTION are read so the
  fields round-trip on save; their effect is at dim creation, not render,
  so they're acknowledged with a no-op block

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-19 13:37:37 +03:00
Hakan Seven 66243e53ca feat: correct viewport scale calculation with correct dimension and text scaling 2026-05-19 00:26:08 +03:00