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]>
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]>
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]>
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]>
- 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]>
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]>
The macos-13 / x86_64-apple-darwin matrix entry was failing in the
v0.3.5 run and we don't plan to ship Intel macOS binaries. Collapse
the macOS job to a single arm64 build on macos-14 (Apple Silicon),
removing the matrix and threading the target literally through the
build / .app / .dmg steps. README updates to match.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- .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]>
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]>
The acadrust fix from https://github.com/hakanaktt/acadrust/pull/27
unblocks files with corrupt hatch boundary-handle counts (anteen.dwg
went from >100 s "stuck" to ~0.7 s release). Until the PR lands on
upstream/main, [patch.crates-io] points at the fork branch carrying
that commit; the comment in Cargo.toml notes the revert.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
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]>
The stub wires emitted at low LOD carried wire.selected = true but
stored the entity's own colour, so the actual on-screen pixel kept
the entity hue. Selecting an object and then zooming past the LOD
boundary made the highlight vanish even though the entity was still
in the selection set.
Swap to WireModel::SELECTED at construction time, matching what
tessellate.rs does for full-LOD wires.
Follow-up to #19.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
At deep zoom-out, entities that projected to under ~5 px were dropped
from the scene entirely (and text below 2 px-baseline followed the
same path). The objects disappeared visually AND fell out of
window / crossing selection, and any prior selection highlight stopped
rendering once the zoom transition crossed the LOD threshold. #19.
Two changes:
1. `tessellate_entity`: replace the sub-5-px `return vec![]` cull, the
sub-2-px text-baseline `return vec![]`, and the empty-greek fall-out
with a new `lod_stub_wire` helper. The stub is a 2-point AABB
diagonal carrying the same `selected` flag, ACI, and AABB as the
entity — so the entity stays visible as a 1-pixel speck, tracks its
highlight colour across LOD changes, and remains hit-test'able.
2. `box_hit` / `poly_hit`: when a wire has no `points` but a finite
`aabb` (greek text emits only fill_tris), fall back to the AABB
rectangle as the hit-test shape. Defense in depth so future fill-
only wires don't silently drop out of selection.
Closes#19
Co-Authored-By: Claude Opus 4.7 <[email protected]>
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]>
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]>
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 <[email protected]>
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]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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]>
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]>
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]>
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]>
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 <[email protected]>
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 <[email protected]>
`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
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>