paper_limits() now reads paper_width/paper_height/plot_rotation from the
Layout object (populated by a patched acadrust from the embedded DWG
PlotSettings). This fixes layouts where min/max_limits do not match the
physical paper size (e.g. A4 portrait content inside A3 landscape limits).
DXF files use codes 44/45/73 from raw_plot_settings_codes; DWG files use
the PlotSettings block embedded in the LAYOUT object. Both fall back to
min/max_limits when paper_width is zero.
Depends on HakanSeven12/acadrust feat/layout-paper-dimensions (PR #21 on
hakanaktt/acadrust); using that fork until the PR is merged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Save camera to named View table entries (H7CAD_Camera_Model / H7CAD_Camera_<layout>)
on every navigation change; restore from these entries on file open and tab switch.
acadrust's DWG writer overrides *Active VPort view_height/view_center with
zoom-extents values, but leaves the View table untouched, so this survives save.
- Fix pitch recovery: clamp vd.z to [-1, 1] instead of [-0.999, 0.999] so that
asin(1.0) = π/2 exactly for plan/top views, preventing the subtle tilt on reload.
- Fix IEEE 754 edge case: atan2(+0, -0) = π for plan view direction (0,0,1);
special-case yaw = 0 when both vd.x and vd.y are near zero.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
view_dir = rotation * Z = (sin(yaw)*cos(pitch), -cos(yaw)*cos(pitch), sin(pitch))
so yaw = atan2(x, -y), not atan2(x, y).
The sign error caused a 180-degree Z rotation on every save/reload cycle.
Applies to apply_active_vport_camera, apply_sheet_viewport_camera, and
camera_for_viewport.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Direct camera.orbit/pan/zoom calls in update.rs bypassed camera_generation
so the Tick handler never detected a change and never wrote the camera back
to the document. Add camera_generation += 1 after each navigation call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- On file open and layout tab switch, restore the camera from the
document's saved view (*Active VPort for model space, sheet viewport
for paper space) instead of fitting to wire bounds.
- After any zoom/pan/rotate, write the new camera position back to the
document and mark the file dirty so it is saved with the file.
- Uses last_synced_camera_gen to avoid marking the file dirty on the
initial camera restore.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Model entity wires are tessellated with world_offset subtracted for GPU
precision, but view_target/view_center were raw model coordinates.
Subtracting world_offset from view_target aligns both coordinate systems
so model content projects correctly into viewport rectangles in paper space.
Also fixes scale priority (view_height before custom_scale) and adds
view_center DCS offset to the projection, completing the paper-space
viewport rendering.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The acadrust DWG reader never assigns a viewport id (always 0).
Some DXF exporters use id=-1 for all viewports. In both cases the
previous id > 1 check excluded every viewport from content rendering.
Replace is_user_viewport_id(id) with is_content_viewport(vp) that
uses a geometry heuristic for unknown ids (0 / negative):
- id=1 → always sheet viewport (excluded)
- id≥2 → always user viewport (included)
- id=0 or id<0 → sheet if center≈(0,0) AND scale≈1.0, else user
This allows DWG files and non-standard DXF files to display model
content inside their paper-space viewports and be entered via
double-click.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two root causes prevented paper-space viewports from loading correctly:
1. acadrust's ViewportStatusFlags::from_bits() maps bit 0 → is_on, but
the real DXF/DWG spec uses bit 15 (0x8000) for "viewport on". Post-load
fixup in io/mod.rs now corrects this for both DXF and DWG files: when
bit 0 is clear but bit 15 is set, is_on is forced true and locked is
derived from bit 14 (0x4000).
2. Some DXF exporters write id=-1 for all viewport entities instead of the
standard id=1 (sheet) / id≥2 (user) convention. The is_user_viewport_id()
helper now treats any id ≠ 0 and ≠ 1 as a user viewport, covering both
the standard id≥2 and the non-standard id=-1 cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Model-space entities shown through a viewport were tessellated using
paper_bg_color (light), but the interactive viewport pane renders them
on bg_color (dark), causing white/black entities to appear incorrectly.
- model_wires_for_viewport: tessellate with bg_color so the viewport
pane (dark background) gets correctly adapted entity colors
- viewport_content_wires: re-adapt projected wire colors using
paper_bg_color before rendering onto the paper canvas
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
White entities on light backgrounds (e.g. paper space) render as black;
black entities on dark backgrounds (e.g. model space) render as white.
All other colors pass through unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Paper-space entities (wires, hatches, wipeouts) live in sheet
coordinates (~mm scale) and do not need the model-space centring
shift. world_offset was incorrectly subtracted from their coordinates,
displacing all paper-space geometry away from the paper sheet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fix: polyline arc center sign was inverted (- → +) causing arcs to render on the
wrong side in LwPolyline and Polyline2D
- fix: store glyph strokes in local space with f64 origin so world_offset subtraction
uses f64 precision; prevents blocky text at large UTM coordinates
- fix: Face3D fill darkened to 45% so edge wires are visually distinct from fill
- fix: wire batch vertex buffer chunked to 256 MB GPU limit (was panic on large files)
- fix: gpu_face3d_edges changed from Option<WireGpu> to Vec<WireGpu> to support chunks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
acadrust Face3D encodes triangles by setting fourth_corner == third_corner.
The fill builder was emitting a zero-area second triangle for these cases.
Now checks the p2==p3 distance and skips the second triangle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3DFACE entities previously created one WireGpu buffer per entity,
resulting in N separate draw calls per frame. For files with large
triangle meshes this was the dominant GPU CPU overhead.
New pipeline:
- face3d.wgsl: simple flat-color vertex + fragment shader (MVP only,
no quad expansion, no pattern distance computation)
- Face3DGpu: batches ALL Face3D entities into a single TriangleList
buffer (6 vertices per face, per-vertex color) — 1 draw call
- WireGpu::from_batch: merges N Face3D edge wire models into one
quad-expanded buffer for edge rendering — 1 draw call
- split_face3d_wires: separates face3d from other wires at Primitive
build time via document handle lookup (O(N), once per epoch)
Hit-testing via WireModel (CPU) is unaffected — face3d wires remain
in entity_wires_arc() and hit_test_wires() as before.
Before: N 3DFACE → N draw calls, N × 96 B/vertex WireVertex
After: N 3DFACE → 2 draw calls (fill + edges), 28 B/vertex fill
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Entity grips() return world-space coordinates (raw DXF values). After
the world_offset change the camera view_proj is in local space, so
projecting world-space grips through it gave wrong screen positions and
hit-test failures.
- refresh_selected_grips: subtract world_offset from every grip.world
so selected_grips are stored in local (camera) space
- Grip drag: cursor/snap positions are already local space; for
GripApply::Absolute add world_offset back before passing to entity
so entity coordinates stay in world space. Translate deltas are
identical in both spaces so no conversion is needed there.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
hatch_model_from_dxf(), solid_hatch_model(), and wipeout_boundary_2d()
were converting DXF world-space coordinates directly to f32 without
subtracting world_offset. After the world_offset change moved all wire
geometry to local space, hatches/solids/wipeouts rendered at the wrong
position (displaced by the centroid offset, up to ~4M m for Turkish UTM).
All three functions now accept world_offset and apply it to every
boundary vertex before the f64→f32 cast.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three root causes caused all geometry and UCS arrows to disappear:
1. TruckObject::Lines and TruckObject::Text variants returned world-space
f32 coordinates without applying world_offset, so fit_all() computed
bounds spanning from local space to UTM world space (~4 M m), placing
the camera impossibly far from all visible geometry.
2. Ray/XLine entities with unnormalized direction vectors (common in some
DXF exporters) produced astronomically large far-point coordinates
(up to 1e28) that overwhelmed the camera fit even after the offset fix.
Fixed by normalizing the direction vector before multiplying by
DISPLAY_EXTENT.
3. Origin-stuck corrupted entities (coordinates ≈ 0 in world space become
≈ -world_offset in local space) pulled the fit_all() bounds far from
the actual drawing. Fixed by computing local_extent_max from EXTMIN/EXTMAX
(10× safety margin) and skipping points outside that range in fit_all().
Additional hardening: wire_gpu.rs now skips ±inf segments (not just NaN),
and snap_pts/key_vertices in all TruckObject variants are offset-corrected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Subtracts the DXF header extents centroid (world_offset) from all coordinates
in f64 BEFORE casting to f32. This eliminates the ~0.5 m ULP error that occurs
at 4,000,000 m UTM coordinates and caused geometry to disappear on zoom-out.
- Scene: adds world_offset field + compute_and_set_world_offset() method
- tessellate.rs: threads world_offset through all f64→f32 conversions
(legacy_geometry, dimension_geometry, solid_wire_fallback, multileader)
- truck_tess.rs: to_local() helper already in place from prior session
- mod.rs: tessellate_entity() and entity_aabb() apply world_offset
- update.rs: compute_and_set_world_offset() called after doc load
- cmd_result.rs / solid3d_cmds.rs: pass correct offset to truck tessellators
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Changed selection overlay pipeline from depth_compare=Greater to
depth_compare=Always so selected wires are redrawn at full brightness
after all other passes, making them visible regardless of what is in
front of them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Selected wires are now visible through occluding objects. A second
wire_xray pipeline (depth_compare=Greater, depth_write_enabled=false)
renders 25%-alpha ghost copies of selected wires only where they are
behind the depth buffer, leaving fully-visible portions at full glow
brightness unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Complex linetypes now render embedded text elements (e.g. GAS_LINE's
"GAS" labels): the LtSegment::Text variant is parsed from LIN files,
scaled along with shape/dash segments, and tessellated via
cxf::tessellate_text_ex aligned to the linetype direction.
Full ellipses gain four Quadrant snap hints at ±major/±minor axis
endpoints in addition to the existing Center snap, matching AutoCAD
snap behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extension lines for linear and aligned dimensions now start at
DIMEXO × DIMSCALE distance from the definition point (instead of at the
point itself) and extend DIMEXE × DIMSCALE units past the dimension
line, matching AutoCAD rendering. Both values fall back to 0 when the
dimstyle is not found.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When path_type == Spline, the leader line is now tessellated as a
Catmull-Rom spline through the bend points (8 segments per span)
instead of connecting them with straight segments. Straight/Invisible
path types are unchanged. Key vertices and tangent geoms still use the
raw control points for snap and tangent snapping.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Arrow size for all dimension types (linear, aligned, radius, diameter,
angular) is now read from the associated DimStyle's dimasz field scaled
by dimscale, instead of being hardcoded at 0.12. Falls back to 0.12
when the style isn't found in the document's dim_styles table.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Spline: if flags.closed/periodic and B-spline endpoints don't meet,
add an explicit closing Wire segment to eliminate the visual gap.
- Hatch BoundaryEdge::Spline: evaluate the B-spline at 16 uniform
parameter samples (de Boor via truck BSplineCurve) instead of
plotting raw control points as a polyline; falls back to control-
point polyline for degenerate knot/degree inputs.
- Ellipse arc key_vertices and LWPolyline midpoint grips committed
separately; this commit wraps the spline-related fixes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Partial ellipse arcs now populate key_vertices with both endpoints so
Endpoint snap works correctly. LWPolyline arc segments (bulge ≠ 0) get
a diamond grip at the arc midpoint; dragging it recomputes the segment
bulge via circumcircle of start/mid/end, giving interactive arc editing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Polyline3D tessellation now separates spline-fit curve vertices (flag 8)
from frame control points (flag 16): the curve vertices form the rendered
wire while control points are exposed as key_vertices for snap. Polylines
without spline flags are unchanged.
DXF post-load fixup now also converts AttributeEntity and AttributeDefinition
rotation from degrees to radians (group code 50 bug), matching the existing
LinearDimension fix. Attribute text rendered from DXF files now has correct
orientation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spline entities now expose fit_points (or control_points when no fit
points are defined) as key_vertices, enabling Endpoint and Midpoint
snap to reach spline construction points.
TextStyle is_backward flag is applied by negating the width_factor,
producing horizontally mirrored text. is_upside_down rotates the text
180°. Both tessellate_text_ex and text_local_bounds now handle negative
width_factor without clamping it to zero.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Block sub-entities with ByBlock color now inherit their INSERT's resolved
color instead of rendering as the default white. ByBlock linetype inherits
the INSERT's linetype pattern, and ByBlock lineweight inherits the INSERT's
effective line weight.
Implemented via render_style_for_block_sub(), called in tessellate_entity()
when expanding INSERT exploded entities.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
INSERT entities now expose their insertion point as a SnapHint::Insertion
pre-baked snap candidate, enabling OSNAP Insertion to locate block
reference origins. The cross marker is also positioned at the actual
insertion point instead of the origin.
Hatch entities now populate key_vertices from Polyline and Line boundary
edges (with correct elevation Z), enabling Endpoint and Midpoint snapping
to hatch boundary geometry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comprehensive backlog of acadrust entity fields, behaviors, and subsystems
not yet integrated into H7CAD rendering, snap, grip, and properties systems.
Covers tessellation gaps, OCS→WCS transform, ByBlock style resolution,
snap/grip point gaps, text rendering, and DXF reader unit bugs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Text/MText values now correctly handle DXF special-character sequences:
- %%d → °, %%p → ±, %%c → ⌀, %%nnn → Unicode scalar (resolved in tessellation)
- %%u / %%o toggle underline and overline strokes for TEXT entities
MText decoration codes (\L/\l, \O/\o, \K/\k) were previously consumed
as argument-bearing codes, silently eating the decorated text. Fixed
strip_mtext_codes to keep them as zero-width markers; tessellate_text_ex
now emits actual line strokes (underline, overline, strikethrough).
measure_mtext_chars and word_wrap skip markers so line widths are correct.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
acadrust's DXF reader does not parse group code 53 (text rotation),
so DimensionBase.text_rotation is always zero for DXF-loaded files.
Fall back to the natural axis-aligned rotation computed from geometry:
Linear dimensions use d.rotation (radians), Aligned dimensions use
atan2 of the second-first vector. Normalize to (-π/2, π/2] so text
never appears upside-down. Explicit non-zero overrides stored by the
DWG reader are preserved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
append_linear_dimension() applied the same perp offset to both d1 and d2,
but only d2 is guaranteed to be at the definition_point level; d1 must be
projected independently onto the dimension line, otherwise non-collinear
first/second points produce a parallelogram instead of a rectangle.
Fix: compute dim_line_pos = def.dot(perp) and project each endpoint
separately so the dimension line is always parallel to axis.
Additionally, the acadrust DXF reader stores LinearDimension.rotation from
group code 50 (degrees) without converting to radians, while DWG and our
own annotation commands store it in radians. Add a post-load fixup in
load_file() that converts the rotation to radians for DXF files so
tessellation can call cos/sin uniformly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Standalone hatches and hatches from block inserts were rendered
regardless of entity invisible flag, layer off/frozen state, or
whether the owning INSERT belongs to the current layout block.
- Filter self.hatches by invisible, layer, and belongs_to_visible_block
- Skip INSERT entities that are invisible, on a hidden layer, or outside
the current layout block before exploding their contents
- Skip individual hatches produced by explode that are invisible or on
a hidden layer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
paper_canvas_wires() now returns Arc<Vec<WireModel>> backed by
paper_canvas_cache (epoch-keyed). Pan/zoom frames hit the cache and
return a pointer copy; only geometry changes rebuild the wire list.
Completes the paper-canvas caching chain (L+M): sheet cache →
projected-per-viewport cache → full canvas Arc cache.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add two epoch-keyed caches to eliminate per-frame work in paper-space layouts:
paper_sheet_cache — caches wires_for_block(layout_block) as Arc<Vec<WireModel>>.
Previously paper_sheet_wires() re-tessellated all paper-space entities (title
block, annotations, viewport borders) on every mouse-move frame. Now it is an
O(1) Arc clone. entity_wires_arc() reuses the same cache so a geometry-change
frame triggers only one tessellation pass even when both the GPU renderer and
the Iced canvas are active.
paper_projected_cache — caches projected+clipped wires per viewport handle.
viewport_content_wires() now uses model_wires_for_viewport_arc() for the
tessellation step (already O(1) via viewport_wire_cache) and stores the
projection+clip result keyed by (vp_handle, epoch). Navigation frames
(pan/zoom on the paper sheet) are O(N_viewports) Arc/slice copies instead of
O(N_entities × N_points × N_viewports) tessellate+project work.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add `aabb: [f32; 4]` field to WireModel (world-space 2-D bounding box).
In tessellate_entity() set each wire's AABB from e.as_entity().bounding_box()
via new entity_aabb() helper; Insert sub-entities each get their own AABB.
Preview/interim wires and any entity returning a zero-extent default box get
UNBOUNDED_AABB so they are never pre-rejected.
Replace the chord-sphere heuristic in Snapper::wire_in_range() with a proper
AABB vs snap-circle overlap test. The new check is four scalar comparisons and
correctly handles closed curves (circles, arcs) which the chord-sphere approach
had to special-case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Option I: add per-viewport Arc wire cache so paper-space viewport rendering
no longer re-tessellates model content every frame.
Option J: hit_test_wires() returns Arc<Vec<WireModel>> — O(1) pointer copy
in model space instead of a full Vec clone on every mouse move during commands.
paper_canvas_hatches/wipeouts() now route through the epoch caches added in F.
Option K: add world_snap_r + wire_in_range() pre-check to Snapper::snap().
All Endpoint/Midpoint/Nearest/Perpendicular/Intersection loops skip wires
whose chord sphere is outside the snap circle using O(1) scalar comparisons,
eliminating O(entities × vertices) matrix multiply cost when zoomed in.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Arc<Vec<...>>-backed epoch caches for hatches, wipeouts, images, and
meshes — the same pattern used for wires. build_primitive() now returns
an O(1) Arc refcount bump on navigation frames instead of rebuilding
every collection from scratch.
Also wrap ImageModel.pixels in Arc<Vec<u8>> so cloning an ImageModel is
O(1) regardless of image resolution.
Navigation frames no longer copy pixel data, tessellated hatch boundaries,
or mesh geometry — all per-frame CPU work on unchanged geometry is now
eliminated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract tessellate_one body into a Send-compatible free function
tessellate_entity, then drive it with rayon::into_par_iter() in
wires_for_block(). Spreads file-open and post-edit tessellation across
all CPU cores. Navigation frames are unaffected (Options A/B cache
eliminates per-frame tessellation entirely).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the O(objects) linear scan inside wires_for_block() with a
sort_cache keyed by geometry_epoch. On a cache miss the index is built
once by scanning all document objects; subsequent calls within the same
epoch do an O(1) HashMap lookup (block_handle → entity→sort map).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add two complementary render caches that make mouse navigation essentially
free regardless of file size:
Option A – Wire tessellation cache: entity_wires_arc() caches the
tessellated Vec<WireModel> keyed by geometry_epoch and returns an Arc,
so navigation frames (no preview wires) incur zero Vec clones and zero
tessellation work. Primitive::wires changed to Arc<Vec<WireModel>>.
Option B – GPU buffer cache: Pipeline stores cached_epoch; prepare()
skips all upload_wires/hatches/images/meshes calls when the epoch is
unchanged, writing only the 192-byte camera uniform instead.
geometry_epoch is a process-wide AtomicU64 incremented by every Scene
mutation (add/erase entity, selection, layer visibility, layout switch,
populate_*, grip, transform, copy, clear, preview wires). Using a global
counter rather than a per-scene counter prevents two tabs that happen to
have the same local bump count from sharing a cached_epoch value, which
was causing the first file's geometry to remain on screen after opening a
second file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Empty LwPolyline and single-control-point Spline fallbacks were calling
builder::line(&v, &v) with the same Rc vertex, triggering the
truck-topology panic "Two same vertices cannot construct an edge."
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>