Commit Graph
418 Commits
Author SHA1 Message Date
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 26bcd89358 feat(lod): preserve 3D position for sub-pixel entity stubs
Yakındayken tessellate edilen 3D entity'ler (Face3D, Solid3D, Mesh,
PolyfaceMesh, PolygonMesh, Body, Region) <5 px eşiğine düştüğünde,
eski tek-çizgili stub'ın z=0 düzlemine indirilmesi yüzünden farklı
bir konumda görünüyordu. Yeni lod_stub_wire_3d entity'nin 3D AABB
kübünü (8 köşe, 12 kenar) çiziyor:
- XY'de entity_aabb local frame'i (world_offset shift edilmiş)
- Z aralığı bbox.min/max.z, world_offset[2] subtract edilmiş

2D entity stub'ı (lod_stub_wire) text fallback'leri ve diğer 2D
geometriler için z=0'da kalıyor.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-27 07:17:42 +03:00
Hakan SevenandClaude Opus 4.7 3a64f98755 fix(render): hide PolyfaceMesh outline edges in shaded modes
PolyfaceMesh / PolygonMesh için outline edges WireModel.points üzerinden
gpu_wires'a giriyor; bu nedenle FlatShaded / GouraudShaded modlarında
shaded yüzeyin üstünde görünmeye devam ediyordu. WireGpu artık kaynak
WireModel'da fill_tris doluysa 'is_3d_mesh_edge' bayrağını set ediyor;
wire pass'i show_3d_edges=false iken bu instance'ları atlıyor.
*WithEdges, Wireframe ve HiddenLine modları show_3d_edges=true ile
geliyor — mesh kenarları orada görünür kalır.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-27 06:36:59 +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 56834dfba6 feat(render): HiddenLine mode via depth-only prepass
3D yüzeyleri (Solid3D mesh, 3DFACE quad, PolyfaceMesh/PolygonMesh face
tris) HiddenLine modunda renk yazmadan z-buffer'a yazıyor; ardından
kenarlar wireframe pipeline ile çizilince LessEqual depth testi
arkadaki segmentleri eliyor. 2D fill'ler (text greek, MultiLeader
arka planı) ayrı bir vertex buffer'a ayrıldı ve her modda renkli
çizilmeye devam ediyor.

- Face3DGpu: vertex_buffer artık 3D / 2D olarak ikiye ayrıldı
- mesh_depth_pipeline + face3d_depth_pipeline (write_mask=empty)
- RenderModeFlags.hidden_line + Primitive.hidden_line plumbing

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-27 05:40:39 +03:00
Hakan SevenandClaude Opus 4.7 8dbdebeedc feat(render): wireframe modes hide mesh & 3D fills
Wireframe 2D/3D modlarında Solid3D meshlerini edge-only çizmek için
LineList topolojili bir mesh wireframe pipeline ve her üçgenin
(a,b),(b,c),(c,a) kenarlarına genişletilmiş ayrı bir index buffer
eklendi. PolyfaceMesh / PolygonMesh face fill'leri (fill_tris + dolu
points) wireframe'de gizleniyor; text greek / MultiLeader gibi 2D
fill'ler (points boş) etkilenmiyor.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-27 05:28:20 +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 e55b280dda fix(paper): handle NaN sentinels when building hatch fill path
`HatchModel.boundary` stores multi-region hatches as a single Vec
with NaN-NaN sentinels separating each disconnected sub-path
(documented on the field). The paper-canvas hatch fill ignored the
sentinels and forwarded every vertex straight to the iced canvas
path builder. When a layout contained a multi-region hatch, lyon
received a polygon containing NaN coordinates and panicked with
`Tessellate path.: Internal(IncorrectActiveEdgeOrder(4))` from
inside `iced_wgpu::geometry::Frame::fill`, taking the whole app down
the first time Layout 1 painted.

Walk the boundary instead, opening a fresh sub-path on each sentinel
and closing whichever was active. Each region is now filled as its
own closed polygon. Reproduces with #22.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 18:22:52 +03:00
Hakan SevenandClaude Opus 4.7 a3e9b05130 fix(hatch): stop drawing ghost line between boundary polylines
The polyline arm of the hatch wire outline guarded its NaN separator
with `if count > 0 && pts.len() != start_idx`. `start_idx` was assigned
to `pts.len()` two lines earlier with nothing in between to grow `pts`,
so the inequality was structurally always false — the separator was
never emitted.

When a hatch carried more than one boundary path (multi-region hatch),
the renderer drew a straight segment from the last vertex of path A to
the first vertex of path B because the two polylines lived in `pts`
back-to-back with no NaN break. The result was a visible stray line
between hatch regions.

Switch the polyline arm to the same `if !pts.is_empty()` guard the
Line / CircularArc / EllipticArc arms use, and capture `start_idx`
*after* the separator so the closed-loop close-back logic still points
at this polyline's first real vertex.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 18:16:40 +03:00
Hakan SevenandClaude Opus 4.7 2e8d7ba878 refactor(hatch): collapse repeated reflect_xy_point calls
The Hatch boundary-mirror transform called
`crate::scene::transform::reflect_xy_point(&mut x.x, &mut x.y, p1, p2)`
nine times with rustfmt expanding each long-path call into a six-line
block. Total: ~55 lines for nine reflects.

Add a local `use crate::scene::transform::reflect_xy_point as reflect;`
at the top of the closure so each reflect collapses to a single line.

-19 lines, no behavior change.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 17:24:18 +03:00
Hakan SevenandClaude Opus 4.7 e66905e9b1 refactor(entities): Solid3D/Region/Body accessor trio in entities/solid3d.rs
The same three-arm `match entity { Solid3D | Region | Body }` block
appeared at five callsites — twice in tessellate.rs (point_of_reference,
fallback wires) and three times in scene/mod.rs (mesh tess dispatch,
apply_grip pre/post position).

Add three accessors in entities/solid3d.rs:

  point_of_reference(e) → Option<&Vector3>
  fallback_wires(e)     → Option<&[Wire]>
  tessellate_volume(e, color, facet_res) → Option<MeshLodSet>

Callsites shrink from 5–8 lines to one call. The apply_grip path
collapses two parallel pre/post matches plus the entity-list `matches!`
guard.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 17:22:02 +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 9b5073752d refactor(entities): collapse trait wrapper boilerplate via macro
Every entity file ended with the same three-trait wrapper block:

    impl Grippable for T {
        fn grips(&self) -> Vec<GripDef> { grips(self) }
        fn apply_grip(&mut self, ...) { apply_grip(self, ...) }
    }
    impl PropertyEditable for T { ... }
    impl Transformable for T { ... }

— 23 lines of pure delegation around five same-named free functions.

Replace with `crate::impl_entity_basics!(T);` (a macro in
entities/traits.rs that expands to the three trait impls).

Applied where the file already follows the free-fn delegation
convention (Arc, Circle, Ellipse, Insert, Leader, Line, LwPolyline,
MultiLeader, Ole2Frame, Point, Spline, Viewport). Files that inlined
the trait method bodies directly (Ray, XLine, Shape, Solid, Tolerance,
MLine, Mesh, Solid3D/Region/Body, Table, RasterImage, Wipeout,
Underlay, Hatch, Polyline, Polyline2D, Polyline3D, Dimension) left
alone — they already skip the indirection. Text/MText/Attribute use
the with-text-style flavour and will move once that variant is needed
beyond the existing two manual impls.

A second macro `impl_entity_basics_with_text_styles!` is included for
the text-like flavour but no entity uses it yet.

Net: -204 lines.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 17:16:06 +03:00
Hakan SevenandClaude Opus 4.7 5e36e85e07 refactor(common): unify bulge → arc math into BulgeArc
The same DXF/DWG bulge → (center, radius, sweep) formula was inlined
in six places (hatch outline, polyline tess + truck path, lwpolyline
×4, scene hatch fill, explode bulge_to_arc) — each with subtly
different sweep-direction handling for the bulge sign.

Add `entities::common::BulgeArc { center, radius, start_angle,
end_angle, sweep }` with one `from_bulge(p0, p1, bulge)` constructor
and a `sample(t)` helper. Callsites collapse to a few lines.

Net: -121 lines, one canonical sweep-direction convention shared by
all consumers.

array.rs (the path-along-curve f32 helper) keeps its inline math —
its sign convention is inverted relative to the others and changing
it without exercising the array-path command risks regressing the
output.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-23 23:42:26 +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 40180e9089 refactor(scene/mod.rs): pull entity-specific helpers into entities/
Five chunks moved out of the 5965-line scene/mod.rs to live with the
entities they belong to:

- wide_lwpolyline_fills → entities/lwpolyline.rs::wide_fills
- wide_polyline2d_fills → entities/polyline.rs::wide_fills
- polyline_segment_fill → entities/common.rs (shared by both above)
- text_obb_corners_native + text_baseline_points + text_greek_obb_tris
  → entities/text_support.rs (handles Text/MText/Attribute alike)
- append_insert_attribute_wires → entities/insert.rs

Net: scene/mod.rs 5965 → 5404 lines (~560 lines extracted). Three
helpers in scene/render.rs / scene/block_cache.rs widened from
pub(super) to pub(crate) so the relocated functions can still reach
them. scene::entity_aabb also widened for the same reason.

scene::tessellate_entity_dim_text stays put — it's a thin wrapper
around the private tessellate_entity dispatcher and moving it would
require exposing tessellate_entity's full signature across the crate.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-23 23:26:14 +03:00
Hakan SevenandClaude Opus 4.7 64d8b2117d refactor(entities): macro-ize EntityTypeOps dispatch
Each of the five dispatch functions in `entities/traits.rs` was a
hand-written `match self { EntityType::Line(e) => ..., EntityType::Arc(e)
=> ..., ... }` block listing ~35 variants. Adding a new entity meant
adding five match arms across the file.

Replace with a `dispatch!` macro that takes `(self, |e| body, [variant
list], _ => default)`. Each dispatch function keeps its own variant
list (the supported sets differ per trait), but the per-arm
boilerplate is gone.

File shrinks from 386 to 145 lines. Adding a new entity now means
adding one identifier to one (or more) of the variant lists.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-23 23:18:22 +03:00
Hakan SevenandClaude Opus 4.7 b413fe0efe refactor(tess): rename LegacyTess → FallbackTess
The trait isn't deprecating anything — it carries the path entities
take when they're not covered by the truck topology pipeline
(Viewport, Insert, Hatch outline, Ole2Frame). "Fallback" names the
purpose; "Legacy" implied retirement that isn't actually on the
roadmap.

Same rename for LegacyGeometry → FallbackGeometry,
legacy_geometry() → fallback_geometry().

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-23 23:16:57 +03:00
Hakan SevenandClaude Opus 4.7 aadb64860d refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:

  LegacyTess     — Viewport / Insert / Hatch / Ole2Frame
                   (fallback path for entities not on the truck pipeline)
  LeaderTess     — Leader
  MultiLeaderTess — MultiLeader
  DimensionTess  — Dimension (carries its dim-only helpers too:
                   ~1700 lines of dim_geometry, dim_text_*, format_*,
                   tolerance, suppression, etc.)

scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.

scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-23 23:12:27 +03:00
Hakan SevenandClaude Opus 4.7 163725ca22 chore(deps): bump to latest stable versions
Bumps bytemuck 1.14->1.25, glam 0.27->0.33, rfd 0.15->0.17,
windows-sys 0.59->0.61, ureq 2->3. ureq 3 changed the agent API
and feature flags, so update_check.rs switches to Agent::config_builder
and body_mut().read_to_string(), and the feature toggles from tls
to rustls.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-23 22:21:30 +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 a3c982c4f3 perf(wire): pack instance color as Unorm8x4 (88 -> 76 B per instance)
Step 4 (final) of the wire pipeline cold-open optimization plan. Color
shrinks from `[f32; 4]` (16 B) to `[u8; 4]` (4 B) and the vertex attribute
format flips to `Unorm8x4`; the GPU still hands the shader a `vec4<f32>`
in [0, 1], so wire.wgsl needs no change. 8-bit per channel is below
display precision.

Net: 76 B per instance, ~14 % smaller than the Step-3 instance layout and
~7.6× smaller than the pre-Step-3 per-vertex format. Wire upload memory
and PCIe bandwidth on cold open scale down accordingly.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-23 09:53:49 +03:00
HakanandClaude Opus 4.7 4b4f5c94cd perf(wire): switch wire pipeline to instanced rendering
Step 3 of the wire pipeline cold-open optimization plan — the biggest
single lever.

Each segment used to upload as 6 vertices × 96 B = 576 B with the segment
payload (pos_a/pos_b/color/half_width/distance/pattern_length/pat0/pat1)
duplicated across all six corners of the quad. Wire pipeline now uploads
one INSTANCE per segment (88 B) and the vertex shader derives `which_end`
and `side` from `@builtin(vertex_index)` against a 6-entry corner table.

Net effect on cold open: ~6.5× less wire vertex memory and PCIe upload,
plus a smaller Vec to allocate and copy on the CPU side.

Wire instance attributes also collapse distance_a / distance_b into a
single mix() inside the vertex shader, so per-segment distance still
interpolates correctly for the GPU-side linetype dash test.

Touches the wire, wire_xray, and face3d-edges draw passes.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-23 09:52:23 +03:00
HakanandClaude Opus 4.7 0ad7af7742 perf(wire): parallelize per-wire vertex emission with rayon
Step 2 of the wire pipeline cold-open optimization plan. Extracts the
per-wire vertex expansion into a free function and drives it from
`from_batch` via `par_iter().map(...).reduce(...)`. Per-wire emission is
already independent (no shared state, no order coupling — `block_cache`
groups wires by style upstream), so the parallel-then-concatenate shape
is correctness-preserving.

Cold-open wire upload now scales with available cores; a drawing with
many short wires shows the biggest win. Also collapses two near-identical
emission loops (`from_batch`, `build`) into one shared helper.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-23 09:49:06 +03:00
HakanandClaude Opus 4.7 522b123d82 perf(wire): upload vertex buffers via mapped_at_creation
Step 1 of the wire pipeline cold-open optimization plan. Replaces
`create_buffer_init` (intermediate staging-buffer memcpy) with a
`mapped_at_creation` buffer the build path writes into directly.
Cuts one alloc + one memcpy per wire buffer; on cold open with hundreds
of MB of wire data this shows up immediately as lower peak memory and
faster upload.

Guarded against size-0 buffers by allocating a single-vertex placeholder
when the wire is empty (the renderer already skips draws on vertex_count
below the per-segment minimum).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-23 09:47:07 +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 8e6491d4a8 fix(wipeout): use real clip vertices, correct image-pixel→WCS mapping
acadrust's DWG reader read the wipeout/raster-image clip boundary
points only to advance the bit cursor and then discarded them, so every
wipeout fell back to `Wipeout::new()`'s default placeholder rectangle
(`-0.5..0.5`). For files that store the real polygon mask in clip
vertices (anteen.dwg's `DIMBLOCK` style: u_vector ≈ 643k, size = (1, 1),
clip polygon ~0.0016×0.0033 in pixel space), the wipeout rendered at the
full image bbox — hundreds of thousands of units wide — instead of the
intended ~1000×2000 mask.

Pinned to the new `fix/wipeout-clip-vertices` acadrust branch (carries
the upstream parser fix). On the H7CAD side, the polygon mapping now
follows the DXF convention:

  x_off = (clip.x + size.x/2) × u_vec
  y_off = (size.y/2 − clip.y) × v_vec      (image-Y points down)

…in `wipeout_boundary_2d` (fill), `Wipeout::to_truck` (border line),
and `Wipeout::grips` (vertex handles) so all three stay in sync. The
fill path also explicitly closes the polygon loop — without it the GPU
`in_polygon` ray-cast skipped the v(n-1)→v(0) edge and the solid mask
bled far past the boundary.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-22 14:14:22 +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 30f7ad0918 feat(hatch): rasterise PAT pattern lines for paper canvas + PDF
Both the on-screen paper canvas and the PDF / print pipeline only knew how
to draw the boundary outline of pattern hatches (ANSI31, ANSI37, dotted
brick, etc.) — the actual hatch lines were a GPU-only feature, so any
paper-space view or PDF export showed a hollow polygon where there should
be ruled fill.

Add a CPU rasteriser as `HatchModel::pattern_segments()`:

- builds an edge list from the boundary, splitting at NaN sentinels and
  closing each sub-path so even-odd inside tests handle islands / holes;
- for every PAT family computes the world-space step vector
  (`(dx,dy)` rotated into the family's frame, scaled by `model.scale`)
  and the inclusive k-range bounded by the boundary AABB plus a 4096-
  line safety cap;
- intersects each line with every boundary edge, sorts the hit
  parameters, walks them in pairs (even-odd) and emits one segment per
  in/out interval. Dash patterns are walked along each clipped span with
  phase preserved via `t mod period`, matching the GPU shader's
  absolute-t dash evaluation;
- pattern origin (`family.x0 / y0`) is rotated by `angle_offset` and
  scaled the same way as `hatch_batched.wgsl`, so PAT patterns with
  non-zero origins (brick / offset herringbone) line up with the GPU
  render.

`paper_canvas::draw_hatch` and `pdf_export::emit_hatch` now route
pattern hatches through this rasteriser; solid / gradient hatches keep
their existing fast paths. The previous outline-only behaviour was a
visible regression on every layout containing a ruled fill.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-22 06:52:20 +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
Hakan SevenandClaude Opus 4.7 4e5d2c8d04 feat(text): route every text-bearing entity through the run-aware MText pipeline
Lift the MText parser + renderer into a reusable `layout_mtext` helper in
`text_support` and re-point the remaining text-bearing entities at it, so
every inline format code (`\f`, `\C`/`\c`, `\H`, `\W`, `\Q`, `\T`, `\A`,
`\p…`, decorations, stacked fractions, paragraph indents, tab stops, …)
reaches the stroke output uniformly.

Per-entity changes:
- TABLE cell ([entities/table.rs]) — anchor + alignment math now feeds
  `layout_mtext`; `\C7;Red`-style codes no longer render as literal
  glyph noise.
- MLEADER MText content (both entity path in [entities/multileader.rs]
  and the LOD-aware path in [scene/tessellate.rs]) — drops the
  `strip_mtext_codes` + per-line `tessellate_text_ex` chain; the LOD 0
  baseline / LOD 1 greek rect / LOD 2 stroke ladder and the text-frame /
  bg-fill rectangle now read line widths from the layout.
- ATTRIB / ATTDEF — when `mtext_flag` is MultiLine / ConstantMultiLine
  the value goes through the MText pipeline (SingleLine stays on the
  Text-style anchor maths because the DXF spec disallows codes there).
- DIMENSION text_override — `dimension_text_entity` emits a synthetic
  MText (instead of Text) whenever the value contains MText escapes;
  the LOD ladder via `tessellate_entity_dim_text` handles both kinds.

Infrastructure:
- `MTextVAnchor` gains `MiddleOfTopLine`, `MiddleOfBottomLine`,
  `BottomOfTopLine` so MLEADER's 9 attachment variants all map cleanly.
- `MTextLayout` returns `line_widths` / `line_height` / `v_offset` so
  callers that draw chrome (frames, fills, baselines, greek rects)
  share the same numbers as the rendered strokes.
- `mtext_line_count` re-routed through the new parser so LOD row counts
  and rendered row counts can never disagree.
- Legacy helpers (`strip_mtext_codes`, `split_mtext_lines`,
  `measure_mtext_chars`, `word_wrap`, `cxf::measure_text`, the
  `v_offset_for_attachment` MLEADER offset table, `MTextLine::plain_text`)
  deleted — all callers migrated.

Audit (TEXT_FORMAT_CODE_INTEGRATION.md) removed; its plan is implemented
here.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-22 05:53:01 +03:00
Hakan SevenandClaude Opus 4.7 f12e864aa6 feat(mtext): integrate every inline format code in MTEXT renderer
Replace the line-based MTEXT pipeline with a run-aware parser + renderer.
Every DXF MTEXT inline code now reaches the stroke output: \H \W \Q \T \A
\C \c \f \F \M \X, the \p block (q/i/l/r/t/s), brace-scoped state, and
the \L\l \O\o \K\k decoration toggles.

Parser ([text_support]): emits Vec<MTextLine{ runs, align, indent_*,
tab_stops }> with per-run RunState; braces scope character state, paragraph
props persist across them.

Renderer ([mtext]): atomise → wrap → render. Per-run height/width/oblique/
tracking/font/colour/decorations/valign; paragraph indents and tab stops
honoured; default 4-em tab fallback.

Plumbing: tessellate() now returns Vec<WireModel>; TextStroke gains an
optional inline-colour override so per-run \C/\c emits one wire per colour.
cxf::tessellate_text_run adds a tracking multiplier. block_cache folds
multi-wire results into a single LocalWire (colour split preserved at
top-level, flattened inside cached defns).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-21 23:42:22 +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
HakanandClaude Opus 4.7 3fb23ae31b feat(scene): integrate text LOD across every text-bearing entity
Moves the Dimension and Table baked-block paths up into
`tessellate_entity` so sub-Text/MText recurse through the same LOD
ladder (baseline / greek / full) the top-level entities already use,
and routes the synth-dim text path through `tessellate_entity` as well
so files that omit `block_name` (e.g. typical DXFs) also get LOD.

Extends the text-height LOD branch from Text/MText to
AttributeDefinition, AttributeEntity, and Tolerance — the OBB helpers
gained the same coverage so the baseline/greek rect lines up.

When a sub-entity inside a baked dim/table block has ByBlock colour,
the recursive call's resolved colour is replaced with the parent's so
dimension text no longer renders in the raw ByBlock fallback hue.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-21 15:29:57 +03:00
HakanandClaude Opus 4.7 eab72062f9 fix(scene): render baked block geometry for Dimension and Table
AutoCAD bakes a per-instance block with the final, displayed geometry
into Dimension and Table entities (typically *D### / *T###, but custom
names also occur — sample-file-H uses DIMBLOCK*-4NP). The block's text
already encodes DIMTXT * DIMSCALE (or the table's scale factor), so
synthesising the entity from the dim/table style on top of that
double-applies the scale.

When the referenced block exists with content, render it directly:
- Dimension: dim.base.block_name -> block_record
- Table: block_record_handle -> block_record

The synthesis fallback stays in place for files where the block is
missing or empty.

Fixes sample-file-H.dwg dimensions rendering at 60000 instead of 200
(DIMTXT=199.196 * DIMSCALE=304.8).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-21 12:54:49 +03:00
HakanandClaude Opus 4.7 62876b1081 fix(text): integrate ignored fields across text-bearing entities
- MultiLeader: stop double-scaling when ctx.text_height (WCS-baked) is set;
  use it as-is and only multiply by scale_factor + anno on the fallback path.
  ctx.text_width gets the same treatment.
- Text / Attribute: width_factor and oblique_angle are entity-authoritative.
  The previous entity*style multiplication / addition mis-rendered any text
  whose TXSTYLE had a non-default width_factor or oblique_angle (RR 0.6,
  STYLE2 15°, Romans/ARIEL/A32 0.65-0.8 in the sample file).
- MText: honour TXSTYLE is_backward (negative width_factor mirror) and
  is_upside_down (180° rotation). measure_mtext_chars uses positive
  magnitude so anchor math stays correct under backward text.
- Tolerance: use tol.dimension_gap when set instead of the hard-coded
  0.35 * height heuristic.
- Table: cell text height / font / alignment resolve through
  CellContent -> CellStyle -> TableStyle.{title|header|data}_row_style
  -> compiled-in defaults. Row classification respects the suppressed
  flags. Cell + content rotation applied.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-21 12:38:03 +03:00
HakanandClaude Opus 4.7 ace99541d0 fix(io): drop more parser-garbage entities to prevent OOM on open
Adds vertex-coordinate, thickness, and dimension-validity checks to
`is_entity_corrupt`. Sample files with degenerate Arcs (radius ~5e-14
and zero sweep) or 100k-vertex LwPolylines at coords ~1e244 used to
pass the previous check, then crash inside truck's parameter_division
with a 3 GB allocation.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-21 10:39:39 +03:00
Hakan SevenandClaude Opus 4.7 9527509ea2 chore(release): drop flatpak, add macOS .dmg, hide Windows console
- .github/workflows/release.yml: remove the build-flatpak job; matrix-
  add macOS (arm64 + x86_64) that builds H7CAD.app by hand, renders
  .icns from assets/logo.svg via librsvg + iconutil, and packages a
  UDZO .dmg. AppImage path updated to packaging/H7CAD.desktop.
- packaging/H7CAD.desktop: moved out of flatpak/ so the AppImage job
  doesn't depend on a Flatpak layout.
- packaging/Info.plist: macOS bundle template; __VERSION__ swapped
  for the release tag at build time.
- flatpak/: deleted (manifests, metainfo, desktop, builder cache).
- src/main.rs: `#![cfg_attr(all(windows, not(debug_assertions)),
  windows_subsystem = "windows")]` so the Windows GUI binary no
  longer spawns a console window alongside it (debug builds keep
  the console for log output).
- README: add macOS install section, document Gatekeeper / SmartScreen
  on first launch since the binaries are unsigned.

Bump to 0.3.5.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-20 21:13:02 +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 ff2485eba9 fix(scene): bake selection colour into greek-LOD text fill
The greek-LOD text path emits no `points` and only `fill_tris`; the
face3d fill pipeline colours each triangle with `wire.color`. We were
storing the entity's own colour there, so selecting a text and then
zooming past the LOD boundary kept the wire.selected flag but the
on-screen colour dropped back to the entity hue.

Swap `wire.color` to `WireModel::SELECTED` when `selected` is set, the
same way `lod_stub_wire` already does it.

Follow-up to #19.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-20 06:15:44 +03:00