Commit Graph
449 Commits
Author SHA1 Message Date
Hakan Seven 136e4581c0 feat: add HeaderVariables audit documentation 2026-05-20 05:57:16 +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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
2026-05-20 05:43:55 +03:00
Hakan SevenandClaude Opus 4.7 9c4043ef45 feat(scene): scale Solid3D / Region / Body tessellation by header FACETRES
ACIS-bearing entities tessellated with a hard-coded LOD ladder
(circ_segs / grid_u / grid_v) so a high-detail drawing produced the
same coarse mesh as a low-detail one. Multiply the per-LOD counts by
`header.facet_resolution` (FACETRES), clamped to AutoCAD's
documented [0.01, 10.0] range with a floor of 4 segments to keep
degenerate values harmless.

All four call sites (build_caches, commit_entity mesh seed, undo/redo
populate_meshes_from_document, and the helper signatures themselves)
take a FACETRES argument now.

ISOLINES / surface_u_density / surface_v_density and the
camera / sun / geo header fields stay metadata until matching render
work lands.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 04:51:26 +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 <noreply@anthropic.com>
2026-05-20 04:47:15 +03:00
Hakan SevenandClaude Opus 4.7 65920959d4 feat(scene): fall back to header EXTMIN/EXTMAX for fit-all
When a fresh document hasn't tessellated yet (no wire AABB cache,
no per-entity key vertices found), compute_model_space_extents()
returned None, so ZOOM EXTENTS / auto_fit_viewport had to wait for
the next pass before they could fit. Use the header's saved
model_space_extents_min/max as a last-resort answer so the very first
ZOOM EXTENTS / fit-all after open lands on the drawing immediately.

LIMMIN/LIMMAX and INSBASE remain read-only metadata for now — they
need a bounded-grid mode (LIMMIN/LIMMAX) and a "save as block" anchor
(INSBASE) before they map onto a visible behaviour.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 04:45:37 +03:00
Hakan SevenandClaude Opus 4.7 2d795e64ed feat(scene): honour render-affecting HeaderVariables (PDMODE/ATTMODE/FILLMODE/LWDISPLAY/MIRRTEXT)
Header system variables that change how the file renders were being
ignored. Wire them through the render path:

- PDMODE + PDSIZE: Point entities draw the requested glyph shape
  (dot / + / × / | with optional enclosing circle and square) sized by
  PDSIZE. Default PDMODE 0 keeps the single-vertex fast path.
- ATTMODE: INSERT attribute rendering now respects 0=Off (no attribs),
  1=Normal (per-attrib invisible flag), 2=On (force all visible).
- FILLMODE: when false, hatch / wipeout / face3d-fill uploads are
  short-circuited so the renderer draws wireframe only.
- LWDISPLAY: when false every entity falls back to the 1-pixel base
  width, matching AutoCAD's "Show Lineweight" toggle.
- MIRRTEXT: when false, MIRROR keeps text / mtext / shape rotation +
  oblique angle (so text stays right-reading) while still mirroring
  position.

DISPSILH / PLINEGEN were also reviewed: render path already honours
the per-polyline plinegen bit and we don't yet render Solid3D
silhouettes, so the header values are read for round-trip but produce
no extra effect here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 04:44:07 +03:00
Hakan SevenandClaude Opus 4.7 6110da99a5 feat(entities): surface remaining 3D fields + add Mesh render path
Closes out the 3D-entity audit. Mesh (modern SubD mesh) gained a full
render impl since it had no TruckConvertible / Grippable / PropertyEditable
hookup at all — it was silently dropped on open. Remaining mesh-style
entities pick up the few fields the parser populated:

- Mesh (SubD): wireframe + face-fan fill_tris rendering, per-vertex grips,
  Properties panel surfaces version, subdivision_level, blend_crease,
  creased-edge count, vertex / face / edge counts; wired into the
  Transformable / Grippable / PropertyEditable / TruckConvertible
  dispatch in entities/traits.rs
- Polyline3D: smooth_type, default_start_width, default_end_width,
  mesh_m_count, mesh_n_count, smooth_m_density, smooth_n_density
  (widths editable)
- PolygonMesh: smooth_type, m/n_smooth_density, elevation, normal
- PolyfaceMesh: smooth_surface, seqend_handle, elevation, normal,
  thickness

Solid3D / Region / Body's uid + silhouettes + history_handle were
included in the prior 2D commit since they share the same file.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 21:35:14 +03:00
Hakan SevenandClaude Opus 4.7 0b4facc50d feat(entities): surface remaining 2D + Region fields in properties
Closes out the 2D-entity audit (and the Region partner that ships next
to Solid3D / Body). Round-trip-only metadata fields plus a few editable
ones gain Properties-panel rows so the values the parser populates are
visible and don't get silently dropped on save:

- Hatch: seed_points count, pixel_size, normal
- Polyline2D: smooth_surface, default_start_width, default_end_width,
  thickness, normal (with editors for the widths/thickness)
- Region: uid, silhouettes (shared file with Solid3D / Body)
- Underlay: clip_inverted
- RasterImage: class_version, definition_handle, definition_reactor_handle
- Wipeout: class_version, clip_mode, definition_handle,
  definition_reactor_handle
- Leader: annotation_handle, override_color
- Tolerance: dimension_style_name, dimension_style_handle, dimension_gap,
  text_height (editable), direction, normal
- MLine: style_handle, style_element_count, justification picker
- Ole2Frame: version, source_application, binary_data size, dwg_mode,
  dwg_trailing_byte, is_paper_space
- Viewport: snap_base/spacing/angle, twist_angle, front/back clip Z,
  circle_sides, grid_flags, grid_major, base_ucs_handle, ucs_ortho_type,
  background_handle, shade_plot_handle, visual_style_handle,
  default_lighting, default_lighting_type, ambient_color (with editors
  for the snap / clip / circle_sides values)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 21:28:16 +03:00
Hakan SevenandClaude Opus 4.7 613d6cf99d feat(entities): integrate remaining fields on block-based entities
MultiLeader and Table were ignoring most of the styling and content
fields that the parser populated. Bring both up to file-driven render:

MultiLeader
- line_color: leader stroke / arrow colour with ByBlock/ByLayer fallback
- line_type_handle: dashed leader via line_types lookup by handle
- line_weight (ml.line_weight): per-leader pixel width override
- arrowhead_handle: arrow shape resolved through the dim arrow emitter
  (Closed Filled / Open / Dot / Origin / Box / Datum / Tick / None)
- extend_leader_to_text: continue the leader past the landing to the
  text insertion point
- text_angle_type: Horizontal forces 0, Optimized clamps to a readable
  half-plane, ParallelToLastLeaderLine keeps the stored direction
- text_direction_negative: adds π to the rotation
- text_alignment: drives the horizontal anchor when more specific than
  the context's text_attachment_point
- text_attachment_direction + text_top_attachment + text_bottom_attachment:
  Vertical mode uses the top / bottom attachment values chosen from the
  first root's direction.y sign
- has_block_contents + block_content_handle + block_content_location +
  block_rotation + block_scale + block_content_color: synthesise an
  Insert and explode it through the standard tessellator so block-
  content MultiLeaders render their referenced block
- style_handle, arrowhead_handle, line_type_handle, text_style_handle,
  block_content_handle, property_override_flags, text_align_in_ipe,
  block_scale, block_rotation, extend_leader_to_text,
  text_direction_negative, text_top_attachment, text_bottom_attachment
  surfaced in the Properties panel

Table
- Per-cell borders: drop the always-grid pass, walk the cells and
  emit each border honouring its `invisible` flag with dedup so shared
  edges aren't doubled
- table_style_handle, block_record_handle, data_version, value_flags,
  override_flag / override_border_* flags, break_options /
  break_flow_direction / break_spacing, normal surfaced in the
  Properties panel

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 21:17:05 +03:00
Hakan SevenandClaude Opus 4.7 e0ec7f1999 fix(scene): render INSERT attribute values
Inserting a block with attributes (ATTRIB entities attached to the
INSERT) drew only the block's primary geometry — the attribute values
the user filled in were silently dropped. block_cache::expand_insert
walks the block's BlockRecord which lists the ATTDEF templates, not the
per-INSERT ATTRIB instances, so they were never tessellated.

Add append_insert_attribute_wires: after expanding the block sub-wires,
iterate ins.attributes, skip the entries flagged invisible, and emit a
text wire for each at its stored WCS position. The wire's name reuses
the parent INSERT handle so selection / picking still treats the block
and its attribute text as one entity. Runs on both the block-cache
fast path and the legacy explode fallback.

Closes #20

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 21:04:40 +03:00
Hakan SevenandClaude Opus 4.7 c0e2b6282a feat(entities): integrate remaining fields on text-based entities
AttributeDefinition / AttributeEntity were rendering as left-aligned
single-line text regardless of their stored alignment, generation flags,
or multiline settings. Bring both to parity with Text plus expose every
remaining field through the Properties panel:

- horizontal_alignment / vertical_alignment with bounds-based anchor
  using alignment_point, mirroring Text's logic
- text_generation_flags bit 2 (backward) flips width_factor sign; bit 4
  (upside-down) adds π to the rotation; combined with the TextStyle
  flags via XOR (double-mirror cancels)
- multi-line splitting on \n / \P with line_count surfaced as info
- mtext_flag, is_multiline, line_count, lock_position, field_length,
  AttributeFlags bits (invisible / constant / verify / preset /
  annotative) surfaced as properties
- AttributeEntity.attdef_handle shown as the definition reference
- flags.constant marks the default-value field read-only
- flags.locked_position / lock_position blocks the position grip

Shape entity now applies rotation, relative_x_scale, and oblique_angle
to its placeholder diamond marker, and surfaces shape_number,
style_handle, thickness, and normal in the Properties panel.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 20:58:58 +03:00
Hakan SevenandClaude Opus 4.7 e59917f798 feat(scene): integrate the last Dimension + DimStyle fields
Closes out the file-driven dim render. Remaining entity fields and the
last handful of DimStyle knobs are now honoured:

- leader_length: extend the radius/diameter dim line past the arrow tip
  toward the text by this distance
- ext_line_rotation: rotate extension lines off perpendicular (DIMEDIT
  Oblique result) for Linear / Aligned dims
- attachment_point: map the 1..9 grid to Text horizontal / vertical
  alignment so the saved anchor is used
- horizontal_direction: use as in-plane text reading direction override
- DIMTXSTY by handle: prefer dimtxsty_handle over the name; survives
  text-style renames
- DIMTFAC + DIMTOLJ: tolerance text rendered as a separate Text entity
  scaled by DIMTFAC and aligned vertically by DIMTOLJ
- DIMALTTD + DIMALTTZ: alternate-units tolerance suffix formatted with
  these decimal-place / zero-suppression knobs
- DIMARCSYM / DIMJOGANG / DIMUNIT / normal / line_spacing_factor /
  insertion_point / block_name / version: read so the file round-trips
  but no-op in the render path until the matching dim variants / 3D
  text plane are supported

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 20:28:09 +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 <noreply@anthropic.com>
2026-05-19 20:13:03 +03:00
Hakan SevenandClaude Opus 4.7 0aa026155a chore: bump to 0.3.3
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v0.3.3
2026-05-19 13:45:56 +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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
2026-05-19 13:37:37 +03:00
Hakan SevenandClaude Opus 4.7 9d0b406b31 feat(scene): expand DimStyle integration to the remaining visual fields
Drives more of the dim render off the parsed DimStyle so files round-trip
visually identical to AutoCAD instead of using hard-coded fallbacks:

- text format dispatch via DIMLUNIT (Sci / Decimal / Engineering /
  Architectural / Fractional / Windows) + DIMFRAC fraction denominator
- alternate units appended in brackets per DIMALT / DIMALTF / DIMALTD /
  DIMALTU / DIMALTRND / DIMAPOST / DIMALTZ
- tolerance / limits text per DIMTOL / DIMLIM / DIMTP / DIMTM / DIMTDEC /
  DIMTZIN; "value ± t", "+tp / -tm", or stacked "high/low"
- center mark for radius/diameter from DIMCEN (positive = "+", negative =
  "+" plus radial strokes)
- DIMDLE dim-line overshoot at tick endpoints
- DIMJUST horizontal text slide along the dim axis
- DIMTVP perpendicular text offset multiplier when DIMTAD=0
- DIMFXL/DIMFXLON fixed extension-line length override
- DIMTFILL/DIMTFILLCLR text background fill rectangle
- DIMLTEX/DIMLTEX1/DIMLTEX2 linetype handles resolved per-wire; ext1 and
  ext2 split into separate wires when their patterns differ
- DIMSOXD read; left as a no-op until autofit is added

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 09:44:59 +03:00
Hakan SevenandClaude Opus 4.7 1b2415bde5 feat(scene): render dimensions from DimStyle values read from file
Dimension tessellation was hardcoding arrow shape (open-V line pair) and
text format ("{:.4}"), ignoring almost every DimStyle field the parser
populated. Drives the wire/text geometry from the resolved DimStyle:

- text: DIMDEC, DIMZIN, DIMLFAC, DIMRND, DIMDSEP, DIMPOST, DIMADEC,
  DIMAZIN, DIMAUNIT (decimal/DMS/grad/rad), DIMTXSTY
- arrows: DIMBLK/DIMBLK1/DIMBLK2/DIMSAH resolution against block_records,
  supporting ClosedFilled (default), Closed/Closed-Blank, Small, Open
  (30/90), Dot (small/blank), Origin, Oblique/ArchTick, Box, Datum, None;
  DIMTSZ overrides to oblique ticks
- colours: DIMCLRD / DIMCLRE / DIMCLRT split into three wires (ext, dim,
  text); BYBLOCK/BYLAYER fall through to entity colour
- weights: DIMLWD / DIMLWE drive line_weight_px per wire
- suppress: DIMSE1/DIMSE2 (ext) and DIMSD1+DIMSD2 (dim) honoured
- text placement: DIMTAD vertical offset using DIMGAP + text height when
  text_middle_point is unset; DIMTIH/DIMTOH force horizontal text

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 09:34:27 +03:00
Hakan Seven a73dffdd76 fix(scene): rescale dash pattern when projecting wires to paper space
`viewport_content_wires` cloned model-space tessellations and overwrote
`points` with paper-space projections (× scale) but left `pattern_length`
and `pattern` at their model-coord values (pre-multiplied by PSLTSCALE's
1/vp_scale). The GPU then compared a paper-coord distance against a
model-coord pattern, so dashed linetypes in viewport content collapsed to
solid (a typical 9 mm paper line fell inside the first 180 mm dash).

Multiply the projected wire's pattern by the same vp scale used for the
points. Works for both PSLTSCALE on (paper-uniform dashes) and off
(dashes shrink with viewport scale).

Adds two diagnostic examples used to track this down:
  - inspect_lt: which layers/entities carry dashed linetypes
  - inspect_block: enumerate entities + linetypes inside a named block
2026-05-19 08:27:05 +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 Seven a0aa0f3c51 feat: add new screenshot for documentation 2026-05-19 00:20:32 +03:00
Hakan SevenandClaude Opus 4.7 bebe3374e2 docs: add file-open and render performance roadmap
Tracks planned work for cutting file open time and per-frame render
cost: single-pass entity walk, incremental wire cache, batched wire
pipeline, hardware-instanced block inserts, and profiling spans.
Unignore ROADMAP.md so it's tracked.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 21:34:12 +03:00
Hakan Seven 430211ff28 chore: remove obsolete Rendering Optimization roadmap document 2026-05-17 20:26:02 +03:00
Hakan Seven e84ae87bc5 chore: release v0.3.2 v0.3.2 2026-05-17 20:19:39 +03:00
Hakan SevenandClaude Opus 4.7 66fca6e591 docs(rendering): mark Phase 4 done, drop deferred / non-goal phases
Phase 4-B (batched hatch pipeline) is in; Phase 4.1 (multi-draw
indirect / GPU compute cull) stays as a deliberate non-goal because
iced 0.14 doesn't expose the required wgpu features to widget
pipelines, and the single-draw batched path already collapses N
bind-group swaps + N draw calls into one. Phase 4.2 (Hi-Z occlusion)
only matters for perspective 3D, which H7CAD doesn't ship.

The doc is now a short stub — the per-phase rationale lives at the
call sites (search for "Phase N.X" comments).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 20:18:55 +03:00
Hakan SevenandClaude Opus 4.7 798a391207 refactor(pipeline): drop legacy per-hatch path (Phase 4-B, step 5)
The batched hatch pipeline has soaked through visibility + LOD; it's
correct on every file tested (Floor plan.dxf, UTM site plans, the
non-UTM regression case). Time to remove the parallel per-hatch
plumbing that's been sitting empty since step 3:

- `gpu_hatches: Vec<HatchGpu>` — never populated since
  `upload_hatches` switched to the batched builder; dropped.
- `hatch_pixel_scissors`, `hatch_skip_flags` — same story; dropped.
- `compute_hatch_scissors` — no longer has a consumer; dropped, with
  the render-time call removed from `Primitive::prepare`.
- Hatch render pass — the dead `else if` branch over `gpu_hatches`
  is gone; only the single batched `pass.draw(0..vertex_count, 0..1)`
  remains.
- `compute_hatch_lod` — no fallback path; if `gpu_hatch_batched` is
  `None` (empty hatch list) it just returns early.

`HatchGpu` itself stays — the wipeout pipeline still uses it. So do
`hatch_pipeline` / `hatch_bgl1`, kept around for that path.

`#[allow(dead_code)]` on the four storage-buffer fields in
`HatchBatchedGpu` — they exist only to keep the bind group's
referenced resources alive; nothing reads them directly.

Known gap left as a follow-up: per-hatch viewport scissor for paper-
space MSPACE viewports isn't ported to the batched path yet. Hatches
inside a paper-space viewport can render past the viewport border
until that's added. Hasn't surfaced as a visible regression in the
test files so far.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 20:18:06 +03:00
Hakan SevenandClaude Opus 4.7 398eee551d feat(pipeline): per-instance visibility for batched hatches (Phase 4-B, step 4)
Adds a dedicated `visibility: array<u32>` storage buffer (binding 4)
alongside the existing batched hatch buffers. Vertex shader reads
`visibility[v.instance_index]` — 0 → emit an out-of-NDC clip
position so the GPU rasterizer clips the primitive before the
fragment stage runs.

`compute_hatch_lod` now drives this buffer instead of (or alongside,
for the per-hatch fallback) `hatch_skip_flags`. Walks a per-instance
CPU AABB mirror that the builder fills at upload time (`instance_aabbs`),
applies the same `aabb_below_pixel` (sub-pixel LOD) OR `aabb_offscreen`
(frustum cull) rule the legacy path used, and pushes the resulting
0/1 flags via `queue.write_buffer` on a separate 4 B-per-hatch buffer
— ~40 KB per pan tick for 10 k hatches, far cheaper than re-writing
the 112 B-per-instance data.

Step 5 will remove the legacy `gpu_hatches` Vec, the per-hatch draw
loop, and `hatch_skip_flags` once the batched path has soaked.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 20:11:28 +03:00
Hakan SevenandClaude Opus 4.7 cfe53bd789 fix(pipeline): batched-hatch pattern math + invisible-instance clip
Two issues in the step-3 batched path surfaced during testing:

1. WGSL rejects compile-time NaN literals (`0.0 / 0.0`), so the
   visibility-skip path that emitted a NaN clip position failed
   `Device::create_shader_module` validation. Replaced with an
   out-of-frustum clip position `(2, 2, 2, 1)` — the GPU rasterizer
   clips the primitive and the fragment shader never runs.

2. Pattern hatches rendered with the wrong spacing / direction. The
   batched builder was rotating `perp_step` / `along_step` into the
   global frame, but the per-hatch shader (`hatch.wgsl`) expects them
   in QCAD PAT local-frame convention: `perp_step = family.dy`,
   `along_step = family.dx`, with the cos_off/sin_off rotation applied
   inside the shader. Now matches `build_family_batch` from
   `hatch_gpu.rs` line-for-line. `line_width` also reset to 0 (the
   shader picks 1 px from a screen-space derivative; the stored field
   is unused).

3. Padding's `max_spacing` likewise dropped its bogus rotation and
   now uses `|dy|` per family, matching the new perp_step convention.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 20:04:16 +03:00
Hakan SevenandClaude Opus 4.7 3593e9dabc feat(pipeline): wire batched-hatch pipeline + single draw call (Phase 4-B, step 3)
Pipeline carries a new `hatch_batched_pipeline` + matching `bgl1`
alongside the existing per-hatch pipeline. `upload_hatches` now packs
the renderable hatch list into the single `HatchBatchedGpu` and
clears `gpu_hatches`; the draw loop prefers the batched path when
that resource is set, falling back to the per-hatch loop only when
build returned `None` (empty list).

One draw call replaces N — for a 100-hatch frame that's a 99-call
saving plus 99 fewer `set_bind_group(1, ...)` / `set_vertex_buffer`
pairs.

Caveats acknowledged for step 4 cleanup:
- `compute_hatch_lod` still writes into `hatch_skip_flags`, which the
  batched path ignores. Step 4 will push the same flags into the
  per-instance `visible` field on the GPU instance buffer so the
  sub-pixel + frustum cull keeps working.
- Per-hatch viewport scissors (paper-space MSPACE) aren't ported yet;
  the legacy per-hatch path still serves that case when the batched
  build is skipped, but a model-space → paper-space transition keeps
  the batched buffer intact and may render hatches outside the
  intended viewport. Will be addressed alongside visibility.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 19:52:28 +03:00
Hakan SevenandClaude Opus 4.7 60c742e21a feat(pipeline): batched-hatch WGSL shader (Phase 4-B, step 2)
Companion to the storage-buffer layout from step 1. Vertex shader
reads `(corner, instance_index)` per vertex, fetches the matching
`HatchInstance`, projects an AABB-quad corner; a zero `visible` flag
emits a NaN clip position so the GPU skips the fragment stage for
culled hatches without any per-frame buffer re-upload.

Fragment shader runs the same three-stage in-polygon → mode dispatch
→ pattern evaluation pipeline as `hatch.wgsl`, but with all ranges
indexed: `BoundaryBuffer[boundary_offset..offset+count]` for the
ray-cast, `FamilyBuffer[family_offset..offset+count]` for pattern
lines, `DashBuffer[dash_offset..]` for dash sequences. Phase 3.3
sub-pixel LOD substitution is preserved.

Step 3 will wire a pipeline + bind-group-layout around this and call
`upload_hatches_batched` from render.rs (the per-hatch
`upload_hatches` path stays in place for the bisect window).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 19:46:51 +03:00
Hakan SevenandClaude Opus 4.7 fe202f5f8e feat(pipeline): batched-hatch GPU resources + builder (Phase 4-B, step 1)
Stand-alone module — no consumers yet. Packs an entire `&[HatchModel]`
into four storage buffers + a per-vertex (corner, instance_index)
stream:

- `InstanceBuffer`  — `HatchInstance[]` (112 B each): color, mode,
  gradient params, world_origin, local-space aabb, boundary range,
  family range, visibility flag.
- `BoundaryBuffer`  — flat `vec4<f32>[]` (NaN markers preserved as
  per-path separators, just like the per-hatch shader expects).
- `FamilyBuffer`    — flat `LineFamilyGpu[]` (matches the per-hatch
  family layout one-for-one apart from `dash_offset` indexing into a
  separate dash buffer).
- `DashBuffer`      — flat `f32[]`.

The per-vertex buffer holds `6 × instance_count` records of
`(corner: u32, instance_index: u32)`. Step 2 will add the matching
WGSL shader that uses these to project a per-instance AABB quad and
run the in-polygon test against `BoundaryBuffer[offset..offset+count]`.

iced reports 8 storage buffers per stage, 128 MB binding cap, 256 MB
buffer cap — plenty for ~10 k hatches × 112 B + ~32 MB boundaries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 19:45:26 +03:00
Hakan Seven a0412534ae chore: release v0.3.1 v0.3.1 2026-05-17 19:35:37 +03:00
Hakan SevenandClaude Opus 4.7 0ec56015e0 chore(quadtree): silence dead-code warnings on planned API
`len` / `is_empty` / `remove` / `update` are exercised by unit tests
and are part of the quadtree's public surface, but no production
caller exists yet — Scene::add/erase/transform still trigger an
epoch-based full rebuild rather than mutating the index in place.
Annotating them with `#[allow(dead_code)]` keeps the API intact for
the incremental-update follow-up without spamming the release build.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 19:34:56 +03:00
Hakan SevenandClaude Opus 4.7 416b0d4dc3 fix(quadtree): split() on internal node was orphaning the entire subtree
Symptom: open a non-UTM drawing, most lines/polylines/hatches/etc.
vanish — only ~5% of indexed entities reachable from a huge query
(walking the nodes flat showed all 2975 items present, but recursing
through `node.children` from root reached only 161).

Root cause: when straddler items piled up at an INTERNAL node and the
count exceeded LEAF_CAPACITY, `insert()` re-invoked `split()` on that
node. `split()` allocates four fresh children and assigns them to
`node.children = Some([...])` — overwriting the existing children
pointer and orphaning the entire previous subtree (the descendant
nodes still hold their items; nothing reaches them).

Splitting an internal node is futile anyway: straddlers don't fit
smaller children either. Fix: gate the split on
`children.is_none()` so only leaves split. Internal nodes accumulate
straddlers as a linear-scan list — slower per-query for big strad-
dler counts, but correct.

Adds a regression test (`many_straddlers_dont_orphan_subtree`) that
floods an internal node with straddlers and verifies a huge query
still returns every inserted handle.

Bonus: `examples/dump_index_stats.rs` — standalone tool that loads a
DWG/DXF and dumps per-type counts (indexed / unbounded / top-level /
block-internal) plus a sample of bboxes. Useful for future bug
triage; built and ran cleanly against the file that exposed this.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 19:28:57 +03:00
Hakan SevenandClaude Opus 4.7 b4745efb24 fix(scene): emit entities with degenerate bbox from the quadtree path
Phase 2.1's wire path was silently dropping any entity whose
`bounding_box()` returns the degenerate default (or non-finite
coords). The legacy `entity_aabb` mapped those to the
`UNBOUNDED_AABB` sentinel, which `tessellate_entity` treated as
"render unconditionally". The new path filtered them at index time
(no bbox → no quadtree entry) AND `is_unindexable_entity` only
matched type variants (Insert/Viewport/Block/BlockEnd), so those
entities fell through neither path.

Symptom: opening a non-UTM file showed the floor plan with most
walls / dimensions / hatching dropped while isolated entities (text,
circles) still rendered.

Fix: `Scene::entity_index()` now returns an `EntityIndex` carrying
both the quadtree and a parallel `unbounded_handles: Vec<Handle>` of
entities whose bbox came back degenerate. The wire path always emits
that list in addition to the view-rect candidates, mirroring the
legacy "no bbox → never cull" behaviour.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 16:02:09 +03:00
Hakan Seven 0c10ca9dc1 chore: release v0.3.0 v0.3.0 2026-05-17 15:50:17 +03:00
Hakan SevenandClaude Opus 4.7 6ae3a9d91d fix(pipeline): hatch frustum cull tests AABB in wrong space
`HatchGpu.world_aabb` was built by iterating `model.boundary` — but
those vertices are stored as f32 offsets from `model.world_origin`
(precision anchor for UTM-scale WCS). So the AABB was in
*origin-relative* space, not the local space `view_proj` projects
from. `aabb_offscreen` (Phase 2.3) then asked "is this rect near (0,0)
in NDC?" instead of "is this rect at the hatch's actual position?" —
which culled in-view hatches whenever the camera moved off the
origin and rendered out-of-view ones near it.

Sub-pixel LOD (`aabb_below_pixel`, Phase 3.3) survived because it
measures a projected diagonal, which is translation-invariant.

Fixes:
- Add `world_origin` back when building `HatchGpu.world_aabb`, so the
  stored rect is in absolute local space.
- 25 % margin on the viewport rect in `aabb_offscreen`, matching the
  wire path's `view_world_aabb` margin — keeps hatches at the edge
  rendered while panning.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 15:36:15 +03:00
Hakan SevenandClaude Opus 4.7 38f9733eb1 docs(rendering): drop completed Phase 2
Phase 2.1/2.2/2.3 landed across the previous commits:
- 727992b quadtree primitive
- 3e87460 Scene::entity_index lazy build
- e43498d wire tessellation uses query_rect
- fcf4d4f mesh draw-time frustum cull
- b70c9f9 hatch + wipeout draw-time frustum cull

What was skipped on purpose:
- Octree-with-Aabb3: meshes only carry XY world_aabb today; the
  pragmatic adaptation is a 4-corner projection test in
  `compute_mesh_lod`. Revisit if a true 3D viewport mode lands.
- Per-mutation incremental quadtree updates: `entity_index_cache` is
  geometry_epoch-keyed and rebuilds lazily on next query. Full
  rebuild is ~50 ms on a 100 k-entity doc — acceptable for occasional
  edits; promote to incremental updates if profiling shows the
  rebuild cost.

Phase 4 (GPU-side culling) remains the next big lever.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 15:03:33 +03:00
Hakan SevenandClaude Opus 4.7 b70c9f9262 perf(pipeline): per-frame hatch+wipeout frustum cull at draw time (Phase 2.3)
The Phase 2.1 step-4 attempt at culling hatches inside
`synced_hatch_models` was wrong: GPU hatch/wipeout buffers are
uploaded on geometry_epoch only (see render.rs — they're "static
buffers"), so any CPU-side cull at build time would freeze the
visible set at the geometry epoch boundary and never re-evaluate on
pan. Reverted that approach; the build-time function now returns the
full visible set again and the cache key is back to geometry_epoch.

The actual per-frame frustum cull moves into the existing draw-time
skip-flag machinery:

- `compute_hatch_lod` now also folds in `aabb_offscreen` (ORed with
  the existing Phase 3.3 sub-pixel skip), so out-of-view hatches are
  skipped at the draw call.
- New `compute_wipeout_lod` + `wipeout_skip_flags` mirror the hatch
  path. The wipeout draw loop honors the flag.
- `Primitive::prepare` calls the new compute step alongside the rest.

Per-frame projection + skip is a few hundred microseconds on dense
docs; far cheaper than re-uploading GPU buffers per pan tick.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 15:02:07 +03:00
Hakan SevenandClaude Opus 4.7 49fddea1ef fix(scene): include camera_generation in hatch/wipeout cache keys
The Phase 2.1 step-4 change made `synced_hatch_models` and
`wipeout_models` view-cull through `entity_index().query_rect`, but
left both caches keyed on `geometry_epoch` alone. Cache hit would
return a list culled to a previous camera's view, dropping hatches as
the user panned. Now keyed by `(geometry_epoch, camera_generation)`
— same pattern as `wire_cache`.

`image_cache` stays geometry-only: images self-cull per frame via
`vp_scissor` in the GPU pipeline; the cached list is camera-agnostic.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:56:20 +03:00
Hakan SevenandClaude Opus 4.7 fcf4d4fe28 perf(pipeline): per-frame mesh frustum cull (Phase 2.2)
Roadmap calls for an Aabb3 octree, but H7CAD's mesh entities only
carry XY world_aabb (Phase 3.4 stored XY for the LOD selector and
never extended to Z) and the camera is orthographic top-down for
every realistic workflow. So Phase 2.2 here becomes a draw-side cull:
`compute_mesh_lod` now also populates a `mesh_visible: Vec<bool>`
flag using the same 4-corner projection it already does for LOD
picking, and the mesh render pass skips draws whose projected AABB
sits entirely outside the viewport rect.

Mesh quadtree-indexing is still in place from Phase 2.1 (Solid3D /
Region / Body are not in `is_unindexable_entity`, so they're inserted
into `entity_index`). When mesh entity counts grow large enough that
the upload-time scan dominates over per-frame draws, Phase 2.3 can
plug `query_rect` into `upload_meshes` too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:54:17 +03:00
Hakan SevenandClaude Opus 4.7 5b8cdd940e perf(scene): quadtree culling in hatch + wipeout passes (Phase 2.1, step 4)
`synced_hatch_models` and `wipeout_models` now pre-filter their
per-entity scans through `entity_index().query_rect(view)` when a
Model-layout view AABB is available. Affects:

- Top-level hatch HashMap iteration (the dominant cost — clone +
  render_style + selection tint per hatch).
- Wide LWPolyline / Polyline2D solid-fill loop.
- Wipeout entity scan.

The Insert→exploded-hatch loop is left alone; Insert is not
quadtree-indexed (its WCS bbox depends on block defn × insert
transform, handled inside block_cache).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:50:00 +03:00
Hakan SevenandClaude Opus 4.7 e43498d6c4 perf(scene): quadtree culling in wire tessellation (Phase 2.1, step 3)
`wires_for_block` now drives candidate selection from the quadtree
when a Model-layout view AABB is available — `query_rect` replaces
the O(N) `doc.entities().filter(...)` scan. Visibility predicate is
hoisted into a closure and reused for the quadtree-candidate path
and the unindexable-entity append (Insert/Viewport).

Paper space and the pre-fit first-frame settle path keep the
original full-scan behaviour (no view AABB → no cull). Per-entity
LOD + frustum checks inside `tessellate_entity` stay in place; the
quadtree is purely a candidate reducer so culling correctness still
funnels through one place.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:46:51 +03:00
Hakan SevenandClaude Opus 4.7 3e874602e2 feat(scene): Scene::entity_index — lazy quadtree build (Phase 2.1, step 2)
Adds `entity_index_cache: RefCell<Option<(u64, QuadTree)>>` and an
accessor that lazily (re)builds on geometry_epoch change. Skips
`Insert`/`Viewport`/`Block`/`BlockEnd` and entities whose
`bounding_box()` is degenerate or non-finite — those callers must
iterate separately. Root bounds derived from the union of indexed
entity AABBs with a 1% margin, so the tree is always tight.

Still unused; consumers wired in upcoming steps. Cargo emits dead-code
warnings for the helpers; they evaporate when the wire/hatch paths
switch to `query_rect`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:43:09 +03:00
Hakan SevenandClaude Opus 4.7 727992bc84 feat(scene): quadtree primitive for 2D spatial index (Phase 2.1, step 1)
Standalone data structure — no consumers yet. Flat-vector node
storage, leaf capacity 32 / max depth 16, O(1) remove/update via
handle locator map, out-of-root items kept in an overflow list that
surfaces on every query.

Foundation for replacing the O(N) entity scan in the wire tessellation
path (scene/mod.rs:1232) with a query_rect call.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:40:11 +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 <noreply@anthropic.com>
2026-05-16 19:55:57 +03:00
Hakan SevenandClaude Opus 4.7 de37ee04bf fix(scene): viewport auto-fit fallback + f64-precision projection
Three changes to make paper-space viewports render correctly on
UTM-scale drawings whose viewports were saved with default
view_target=(0, 0, 0) and a view_center pointing at empty WCS:

1. **Auto-fit fallback.** When the saved view's WCS rect doesn't
   overlap the IQR-bounded content cluster (`world_offset ±
   local_extent_max`), override `view_center` with `world_offset` and
   recompute `view_height` to fit the cluster into the paper viewport.
   Matches AutoCAD's silent auto-fit-on-open for stale / uninitialized
   viewports; without it those viewports rendered blank.

2. **f64 projection inner loop.** The previous f32 path computed
   `(wire_offset_rel - target_offset_rel).dot(view_right) -
   view_center` by subtracting values at ~5e6 magnitude (f32 ULP
   ~0.5 m) to land on a small paper offset. The cancellation produced
   centimetre-scale jitter on paper output even when the model was
   clean. Reconstruct the wire WCS coord, subtract the display center,
   and dot-project in f64; cast to f32 only at the final paper
   position where magnitudes are bounded by the viewport rect.

3. **examples/inspect_viewports.rs** — diagnostic that dumps every
   viewport's saved view fields (view_target, view_center, view_height
   vs paper height, custom_scale, status flags) plus drawing-level
   insertion_units and model-space extents. Used to verify that
   acadrust's DWG reader reads view_height correctly; the
   `view_height == vp.height` patterns observed on real files turn out
   to be genuine "default 1:1" file content rather than a reader bug
   (a round-trip test through DwgWriter / DwgReader survives all
   per-viewport values).

Also: switch the `acadrust` patch from `HakanSeven12/acadrust@feat/
expose-blockrecord-is-loaded` (a stale branch with the abandoned
is_loaded experiment) to `hakanaktt/acadrust@main`. Upstream main
now carries the layout paper-dimensions and mirrored-arc fixes that
this branch used to add — plus PR #20's DXF reading fixes that the
old branch was missing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 19:32:36 +03:00
Hakan SevenandClaude Opus 4.7 68fe77c133 chore: release v0.2.9
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v0.2.9
2026-05-16 13:04:58 +03:00