Commit Graph
191 Commits
Author SHA1 Message Date
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
Hakan SevenandClaude Opus 4.7 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 <[email protected]>
2026-05-16 19:55:57 +03:00
Hakan SevenandClaude Opus 4.7 c815be9c6f fix(hit_test): correct hatch selection — NaN-aware ray-cast + block-insert resolve
Two related click bugs:

1. `point_in_polygon` treated NaN points (the path separators used by
   multi-path hatches with islands / holes) as regular vertices,
   producing spurious closing edges between sub-paths. The cursor
   inside a real hatch failed the test → the click iterator moved on
   to the NEXT hatch in HashMap order and silently returned the wrong
   one. Rewrite the ray-cast to reset the previous-vertex / path-start
   state at every NaN, so each sub-path closes against its own first
   vertex and contributes its parity flip correctly.

2. Clicking on a hatch *inside* a block was either missing entirely
   (after the visible_hatches_for_click filter) or returning the
   block-defn source hatch handle (misleading — that handle isn't the
   thing the user sees). AutoCAD selects the parent Insert when a
   sub-entity of a block is clicked. Add `Scene::insert_hatches_for_click`
   which walks the same explode path `synced_hatch_models` uses and
   tags every exploded sub-hatch with its parent Insert handle, plus
   `hit_test::click_hit_insert_hatch` which iterates that list. The
   click resolver now tries wire → MSPACE hatch → insert-internal hatch
   in order, so clicking on a block-internal hatch selects the Insert.

Box / lasso select paths are unchanged — wire hit-test already picks
up Inserts, and dragging a box over block-internal hatches is rare
enough not to need its own resolution path yet.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-16 13:02:57 +03:00
Hakan SevenandClaude Opus 4.7 412fb857ba chore: drop legacy comments and the last unused dead_code allows
* io/xref.rs — replaced the 7-line "BlockRecord::is_loaded turned out to
  be unreliable" rationale with a one-liner. The reverted experiment
  lives in git history (e88a946, 0996b75).
* refedit::RefEditSession.insert_handle — set at construction but never
  read; the "kept for future REFCLOSE DISCARD" TODO can come back via
  git history when that feature lands.
* scene::mod — dropped the placeholder MeshModel re-export; nothing
  outside `mesh_model` consumes the bare name and MeshLodSet is the
  pipeline-facing type now.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-16 02:04:41 +03:00
Hakan SevenandClaude Opus 4.7 a1ed87da3d chore: drop dead scene/IO helpers + simplify XATTACH command state
* helpers::wcs_to_ucs — UCS inverse transform with zero call sites.
* scene::Scene::compute_and_set_world_offset — never called; the load
  path uses the free function `compute_world_offset` directly.
* tessellate::tessellate_mesh — never called; mesh tessellation goes
  through `solid3d_tess`, not the truck Shell/Solid path.
* truck_tess::tess_to_mesh_model — orphaned with tessellate_mesh's
  removal.
* MeshModel::WHITE / MeshModel::SELECTED — only used by the removed
  tessellate_mesh.
* xattach::Step::FilePath variant — never constructed; XATTACH always
  enters through `with_path` after the file picker. Replaced the Step
  enum + prefilled_path field with flat `path` / `block_name` fields
  and dropped the unreachable text-input flow.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-16 00:10:20 +03:00
Hakan SevenandClaude Opus 4.7 8c08ae104a perf(scene): multi-resolution mesh LOD (Phase 3.4)
ACIS SAT entities (Solid3D / Region / Body) were tessellated at one
fixed sampling density (CIRC_SEGS=48, GRID_U=32, GRID_V=16) regardless
of how far they were from the camera. Distant solids paid the full
triangle bill for fragments that mostly resolved to sub-pixel size.

Generate three pre-built LODs per entity at load time:

  * LOD 0 (HIGH)  — 48/32/16 segments. Use above 200 px projected diag.
  * LOD 1 (MID)   — 24/16/8.  Use 50–200 px.
  * LOD 2 (LOW)   — 12/8/4.   Use below 50 px.

Tessellation cost is paid 3× on load (cheap — typical files have few
solids). Render picks per-frame via `compute_mesh_lod` from each mesh's
`world_aabb` projected diagonal. When a level is missing (e.g. a solid
entity that only emits LOD 0), the selector walks down to the nearest
available slot.

API ripples:
  * New `LodConfig` (HIGH/MID/LOW) drives `tess_sat`, `tess_cone_face`,
    `tess_sphere_face`, `tess_torus_face`.
  * `tessellate_solid3d / _region / _body` now return `MeshLodSet`.
  * `MeshLodSet { lods: Vec<MeshModel>, world_aabb }` + `from_single`
    constructor so interactive truck-based commands (BOX/CYLINDER/etc.)
    that only generate one tessellation stay one-line.
  * GPU side gets `MeshLodGpu { lods: Vec<MeshGpu>, world_aabb }` and
    a `mesh_lod_levels` per-frame index vector.
  * STL/STEP export pulls slot 0 explicitly so the on-disk geometry
    isn't downgraded by the view-dependent ladder.

Background-thread lazy generation (per the roadmap) is deferred — the
synchronous eager build is fine for typical CAD files. Switch to a
queue + background tessellator when files with hundreds of solids
become a load-time bottleneck.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-15 23:30:13 +03:00
HakanandClaude Opus 4.7 f738a20457 feat(properties): label xref Insert as "External Reference"
Match AutoCAD properties panel — Insert whose block_record carries
is_xref/is_xref_overlay now shows "External Reference" instead of
"Block Reference". Silence dead_code warning on XrefStatus::Unloaded
(intentional, awaiting on-disk Unload-bit detection).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-15 09:19:57 +03:00
HakanandClaude Opus 4.7 e88a946176 feat(xref): honor BlockRecord is_loaded — skip Unloaded xrefs
When the host DWG marks an xref as Unloaded (via XREF→Unload in
AutoCAD), the user explicitly chose NOT to load that reference. We
were ignoring that and pulling the external file in anyway, which
on large drawings could push GPU memory past the device budget.

Bump the acadrust dep to the branch that exposes
`BlockRecord::is_loaded`, then short-circuit `resolve_xrefs` for
any entry with `is_loaded == Some(false)`. New `XrefStatus::Unloaded`
surfaces this in the command-line log so the user can see which
references were skipped (and can re-load them via the XREF dialog).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-14 11:20:11 +03:00
HakanandClaude Opus 4.7 def72e435e perf(scene): cache block defns + batch wires by style
Tessellates each block_record once into local-coord LocalWires; Insert
references transform-copy + ByBlock-resolve the cache rather than
explode+re-tessellate per reference. Same-style wires inside one Insert
are NaN-batched into a single WireModel to keep wgpu buffer counts
sane — large blocks that previously bailed to a marker now render in
a handful of buffers each.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-14 11:12:08 +03:00
Hakan SevenandClaude Opus 4.7 c61d648dc7 fix(scene): unfreeze open on huge xref Inserts
A real DWG with an xref containing ~74k sub-entities froze on the first
render: Insert tessellation exploded the block, style-resolved every sub
serially, and one bad sub could hang the loop forever.

- Insert short-circuits to an insertion marker when the referenced block
  has more than INSERT_SUB_LIMIT (5_000) sub-entities. Documented as
  temporary in RENDERING_OPTIMIZATION.md.
- Re-run purge_corrupt_entities after resolve_xrefs so parser-garbage
  inside xref content is caught (initial purge runs before xref load).
- build_derived_caches: single pass over entities collecting handles
  per kind; tessellation reads via doc.get_entity instead of cloning
  every EntityType. Image decode now parallel too.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-13 23:02:48 +03:00
HakanandClaude Opus 4.7 b2588e1790 feat(io): drop parser-garbage entities at load time
acadrust's DWG reader occasionally desynchronises on certain files
(observed on render.dwg: 1519 of 1763 LwPolylines arrived with non-unit
normals in the 1e200+ range and 100k phantom vertices). Tessellating
those entities allocates gigabytes and produces NaN-laden wires,
freezing the UI.

Add a pre-tessellation guard that scans freshly loaded documents and
drops any entity failing a cheap sanity check: non-unit normal,
non-finite or absurd coordinate, or vertex count above 100k. Count of
dropped entities is threaded through DerivedCaches and surfaced as a
warning in the command line so the user knows the file was patched up
on the way in.

Same file in DXF form parses cleanly (0 corrupt), so this is strictly
a DWG-path workaround until the upstream parser bug is fixed.

The new examples/inspect_polyline.rs is a one-off diagnostic that dumps
LwPolyline/Polyline2D/Polyline3D fields plus a corrupt-entity count;
useful for narrowing down the acadrust bug.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-13 10:00:16 +03:00
HakanandClaude Opus 4.7 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 <[email protected]>
2026-05-13 09:59:07 +03:00
Hakan ca07268c7c cargo fmt 2026-05-12 10:36:15 +03:00
Hakan Seven 048bb41aef feat(render): solid fill for PolyfaceMesh and PolygonMesh entities
PolyfaceMesh and PolygonMesh were wireframe-only. They now render with
the same flat-color solid fill as Face3D entities.

- Add fill_tris: Vec<[f32;3]> to TruckEntity and WireModel
- PolyfaceMesh: fan-triangulate each face into fill_tris
- PolygonMesh: triangulate each grid quad into fill_tris
- tessellate.rs Lines branch: apply world_offset to fill_tris
- Face3DGpu::from_wires: collect fill_tris from all wires in addition
  to Face3D key_vertices quads
- upload_face3d: pass all_wires so fill_tris are batched into one draw call
2026-05-06 23:24:26 +03:00
Hakan Seven ca9ec8f47d fix(scene): bump geometry epoch after cache assignment on file open
populate_*_from_document() each called bump_geometry(); direct cache
assignment skipped it, leaving the wire cache stale on first open.
2026-05-06 23:10:35 +03:00
Hakan Seven 2da3640b45 perf: build hatch/image/mesh caches off the UI thread
Parsing + DerivedCaches construction now runs entirely in a dedicated
OS thread inside open_path(). FileOpened handler only assigns the
pre-built caches, keeping the UI thread free during file load.

- Add build_derived_caches(): parallel hatch + mesh tessellation via rayon
- Extend Message::FileOpened / open_path return type with DerivedCaches
- Drop populate_*_from_document() calls from FileOpened handler
2026-05-06 23:04:53 +03:00
HakanandClaude Sonnet 4.6 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 <[email protected]>
2026-05-06 17:50:46 +03:00
Hakan SevenandClaude Sonnet 4.6 899b3f4796 feat: GPU scissor rect for paper-space viewport wires
Viewport-projected wires now clip at pixel level to their viewport boundary.

- WireModel.vp_scissor: Option<[f32; 4]> — paper-space bbox set on projection
- WireGpu.vp_scissor carries it through to the draw loop
- Pipeline.compute_wire_scissors() converts paper→NDC→pixel each frame
- render() calls set_scissor_rect() per wire, reset after viewport group
- INTEGRATION_GAPS.md: Viewport ⚠️ → ✅, all 51 gaps resolved

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-06 07:12:05 +03:00
Hakan SevenandClaude Sonnet 4.6 27aca41dd1 feat: plinegen + SAB parsing for Solid3D/Region/Body
LwPolyline PLINEGEN=0 (DXF default): emit SegmentedLines with NaN
separators between vertex-to-vertex segments; wire_gpu resets
arc-length to 0 after each NaN so linetype pattern restarts per vertex.
PLINEGEN=1: existing Contour path unchanged (continuous pattern).
WireModel gains plinegen field propagated through tessellate pipeline.

Solid3D/Region/Body: call SabReader::read() when parse_sat() returns
None due to binary SAB format (DWG files / newer AutoCAD exports).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-06 06:55:28 +03:00
Hakan Seven d7422b9396 fix: store arc angles in radians 2026-05-03 20:14:28 +03:00