refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
fix(plot): correct hatch rendering in PDF / plot export
Hatched content — especially fills nested inside a block INSERT, such as a
title-block logo — plotted incorrectly: missing, mis-spaced, or with phantom
bars. Five related fixes make the PDF/plot output match AutoCAD.
1. Export block-internal hatch fills.
The export collected only hatches owned directly by the layout block, so a
hatch nested in a block INSERT was dropped and printed as bare monochrome
outlines. The insert-explosion the viewport already does is extracted into a
shared `exploded_insert_hatch_models()` and called from both the viewport
(`synced_hatch_models`) and the export (`paper_canvas_hatches`), so a plot
draws block-internal hatches identically to the screen.
2. Honour the hatch's own stored pattern-line spacing.
`hatch_model_from_dxf` re-derived pattern spacing from the name-matched
catalog entry x pattern_scale, ignoring the resolved line geometry the DWG
stores on the hatch. When a drawing was authored against a different base
spacing (imperial 0.125 vs the catalog's metric 3.175 for ANSI31), lines
came out up to ~25x too coarse and a dense fill collapsed to a few stray
lines. When the hatch carries its own line geometry it is now used directly
(identity scale/angle); the catalog path remains the fallback.
3. Fill far-from-origin pattern hatches.
`pattern_segments` clamped the ABSOLUTE scan-line index to
+/-MAX_LINES_PER_FAMILY. A fine-spaced hatch far from the pattern origin has
large-magnitude indices at both ends but a small span, so the clamp inverted
the range and emitted nothing — silently dropping the fill. Cap the line
count (span) instead of the absolute index.
4. Skip TEXTBOX boundary paths.
TEXTBOX boundary paths (flag bit 3) are text bounding-boxes AutoCAD derives
for island detection; they are never drawn or filled. Treating one as a fill
boundary painted its rectangle solid — a phantom bar. It is now skipped when
building the fill boundary.
5. Print white/ACI-7 hatch fills black on paper.
Hatch fills arrive adapted to the dark screen background, so a white/ACI-7
fill would vanish white-on-white on the sheet. Mirror the wire pass:
near-white/near-yellow -> black, near-cyan -> dark blue, matching AutoCAD's
colour-7-on-white plotting. Wipeouts keep their paper-white mask.
Adds `tests/block_hatch_export.rs` covering: block-internal hatch reaches the
export set, stored-line spacing is honoured, far-from-origin fills are not
dropped, and TEXTBOX paths are not filled.
2026-07-06 12:24:10 +02:00
|
|
|
|
/// Convert a HATCH's own resolved pattern line (world-unit `offset` = step to
|
|
|
|
|
|
/// the next parallel line, plus a base angle) into a render `PatFamily` whose
|
|
|
|
|
|
/// geometry is already final — the HatchModel that carries it uses scale 1 and
|
|
|
|
|
|
/// angle_offset 0 (see `prebaked` in `hatch_model_from_dxf`). The world-space
|
|
|
|
|
|
/// offset is rotated into the line's local frame so `pattern_segments` and the
|
|
|
|
|
|
/// GPU shader, which rotate `(dx, dy)` back out by the family angle, reproduce
|
2026-07-08 22:48:33 +03:00
|
|
|
|
/// the exact stored step. `x0/y0` are filled in by the caller (from the stored
|
|
|
|
|
|
/// `base_point`, relative to `world_origin`) once the boundary anchor is known;
|
|
|
|
|
|
/// they set the pattern origin, observable for dashed / offset patterns.
|
2026-07-21 09:28:07 +03:00
|
|
|
|
/// Order a hatch boundary path's sampled edges into one tip-to-tail loop.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Real files do not store boundary edges as a sequential walk: associative
|
|
|
|
|
|
/// hatches list them in boundary-source-entity order, with arbitrary
|
|
|
|
|
|
/// direction — the next edge in the list may attach to either end of the
|
|
|
|
|
|
/// chain built so far, or belong to the far side of the loop entirely.
|
|
|
|
|
|
/// Concatenating them verbatim draws a self-crossing "bowtie" outline and
|
|
|
|
|
|
/// flips the even-odd fill over the wrong region.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Greedy nearest-endpoint assembly: keep the chain open at both ends and, at
|
|
|
|
|
|
/// each step, attach the unused edge whose endpoint lies closest to either
|
|
|
|
|
|
/// end (reversing / prepending as needed). Distance comparison, no tolerance:
|
|
|
|
|
|
/// a correctly-ordered file matches at distance 0 and reproduces exactly.
|
|
|
|
|
|
fn chain_path_edges(mut polys: Vec<Vec<[f64; 2]>>) -> Vec<[f64; 2]> {
|
|
|
|
|
|
let d2 = |a: [f64; 2], b: [f64; 2]| (a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2);
|
|
|
|
|
|
polys.retain(|p| !p.is_empty());
|
|
|
|
|
|
if polys.is_empty() {
|
|
|
|
|
|
return Vec::new();
|
|
|
|
|
|
}
|
|
|
|
|
|
let mut chain: std::collections::VecDeque<[f64; 2]> = polys.swap_remove(0).into();
|
|
|
|
|
|
while !polys.is_empty() {
|
|
|
|
|
|
let head = *chain.front().unwrap();
|
|
|
|
|
|
let tail = *chain.back().unwrap();
|
|
|
|
|
|
// (distance, index, reverse-points, attach-at-front)
|
|
|
|
|
|
let mut best = (f64::MAX, 0usize, false, false);
|
|
|
|
|
|
for (i, p) in polys.iter().enumerate() {
|
|
|
|
|
|
let s = p[0];
|
|
|
|
|
|
let e = *p.last().unwrap();
|
|
|
|
|
|
for c in [
|
|
|
|
|
|
(d2(tail, s), i, false, false),
|
|
|
|
|
|
(d2(tail, e), i, true, false),
|
|
|
|
|
|
(d2(head, e), i, false, true),
|
|
|
|
|
|
(d2(head, s), i, true, true),
|
|
|
|
|
|
] {
|
|
|
|
|
|
if c.0 < best.0 {
|
|
|
|
|
|
best = c;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let (_, idx, rev, at_front) = best;
|
|
|
|
|
|
let mut p = polys.swap_remove(idx);
|
|
|
|
|
|
if rev {
|
|
|
|
|
|
p.reverse();
|
|
|
|
|
|
}
|
|
|
|
|
|
if at_front {
|
|
|
|
|
|
if d2(p[p.len() - 1], head) < 1e-18 {
|
|
|
|
|
|
p.pop();
|
|
|
|
|
|
}
|
|
|
|
|
|
for q in p.into_iter().rev() {
|
|
|
|
|
|
chain.push_front(q);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let mut it = p.into_iter();
|
|
|
|
|
|
if let Some(first) = it.next() {
|
|
|
|
|
|
if d2(first, tail) >= 1e-18 {
|
|
|
|
|
|
chain.push_back(first);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
chain.extend(it);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
chain.into()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
fix(plot): correct hatch rendering in PDF / plot export
Hatched content — especially fills nested inside a block INSERT, such as a
title-block logo — plotted incorrectly: missing, mis-spaced, or with phantom
bars. Five related fixes make the PDF/plot output match AutoCAD.
1. Export block-internal hatch fills.
The export collected only hatches owned directly by the layout block, so a
hatch nested in a block INSERT was dropped and printed as bare monochrome
outlines. The insert-explosion the viewport already does is extracted into a
shared `exploded_insert_hatch_models()` and called from both the viewport
(`synced_hatch_models`) and the export (`paper_canvas_hatches`), so a plot
draws block-internal hatches identically to the screen.
2. Honour the hatch's own stored pattern-line spacing.
`hatch_model_from_dxf` re-derived pattern spacing from the name-matched
catalog entry x pattern_scale, ignoring the resolved line geometry the DWG
stores on the hatch. When a drawing was authored against a different base
spacing (imperial 0.125 vs the catalog's metric 3.175 for ANSI31), lines
came out up to ~25x too coarse and a dense fill collapsed to a few stray
lines. When the hatch carries its own line geometry it is now used directly
(identity scale/angle); the catalog path remains the fallback.
3. Fill far-from-origin pattern hatches.
`pattern_segments` clamped the ABSOLUTE scan-line index to
+/-MAX_LINES_PER_FAMILY. A fine-spaced hatch far from the pattern origin has
large-magnitude indices at both ends but a small span, so the clamp inverted
the range and emitted nothing — silently dropping the fill. Cap the line
count (span) instead of the absolute index.
4. Skip TEXTBOX boundary paths.
TEXTBOX boundary paths (flag bit 3) are text bounding-boxes AutoCAD derives
for island detection; they are never drawn or filled. Treating one as a fill
boundary painted its rectangle solid — a phantom bar. It is now skipped when
building the fill boundary.
5. Print white/ACI-7 hatch fills black on paper.
Hatch fills arrive adapted to the dark screen background, so a white/ACI-7
fill would vanish white-on-white on the sheet. Mirror the wire pass:
near-white/near-yellow -> black, near-cyan -> dark blue, matching AutoCAD's
colour-7-on-white plotting. Wipeouts keep their paper-white mask.
Adds `tests/block_hatch_export.rs` covering: block-internal hatch reaches the
export set, stored-line spacing is honoured, far-from-origin fills are not
dropped, and TEXTBOX paths are not filled.
2026-07-06 12:24:10 +02:00
|
|
|
|
fn family_from_stored_line(
|
|
|
|
|
|
ln: &acadrust::entities::hatch::HatchPatternLine,
|
|
|
|
|
|
) -> crate::scene::model::hatch_model::PatFamily {
|
|
|
|
|
|
let (ca, sa) = (ln.angle.cos(), ln.angle.sin());
|
|
|
|
|
|
let dx = ln.offset.x * ca + ln.offset.y * sa;
|
|
|
|
|
|
let dy = -ln.offset.x * sa + ln.offset.y * ca;
|
|
|
|
|
|
crate::scene::model::hatch_model::PatFamily {
|
|
|
|
|
|
angle_deg: ln.angle.to_degrees() as f32,
|
|
|
|
|
|
x0: 0.0,
|
|
|
|
|
|
y0: 0.0,
|
|
|
|
|
|
dx: dx as f32,
|
|
|
|
|
|
dy: dy as f32,
|
|
|
|
|
|
dashes: ln.dash_lengths.iter().map(|&d| d as f32).collect(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
impl Scene {
|
|
|
|
|
|
// ── Entity management ─────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-07-02 20:36:16 +03:00
|
|
|
|
/// Register `name` in the layer table if it isn't already there, giving the
|
|
|
|
|
|
/// new layer a real handle so it survives a DWG save (handle-based format;
|
|
|
|
|
|
/// issue #67). Called whenever an entity is added or edited: an entity that
|
|
|
|
|
|
/// names a layer no explicit LAYER command ever created — e.g. one supplied
|
|
|
|
|
|
/// by a plugin through `add_entity` — otherwise has no table entry, so the
|
|
|
|
|
|
/// DWG writer resolves its layer name to a NULL handle and it reopens on
|
|
|
|
|
|
/// layer 0. Auto-registering keeps it on its own layer (#252). Names are
|
|
|
|
|
|
/// registered verbatim so the writer's (case-insensitive) lookup matches;
|
|
|
|
|
|
/// the always-present default layer "0" and empty names are no-ops.
|
|
|
|
|
|
pub fn ensure_layer(&mut self, name: &str) {
|
|
|
|
|
|
if name.trim().is_empty() || self.document.layers.contains(name) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
let mut layer = acadrust::tables::Layer::new(name);
|
|
|
|
|
|
layer.handle = self.document.allocate_handle();
|
|
|
|
|
|
let _ = self.document.layers.add(layer);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
pub fn add_entity(&mut self, mut entity: EntityType) -> Handle {
|
|
|
|
|
|
// Only Insert / Block entities can introduce or reference a block
|
|
|
|
|
|
// definition that the block cache must (re)build. Adding a plain
|
|
|
|
|
|
// top-level entity (line, arc, text, …) leaves every block defn intact,
|
|
|
|
|
|
// so it keeps the cache and skips the all-blocks re-tessellation.
|
|
|
|
|
|
let affects_blocks = matches!(
|
|
|
|
|
|
&entity,
|
|
|
|
|
|
EntityType::Insert(_) | EntityType::Block(_) | EntityType::BlockEnd(_)
|
|
|
|
|
|
);
|
|
|
|
|
|
let hatch_seed = if let EntityType::Hatch(dxf) = &entity {
|
|
|
|
|
|
let color = self.render_style(&entity).0;
|
|
|
|
|
|
Self::hatch_model_from_dxf(dxf, color)
|
|
|
|
|
|
} else if let EntityType::Solid(solid) = &entity {
|
|
|
|
|
|
let color = self.render_style(&entity).0;
|
|
|
|
|
|
Some(Self::solid_hatch_model(solid, color))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
let image_seed = if let EntityType::RasterImage(img) = &entity {
|
|
|
|
|
|
ImageModel::from_raster_image(img)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
let facet_res = self.document.header.facet_resolution;
|
2026-07-17 19:45:42 +03:00
|
|
|
|
let isolines = self.document.header.isolines.max(0) as usize;
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let mesh_seed = if matches!(
|
|
|
|
|
|
&entity,
|
|
|
|
|
|
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_)
|
|
|
|
|
|
) {
|
|
|
|
|
|
let color = self.render_style(&entity).0;
|
2026-07-17 19:45:42 +03:00
|
|
|
|
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines)
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
.map(|m| offset_mesh_lod_set(m))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-create an ImageDefinition object for new RasterImage entities
|
|
|
|
|
|
// that don't already reference one.
|
|
|
|
|
|
if let EntityType::RasterImage(ref mut img) = entity {
|
|
|
|
|
|
if img.definition_handle.is_none() {
|
|
|
|
|
|
use acadrust::objects::{ImageDefinition, ObjectType};
|
|
|
|
|
|
let def_handle = Handle::new(self.document.next_handle());
|
|
|
|
|
|
let mut img_def = ImageDefinition::with_dimensions(
|
|
|
|
|
|
&img.file_path,
|
|
|
|
|
|
img.size.x as u32,
|
|
|
|
|
|
img.size.y as u32,
|
|
|
|
|
|
);
|
|
|
|
|
|
img_def.handle = def_handle;
|
|
|
|
|
|
img_def.is_loaded = true;
|
|
|
|
|
|
self.document
|
|
|
|
|
|
.objects
|
|
|
|
|
|
.insert(def_handle, ObjectType::ImageDefinition(img_def));
|
|
|
|
|
|
img.definition_handle = Some(def_handle);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-02 20:36:16 +03:00
|
|
|
|
// Register the entity's layer if it names one no LAYER command created
|
|
|
|
|
|
// (e.g. a plugin-supplied layer) so it survives a DWG save instead of
|
|
|
|
|
|
// collapsing to layer 0 in the reopened file (#252).
|
|
|
|
|
|
let layer = entity.common().layer.clone();
|
perf(undo): record entity deltas instead of cloning the whole document
Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.
They now record only the touched entities' before/after images:
- Scene self-records through the five mutation primitives when a recording is
open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
the original handle / remove) plus a per-handle cache reseed, then a single
bump_entities so the incremental caches patch instead of a full re-tess —
undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
between the undo/redo stacks without re-capturing. Commands that touch
non-entity state (new layer, group, *D block, viewport) fall back to the
full snapshot via per-command predicates, and a poison flag debug-asserts
any mis-scope.
remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.
Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 11:11:17 +03:00
|
|
|
|
// Delta-undo poison inputs (captured before the mutations below): an
|
|
|
|
|
|
// add that also creates a new layer, adds a block, or inserts an image
|
|
|
|
|
|
// definition mutates non-entity state a pure-entity delta can't undo.
|
|
|
|
|
|
let creates_layer =
|
|
|
|
|
|
self.is_recording_undo() && !layer.trim().is_empty() && !self.document.layers.contains(&layer);
|
|
|
|
|
|
let is_image = matches!(&entity, EntityType::RasterImage(_));
|
2026-07-02 20:36:16 +03:00
|
|
|
|
self.ensure_layer(&layer);
|
|
|
|
|
|
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
// Route to the correct block based on current editing mode:
|
2026-07-14 13:03:04 +03:00
|
|
|
|
// - BEDIT block editor: geometry belongs to the edited block record,
|
|
|
|
|
|
// so it becomes part of the block definition (issue #261).
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
// - PSPACE (paper layout, no active viewport): paper-space layout block.
|
|
|
|
|
|
// - MSPACE or model layout: model space (document default).
|
2026-07-14 13:03:04 +03:00
|
|
|
|
let handle = if let Some(br) = self.block_edit_block {
|
|
|
|
|
|
entity.common_mut().owner_handle = br;
|
|
|
|
|
|
self.document.add_entity(entity).unwrap_or(Handle::NULL)
|
|
|
|
|
|
} else if self.current_layout != "Model" && self.active_viewport.is_none() {
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let layout_name = self.current_layout.clone();
|
|
|
|
|
|
self.document
|
|
|
|
|
|
.add_entity_to_layout(entity, &layout_name)
|
|
|
|
|
|
.unwrap_or(Handle::NULL)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.document.add_entity(entity).unwrap_or(Handle::NULL)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if !handle.is_null() {
|
|
|
|
|
|
if let Some(model) = hatch_seed {
|
|
|
|
|
|
self.hatches.insert(handle, model);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(model) = image_seed {
|
|
|
|
|
|
self.images.insert(handle, model);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(model) = mesh_seed {
|
|
|
|
|
|
self.meshes.insert(handle, model);
|
|
|
|
|
|
}
|
perf(undo): record entity deltas instead of cloning the whole document
Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.
They now record only the touched entities' before/after images:
- Scene self-records through the five mutation primitives when a recording is
open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
the original handle / remove) plus a per-handle cache reseed, then a single
bump_entities so the incremental caches patch instead of a full re-tess —
undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
between the undo/redo stacks without re-capturing. Commands that touch
non-entity state (new layer, group, *D block, viewport) fall back to the
full snapshot via per-command predicates, and a poison flag debug-asserts
any mis-scope.
remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.
Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 11:11:17 +03:00
|
|
|
|
// Delta-undo: the new handle's before-image is "nothing" (it did not
|
|
|
|
|
|
// exist). Poison the recording if this add also mutated non-entity
|
|
|
|
|
|
// state (a new layer / block / image definition) so the app knows a
|
|
|
|
|
|
// pure-entity delta would be incomplete.
|
|
|
|
|
|
if self.is_recording_undo() {
|
|
|
|
|
|
self.record_undo_before(handle, None);
|
|
|
|
|
|
if creates_layer || affects_blocks || is_image {
|
|
|
|
|
|
self.poison_undo_recording();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
if affects_blocks {
|
|
|
|
|
|
self.bump_geometry();
|
|
|
|
|
|
} else {
|
perf(scene): delta journal so edits touch only changed entities (phase 0-1)
Every geometry mutation previously bumped a monotonic geometry_epoch and each
derived cache compared its stamp and, on a miss, re-walked the whole document —
so drawing or moving one line rebuilt every hatch / image / mesh / wipeout model
even though none of them changed.
Add a per-mutation delta journal: bump_entities(&[(Handle, ChangeKind)]) records
exactly which handles changed (Added / Removed / Modified), folding the memo drop
and the epoch bump into one call a site cannot forget. A bounded ring keeps the
recent deltas; replay_since(last_epoch) coalesces the changes past a consumer or
returns None (full rebuild) when a spanned delta is `full` or the consumer fell
off the ring. bump_geometry / bump_geometry_no_blocks push a `full` delta so the
journal is never missing a step.
Route the common single-entity ops through it: add (Added), update (Modified),
erase (Removed), move/rotate/scale/mirror (Modified), copy (Added). Gate the four
per-category model caches on category relevance — an edit whose handles are not in
a category leaves that category cache warm; a removal is treated conservatively
(the erased handle can no longer be classified), and the mesh cache also
invalidates on any INSERT edit since it carries the instanced block meshes.
Net: a plain-entity edit no longer rebuilds the hatch/image/mesh/wipeout model
sets. The resident wire clone-all and the GPU re-upload are still whole-set — the
journal is the groundwork for making those per-entity next. Adds 6 journal unit
tests (replay, coalescing, full-fallback, ring overflow, category gate).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 23:50:04 +03:00
|
|
|
|
// Plain top-level add: name the new handle so every derived cache
|
|
|
|
|
|
// patches in just this one entity instead of rebuilding.
|
|
|
|
|
|
self.bump_entities(&[(handle, ChangeKind::Added)]);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
handle
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-14 13:03:04 +03:00
|
|
|
|
/// Rename a block definition: re-key its record, update the Block marker's
|
|
|
|
|
|
/// name, and repoint every INSERT that referenced the old name so all
|
|
|
|
|
|
/// instances keep resolving. Returns false if `old` is missing, `new` is
|
|
|
|
|
|
/// already taken, or the names are equal (case-insensitive). (#261)
|
|
|
|
|
|
pub fn rename_block(&mut self, old: &str, new: &str) -> bool {
|
|
|
|
|
|
if old.eq_ignore_ascii_case(new) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.document.block_records.get(new).is_some() {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
let Some(mut br) = self.document.block_records.remove(old) else {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
|
|
|
|
|
let block_marker = br.block_entity_handle;
|
|
|
|
|
|
br.name = new.to_string();
|
|
|
|
|
|
if self.document.block_records.add(br).is_err() {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Keep the Block marker entity's name in sync (used on DXF/DWG save).
|
|
|
|
|
|
if let Some(EntityType::Block(b)) = self.document.get_entity_mut(block_marker) {
|
|
|
|
|
|
b.name = new.to_string();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Repoint every INSERT reference so all instances keep resolving.
|
|
|
|
|
|
for e in self.document.entities_mut() {
|
|
|
|
|
|
if let EntityType::Insert(ins) = e {
|
|
|
|
|
|
if ins.block_name.eq_ignore_ascii_case(old) {
|
|
|
|
|
|
ins.block_name = new.to_string();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.bump_geometry();
|
|
|
|
|
|
true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-02 15:39:26 +03:00
|
|
|
|
/// Replace the entity stored under `entity`'s handle with `entity`, keeping
|
|
|
|
|
|
/// its identity (handle + owning block), and refresh the derived
|
|
|
|
|
|
/// hatch/image/mesh caches so the edit is visible. Returns `false` when no
|
|
|
|
|
|
/// entity has that handle. This is the in-place counterpart to
|
|
|
|
|
|
/// [`add_entity`](Self::add_entity) used to commit a plugin's edit of an
|
|
|
|
|
|
/// existing entity.
|
feat(web): render hatches on WebGL2 via a texture-backed renderer
WebGL2 has no vertex/fragment storage buffers, so the batched storage-buffer
hatch renderer (hatch_gpu) is disabled on wasm and real hatch fills never
rendered on the web build — the pick highlighted the boundary and the command
then appeared to cancel (issue #204).
Add a wasm-only per-hatch renderer (hatch_web_gpu + hatch_web.wgsl) that packs
the variable-length boundary / family / dash arrays into one RGBA32F data
texture read with textureLoad, instead of the fixed-size uniforms of the
wipeout path. This removes the MAX_FAMILIES (16) / MAX_HATCH_BOUNDARY_VERTS
(1024) / MAX_DASHES (128) caps, so every hatch — solid, gradient, and
arbitrarily complex line patterns — renders on the web. The fill algorithm
(in_polygon, per-family line/dash/dot, gradient, LOD, double-single vertex
transform) is identical to the desktop path; only the array ACCESS changes.
Pattern phase is anchored at world_origin with the boundary stored raw, and
scale / gradient-range clamped, to match the desktop batched renderer exactly.
Native (hatch_gpu + hatch.wgsl) and the wipeout mask renderer are untouched;
the new module and pipeline are gated on target_arch = "wasm32".
Also silence the resulting (and pre-existing) wasm-only dead-code warnings for
native-only paths so both the native and web builds compile clean.
Closes #204.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 00:53:51 +03:00
|
|
|
|
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
2026-07-02 15:39:26 +03:00
|
|
|
|
pub fn update_entity(&mut self, mut entity: EntityType) -> bool {
|
|
|
|
|
|
let handle = entity.common().handle;
|
|
|
|
|
|
let Some(existing) = self.document.get_entity(handle) else {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
|
|
|
|
|
// The caller edited a snapshot copy; keep the live entity in its block.
|
|
|
|
|
|
entity.common_mut().owner_handle = existing.common().owner_handle;
|
|
|
|
|
|
|
|
|
|
|
|
// Replacing (or becoming) a block entity forces a full block-cache
|
|
|
|
|
|
// rebuild; a plain entity only needs its own wires re-tessellated.
|
|
|
|
|
|
let affects_blocks = matches!(
|
|
|
|
|
|
existing,
|
|
|
|
|
|
EntityType::Insert(_) | EntityType::Block(_) | EntityType::BlockEnd(_)
|
|
|
|
|
|
) || matches!(
|
|
|
|
|
|
&entity,
|
|
|
|
|
|
EntityType::Insert(_) | EntityType::Block(_) | EntityType::BlockEnd(_)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-07-02 20:36:16 +03:00
|
|
|
|
// A plugin edit may retarget the entity to a novel layer; register it
|
|
|
|
|
|
// so the edited entity keeps that layer on save instead of collapsing
|
|
|
|
|
|
// to layer 0 in the reopened file (#252).
|
|
|
|
|
|
let new_layer = entity.common().layer.clone();
|
perf(undo): record entity deltas instead of cloning the whole document
Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.
They now record only the touched entities' before/after images:
- Scene self-records through the five mutation primitives when a recording is
open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
the original handle / remove) plus a per-handle cache reseed, then a single
bump_entities so the incremental caches patch instead of a full re-tess —
undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
between the undo/redo stacks without re-capturing. Commands that touch
non-entity state (new layer, group, *D block, viewport) fall back to the
full snapshot via per-command predicates, and a poison flag debug-asserts
any mis-scope.
remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.
Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 11:11:17 +03:00
|
|
|
|
let creates_layer = self.is_recording_undo()
|
|
|
|
|
|
&& !new_layer.trim().is_empty()
|
|
|
|
|
|
&& !self.document.layers.contains(&new_layer);
|
2026-07-02 20:36:16 +03:00
|
|
|
|
self.ensure_layer(&new_layer);
|
|
|
|
|
|
|
2026-07-02 15:39:26 +03:00
|
|
|
|
// Rebuild the derived-model seeds from the new entity (as add_entity).
|
|
|
|
|
|
let hatch_seed = if let EntityType::Hatch(dxf) = &entity {
|
|
|
|
|
|
let color = self.render_style(&entity).0;
|
|
|
|
|
|
Self::hatch_model_from_dxf(dxf, color)
|
|
|
|
|
|
} else if let EntityType::Solid(solid) = &entity {
|
|
|
|
|
|
let color = self.render_style(&entity).0;
|
|
|
|
|
|
Some(Self::solid_hatch_model(solid, color))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
let image_seed = if let EntityType::RasterImage(img) = &entity {
|
|
|
|
|
|
ImageModel::from_raster_image(img)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
let facet_res = self.document.header.facet_resolution;
|
2026-07-17 19:45:42 +03:00
|
|
|
|
let isolines = self.document.header.isolines.max(0) as usize;
|
2026-07-02 15:39:26 +03:00
|
|
|
|
let mesh_seed = if matches!(
|
|
|
|
|
|
&entity,
|
|
|
|
|
|
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_)
|
|
|
|
|
|
) {
|
|
|
|
|
|
let color = self.render_style(&entity).0;
|
2026-07-17 19:45:42 +03:00
|
|
|
|
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines)
|
2026-07-02 15:39:26 +03:00
|
|
|
|
.map(|m| offset_mesh_lod_set(m))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
|
perf(undo): record entity deltas instead of cloning the whole document
Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.
They now record only the touched entities' before/after images:
- Scene self-records through the five mutation primitives when a recording is
open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
the original handle / remove) plus a per-handle cache reseed, then a single
bump_entities so the incremental caches patch instead of a full re-tess —
undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
between the undo/redo stacks without re-capturing. Commands that touch
non-entity state (new layer, group, *D block, viewport) fall back to the
full snapshot via per-command predicates, and a poison flag debug-asserts
any mis-scope.
remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.
Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 11:11:17 +03:00
|
|
|
|
// Delta-undo: capture the entity's pre-edit image (before the slot is
|
|
|
|
|
|
// overwritten) so an undo can restore it, and poison if this replace
|
|
|
|
|
|
// also created a layer or crossed a block boundary.
|
|
|
|
|
|
if self.is_recording_undo() {
|
|
|
|
|
|
let before = self.document.get_entity(handle).cloned();
|
|
|
|
|
|
self.record_undo_before(handle, before);
|
|
|
|
|
|
if creates_layer || affects_blocks {
|
|
|
|
|
|
self.poison_undo_recording();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-02 15:39:26 +03:00
|
|
|
|
// Write the new entity into the live slot.
|
|
|
|
|
|
let Some(slot) = self.document.get_entity_mut(handle) else {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
|
|
|
|
|
*slot = entity;
|
|
|
|
|
|
|
|
|
|
|
|
// Drop stale derived caches for this handle, then reseed for the new
|
|
|
|
|
|
// entity's type (which may differ from the old one).
|
|
|
|
|
|
self.hatches.remove(&handle);
|
|
|
|
|
|
self.images.remove(&handle);
|
|
|
|
|
|
self.meshes.remove(&handle);
|
|
|
|
|
|
self.solid_models.remove(&handle);
|
|
|
|
|
|
if let Some(model) = hatch_seed {
|
|
|
|
|
|
self.hatches.insert(handle, model);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(model) = image_seed {
|
|
|
|
|
|
self.images.insert(handle, model);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(model) = mesh_seed {
|
|
|
|
|
|
self.meshes.insert(handle, model);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if affects_blocks {
|
perf(scene): delta journal so edits touch only changed entities (phase 0-1)
Every geometry mutation previously bumped a monotonic geometry_epoch and each
derived cache compared its stamp and, on a miss, re-walked the whole document —
so drawing or moving one line rebuilt every hatch / image / mesh / wipeout model
even though none of them changed.
Add a per-mutation delta journal: bump_entities(&[(Handle, ChangeKind)]) records
exactly which handles changed (Added / Removed / Modified), folding the memo drop
and the epoch bump into one call a site cannot forget. A bounded ring keeps the
recent deltas; replay_since(last_epoch) coalesces the changes past a consumer or
returns None (full rebuild) when a spanned delta is `full` or the consumer fell
off the ring. bump_geometry / bump_geometry_no_blocks push a `full` delta so the
journal is never missing a step.
Route the common single-entity ops through it: add (Added), update (Modified),
erase (Removed), move/rotate/scale/mirror (Modified), copy (Added). Gate the four
per-category model caches on category relevance — an edit whose handles are not in
a category leaves that category cache warm; a removal is treated conservatively
(the erased handle can no longer be classified), and the mesh cache also
invalidates on any INSERT edit since it carries the instanced block meshes.
Net: a plain-entity edit no longer rebuilds the hatch/image/mesh/wipeout model
sets. The resident wire clone-all and the GPU re-upload are still whole-set — the
journal is the groundwork for making those per-entity next. Adds 6 journal unit
tests (replay, coalescing, full-fallback, ring overflow, category gate).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 23:50:04 +03:00
|
|
|
|
self.mark_entity_dirty(handle);
|
2026-07-02 15:39:26 +03:00
|
|
|
|
self.bump_geometry();
|
|
|
|
|
|
} else {
|
perf(scene): delta journal so edits touch only changed entities (phase 0-1)
Every geometry mutation previously bumped a monotonic geometry_epoch and each
derived cache compared its stamp and, on a miss, re-walked the whole document —
so drawing or moving one line rebuilt every hatch / image / mesh / wipeout model
even though none of them changed.
Add a per-mutation delta journal: bump_entities(&[(Handle, ChangeKind)]) records
exactly which handles changed (Added / Removed / Modified), folding the memo drop
and the epoch bump into one call a site cannot forget. A bounded ring keeps the
recent deltas; replay_since(last_epoch) coalesces the changes past a consumer or
returns None (full rebuild) when a spanned delta is `full` or the consumer fell
off the ring. bump_geometry / bump_geometry_no_blocks push a `full` delta so the
journal is never missing a step.
Route the common single-entity ops through it: add (Added), update (Modified),
erase (Removed), move/rotate/scale/mirror (Modified), copy (Added). Gate the four
per-category model caches on category relevance — an edit whose handles are not in
a category leaves that category cache warm; a removal is treated conservatively
(the erased handle can no longer be classified), and the mesh cache also
invalidates on any INSERT edit since it carries the instanced block meshes.
Net: a plain-entity edit no longer rebuilds the hatch/image/mesh/wipeout model
sets. The resident wire clone-all and the GPU re-upload are still whole-set — the
journal is the groundwork for making those per-entity next. Adds 6 journal unit
tests (replay, coalescing, full-fallback, ring overflow, category gate).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 23:50:04 +03:00
|
|
|
|
// One entity changed in place: report just this handle so every
|
|
|
|
|
|
// derived cache patches it instead of rebuilding (bump_entities also
|
|
|
|
|
|
// drops it from the tessellation memos).
|
|
|
|
|
|
self.bump_entities(&[(handle, ChangeKind::Modified)]);
|
2026-07-02 15:39:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
perf(undo): record entity deltas instead of cloning the whole document
Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.
They now record only the touched entities' before/after images:
- Scene self-records through the five mutation primitives when a recording is
open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
the original handle / remove) plus a per-handle cache reseed, then a single
bump_entities so the incremental caches patch instead of a full re-tess —
undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
between the undo/redo stacks without re-capturing. Commands that touch
non-entity state (new layer, group, *D block, viewport) fall back to the
full snapshot via per-command predicates, and a poison flag debug-asserts
any mis-scope.
remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.
Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 11:11:17 +03:00
|
|
|
|
/// Rebuild the per-entity derived caches (hatch fill / raster image / solid
|
|
|
|
|
|
/// mesh) for a single handle from whatever entity currently lives at it —
|
|
|
|
|
|
/// or drop them all if the handle is now absent. Mirrors the reseed block in
|
|
|
|
|
|
/// [`Scene::update_entity`]; used by delta-undo when it re-applies an
|
|
|
|
|
|
/// entity's before / after image so the fills and meshes follow.
|
|
|
|
|
|
pub(crate) fn reseed_derived_caches(&mut self, handle: Handle) {
|
|
|
|
|
|
let (hatch_seed, image_seed, mesh_seed) = match self.document.get_entity(handle) {
|
|
|
|
|
|
None => (None, None, None),
|
|
|
|
|
|
Some(entity) => {
|
|
|
|
|
|
let hatch_seed = if let EntityType::Hatch(dxf) = entity {
|
|
|
|
|
|
let color = self.render_style(entity).0;
|
|
|
|
|
|
Self::hatch_model_from_dxf(dxf, color)
|
|
|
|
|
|
} else if let EntityType::Solid(solid) = entity {
|
|
|
|
|
|
let color = self.render_style(entity).0;
|
|
|
|
|
|
Some(Self::solid_hatch_model(solid, color))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
let image_seed = if let EntityType::RasterImage(img) = entity {
|
|
|
|
|
|
ImageModel::from_raster_image(img)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
let facet_res = self.document.header.facet_resolution;
|
2026-07-17 19:45:42 +03:00
|
|
|
|
let isolines = self.document.header.isolines.max(0) as usize;
|
perf(undo): record entity deltas instead of cloning the whole document
Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.
They now record only the touched entities' before/after images:
- Scene self-records through the five mutation primitives when a recording is
open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
the original handle / remove) plus a per-handle cache reseed, then a single
bump_entities so the incremental caches patch instead of a full re-tess —
undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
between the undo/redo stacks without re-capturing. Commands that touch
non-entity state (new layer, group, *D block, viewport) fall back to the
full snapshot via per-command predicates, and a poison flag debug-asserts
any mis-scope.
remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.
Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 11:11:17 +03:00
|
|
|
|
let mesh_seed = if matches!(
|
|
|
|
|
|
entity,
|
|
|
|
|
|
EntityType::Solid3D(_)
|
|
|
|
|
|
| EntityType::Region(_)
|
|
|
|
|
|
| EntityType::Body(_)
|
|
|
|
|
|
| EntityType::Surface(_)
|
|
|
|
|
|
) {
|
|
|
|
|
|
let color = self.render_style(entity).0;
|
2026-07-17 19:45:42 +03:00
|
|
|
|
crate::entities::solid3d::tessellate_volume(entity, color, facet_res, isolines)
|
perf(undo): record entity deltas instead of cloning the whole document
Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.
They now record only the touched entities' before/after images:
- Scene self-records through the five mutation primitives when a recording is
open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
the original handle / remove) plus a per-handle cache reseed, then a single
bump_entities so the incremental caches patch instead of a full re-tess —
undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
between the undo/redo stacks without re-capturing. Commands that touch
non-entity state (new layer, group, *D block, viewport) fall back to the
full snapshot via per-command predicates, and a poison flag debug-asserts
any mis-scope.
remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.
Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 11:11:17 +03:00
|
|
|
|
.map(offset_mesh_lod_set)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
(hatch_seed, image_seed, mesh_seed)
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
self.hatches.remove(&handle);
|
|
|
|
|
|
self.images.remove(&handle);
|
|
|
|
|
|
self.meshes.remove(&handle);
|
|
|
|
|
|
self.solid_models.remove(&handle);
|
|
|
|
|
|
if let Some(model) = hatch_seed {
|
|
|
|
|
|
self.hatches.insert(handle, model);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(model) = image_seed {
|
|
|
|
|
|
self.images.insert(handle, model);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(model) = mesh_seed {
|
|
|
|
|
|
self.meshes.insert(handle, model);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
/// Returns the RGBA color for the given layer name.
|
|
|
|
|
|
pub fn layer_color(&self, layer: &str) -> [f32; 4] {
|
|
|
|
|
|
let layer_entry = self.document.layers.get(layer);
|
|
|
|
|
|
let color = layer_entry
|
|
|
|
|
|
.map(|l| &l.color)
|
|
|
|
|
|
.unwrap_or(&acadrust::types::Color::WHITE);
|
|
|
|
|
|
let [r, g, b, _] = crate::scene::convert::tess_util::aci_to_rgba(color);
|
|
|
|
|
|
[r, g, b, 1.0]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn custom_block_names(&self) -> Vec<String> {
|
|
|
|
|
|
self.document
|
|
|
|
|
|
.block_records
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter(|br| !br.is_standard() && !br.is_layout())
|
|
|
|
|
|
.map(|br| br.name.clone())
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn create_block_from_entities(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
handles: &[Handle],
|
|
|
|
|
|
name: &str,
|
2026-07-12 00:34:26 +03:00
|
|
|
|
base: glam::DVec3,
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
) -> Result<Handle, String> {
|
|
|
|
|
|
let name = name.trim();
|
|
|
|
|
|
if name.is_empty() {
|
|
|
|
|
|
return Err("Block name cannot be empty.".into());
|
|
|
|
|
|
}
|
|
|
|
|
|
if name.starts_with('*') {
|
|
|
|
|
|
return Err("Block name cannot start with '*'.".into());
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.document.block_records.get(name).is_some() {
|
|
|
|
|
|
return Err(format!("Block \"{name}\" already exists."));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let source_entities: Vec<_> = handles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter_map(|&h| self.document.get_entity(h).cloned().map(|e| (h, e)))
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
if source_entities.is_empty() {
|
|
|
|
|
|
return Err("No valid entities selected for block creation.".into());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let next = self.document.next_handle();
|
|
|
|
|
|
let br_handle = Handle::new(next);
|
|
|
|
|
|
let block_handle = Handle::new(next + 1);
|
|
|
|
|
|
let end_handle = Handle::new(next + 2);
|
|
|
|
|
|
|
|
|
|
|
|
let mut block_record = acadrust::tables::BlockRecord::new(name);
|
|
|
|
|
|
block_record.handle = br_handle;
|
|
|
|
|
|
block_record.block_entity_handle = block_handle;
|
|
|
|
|
|
block_record.block_end_handle = end_handle;
|
|
|
|
|
|
self.document
|
|
|
|
|
|
.block_records
|
|
|
|
|
|
.add(block_record)
|
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
|
|
let mut block = Block::new(name, acadrust::types::Vector3::ZERO);
|
|
|
|
|
|
block.common.handle = block_handle;
|
|
|
|
|
|
block.common.owner_handle = br_handle;
|
|
|
|
|
|
self.document
|
|
|
|
|
|
.add_entity(EntityType::Block(block))
|
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
|
|
let mut block_end = BlockEnd::new();
|
|
|
|
|
|
block_end.common.handle = end_handle;
|
|
|
|
|
|
block_end.common.owner_handle = br_handle;
|
|
|
|
|
|
self.document
|
|
|
|
|
|
.add_entity(EntityType::BlockEnd(block_end))
|
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
2026-07-12 00:34:26 +03:00
|
|
|
|
let local = EntityTransform::Translate(-base);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
for (old_handle, mut entity) in source_entities {
|
|
|
|
|
|
view::dispatch::apply_transform(&mut entity, &local);
|
|
|
|
|
|
entity = crate::modules::draw::modify::explode::normalize_entity_for_block(entity);
|
|
|
|
|
|
entity.common_mut().handle = Handle::NULL;
|
|
|
|
|
|
entity.common_mut().owner_handle = br_handle;
|
|
|
|
|
|
self.document
|
|
|
|
|
|
.add_entity(entity)
|
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
self.erase_entities(&[old_handle]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let insert = DxfInsert::new(
|
|
|
|
|
|
name,
|
2026-07-12 00:34:26 +03:00
|
|
|
|
acadrust::types::Vector3::new(base.x, base.y, base.z),
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
);
|
|
|
|
|
|
Ok(self.add_entity(EntityType::Insert(insert)))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Define a new block named `name` from `entities` (owned, not yet in the
|
|
|
|
|
|
/// document), with `base` as its insertion origin. Unlike
|
|
|
|
|
|
/// [`create_block_from_entities`] this does NOT place an insert — the
|
|
|
|
|
|
/// caller starts an interactive insert so paste-as-block can prompt for the
|
|
|
|
|
|
/// drop point. The geometry comes from the clipboard rather than live
|
|
|
|
|
|
/// entities, so there is nothing to stage or erase. (#129)
|
|
|
|
|
|
pub fn define_block_from_owned_entities(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
entities: Vec<EntityType>,
|
|
|
|
|
|
name: &str,
|
|
|
|
|
|
base: glam::DVec3,
|
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
|
let name = name.trim();
|
|
|
|
|
|
if name.is_empty() {
|
|
|
|
|
|
return Err("Block name cannot be empty.".into());
|
|
|
|
|
|
}
|
|
|
|
|
|
if name.starts_with('*') {
|
|
|
|
|
|
return Err("Block name cannot start with '*'.".into());
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.document.block_records.get(name).is_some() {
|
|
|
|
|
|
return Err(format!("Block \"{name}\" already exists."));
|
|
|
|
|
|
}
|
|
|
|
|
|
if entities.is_empty() {
|
|
|
|
|
|
return Err("Nothing to make into a block.".into());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let next = self.document.next_handle();
|
|
|
|
|
|
let br_handle = Handle::new(next);
|
|
|
|
|
|
let block_handle = Handle::new(next + 1);
|
|
|
|
|
|
let end_handle = Handle::new(next + 2);
|
|
|
|
|
|
|
|
|
|
|
|
let mut block_record = acadrust::tables::BlockRecord::new(name);
|
|
|
|
|
|
block_record.handle = br_handle;
|
|
|
|
|
|
block_record.block_entity_handle = block_handle;
|
|
|
|
|
|
block_record.block_end_handle = end_handle;
|
|
|
|
|
|
self.document
|
|
|
|
|
|
.block_records
|
|
|
|
|
|
.add(block_record)
|
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
|
|
let mut block = Block::new(name, acadrust::types::Vector3::ZERO);
|
|
|
|
|
|
block.common.handle = block_handle;
|
|
|
|
|
|
block.common.owner_handle = br_handle;
|
|
|
|
|
|
self.document
|
|
|
|
|
|
.add_entity(EntityType::Block(block))
|
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
|
|
let mut block_end = BlockEnd::new();
|
|
|
|
|
|
block_end.common.handle = end_handle;
|
|
|
|
|
|
block_end.common.owner_handle = br_handle;
|
|
|
|
|
|
self.document
|
|
|
|
|
|
.add_entity(EntityType::BlockEnd(block_end))
|
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
|
|
let local = EntityTransform::Translate(-base);
|
|
|
|
|
|
for mut entity in entities {
|
|
|
|
|
|
view::dispatch::apply_transform(&mut entity, &local);
|
|
|
|
|
|
entity = crate::modules::draw::modify::explode::normalize_entity_for_block(entity);
|
|
|
|
|
|
Self::reset_clone_subhandles(&mut self.document, &mut entity);
|
|
|
|
|
|
entity.common_mut().handle = Handle::NULL;
|
|
|
|
|
|
entity.common_mut().owner_handle = br_handle;
|
|
|
|
|
|
self.document
|
|
|
|
|
|
.add_entity(entity)
|
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Block defns don't render on their own, but the geometry cache must
|
|
|
|
|
|
// pick up the new definition so the interactive insert can preview it.
|
|
|
|
|
|
self.bump_geometry();
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Recreate a block definition verbatim — the entities are already in
|
|
|
|
|
|
/// block-local coordinates (unlike `define_block_from_owned_entities`,
|
|
|
|
|
|
/// which folds in a base offset). No-op if the block already exists.
|
|
|
|
|
|
/// Used when pasting an INSERT whose block this drawing lacks. (#135)
|
|
|
|
|
|
pub fn define_block_raw(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
name: &str,
|
|
|
|
|
|
base_point: acadrust::types::Vector3,
|
|
|
|
|
|
entities: Vec<EntityType>,
|
|
|
|
|
|
) {
|
|
|
|
|
|
if name.is_empty() || self.document.block_records.get(name).is_some() {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
let next = self.document.next_handle();
|
|
|
|
|
|
let br_handle = Handle::new(next);
|
|
|
|
|
|
let block_handle = Handle::new(next + 1);
|
|
|
|
|
|
let end_handle = Handle::new(next + 2);
|
|
|
|
|
|
|
|
|
|
|
|
let mut block_record = acadrust::tables::BlockRecord::new(name);
|
|
|
|
|
|
block_record.handle = br_handle;
|
|
|
|
|
|
block_record.block_entity_handle = block_handle;
|
|
|
|
|
|
block_record.block_end_handle = end_handle;
|
|
|
|
|
|
if self.document.block_records.add(block_record).is_err() {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut block = Block::new(name, base_point);
|
|
|
|
|
|
block.common.handle = block_handle;
|
|
|
|
|
|
block.common.owner_handle = br_handle;
|
|
|
|
|
|
let _ = self.document.add_entity(EntityType::Block(block));
|
|
|
|
|
|
|
|
|
|
|
|
let mut block_end = BlockEnd::new();
|
|
|
|
|
|
block_end.common.handle = end_handle;
|
|
|
|
|
|
block_end.common.owner_handle = br_handle;
|
|
|
|
|
|
let _ = self.document.add_entity(EntityType::BlockEnd(block_end));
|
|
|
|
|
|
|
|
|
|
|
|
for mut entity in entities {
|
|
|
|
|
|
Self::reset_clone_subhandles(&mut self.document, &mut entity);
|
|
|
|
|
|
entity.common_mut().handle = Handle::NULL;
|
|
|
|
|
|
entity.common_mut().owner_handle = br_handle;
|
|
|
|
|
|
let _ = self.document.add_entity(entity);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.bump_geometry();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
fix(viewport): apply per-viewport layer freeze to fills, images and meshes
A content viewport that froze layers still drew those layers' hatches,
2-D solids, images, OLE frames and 3-D solids — only its wires honoured
`Viewport.frozen_layers`. A viewport meant to be empty (all content
layers frozen) leaked the whole model instead.
viewport_data_for fed content viewports the full model hatch/wipeout/
image/mesh sets with no per-viewport frozen filter. Thread the viewport's
frozen-layer set through the builders (synced_hatch_models,
exploded_insert_hatch_models, wipeout_models, image_models, mesh_models,
instanced_block_meshes) via Scene::layer_frozen_in — the same
layer-name→handle→membership test the wire path uses — and expose
*_for_viewport accessors that cache the filtered set per order-independent
frozen-set signature (viewports sharing a frozen set share the build; an
empty set returns the shared unfiltered arc, so model tiles and
non-freezing viewports are byte-identical).
Each viewport renders in its own pipeline slot with the hatch upload
gated on geometry_epoch, so a per-viewport filtered set uploads once per
epoch rather than every frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:01:30 +03:00
|
|
|
|
pub(super) fn synced_hatch_models(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
frozen: Option<&rustc_hash::FxHashSet<Handle>>,
|
|
|
|
|
|
) -> Vec<HatchModel> {
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let layout_block = self.current_layout_block_handle();
|
|
|
|
|
|
|
|
|
|
|
|
let layer_hidden = |layer: &str| {
|
|
|
|
|
|
self.document
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.get(layer)
|
|
|
|
|
|
.map(|l| l.flags.off || l.flags.frozen)
|
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// synced_hatch_models is cached on geometry_epoch and the GPU
|
|
|
|
|
|
// upload is keyed on geometry_epoch only (see render.rs — hatch
|
|
|
|
|
|
// buffers are "static"). Don't view-cull here; the per-frame
|
|
|
|
|
|
// skip flag in compute_hatch_lod handles frustum + sub-pixel
|
|
|
|
|
|
// culling at draw time, which keeps the GPU upload set stable
|
|
|
|
|
|
// across pan/zoom.
|
|
|
|
|
|
//
|
|
|
|
|
|
// We INCLUDE hatches from blocks other than `current_layout`'s
|
|
|
|
|
|
// own block (specifically: paper-layout content viewports want
|
|
|
|
|
|
// model-block hatches). Every hatch's `world_origin` is already
|
|
|
|
|
|
// baked into the correct block coord-space at
|
|
|
|
|
|
// `populate_hatches_from_document` time (offset for model, 0 for
|
|
|
|
|
|
// paper), so projecting them through a camera built for the
|
|
|
|
|
|
// wrong block lands them outside the frustum and the per-vp
|
|
|
|
|
|
// scissor / LOD culls them out — no double-rendering.
|
|
|
|
|
|
let depth_map = self.draw_depth_map();
|
|
|
|
|
|
let mut models: Vec<HatchModel> = self
|
|
|
|
|
|
.hatches
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter(|(&handle, _)| {
|
|
|
|
|
|
let Some(entity) = self.document.get_entity(handle) else {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
let c = entity.common();
|
|
|
|
|
|
if c.invisible || layer_hidden(&c.layer) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
fix(viewport): apply per-viewport layer freeze to fills, images and meshes
A content viewport that froze layers still drew those layers' hatches,
2-D solids, images, OLE frames and 3-D solids — only its wires honoured
`Viewport.frozen_layers`. A viewport meant to be empty (all content
layers frozen) leaked the whole model instead.
viewport_data_for fed content viewports the full model hatch/wipeout/
image/mesh sets with no per-viewport frozen filter. Thread the viewport's
frozen-layer set through the builders (synced_hatch_models,
exploded_insert_hatch_models, wipeout_models, image_models, mesh_models,
instanced_block_meshes) via Scene::layer_frozen_in — the same
layer-name→handle→membership test the wire path uses — and expose
*_for_viewport accessors that cache the filtered set per order-independent
frozen-set signature (viewports sharing a frozen set share the build; an
empty set returns the shared unfiltered arc, so model tiles and
non-freezing viewports are byte-identical).
Each viewport renders in its own pipeline slot with the hatch upload
gated on geometry_epoch, so a per-viewport filtered set uploads once per
epoch rather than every frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:01:30 +03:00
|
|
|
|
// Per-viewport layer freeze: a content viewport that freezes
|
|
|
|
|
|
// this layer hides its fills too, not just its wires.
|
|
|
|
|
|
if self.layer_frozen_in(&c.layer, frozen) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
// Reject block-defn-only hatches (entities owned by a
|
|
|
|
|
|
// BLOCK record that's neither model nor a paper layout
|
|
|
|
|
|
// block) — they're tessellated separately via Insert
|
|
|
|
|
|
// explosion and only the laid-out copies should appear.
|
|
|
|
|
|
self.belongs_to_visible_block(handle, c.owner_handle, layout_block)
|
2026-07-14 13:03:04 +03:00
|
|
|
|
|| (self.block_edit_block.is_none()
|
|
|
|
|
|
&& self.belongs_to_visible_block(
|
|
|
|
|
|
handle,
|
|
|
|
|
|
c.owner_handle,
|
|
|
|
|
|
self.model_space_block_handle(),
|
|
|
|
|
|
))
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
})
|
2026-07-08 23:35:27 +03:00
|
|
|
|
.flat_map(|(&handle, model)| {
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let entity = self.document.get_entity(handle);
|
|
|
|
|
|
let mut m = model.clone();
|
2026-07-08 23:35:27 +03:00
|
|
|
|
// Optional solid backdrop drawn behind the pattern/gradient when
|
|
|
|
|
|
// the hatch carries a HATCHBACKGROUNDCOLOR. Same draw_depth +
|
|
|
|
|
|
// emitted first so LessEqual layering keeps it underneath.
|
|
|
|
|
|
let mut backdrop: Option<HatchModel> = None;
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
if let Some(e) = entity {
|
2026-07-18 22:01:22 +03:00
|
|
|
|
// A gradient's colour is its first stop (already baked into
|
|
|
|
|
|
// the cached model); only solid / pattern fills take the
|
|
|
|
|
|
// entity's resolved colour.
|
|
|
|
|
|
if !matches!(m.pattern, model::hatch_model::HatchPattern::Gradient { .. }) {
|
|
|
|
|
|
m.color = self.render_style(e).0;
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
if let EntityType::Hatch(dxf) = e {
|
2026-07-08 23:35:27 +03:00
|
|
|
|
if let Some(bg) = crate::entities::hatch::background_color(dxf) {
|
|
|
|
|
|
let mut b = m.clone();
|
|
|
|
|
|
b.pattern = model::hatch_model::HatchPattern::Solid;
|
2026-07-21 12:22:15 +03:00
|
|
|
|
// ByLayer / ByBlock backgrounds resolve through the
|
|
|
|
|
|
// normal style chain instead of the raw ACI table
|
|
|
|
|
|
// (#415).
|
|
|
|
|
|
b.color = match bg {
|
|
|
|
|
|
acadrust::types::Color::ByLayer => {
|
|
|
|
|
|
crate::scene::view::render::layer_render_style(
|
|
|
|
|
|
&self.document,
|
|
|
|
|
|
&dxf.common.layer,
|
|
|
|
|
|
)
|
|
|
|
|
|
.color
|
|
|
|
|
|
}
|
|
|
|
|
|
acadrust::types::Color::ByBlock => self.render_style(e).0,
|
|
|
|
|
|
other => {
|
|
|
|
|
|
crate::scene::convert::tess_util::aci_to_rgba(&other)
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-07-08 23:35:27 +03:00
|
|
|
|
b.name = "SOLID".into();
|
|
|
|
|
|
backdrop = Some(b);
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
match &mut m.pattern {
|
fix(plot): correct hatch rendering in PDF / plot export
Hatched content — especially fills nested inside a block INSERT, such as a
title-block logo — plotted incorrectly: missing, mis-spaced, or with phantom
bars. Five related fixes make the PDF/plot output match AutoCAD.
1. Export block-internal hatch fills.
The export collected only hatches owned directly by the layout block, so a
hatch nested in a block INSERT was dropped and printed as bare monochrome
outlines. The insert-explosion the viewport already does is extracted into a
shared `exploded_insert_hatch_models()` and called from both the viewport
(`synced_hatch_models`) and the export (`paper_canvas_hatches`), so a plot
draws block-internal hatches identically to the screen.
2. Honour the hatch's own stored pattern-line spacing.
`hatch_model_from_dxf` re-derived pattern spacing from the name-matched
catalog entry x pattern_scale, ignoring the resolved line geometry the DWG
stores on the hatch. When a drawing was authored against a different base
spacing (imperial 0.125 vs the catalog's metric 3.175 for ANSI31), lines
came out up to ~25x too coarse and a dense fill collapsed to a few stray
lines. When the hatch carries its own line geometry it is now used directly
(identity scale/angle); the catalog path remains the fallback.
3. Fill far-from-origin pattern hatches.
`pattern_segments` clamped the ABSOLUTE scan-line index to
+/-MAX_LINES_PER_FAMILY. A fine-spaced hatch far from the pattern origin has
large-magnitude indices at both ends but a small span, so the clamp inverted
the range and emitted nothing — silently dropping the fill. Cap the line
count (span) instead of the absolute index.
4. Skip TEXTBOX boundary paths.
TEXTBOX boundary paths (flag bit 3) are text bounding-boxes AutoCAD derives
for island detection; they are never drawn or filled. Treating one as a fill
boundary painted its rectangle solid — a phantom bar. It is now skipped when
building the fill boundary.
5. Print white/ACI-7 hatch fills black on paper.
Hatch fills arrive adapted to the dark screen background, so a white/ACI-7
fill would vanish white-on-white on the sheet. Mirror the wire pass:
near-white/near-yellow -> black, near-cyan -> dark blue, matching AutoCAD's
colour-7-on-white plotting. Wipeouts keep their paper-white mask.
Adds `tests/block_hatch_export.rs` covering: block-internal hatch reaches the
export set, stored-line spacing is honoured, far-from-origin fills are not
dropped, and TEXTBOX paths are not filled.
2026-07-06 12:24:10 +02:00
|
|
|
|
// Pattern built from the hatch's own stored lines is
|
|
|
|
|
|
// already final (scale 1 / angle 0) — don't re-apply
|
|
|
|
|
|
// pattern_scale/angle. Only the catalog-derived path
|
|
|
|
|
|
// (empty stored lines) needs the override.
|
|
|
|
|
|
model::hatch_model::HatchPattern::Pattern(_)
|
|
|
|
|
|
if dxf.pattern.lines.is_empty() =>
|
|
|
|
|
|
{
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
m.angle_offset = dxf.pattern_angle as f32;
|
|
|
|
|
|
let anno = if self.current_layout == "Model" {
|
|
|
|
|
|
self.annotation_scale
|
|
|
|
|
|
} else {
|
|
|
|
|
|
1.0
|
|
|
|
|
|
};
|
|
|
|
|
|
m.scale = dxf.pattern_scale as f32 * anno;
|
|
|
|
|
|
}
|
|
|
|
|
|
model::hatch_model::HatchPattern::Gradient { angle_deg, .. } => {
|
|
|
|
|
|
*angle_deg = dxf.pattern_angle.to_degrees() as f32;
|
|
|
|
|
|
}
|
fix(plot): correct hatch rendering in PDF / plot export
Hatched content — especially fills nested inside a block INSERT, such as a
title-block logo — plotted incorrectly: missing, mis-spaced, or with phantom
bars. Five related fixes make the PDF/plot output match AutoCAD.
1. Export block-internal hatch fills.
The export collected only hatches owned directly by the layout block, so a
hatch nested in a block INSERT was dropped and printed as bare monochrome
outlines. The insert-explosion the viewport already does is extracted into a
shared `exploded_insert_hatch_models()` and called from both the viewport
(`synced_hatch_models`) and the export (`paper_canvas_hatches`), so a plot
draws block-internal hatches identically to the screen.
2. Honour the hatch's own stored pattern-line spacing.
`hatch_model_from_dxf` re-derived pattern spacing from the name-matched
catalog entry x pattern_scale, ignoring the resolved line geometry the DWG
stores on the hatch. When a drawing was authored against a different base
spacing (imperial 0.125 vs the catalog's metric 3.175 for ANSI31), lines
came out up to ~25x too coarse and a dense fill collapsed to a few stray
lines. When the hatch carries its own line geometry it is now used directly
(identity scale/angle); the catalog path remains the fallback.
3. Fill far-from-origin pattern hatches.
`pattern_segments` clamped the ABSOLUTE scan-line index to
+/-MAX_LINES_PER_FAMILY. A fine-spaced hatch far from the pattern origin has
large-magnitude indices at both ends but a small span, so the clamp inverted
the range and emitted nothing — silently dropping the fill. Cap the line
count (span) instead of the absolute index.
4. Skip TEXTBOX boundary paths.
TEXTBOX boundary paths (flag bit 3) are text bounding-boxes AutoCAD derives
for island detection; they are never drawn or filled. Treating one as a fill
boundary painted its rectangle solid — a phantom bar. It is now skipped when
building the fill boundary.
5. Print white/ACI-7 hatch fills black on paper.
Hatch fills arrive adapted to the dark screen background, so a white/ACI-7
fill would vanish white-on-white on the sheet. Mirror the wire pass:
near-white/near-yellow -> black, near-cyan -> dark blue, matching AutoCAD's
colour-7-on-white plotting. Wipeouts keep their paper-white mask.
Adds `tests/block_hatch_export.rs` covering: block-internal hatch reaches the
export set, stored-line spacing is honoured, far-from-origin fills are not
dropped, and TEXTBOX paths are not filled.
2026-07-06 12:24:10 +02:00
|
|
|
|
model::hatch_model::HatchPattern::Pattern(_)
|
|
|
|
|
|
| model::hatch_model::HatchPattern::Solid => {}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.selected.contains(&handle) {
|
|
|
|
|
|
m.color = [0.15, 0.55, 1.00, m.color[3]];
|
|
|
|
|
|
}
|
2026-07-21 15:53:28 +03:00
|
|
|
|
let d = depth_map.get(&handle.value()).map_or(0.0, |d| d[0]);
|
2026-07-08 23:35:27 +03:00
|
|
|
|
m.draw_depth = d;
|
|
|
|
|
|
if let Some(b) = &mut backdrop {
|
|
|
|
|
|
b.draw_depth = d;
|
|
|
|
|
|
}
|
|
|
|
|
|
backdrop.into_iter().chain(std::iter::once(m))
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
fix(blocks): layer-0 children inherit the block instance layer (#221)
Entities inside a block definition that sit on layer "0" must take on the
properties of the layer the block reference is inserted on; every other
layer is "sticky" and keeps its own identity. OpenCADStudio resolved a
block child's ByLayer color/linetype/lineweight against the child's own
layer in all cases, so layer-0 content stayed layer-0 (white) instead of
inheriting the instance layer.
Resolution now follows the standard rule for block children — explicit
wins, then ByBlock → the insert's own style, then layer-0 + ByLayer →
the insert's *layer* style, else the child's own layer. The insert's
layer style threads through the whole block-expansion chain (and chains
correctly through nested inserts), with the child's own transparency
preserved.
Covered across every block-render path:
- wires (line/arc/circle/polyline/text) via the block cache + the
explode fallback + insert attributes
- hatch / solid fills (also fixes the PDF/print export path)
- 3D solid block children (per-instance mesh recolor)
- Dimension / Table baked-block sub-entities
New shared helpers in scene::view::render: InheritStyle, layer_render_style,
lineweight_to_px; render_style_for_block_sub gained the layer-0 branch.
Adds unit tests covering the layer-0, sticky-layer, ByBlock, explicit and
transparency cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:24:04 +03:00
|
|
|
|
// Background for adapting block-child hatch colours at the leaf (#221).
|
|
|
|
|
|
let hatch_bg: [f32; 4] = if self.current_layout != "Model" {
|
|
|
|
|
|
self.paper_bg_color
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.bg_color
|
|
|
|
|
|
};
|
fix(plot): correct hatch rendering in PDF / plot export
Hatched content — especially fills nested inside a block INSERT, such as a
title-block logo — plotted incorrectly: missing, mis-spaced, or with phantom
bars. Five related fixes make the PDF/plot output match AutoCAD.
1. Export block-internal hatch fills.
The export collected only hatches owned directly by the layout block, so a
hatch nested in a block INSERT was dropped and printed as bare monochrome
outlines. The insert-explosion the viewport already does is extracted into a
shared `exploded_insert_hatch_models()` and called from both the viewport
(`synced_hatch_models`) and the export (`paper_canvas_hatches`), so a plot
draws block-internal hatches identically to the screen.
2. Honour the hatch's own stored pattern-line spacing.
`hatch_model_from_dxf` re-derived pattern spacing from the name-matched
catalog entry x pattern_scale, ignoring the resolved line geometry the DWG
stores on the hatch. When a drawing was authored against a different base
spacing (imperial 0.125 vs the catalog's metric 3.175 for ANSI31), lines
came out up to ~25x too coarse and a dense fill collapsed to a few stray
lines. When the hatch carries its own line geometry it is now used directly
(identity scale/angle); the catalog path remains the fallback.
3. Fill far-from-origin pattern hatches.
`pattern_segments` clamped the ABSOLUTE scan-line index to
+/-MAX_LINES_PER_FAMILY. A fine-spaced hatch far from the pattern origin has
large-magnitude indices at both ends but a small span, so the clamp inverted
the range and emitted nothing — silently dropping the fill. Cap the line
count (span) instead of the absolute index.
4. Skip TEXTBOX boundary paths.
TEXTBOX boundary paths (flag bit 3) are text bounding-boxes AutoCAD derives
for island detection; they are never drawn or filled. Treating one as a fill
boundary painted its rectangle solid — a phantom bar. It is now skipped when
building the fill boundary.
5. Print white/ACI-7 hatch fills black on paper.
Hatch fills arrive adapted to the dark screen background, so a white/ACI-7
fill would vanish white-on-white on the sheet. Mirror the wire pass:
near-white/near-yellow -> black, near-cyan -> dark blue, matching AutoCAD's
colour-7-on-white plotting. Wipeouts keep their paper-white mask.
Adds `tests/block_hatch_export.rs` covering: block-internal hatch reaches the
export set, stored-line spacing is honoured, far-from-origin fills are not
dropped, and TEXTBOX paths are not filled.
2026-07-06 12:24:10 +02:00
|
|
|
|
// Block-internal hatch fills: explode every visible INSERT of this
|
|
|
|
|
|
// layout block and materialize its fills at world position. Shared with
|
|
|
|
|
|
// the export path so a plot draws them identically. (tint_selected =
|
|
|
|
|
|
// true applies the screen selection highlight.)
|
fix(viewport): apply per-viewport layer freeze to fills, images and meshes
A content viewport that froze layers still drew those layers' hatches,
2-D solids, images, OLE frames and 3-D solids — only its wires honoured
`Viewport.frozen_layers`. A viewport meant to be empty (all content
layers frozen) leaked the whole model instead.
viewport_data_for fed content viewports the full model hatch/wipeout/
image/mesh sets with no per-viewport frozen filter. Thread the viewport's
frozen-layer set through the builders (synced_hatch_models,
exploded_insert_hatch_models, wipeout_models, image_models, mesh_models,
instanced_block_meshes) via Scene::layer_frozen_in — the same
layer-name→handle→membership test the wire path uses — and expose
*_for_viewport accessors that cache the filtered set per order-independent
frozen-set signature (viewports sharing a frozen set share the build; an
empty set returns the shared unfiltered arc, so model tiles and
non-freezing viewports are byte-identical).
Each viewport renders in its own pipeline slot with the hatch upload
gated on geometry_epoch, so a per-viewport filtered set uploads once per
epoch rather than every frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:01:30 +03:00
|
|
|
|
models.extend(self.exploded_insert_hatch_models(layout_block, hatch_bg, true, frozen));
|
fix(plot): correct hatch rendering in PDF / plot export
Hatched content — especially fills nested inside a block INSERT, such as a
title-block logo — plotted incorrectly: missing, mis-spaced, or with phantom
bars. Five related fixes make the PDF/plot output match AutoCAD.
1. Export block-internal hatch fills.
The export collected only hatches owned directly by the layout block, so a
hatch nested in a block INSERT was dropped and printed as bare monochrome
outlines. The insert-explosion the viewport already does is extracted into a
shared `exploded_insert_hatch_models()` and called from both the viewport
(`synced_hatch_models`) and the export (`paper_canvas_hatches`), so a plot
draws block-internal hatches identically to the screen.
2. Honour the hatch's own stored pattern-line spacing.
`hatch_model_from_dxf` re-derived pattern spacing from the name-matched
catalog entry x pattern_scale, ignoring the resolved line geometry the DWG
stores on the hatch. When a drawing was authored against a different base
spacing (imperial 0.125 vs the catalog's metric 3.175 for ANSI31), lines
came out up to ~25x too coarse and a dense fill collapsed to a few stray
lines. When the hatch carries its own line geometry it is now used directly
(identity scale/angle); the catalog path remains the fallback.
3. Fill far-from-origin pattern hatches.
`pattern_segments` clamped the ABSOLUTE scan-line index to
+/-MAX_LINES_PER_FAMILY. A fine-spaced hatch far from the pattern origin has
large-magnitude indices at both ends but a small span, so the clamp inverted
the range and emitted nothing — silently dropping the fill. Cap the line
count (span) instead of the absolute index.
4. Skip TEXTBOX boundary paths.
TEXTBOX boundary paths (flag bit 3) are text bounding-boxes AutoCAD derives
for island detection; they are never drawn or filled. Treating one as a fill
boundary painted its rectangle solid — a phantom bar. It is now skipped when
building the fill boundary.
5. Print white/ACI-7 hatch fills black on paper.
Hatch fills arrive adapted to the dark screen background, so a white/ACI-7
fill would vanish white-on-white on the sheet. Mirror the wire pass:
near-white/near-yellow -> black, near-cyan -> dark blue, matching AutoCAD's
colour-7-on-white plotting. Wipeouts keep their paper-white mask.
Adds `tests/block_hatch_export.rs` covering: block-internal hatch reaches the
export set, stored-line spacing is honoured, far-from-origin fills are not
dropped, and TEXTBOX paths are not filled.
2026-07-06 12:24:10 +02:00
|
|
|
|
|
2026-07-20 16:04:03 +03:00
|
|
|
|
// Wide LwPolyline / Polyline2D bands are no longer hatch fills at
|
|
|
|
|
|
// model level: a flat band is drawn by expanding its centre-line wire
|
|
|
|
|
|
// to `world_width` in the shader (depth-tested, linetype-dashed), and a
|
|
|
|
|
|
// thickened one is extruded into a 3-D tube by the tessellator. Only
|
|
|
|
|
|
// block-internal wide polylines still band via the hatch path (see
|
|
|
|
|
|
// `push_block_wide_fills`), where it scales with the insert transform.
|
fix(plot): correct hatch rendering in PDF / plot export
Hatched content — especially fills nested inside a block INSERT, such as a
title-block logo — plotted incorrectly: missing, mis-spaced, or with phantom
bars. Five related fixes make the PDF/plot output match AutoCAD.
1. Export block-internal hatch fills.
The export collected only hatches owned directly by the layout block, so a
hatch nested in a block INSERT was dropped and printed as bare monochrome
outlines. The insert-explosion the viewport already does is extracted into a
shared `exploded_insert_hatch_models()` and called from both the viewport
(`synced_hatch_models`) and the export (`paper_canvas_hatches`), so a plot
draws block-internal hatches identically to the screen.
2. Honour the hatch's own stored pattern-line spacing.
`hatch_model_from_dxf` re-derived pattern spacing from the name-matched
catalog entry x pattern_scale, ignoring the resolved line geometry the DWG
stores on the hatch. When a drawing was authored against a different base
spacing (imperial 0.125 vs the catalog's metric 3.175 for ANSI31), lines
came out up to ~25x too coarse and a dense fill collapsed to a few stray
lines. When the hatch carries its own line geometry it is now used directly
(identity scale/angle); the catalog path remains the fallback.
3. Fill far-from-origin pattern hatches.
`pattern_segments` clamped the ABSOLUTE scan-line index to
+/-MAX_LINES_PER_FAMILY. A fine-spaced hatch far from the pattern origin has
large-magnitude indices at both ends but a small span, so the clamp inverted
the range and emitted nothing — silently dropping the fill. Cap the line
count (span) instead of the absolute index.
4. Skip TEXTBOX boundary paths.
TEXTBOX boundary paths (flag bit 3) are text bounding-boxes AutoCAD derives
for island detection; they are never drawn or filled. Treating one as a fill
boundary painted its rectangle solid — a phantom bar. It is now skipped when
building the fill boundary.
5. Print white/ACI-7 hatch fills black on paper.
Hatch fills arrive adapted to the dark screen background, so a white/ACI-7
fill would vanish white-on-white on the sheet. Mirror the wire pass:
near-white/near-yellow -> black, near-cyan -> dark blue, matching AutoCAD's
colour-7-on-white plotting. Wipeouts keep their paper-white mask.
Adds `tests/block_hatch_export.rs` covering: block-internal hatch reaches the
export set, stored-line spacing is honoured, far-from-origin fills are not
dropped, and TEXTBOX paths are not filled.
2026-07-06 12:24:10 +02:00
|
|
|
|
|
|
|
|
|
|
models
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Materialize the hatch fills that live *inside* the visible INSERTs of
|
|
|
|
|
|
/// `layout_block`, baking each INSERT's transform (and nested-INSERT
|
|
|
|
|
|
/// transforms) so the fills land in world position with resolved
|
|
|
|
|
|
/// ByBlock / layer-0 colours.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Shared by the on-screen hatch set (`synced_hatch_models`) and the
|
|
|
|
|
|
/// paper/export hatch set (`paper_canvas_hatches`) so a plot draws
|
|
|
|
|
|
/// block-internal hatches identically to the viewport. Without this the
|
|
|
|
|
|
/// export path only saw top-level hatches, so any fill nested in a block
|
|
|
|
|
|
/// INSERT printed as bare monochrome outlines.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// `hatch_bg` adapts pure black/white leaf colours to the target
|
|
|
|
|
|
/// background; `tint_selected` re-colours fills of a selected INSERT
|
|
|
|
|
|
/// (screen highlight) and should be `false` for export.
|
|
|
|
|
|
pub(super) fn exploded_insert_hatch_models(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
layout_block: Handle,
|
|
|
|
|
|
hatch_bg: [f32; 4],
|
|
|
|
|
|
tint_selected: bool,
|
fix(viewport): apply per-viewport layer freeze to fills, images and meshes
A content viewport that froze layers still drew those layers' hatches,
2-D solids, images, OLE frames and 3-D solids — only its wires honoured
`Viewport.frozen_layers`. A viewport meant to be empty (all content
layers frozen) leaked the whole model instead.
viewport_data_for fed content viewports the full model hatch/wipeout/
image/mesh sets with no per-viewport frozen filter. Thread the viewport's
frozen-layer set through the builders (synced_hatch_models,
exploded_insert_hatch_models, wipeout_models, image_models, mesh_models,
instanced_block_meshes) via Scene::layer_frozen_in — the same
layer-name→handle→membership test the wire path uses — and expose
*_for_viewport accessors that cache the filtered set per order-independent
frozen-set signature (viewports sharing a frozen set share the build; an
empty set returns the shared unfiltered arc, so model tiles and
non-freezing viewports are byte-identical).
Each viewport renders in its own pipeline slot with the hatch upload
gated on geometry_epoch, so a per-viewport filtered set uploads once per
epoch rather than every frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:01:30 +03:00
|
|
|
|
frozen: Option<&rustc_hash::FxHashSet<Handle>>,
|
fix(plot): correct hatch rendering in PDF / plot export
Hatched content — especially fills nested inside a block INSERT, such as a
title-block logo — plotted incorrectly: missing, mis-spaced, or with phantom
bars. Five related fixes make the PDF/plot output match AutoCAD.
1. Export block-internal hatch fills.
The export collected only hatches owned directly by the layout block, so a
hatch nested in a block INSERT was dropped and printed as bare monochrome
outlines. The insert-explosion the viewport already does is extracted into a
shared `exploded_insert_hatch_models()` and called from both the viewport
(`synced_hatch_models`) and the export (`paper_canvas_hatches`), so a plot
draws block-internal hatches identically to the screen.
2. Honour the hatch's own stored pattern-line spacing.
`hatch_model_from_dxf` re-derived pattern spacing from the name-matched
catalog entry x pattern_scale, ignoring the resolved line geometry the DWG
stores on the hatch. When a drawing was authored against a different base
spacing (imperial 0.125 vs the catalog's metric 3.175 for ANSI31), lines
came out up to ~25x too coarse and a dense fill collapsed to a few stray
lines. When the hatch carries its own line geometry it is now used directly
(identity scale/angle); the catalog path remains the fallback.
3. Fill far-from-origin pattern hatches.
`pattern_segments` clamped the ABSOLUTE scan-line index to
+/-MAX_LINES_PER_FAMILY. A fine-spaced hatch far from the pattern origin has
large-magnitude indices at both ends but a small span, so the clamp inverted
the range and emitted nothing — silently dropping the fill. Cap the line
count (span) instead of the absolute index.
4. Skip TEXTBOX boundary paths.
TEXTBOX boundary paths (flag bit 3) are text bounding-boxes AutoCAD derives
for island detection; they are never drawn or filled. Treating one as a fill
boundary painted its rectangle solid — a phantom bar. It is now skipped when
building the fill boundary.
5. Print white/ACI-7 hatch fills black on paper.
Hatch fills arrive adapted to the dark screen background, so a white/ACI-7
fill would vanish white-on-white on the sheet. Mirror the wire pass:
near-white/near-yellow -> black, near-cyan -> dark blue, matching AutoCAD's
colour-7-on-white plotting. Wipeouts keep their paper-white mask.
Adds `tests/block_hatch_export.rs` covering: block-internal hatch reaches the
export set, stored-line spacing is honoured, far-from-origin fills are not
dropped, and TEXTBOX paths are not filled.
2026-07-06 12:24:10 +02:00
|
|
|
|
) -> Vec<HatchModel> {
|
|
|
|
|
|
let layer_hidden = |layer: &str| {
|
|
|
|
|
|
self.document
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.get(layer)
|
|
|
|
|
|
.map(|l| l.flags.off || l.flags.frozen)
|
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
|
};
|
|
|
|
|
|
let mut models: Vec<HatchModel> = Vec::new();
|
2026-07-21 15:53:28 +03:00
|
|
|
|
// Draw-order composition (#408): every fill inherits its INSERT's
|
|
|
|
|
|
// draw-depth slot, offset by the child's rank INSIDE its block
|
|
|
|
|
|
// (draw_depth_map ranks per owning block), scaled to stay within the
|
|
|
|
|
|
// insert's half-gap in the host ordering. Without this every block /
|
|
|
|
|
|
// xref fill landed at depth 0 and the stack's LIFO pop reversed the
|
|
|
|
|
|
// block's internal stacking.
|
|
|
|
|
|
let depth_map = self.draw_depth_map();
|
|
|
|
|
|
let block_count = |name: &str| -> usize {
|
|
|
|
|
|
self.document
|
|
|
|
|
|
.block_records
|
|
|
|
|
|
.get(name)
|
|
|
|
|
|
.map(|br| br.entity_handles.len())
|
|
|
|
|
|
.unwrap_or(1)
|
|
|
|
|
|
.max(1)
|
|
|
|
|
|
};
|
2026-07-05 02:03:16 +03:00
|
|
|
|
// Exploding an INSERT materializes (clones) every child of its block —
|
|
|
|
|
|
// including 3D solids that each carry megabytes of SAB geometry — just
|
|
|
|
|
|
// to scan the result for hatch fills. On xref-heavy drawings whose
|
|
|
|
|
|
// blocks hold thousands of solids and NO hatches, that cloned ~all block
|
|
|
|
|
|
// content on every open for nothing (~20 s on a 5-xref BIM aggregate).
|
2026-07-05 02:10:56 +03:00
|
|
|
|
// Skip the explode for blocks with no hatch (transitively) via the same
|
|
|
|
|
|
// memoised `block_has_hatch` scan the pick path uses. Blocks that DO
|
|
|
|
|
|
// contain a hatch still explode exactly as before, so output is unchanged.
|
|
|
|
|
|
let mut hatch_block_memo: std::collections::HashMap<String, bool> =
|
|
|
|
|
|
std::collections::HashMap::new();
|
2026-07-08 15:56:56 +03:00
|
|
|
|
// Same gate for wide LwPolyline / Polyline2D fills — a block with a wide
|
|
|
|
|
|
// polyline (but no hatch) must still explode so its width band renders
|
|
|
|
|
|
// at world position, like a block-internal hatch. (#222)
|
|
|
|
|
|
let mut wide_block_memo: std::collections::HashMap<String, bool> =
|
|
|
|
|
|
std::collections::HashMap::new();
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
for entity in self.document.entities() {
|
|
|
|
|
|
let EntityType::Insert(ins) = entity else {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
};
|
|
|
|
|
|
if ins.common.invisible || layer_hidden(&ins.common.layer) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
fix(viewport): apply per-viewport layer freeze to fills, images and meshes
A content viewport that froze layers still drew those layers' hatches,
2-D solids, images, OLE frames and 3-D solids — only its wires honoured
`Viewport.frozen_layers`. A viewport meant to be empty (all content
layers frozen) leaked the whole model instead.
viewport_data_for fed content viewports the full model hatch/wipeout/
image/mesh sets with no per-viewport frozen filter. Thread the viewport's
frozen-layer set through the builders (synced_hatch_models,
exploded_insert_hatch_models, wipeout_models, image_models, mesh_models,
instanced_block_meshes) via Scene::layer_frozen_in — the same
layer-name→handle→membership test the wire path uses — and expose
*_for_viewport accessors that cache the filtered set per order-independent
frozen-set signature (viewports sharing a frozen set share the build; an
empty set returns the shared unfiltered arc, so model tiles and
non-freezing viewports are byte-identical).
Each viewport renders in its own pipeline slot with the hatch upload
gated on geometry_epoch, so a per-viewport filtered set uploads once per
epoch rather than every frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:01:30 +03:00
|
|
|
|
// Per-viewport freeze: an INSERT on a layer frozen in this content
|
|
|
|
|
|
// viewport hides the whole instance, its internal fills included.
|
|
|
|
|
|
if self.layer_frozen_in(&ins.common.layer, frozen) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
if !self.belongs_to_visible_block(
|
|
|
|
|
|
ins.common.handle,
|
|
|
|
|
|
ins.common.owner_handle,
|
|
|
|
|
|
layout_block,
|
|
|
|
|
|
) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-07-08 15:56:56 +03:00
|
|
|
|
if !self.block_has_hatch(&ins.block_name, &mut hatch_block_memo)
|
|
|
|
|
|
&& !self.block_has_wide_poly(&ins.block_name, &mut wide_block_memo)
|
|
|
|
|
|
{
|
2026-07-05 02:10:56 +03:00
|
|
|
|
continue;
|
2026-07-05 02:03:16 +03:00
|
|
|
|
}
|
fix(plot): correct hatch rendering in PDF / plot export
Hatched content — especially fills nested inside a block INSERT, such as a
title-block logo — plotted incorrectly: missing, mis-spaced, or with phantom
bars. Five related fixes make the PDF/plot output match AutoCAD.
1. Export block-internal hatch fills.
The export collected only hatches owned directly by the layout block, so a
hatch nested in a block INSERT was dropped and printed as bare monochrome
outlines. The insert-explosion the viewport already does is extracted into a
shared `exploded_insert_hatch_models()` and called from both the viewport
(`synced_hatch_models`) and the export (`paper_canvas_hatches`), so a plot
draws block-internal hatches identically to the screen.
2. Honour the hatch's own stored pattern-line spacing.
`hatch_model_from_dxf` re-derived pattern spacing from the name-matched
catalog entry x pattern_scale, ignoring the resolved line geometry the DWG
stores on the hatch. When a drawing was authored against a different base
spacing (imperial 0.125 vs the catalog's metric 3.175 for ANSI31), lines
came out up to ~25x too coarse and a dense fill collapsed to a few stray
lines. When the hatch carries its own line geometry it is now used directly
(identity scale/angle); the catalog path remains the fallback.
3. Fill far-from-origin pattern hatches.
`pattern_segments` clamped the ABSOLUTE scan-line index to
+/-MAX_LINES_PER_FAMILY. A fine-spaced hatch far from the pattern origin has
large-magnitude indices at both ends but a small span, so the clamp inverted
the range and emitted nothing — silently dropping the fill. Cap the line
count (span) instead of the absolute index.
4. Skip TEXTBOX boundary paths.
TEXTBOX boundary paths (flag bit 3) are text bounding-boxes AutoCAD derives
for island detection; they are never drawn or filled. Treating one as a fill
boundary painted its rectangle solid — a phantom bar. It is now skipped when
building the fill boundary.
5. Print white/ACI-7 hatch fills black on paper.
Hatch fills arrive adapted to the dark screen background, so a white/ACI-7
fill would vanish white-on-white on the sheet. Mirror the wire pass:
near-white/near-yellow -> black, near-cyan -> dark blue, matching AutoCAD's
colour-7-on-white plotting. Wipeouts keep their paper-white mask.
Adds `tests/block_hatch_export.rs` covering: block-internal hatch reaches the
export set, stored-line spacing is honoured, far-from-origin fills are not
dropped, and TEXTBOX paths are not filled.
2026-07-06 12:24:10 +02:00
|
|
|
|
let selected = tint_selected && self.selected.contains(&ins.common.handle);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
// XCLIP: clip this insert's exploded hatch fills to the boundary,
|
|
|
|
|
|
// matching how the line geometry is clipped in expand_insert.
|
|
|
|
|
|
let clip_poly = pick::xclip::insert_spatial_filter(&self.document, ins)
|
|
|
|
|
|
.map(|sf| pick::xclip::world_clip_polygon(sf, ins));
|
|
|
|
|
|
// Walk the full block tree: `explode_from_document` only descends
|
|
|
|
|
|
// one level, so nested INSERTs are re-exploded here. Each level
|
|
|
|
|
|
// bakes its transform into the children it returns, so nested
|
|
|
|
|
|
// hatches land in the correct world position. A depth guard keeps
|
|
|
|
|
|
// a malformed cyclic block reference from looping forever.
|
|
|
|
|
|
let normalize = crate::modules::draw::modify::explode::normalize_insert_entity;
|
fix(blocks): layer-0 children inherit the block instance layer (#221)
Entities inside a block definition that sit on layer "0" must take on the
properties of the layer the block reference is inserted on; every other
layer is "sticky" and keeps its own identity. OpenCADStudio resolved a
block child's ByLayer color/linetype/lineweight against the child's own
layer in all cases, so layer-0 content stayed layer-0 (white) instead of
inheriting the instance layer.
Resolution now follows the standard rule for block children — explicit
wins, then ByBlock → the insert's own style, then layer-0 + ByLayer →
the insert's *layer* style, else the child's own layer. The insert's
layer style threads through the whole block-expansion chain (and chains
correctly through nested inserts), with the child's own transparency
preserved.
Covered across every block-render path:
- wires (line/arc/circle/polyline/text) via the block cache + the
explode fallback + insert attributes
- hatch / solid fills (also fixes the PDF/print export path)
- 3D solid block children (per-instance mesh recolor)
- Dimension / Table baked-block sub-entities
New shared helpers in scene::view::render: InheritStyle, layer_render_style,
lineweight_to_px; render_style_for_block_sub gained the layer-0 branch.
Adds unit tests covering the layer-0, sticky-layer, ByBlock, explicit and
transparency cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:24:04 +03:00
|
|
|
|
// Block-child colour inheritance sources (#221), kept RAW and
|
|
|
|
|
|
// adapted at the leaf. `ins_color` feeds ByBlock; `l0` (the
|
|
|
|
|
|
// INSERT's *layer* style) feeds the layer-0 rule. Both chain
|
|
|
|
|
|
// through nested inserts, mirroring expand_insert.
|
|
|
|
|
|
let ins_color =
|
|
|
|
|
|
crate::scene::view::render::render_style_for(&self.document, entity).0;
|
|
|
|
|
|
let l0 = crate::scene::view::render::layer_render_style(
|
|
|
|
|
|
&self.document,
|
|
|
|
|
|
&ins.common.layer,
|
|
|
|
|
|
);
|
2026-07-21 15:53:28 +03:00
|
|
|
|
let [base_depth, half_gap] = depth_map
|
|
|
|
|
|
.get(&ins.common.handle.value())
|
|
|
|
|
|
.copied()
|
|
|
|
|
|
.unwrap_or([0.0, 0.0]);
|
fix(blocks): layer-0 children inherit the block instance layer (#221)
Entities inside a block definition that sit on layer "0" must take on the
properties of the layer the block reference is inserted on; every other
layer is "sticky" and keeps its own identity. OpenCADStudio resolved a
block child's ByLayer color/linetype/lineweight against the child's own
layer in all cases, so layer-0 content stayed layer-0 (white) instead of
inheriting the instance layer.
Resolution now follows the standard rule for block children — explicit
wins, then ByBlock → the insert's own style, then layer-0 + ByLayer →
the insert's *layer* style, else the child's own layer. The insert's
layer style threads through the whole block-expansion chain (and chains
correctly through nested inserts), with the child's own transparency
preserved.
Covered across every block-render path:
- wires (line/arc/circle/polyline/text) via the block cache + the
explode fallback + insert attributes
- hatch / solid fills (also fixes the PDF/print export path)
- 3D solid block children (per-instance mesh recolor)
- Dimension / Table baked-block sub-entities
New shared helpers in scene::view::render: InheritStyle, layer_render_style,
lineweight_to_px; render_style_for_block_sub gained the layer-0 branch.
Adds unit tests covering the layer-0, sticky-layer, ByBlock, explicit and
transparency cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:24:04 +03:00
|
|
|
|
let mut stack: Vec<(
|
|
|
|
|
|
EntityType,
|
|
|
|
|
|
usize,
|
|
|
|
|
|
[f32; 4],
|
|
|
|
|
|
crate::scene::view::render::InheritStyle,
|
2026-07-21 15:53:28 +03:00
|
|
|
|
(f32, f32),
|
fix(blocks): layer-0 children inherit the block instance layer (#221)
Entities inside a block definition that sit on layer "0" must take on the
properties of the layer the block reference is inserted on; every other
layer is "sticky" and keeps its own identity. OpenCADStudio resolved a
block child's ByLayer color/linetype/lineweight against the child's own
layer in all cases, so layer-0 content stayed layer-0 (white) instead of
inheriting the instance layer.
Resolution now follows the standard rule for block children — explicit
wins, then ByBlock → the insert's own style, then layer-0 + ByLayer →
the insert's *layer* style, else the child's own layer. The insert's
layer style threads through the whole block-expansion chain (and chains
correctly through nested inserts), with the child's own transparency
preserved.
Covered across every block-render path:
- wires (line/arc/circle/polyline/text) via the block cache + the
explode fallback + insert attributes
- hatch / solid fills (also fixes the PDF/print export path)
- 3D solid block children (per-instance mesh recolor)
- Dimension / Table baked-block sub-entities
New shared helpers in scene::view::render: InheritStyle, layer_render_style,
lineweight_to_px; render_style_for_block_sub gained the layer-0 branch.
Adds unit tests covering the layer-0, sticky-layer, ByBlock, explicit and
transparency cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:24:04 +03:00
|
|
|
|
)> = ins
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
.explode_from_document(&self.document)
|
|
|
|
|
|
.into_iter()
|
2026-07-21 15:53:28 +03:00
|
|
|
|
.map(|e| (normalize(e), 0usize, ins_color, l0, (base_depth, half_gap)))
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
.collect();
|
2026-07-21 15:53:28 +03:00
|
|
|
|
while let Some((sub, depth, sub_ins_color, sub_l0, (d_base, d_half))) = stack.pop() {
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
match sub {
|
|
|
|
|
|
EntityType::Insert(nins) => {
|
|
|
|
|
|
if depth >= 32 {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
fix(blocks): layer-0 children inherit the block instance layer (#221)
Entities inside a block definition that sit on layer "0" must take on the
properties of the layer the block reference is inserted on; every other
layer is "sticky" and keeps its own identity. OpenCADStudio resolved a
block child's ByLayer color/linetype/lineweight against the child's own
layer in all cases, so layer-0 content stayed layer-0 (white) instead of
inheriting the instance layer.
Resolution now follows the standard rule for block children — explicit
wins, then ByBlock → the insert's own style, then layer-0 + ByLayer →
the insert's *layer* style, else the child's own layer. The insert's
layer style threads through the whole block-expansion chain (and chains
correctly through nested inserts), with the child's own transparency
preserved.
Covered across every block-render path:
- wires (line/arc/circle/polyline/text) via the block cache + the
explode fallback + insert attributes
- hatch / solid fills (also fixes the PDF/print export path)
- 3D solid block children (per-instance mesh recolor)
- Dimension / Table baked-block sub-entities
New shared helpers in scene::view::render: InheritStyle, layer_render_style,
lineweight_to_px; render_style_for_block_sub gained the layer-0 branch.
Adds unit tests covering the layer-0, sticky-layer, ByBlock, explicit and
transparency cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:24:04 +03:00
|
|
|
|
use acadrust::types::Color;
|
|
|
|
|
|
// Chain the inheritance sources into the nested insert,
|
|
|
|
|
|
// mirroring expand_insert's nested resolution: ByBlock →
|
|
|
|
|
|
// parent source; layer-0 + ByLayer → parent layer-0
|
|
|
|
|
|
// target; else the nested insert's own resolved style.
|
|
|
|
|
|
let child_ins_color = if nins.common.color == Color::ByBlock {
|
|
|
|
|
|
sub_ins_color
|
|
|
|
|
|
} else if nins.common.layer == "0"
|
|
|
|
|
|
&& nins.common.color == Color::ByLayer
|
|
|
|
|
|
{
|
|
|
|
|
|
sub_l0.color
|
|
|
|
|
|
} else {
|
|
|
|
|
|
crate::scene::view::render::render_style_for(
|
|
|
|
|
|
&self.document,
|
|
|
|
|
|
&EntityType::Insert(nins.clone()),
|
|
|
|
|
|
)
|
|
|
|
|
|
.0
|
|
|
|
|
|
};
|
|
|
|
|
|
let child_l0 = if nins.common.layer == "0" {
|
|
|
|
|
|
sub_l0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
crate::scene::view::render::layer_render_style(
|
|
|
|
|
|
&self.document,
|
|
|
|
|
|
&nins.common.layer,
|
|
|
|
|
|
)
|
|
|
|
|
|
};
|
2026-07-21 15:53:28 +03:00
|
|
|
|
// Nested insert: its slot = parent slot + its rank in
|
|
|
|
|
|
// the parent block, subdivided by its own block size.
|
|
|
|
|
|
let nd_base = d_base
|
|
|
|
|
|
+ depth_map
|
|
|
|
|
|
.get(&nins.common.handle.value())
|
|
|
|
|
|
.map_or(0.0, |d| d[0])
|
|
|
|
|
|
* d_half;
|
|
|
|
|
|
let nd_half = d_half / (block_count(&nins.block_name) as f32 + 1.0);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
for e in nins.explode_from_document(&self.document) {
|
2026-07-21 15:53:28 +03:00
|
|
|
|
stack.push((
|
|
|
|
|
|
normalize(e),
|
|
|
|
|
|
depth + 1,
|
|
|
|
|
|
child_ins_color,
|
|
|
|
|
|
child_l0,
|
|
|
|
|
|
(nd_base, nd_half),
|
|
|
|
|
|
));
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
EntityType::Hatch(dxf) => {
|
|
|
|
|
|
if dxf.common.invisible || layer_hidden(&dxf.common.layer) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
fix(blocks): layer-0 children inherit the block instance layer (#221)
Entities inside a block definition that sit on layer "0" must take on the
properties of the layer the block reference is inserted on; every other
layer is "sticky" and keeps its own identity. OpenCADStudio resolved a
block child's ByLayer color/linetype/lineweight against the child's own
layer in all cases, so layer-0 content stayed layer-0 (white) instead of
inheriting the instance layer.
Resolution now follows the standard rule for block children — explicit
wins, then ByBlock → the insert's own style, then layer-0 + ByLayer →
the insert's *layer* style, else the child's own layer. The insert's
layer style threads through the whole block-expansion chain (and chains
correctly through nested inserts), with the child's own transparency
preserved.
Covered across every block-render path:
- wires (line/arc/circle/polyline/text) via the block cache + the
explode fallback + insert attributes
- hatch / solid fills (also fixes the PDF/print export path)
- 3D solid block children (per-instance mesh recolor)
- Dimension / Table baked-block sub-entities
New shared helpers in scene::view::render: InheritStyle, layer_render_style,
lineweight_to_px; render_style_for_block_sub gained the layer-0 branch.
Adds unit tests covering the layer-0, sticky-layer, ByBlock, explicit and
transparency cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:24:04 +03:00
|
|
|
|
// Resolve ByBlock / layer-0 inheritance for this block
|
|
|
|
|
|
// child, then adapt to the background (#221). Pattern /
|
|
|
|
|
|
// lineweight args are unused by a hatch's colour.
|
|
|
|
|
|
let color = crate::scene::view::render::render_style_for_block_sub(
|
|
|
|
|
|
&self.document,
|
|
|
|
|
|
&EntityType::Hatch(dxf.clone()),
|
|
|
|
|
|
sub_ins_color,
|
|
|
|
|
|
0.0,
|
|
|
|
|
|
[0.0; 8],
|
|
|
|
|
|
0.0,
|
|
|
|
|
|
sub_l0,
|
|
|
|
|
|
)
|
|
|
|
|
|
.0;
|
|
|
|
|
|
let color =
|
|
|
|
|
|
crate::scene::view::render::adapt_to_bg(color, hatch_bg);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
if let Some(mut model) =
|
|
|
|
|
|
Self::hatch_model_from_dxf(&dxf, color)
|
|
|
|
|
|
{
|
2026-07-21 15:53:28 +03:00
|
|
|
|
// In-block rank → within the insert's depth slot.
|
|
|
|
|
|
model.draw_depth = d_base
|
|
|
|
|
|
+ depth_map
|
|
|
|
|
|
.get(&dxf.common.handle.value())
|
|
|
|
|
|
.map_or(0.0, |d| d[0])
|
|
|
|
|
|
* d_half;
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
if let Some(poly) = &clip_poly {
|
|
|
|
|
|
let clipped = pick::xclip::clip_hatch_boundary(
|
|
|
|
|
|
&model.boundary,
|
|
|
|
|
|
model.world_origin,
|
|
|
|
|
|
poly,
|
|
|
|
|
|
);
|
|
|
|
|
|
if clipped.is_empty() {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
model.boundary = std::sync::Arc::new(clipped);
|
|
|
|
|
|
}
|
|
|
|
|
|
if selected {
|
|
|
|
|
|
model.color = [0.15, 0.55, 1.00, model.color[3]];
|
|
|
|
|
|
}
|
|
|
|
|
|
models.push(model);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-20 16:04:03 +03:00
|
|
|
|
// Wide LwPolyline / Polyline2D bands inside a block are drawn
|
|
|
|
|
|
// by the shader wire path — block_cache carries each centre-
|
|
|
|
|
|
// line's `world_width` and scales it by the insert transform
|
|
|
|
|
|
// (`emit_wire`) — not as hatch fills here. Same depth-tested,
|
|
|
|
|
|
// linetype-dashed band as at model level. (#222)
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
models
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Wipeout fill models — rendered in a separate pass AFTER wires so that
|
|
|
|
|
|
/// wipeouts correctly mask everything below them in the draw order.
|
fix(viewport): apply per-viewport layer freeze to fills, images and meshes
A content viewport that froze layers still drew those layers' hatches,
2-D solids, images, OLE frames and 3-D solids — only its wires honoured
`Viewport.frozen_layers`. A viewport meant to be empty (all content
layers frozen) leaked the whole model instead.
viewport_data_for fed content viewports the full model hatch/wipeout/
image/mesh sets with no per-viewport frozen filter. Thread the viewport's
frozen-layer set through the builders (synced_hatch_models,
exploded_insert_hatch_models, wipeout_models, image_models, mesh_models,
instanced_block_meshes) via Scene::layer_frozen_in — the same
layer-name→handle→membership test the wire path uses — and expose
*_for_viewport accessors that cache the filtered set per order-independent
frozen-set signature (viewports sharing a frozen set share the build; an
empty set returns the shared unfiltered arc, so model tiles and
non-freezing viewports are byte-identical).
Each viewport renders in its own pipeline slot with the hatch upload
gated on geometry_epoch, so a per-viewport filtered set uploads once per
epoch rather than every frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:01:30 +03:00
|
|
|
|
pub(crate) fn wipeout_models(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
frozen: Option<&rustc_hash::FxHashSet<Handle>>,
|
|
|
|
|
|
) -> Vec<HatchModel> {
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let is_paper = self.current_layout != "Model";
|
|
|
|
|
|
let bg_color: [f32; 4] = if is_paper {
|
|
|
|
|
|
self.paper_bg_color
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.bg_color
|
|
|
|
|
|
};
|
|
|
|
|
|
// No per-frame view-cull here: GPU wipeout buffer upload is
|
|
|
|
|
|
// gated on geometry_epoch only (see render.rs), so any cull at
|
|
|
|
|
|
// build time would freeze the visible subset at the geometry
|
|
|
|
|
|
// epoch boundary and never re-evaluate as the user pans. The
|
|
|
|
|
|
// pipeline's `wipeout_skip_flags` (compute_wipeout_lod) does
|
|
|
|
|
|
// the per-frame skip at draw time instead.
|
|
|
|
|
|
let mut models = Vec::new();
|
|
|
|
|
|
for entity in self.document.entities() {
|
|
|
|
|
|
let EntityType::Wipeout(wo) = entity else {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
};
|
|
|
|
|
|
if entity.common().invisible {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if self
|
|
|
|
|
|
.document
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.get(&entity.common().layer)
|
|
|
|
|
|
.map(|l| l.flags.off || l.flags.frozen)
|
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
|
{
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
fix(viewport): apply per-viewport layer freeze to fills, images and meshes
A content viewport that froze layers still drew those layers' hatches,
2-D solids, images, OLE frames and 3-D solids — only its wires honoured
`Viewport.frozen_layers`. A viewport meant to be empty (all content
layers frozen) leaked the whole model instead.
viewport_data_for fed content viewports the full model hatch/wipeout/
image/mesh sets with no per-viewport frozen filter. Thread the viewport's
frozen-layer set through the builders (synced_hatch_models,
exploded_insert_hatch_models, wipeout_models, image_models, mesh_models,
instanced_block_meshes) via Scene::layer_frozen_in — the same
layer-name→handle→membership test the wire path uses — and expose
*_for_viewport accessors that cache the filtered set per order-independent
frozen-set signature (viewports sharing a frozen set share the build; an
empty set returns the shared unfiltered arc, so model tiles and
non-freezing viewports are byte-identical).
Each viewport renders in its own pipeline slot with the hatch upload
gated on geometry_epoch, so a per-viewport filtered set uploads once per
epoch rather than every frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:01:30 +03:00
|
|
|
|
// Per-viewport freeze: a wipeout on a layer frozen in this content
|
|
|
|
|
|
// viewport is hidden along with the rest of that layer.
|
|
|
|
|
|
if self.layer_frozen_in(&entity.common().layer, frozen) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
// Per-entity world_offset selection so paper-layout content
|
|
|
|
|
|
// viewports still see model-block wipeouts at the right local
|
|
|
|
|
|
// coordinates (same rationale as hatches).
|
|
|
|
|
|
let (fill_origin, boundary) = Self::wipeout_boundary_2d(wo);
|
|
|
|
|
|
if boundary.len() >= 3 {
|
|
|
|
|
|
let mut fill_color = bg_color;
|
|
|
|
|
|
if self.selected.contains(&wo.common.handle) {
|
|
|
|
|
|
fill_color = [0.15, 0.55, 1.00, 0.35];
|
|
|
|
|
|
}
|
|
|
|
|
|
models.push(HatchModel {
|
|
|
|
|
|
boundary: Arc::new(boundary),
|
2026-07-11 21:55:10 +03:00
|
|
|
|
boundary_wcs: None,
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
pattern: model::hatch_model::HatchPattern::Solid,
|
|
|
|
|
|
name: "WIPEOUT_FILL".into(),
|
|
|
|
|
|
color: fill_color,
|
|
|
|
|
|
angle_offset: 0.0,
|
|
|
|
|
|
scale: 1.0,
|
|
|
|
|
|
world_origin: fill_origin,
|
|
|
|
|
|
draw_depth: 0.0,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
models
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Compute the 2D (XY) boundary polygon for a Wipeout entity.
|
|
|
|
|
|
/// Wipeout fill boundary as small f32 offsets from the returned `world_origin`
|
|
|
|
|
|
/// (the insertion point, kept in f64). Building it in absolute WCS f32
|
|
|
|
|
|
/// collapsed the boundary into squares at UTM-scale coordinates; the
|
|
|
|
|
|
/// relative-to-eye hatch fill reconstructs `world_origin + offset` precisely.
|
|
|
|
|
|
pub(super) fn wipeout_boundary_2d(
|
|
|
|
|
|
wo: &acadrust::entities::Wipeout,
|
|
|
|
|
|
) -> ([f64; 2], Vec<[f32; 2]>) {
|
|
|
|
|
|
use acadrust::entities::WipeoutClipType;
|
|
|
|
|
|
|
|
|
|
|
|
let origin = [wo.insertion_point.x, wo.insertion_point.y];
|
|
|
|
|
|
|
|
|
|
|
|
let is_polygon = wo.clipping_enabled
|
|
|
|
|
|
&& wo.clip_boundary_vertices.len() >= 3
|
|
|
|
|
|
&& matches!(wo.clip_type, WipeoutClipType::Polygonal);
|
|
|
|
|
|
|
|
|
|
|
|
if is_polygon {
|
|
|
|
|
|
// DXF clip vertices live in image-pixel space, centred on the
|
|
|
|
|
|
// image (range −size/2 … +size/2). Image-bottom-left → insertion,
|
|
|
|
|
|
// image-y-axis points DOWN (per the DXF "v_vector points down the
|
|
|
|
|
|
// image" convention), so map:
|
|
|
|
|
|
// x_off = (clip.x + size.x/2) × u_vec
|
|
|
|
|
|
// y_off = (size.y/2 − clip.y) × v_vec ← y flipped
|
|
|
|
|
|
// Offsets are relative to `origin` (the insertion point).
|
|
|
|
|
|
let cx_of = |v: &acadrust::types::Vector2| v.x + wo.size.x * 0.5;
|
|
|
|
|
|
let cy_of = |v: &acadrust::types::Vector2| wo.size.y * 0.5 - v.y;
|
|
|
|
|
|
let mut poly: Vec<[f32; 2]> = wo
|
|
|
|
|
|
.clip_boundary_vertices
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|v| {
|
|
|
|
|
|
let cx = cx_of(v);
|
|
|
|
|
|
let cy = cy_of(v);
|
|
|
|
|
|
let wx = (wo.u_vector.x * cx + wo.v_vector.x * cy) as f32;
|
|
|
|
|
|
let wy = (wo.u_vector.y * cx + wo.v_vector.y * cy) as f32;
|
|
|
|
|
|
[wx, wy]
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
// Close the loop: the GPU `in_polygon` ray-cast walks
|
|
|
|
|
|
// sequential pairs and doesn't wrap, so without an explicit
|
|
|
|
|
|
// closing vertex the last edge (vN-1 → v0) is never tested and
|
|
|
|
|
|
// the fill bleeds far past the boundary.
|
|
|
|
|
|
if let Some(&first) = poly.first() {
|
|
|
|
|
|
if poly.last() != Some(&first) {
|
|
|
|
|
|
poly.push(first);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
(origin, poly)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Rectangular boundary from 4 corners, as offsets from `origin`.
|
|
|
|
|
|
let ux = (wo.u_vector.x * wo.size.x) as f32;
|
|
|
|
|
|
let uy = (wo.u_vector.y * wo.size.x) as f32;
|
|
|
|
|
|
let vx = (wo.v_vector.x * wo.size.y) as f32;
|
|
|
|
|
|
let vy = (wo.v_vector.y * wo.size.y) as f32;
|
|
|
|
|
|
// Close the loop (repeat corner 0): the GPU `in_polygon` ray-cast
|
|
|
|
|
|
// walks sequential vertex pairs and never wraps last→first, so an
|
|
|
|
|
|
// unclosed quad leaves the v3→v0 edge untested and the solid mask
|
|
|
|
|
|
// bleeds past the boundary — same reason the polygon branch closes.
|
|
|
|
|
|
(
|
|
|
|
|
|
origin,
|
|
|
|
|
|
vec![
|
|
|
|
|
|
[0.0, 0.0],
|
|
|
|
|
|
[ux, uy],
|
|
|
|
|
|
[ux + vx, uy + vy],
|
|
|
|
|
|
[vx, vy],
|
|
|
|
|
|
[0.0, 0.0],
|
|
|
|
|
|
],
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn hatch_model_from_dxf(
|
|
|
|
|
|
dxf: &DxfHatch,
|
|
|
|
|
|
color: [f32; 4],
|
|
|
|
|
|
) -> Option<HatchModel> {
|
|
|
|
|
|
let normal = (dxf.normal.x, dxf.normal.y, dxf.normal.z);
|
|
|
|
|
|
// Build the boundary in f64 first so the precision-preserving
|
|
|
|
|
|
// origin computation below sees full WCS precision. We only cast
|
|
|
|
|
|
// to f32 once at the end, after subtracting the AABB centre, so
|
|
|
|
|
|
// the stored offsets are small-magnitude with high f32 precision
|
|
|
|
|
|
// even on large UTM-scale drawings.
|
|
|
|
|
|
let to_xy = |x: f64, y: f64| -> [f64; 2] {
|
|
|
|
|
|
let (wx, wy, _) =
|
|
|
|
|
|
crate::scene::view::transform::ocs_point_to_wcs((x, y, dxf.elevation), normal);
|
|
|
|
|
|
[wx, wy]
|
|
|
|
|
|
};
|
|
|
|
|
|
if dxf.paths.is_empty() {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut boundary: Vec<[f64; 2]> = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
for path in &dxf.paths {
|
fix(plot): correct hatch rendering in PDF / plot export
Hatched content — especially fills nested inside a block INSERT, such as a
title-block logo — plotted incorrectly: missing, mis-spaced, or with phantom
bars. Five related fixes make the PDF/plot output match AutoCAD.
1. Export block-internal hatch fills.
The export collected only hatches owned directly by the layout block, so a
hatch nested in a block INSERT was dropped and printed as bare monochrome
outlines. The insert-explosion the viewport already does is extracted into a
shared `exploded_insert_hatch_models()` and called from both the viewport
(`synced_hatch_models`) and the export (`paper_canvas_hatches`), so a plot
draws block-internal hatches identically to the screen.
2. Honour the hatch's own stored pattern-line spacing.
`hatch_model_from_dxf` re-derived pattern spacing from the name-matched
catalog entry x pattern_scale, ignoring the resolved line geometry the DWG
stores on the hatch. When a drawing was authored against a different base
spacing (imperial 0.125 vs the catalog's metric 3.175 for ANSI31), lines
came out up to ~25x too coarse and a dense fill collapsed to a few stray
lines. When the hatch carries its own line geometry it is now used directly
(identity scale/angle); the catalog path remains the fallback.
3. Fill far-from-origin pattern hatches.
`pattern_segments` clamped the ABSOLUTE scan-line index to
+/-MAX_LINES_PER_FAMILY. A fine-spaced hatch far from the pattern origin has
large-magnitude indices at both ends but a small span, so the clamp inverted
the range and emitted nothing — silently dropping the fill. Cap the line
count (span) instead of the absolute index.
4. Skip TEXTBOX boundary paths.
TEXTBOX boundary paths (flag bit 3) are text bounding-boxes AutoCAD derives
for island detection; they are never drawn or filled. Treating one as a fill
boundary painted its rectangle solid — a phantom bar. It is now skipped when
building the fill boundary.
5. Print white/ACI-7 hatch fills black on paper.
Hatch fills arrive adapted to the dark screen background, so a white/ACI-7
fill would vanish white-on-white on the sheet. Mirror the wire pass:
near-white/near-yellow -> black, near-cyan -> dark blue, matching AutoCAD's
colour-7-on-white plotting. Wipeouts keep their paper-white mask.
Adds `tests/block_hatch_export.rs` covering: block-internal hatch reaches the
export set, stored-line spacing is honoured, far-from-origin fills are not
dropped, and TEXTBOX paths are not filled.
2026-07-06 12:24:10 +02:00
|
|
|
|
// Skip TEXTBOX boundary paths (flag bit 3). These are text
|
|
|
|
|
|
// bounding-boxes AutoCAD derives for island detection; they are
|
|
|
|
|
|
// never drawn or filled. Treating one as a fill boundary paints its
|
|
|
|
|
|
// rectangle solid — a phantom bar AutoCAD never shows.
|
|
|
|
|
|
if path.flags.bits() & 8 != 0 {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let before_path = boundary.len();
|
|
|
|
|
|
if !boundary.is_empty() {
|
|
|
|
|
|
boundary.push([f64::NAN, f64::NAN]);
|
|
|
|
|
|
}
|
|
|
|
|
|
let path_start = boundary.len();
|
|
|
|
|
|
|
2026-07-21 09:28:07 +03:00
|
|
|
|
let mut edge_polys: Vec<Vec<[f64; 2]>> = Vec::new();
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
for edge in &path.edges {
|
|
|
|
|
|
match edge {
|
|
|
|
|
|
BoundaryEdge::Polyline(poly) => {
|
|
|
|
|
|
let verts = &poly.vertices;
|
|
|
|
|
|
let count = verts.len();
|
|
|
|
|
|
if count == 0 {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
let seg_count = if poly.is_closed {
|
|
|
|
|
|
count
|
|
|
|
|
|
} else {
|
|
|
|
|
|
count.saturating_sub(1)
|
|
|
|
|
|
};
|
|
|
|
|
|
for i in 0..seg_count {
|
|
|
|
|
|
let v0 = &verts[i];
|
|
|
|
|
|
let v1 = &verts[(i + 1) % count];
|
|
|
|
|
|
let bulge = v0.z;
|
|
|
|
|
|
// Tess in f64 to preserve ~1 cm precision at
|
|
|
|
|
|
// UTM-scale WCS (the f32 path used to produce
|
|
|
|
|
|
// visibly wavy hatch arcs at 1e5+ magnitude).
|
|
|
|
|
|
let arc = if bulge.abs() < 1e-9 {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else {
|
|
|
|
|
|
crate::entities::common::BulgeArc::from_bulge(
|
|
|
|
|
|
[v0.x, v0.y],
|
|
|
|
|
|
[v1.x, v1.y],
|
|
|
|
|
|
bulge,
|
|
|
|
|
|
)
|
|
|
|
|
|
};
|
|
|
|
|
|
let Some(arc) = arc else {
|
|
|
|
|
|
boundary.push(to_xy(v0.x, v0.y));
|
|
|
|
|
|
continue;
|
|
|
|
|
|
};
|
|
|
|
|
|
let segs = convert::tess_util::arc_segments(
|
|
|
|
|
|
arc.radius,
|
|
|
|
|
|
arc.sweep.abs(),
|
|
|
|
|
|
convert::tess_util::fill_chord_tol(arc.radius),
|
|
|
|
|
|
);
|
|
|
|
|
|
for j in 0..segs {
|
|
|
|
|
|
let s = arc.sample(j as f64 / segs as f64);
|
|
|
|
|
|
boundary.push(to_xy(s[0], s[1]));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if poly.is_closed {
|
|
|
|
|
|
if let Some(&first) = boundary.get(path_start) {
|
|
|
|
|
|
boundary.push(first);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
BoundaryEdge::Line(line) => {
|
2026-07-21 09:28:07 +03:00
|
|
|
|
edge_polys.push(vec![
|
|
|
|
|
|
to_xy(line.start.x, line.start.y),
|
|
|
|
|
|
to_xy(line.end.x, line.end.y),
|
|
|
|
|
|
]);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
BoundaryEdge::CircularArc(arc) => {
|
|
|
|
|
|
let (sa, span) = convert::tess_util::arc_signed_span(
|
|
|
|
|
|
arc.start_angle,
|
|
|
|
|
|
arc.end_angle,
|
|
|
|
|
|
arc.counter_clockwise,
|
|
|
|
|
|
);
|
|
|
|
|
|
let segs = convert::tess_util::arc_segments(
|
|
|
|
|
|
arc.radius,
|
|
|
|
|
|
span.abs(),
|
|
|
|
|
|
convert::tess_util::fill_chord_tol(arc.radius),
|
|
|
|
|
|
);
|
2026-07-21 09:28:07 +03:00
|
|
|
|
let mut pts = Vec::with_capacity(segs as usize + 1);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
for i in 0..=segs {
|
|
|
|
|
|
let t = sa + span * (i as f64 / segs as f64);
|
2026-07-21 09:28:07 +03:00
|
|
|
|
pts.push(to_xy(
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
arc.center.x + arc.radius * t.cos(),
|
|
|
|
|
|
arc.center.y + arc.radius * t.sin(),
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
2026-07-21 09:28:07 +03:00
|
|
|
|
edge_polys.push(pts);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
BoundaryEdge::EllipticArc(ell) => {
|
|
|
|
|
|
let r_maj = (ell.major_axis_endpoint.x * ell.major_axis_endpoint.x
|
|
|
|
|
|
+ ell.major_axis_endpoint.y * ell.major_axis_endpoint.y)
|
|
|
|
|
|
.sqrt();
|
|
|
|
|
|
let r_min = r_maj * ell.minor_axis_ratio;
|
|
|
|
|
|
let rot = ell
|
|
|
|
|
|
.major_axis_endpoint
|
|
|
|
|
|
.y
|
|
|
|
|
|
.atan2(ell.major_axis_endpoint.x);
|
|
|
|
|
|
let (sa, span) = convert::tess_util::arc_signed_span(
|
|
|
|
|
|
ell.start_angle,
|
|
|
|
|
|
ell.end_angle,
|
|
|
|
|
|
ell.counter_clockwise,
|
|
|
|
|
|
);
|
|
|
|
|
|
let segs = convert::tess_util::arc_segments(
|
|
|
|
|
|
r_maj,
|
|
|
|
|
|
span.abs(),
|
|
|
|
|
|
convert::tess_util::fill_chord_tol(r_maj),
|
|
|
|
|
|
);
|
|
|
|
|
|
let (cr, sr) = (rot.cos(), rot.sin());
|
2026-07-21 09:28:07 +03:00
|
|
|
|
let mut pts = Vec::with_capacity(segs as usize + 1);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
for i in 0..=segs {
|
|
|
|
|
|
let t = sa + span * (i as f64 / segs as f64);
|
|
|
|
|
|
let lx = r_maj * t.cos();
|
|
|
|
|
|
let ly = r_min * t.sin();
|
2026-07-21 09:28:07 +03:00
|
|
|
|
pts.push(to_xy(
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
ell.center.x + lx * cr - ly * sr,
|
|
|
|
|
|
ell.center.y + lx * sr + ly * cr,
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
2026-07-21 09:28:07 +03:00
|
|
|
|
edge_polys.push(pts);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
BoundaryEdge::Spline(spline) => {
|
|
|
|
|
|
// DXF spline control_points pack (x, y, weight) into
|
|
|
|
|
|
// a Vector3 — the z field is the rational weight, NOT
|
|
|
|
|
|
// a Z coordinate. The legacy code dropped weight and
|
|
|
|
|
|
// sampled with a fixed 16 segments; both bugs
|
|
|
|
|
|
// produced visibly wrong fill regions for spline-
|
|
|
|
|
|
// bounded hatches (especially block-internal ones,
|
|
|
|
|
|
// where boundaries are often spline curves with
|
|
|
|
|
|
// rational weights and short cubic segments).
|
|
|
|
|
|
//
|
|
|
|
|
|
// Build a NurbsCurve when `rational`, otherwise a
|
|
|
|
|
|
// plain BSplineCurve, and sample adaptively via
|
|
|
|
|
|
// truck's `parameter_division` at the same chord
|
|
|
|
|
|
// tolerance the fill polygon uses for arcs.
|
|
|
|
|
|
let degree = spline.degree.max(0) as usize;
|
|
|
|
|
|
let knot_vec = if !spline.knots.is_empty() {
|
|
|
|
|
|
KnotVec::from(spline.knots.clone())
|
|
|
|
|
|
} else if spline.control_points.len() >= degree + 1 {
|
|
|
|
|
|
KnotVec::uniform_knot(degree, spline.control_points.len() - 1)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
KnotVec::from(vec![])
|
|
|
|
|
|
};
|
|
|
|
|
|
let knot_ok = spline.control_points.len() >= 2
|
|
|
|
|
|
&& degree >= 1
|
|
|
|
|
|
&& knot_vec.len() == spline.control_points.len() + degree + 1;
|
|
|
|
|
|
|
|
|
|
|
|
// Rough chord-tolerance: 0.1% of the control-poly
|
|
|
|
|
|
// diagonal so adaptive sampling produces enough
|
|
|
|
|
|
// points to follow the curve without exploding on
|
|
|
|
|
|
// huge splines.
|
|
|
|
|
|
let (mut sp_min_x, mut sp_min_y) = (f64::INFINITY, f64::INFINITY);
|
|
|
|
|
|
let (mut sp_max_x, mut sp_max_y) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
|
|
|
|
|
|
for cp in &spline.control_points {
|
|
|
|
|
|
sp_min_x = sp_min_x.min(cp.x);
|
|
|
|
|
|
sp_min_y = sp_min_y.min(cp.y);
|
|
|
|
|
|
sp_max_x = sp_max_x.max(cp.x);
|
|
|
|
|
|
sp_max_y = sp_max_y.max(cp.y);
|
|
|
|
|
|
}
|
|
|
|
|
|
let diag = ((sp_max_x - sp_min_x).powi(2)
|
|
|
|
|
|
+ (sp_max_y - sp_min_y).powi(2))
|
|
|
|
|
|
.sqrt();
|
|
|
|
|
|
let tol = convert::tess_util::fill_chord_tol(diag.max(1.0));
|
|
|
|
|
|
|
2026-07-21 09:28:07 +03:00
|
|
|
|
let mut epts: Vec<[f64; 2]> = Vec::new();
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let mut sampled = false;
|
|
|
|
|
|
if knot_ok {
|
|
|
|
|
|
if spline.rational {
|
|
|
|
|
|
// NURBS: pack (x, y, 0, w) into Vector4.
|
|
|
|
|
|
let cps: Vec<Vector4> = spline
|
|
|
|
|
|
.control_points
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|p| {
|
|
|
|
|
|
let w = if p.z.abs() > 1e-12 { p.z } else { 1.0 };
|
|
|
|
|
|
Vector4::new(p.x * w, p.y * w, 0.0, w)
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let bspl = TruckBSpline::new(knot_vec.clone(), cps);
|
|
|
|
|
|
let curve = NurbsCurve::new(bspl);
|
|
|
|
|
|
let (t0, t1) = curve.range_tuple();
|
|
|
|
|
|
let (_, pts) = curve.parameter_division((t0, t1), tol);
|
|
|
|
|
|
for p in pts {
|
2026-07-21 09:28:07 +03:00
|
|
|
|
epts.push(to_xy(p.x, p.y));
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
sampled = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let cps: Vec<Point3> = spline
|
|
|
|
|
|
.control_points
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|p| Point3::new(p.x, p.y, 0.0))
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let bspl = TruckBSpline::new(knot_vec, cps);
|
|
|
|
|
|
let (t0, t1) = bspl.range_tuple();
|
|
|
|
|
|
let (_, pts) = bspl.parameter_division((t0, t1), tol);
|
|
|
|
|
|
for p in pts {
|
2026-07-21 09:28:07 +03:00
|
|
|
|
epts.push(to_xy(p.x, p.y));
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
sampled = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !sampled {
|
|
|
|
|
|
// Fallback: prefer fit_points (which lie on the
|
|
|
|
|
|
// curve) over control_points (which usually
|
|
|
|
|
|
// don't). A control-point polyline would draw
|
|
|
|
|
|
// the convex-hull silhouette — visibly wrong.
|
|
|
|
|
|
let pts: &[_] = if !spline.fit_points.is_empty() {
|
|
|
|
|
|
&spline.fit_points
|
|
|
|
|
|
} else {
|
|
|
|
|
|
&[]
|
|
|
|
|
|
};
|
|
|
|
|
|
if !pts.is_empty() {
|
|
|
|
|
|
for p in pts {
|
2026-07-21 09:28:07 +03:00
|
|
|
|
epts.push(to_xy(p.x, p.y));
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for cp in &spline.control_points {
|
2026-07-21 09:28:07 +03:00
|
|
|
|
epts.push(to_xy(cp.x, cp.y));
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-21 09:28:07 +03:00
|
|
|
|
edge_polys.push(epts);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-21 09:28:07 +03:00
|
|
|
|
boundary.extend(chain_path_edges(edge_polys));
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
|
|
|
|
|
|
if boundary.len() == path_start {
|
|
|
|
|
|
boundary.truncate(before_path);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if boundary.len() >= path_start + 3 {
|
|
|
|
|
|
let first = boundary[path_start];
|
|
|
|
|
|
let last = *boundary.last().unwrap();
|
|
|
|
|
|
if (first[0] - last[0]).abs() > 1e-5 || (first[1] - last[1]).abs() > 1e-5 {
|
|
|
|
|
|
boundary.push(first);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if boundary.is_empty() {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
// The batched hatch renderer keeps boundaries in a GPU storage
|
|
|
|
|
|
// buffer (no fixed length), so a hatch with many island loops must
|
|
|
|
|
|
// retain *every* loop or even-odd island detection breaks. The old
|
|
|
|
|
|
// flat `truncate(1024)` cut complex multi-loop hatches mid-boundary:
|
|
|
|
|
|
// trailing islands were dropped and the final partial loop was left
|
|
|
|
|
|
// open, flipping the even-odd parity so the fill bled across the
|
|
|
|
|
|
// rest of the shape. Only guard against pathological vertex counts,
|
|
|
|
|
|
// and when trimming, cut at a whole-loop (NaN sentinel) boundary so
|
|
|
|
|
|
// no sub-loop is ever left open. (#148)
|
|
|
|
|
|
const MAX_HATCH_MODEL_VERTS: usize = 16_384;
|
|
|
|
|
|
if boundary.len() > MAX_HATCH_MODEL_VERTS {
|
|
|
|
|
|
// Drop only whole trailing loops: cut at the last NaN sentinel
|
|
|
|
|
|
// at/before the cap. If the first loop alone exceeds the cap,
|
|
|
|
|
|
// keep it whole rather than leaving it open.
|
|
|
|
|
|
let cut = boundary[..MAX_HATCH_MODEL_VERTS]
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.rposition(|&[x, y]| x.is_nan() || y.is_nan())
|
|
|
|
|
|
.unwrap_or(boundary.len());
|
|
|
|
|
|
boundary.truncate(cut);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
fix(plot): correct hatch rendering in PDF / plot export
Hatched content — especially fills nested inside a block INSERT, such as a
title-block logo — plotted incorrectly: missing, mis-spaced, or with phantom
bars. Five related fixes make the PDF/plot output match AutoCAD.
1. Export block-internal hatch fills.
The export collected only hatches owned directly by the layout block, so a
hatch nested in a block INSERT was dropped and printed as bare monochrome
outlines. The insert-explosion the viewport already does is extracted into a
shared `exploded_insert_hatch_models()` and called from both the viewport
(`synced_hatch_models`) and the export (`paper_canvas_hatches`), so a plot
draws block-internal hatches identically to the screen.
2. Honour the hatch's own stored pattern-line spacing.
`hatch_model_from_dxf` re-derived pattern spacing from the name-matched
catalog entry x pattern_scale, ignoring the resolved line geometry the DWG
stores on the hatch. When a drawing was authored against a different base
spacing (imperial 0.125 vs the catalog's metric 3.175 for ANSI31), lines
came out up to ~25x too coarse and a dense fill collapsed to a few stray
lines. When the hatch carries its own line geometry it is now used directly
(identity scale/angle); the catalog path remains the fallback.
3. Fill far-from-origin pattern hatches.
`pattern_segments` clamped the ABSOLUTE scan-line index to
+/-MAX_LINES_PER_FAMILY. A fine-spaced hatch far from the pattern origin has
large-magnitude indices at both ends but a small span, so the clamp inverted
the range and emitted nothing — silently dropping the fill. Cap the line
count (span) instead of the absolute index.
4. Skip TEXTBOX boundary paths.
TEXTBOX boundary paths (flag bit 3) are text bounding-boxes AutoCAD derives
for island detection; they are never drawn or filled. Treating one as a fill
boundary painted its rectangle solid — a phantom bar. It is now skipped when
building the fill boundary.
5. Print white/ACI-7 hatch fills black on paper.
Hatch fills arrive adapted to the dark screen background, so a white/ACI-7
fill would vanish white-on-white on the sheet. Mirror the wire pass:
near-white/near-yellow -> black, near-cyan -> dark blue, matching AutoCAD's
colour-7-on-white plotting. Wipeouts keep their paper-white mask.
Adds `tests/block_hatch_export.rs` covering: block-internal hatch reaches the
export set, stored-line spacing is honoured, far-from-origin fills are not
dropped, and TEXTBOX paths are not filled.
2026-07-06 12:24:10 +02:00
|
|
|
|
// When the HATCH carries its own resolved pattern-line geometry
|
|
|
|
|
|
// (angle + world-unit offset, exactly as the DWG stores it), use THAT
|
|
|
|
|
|
// instead of re-deriving spacing from the name-matched catalog entry
|
|
|
|
|
|
// × pattern_scale. The catalog's base spacing (e.g. metric acadiso
|
|
|
|
|
|
// ANSI31 = 3.175) rarely matches what the drawing was authored against
|
|
|
|
|
|
// (imperial 0.125), so the catalog path rendered lines up to ~25×
|
|
|
|
|
|
// (= inch→mm) too coarse — a dense fill collapsed to a few stray
|
|
|
|
|
|
// lines. The stored offset is the authoritative world-unit spacing, so
|
|
|
|
|
|
// the resulting families are already final: no pattern_scale / angle
|
|
|
|
|
|
// is re-applied (see `prebaked` below and the HatchModel fields).
|
|
|
|
|
|
let prebaked = !dxf.is_solid
|
|
|
|
|
|
&& !dxf.gradient_color.is_enabled()
|
|
|
|
|
|
&& !dxf.pattern.lines.is_empty();
|
|
|
|
|
|
|
2026-07-18 22:01:22 +03:00
|
|
|
|
// The gradient's first stop is the fill's start colour (not the
|
|
|
|
|
|
// entity colour); capture it so the HatchModel draws stop-0 → stop-1.
|
|
|
|
|
|
let mut gradient_color1: Option<[f32; 4]> = None;
|
2026-07-08 22:48:33 +03:00
|
|
|
|
let mut pattern = if dxf.gradient_color.is_enabled() {
|
2026-07-18 22:01:22 +03:00
|
|
|
|
let stop = |i: usize| {
|
|
|
|
|
|
dxf.gradient_color.colors.get(i).and_then(|e| e.color.rgb()).map(
|
|
|
|
|
|
|(r, g, b)| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0],
|
|
|
|
|
|
)
|
|
|
|
|
|
};
|
|
|
|
|
|
gradient_color1 = stop(0);
|
|
|
|
|
|
let color2 = stop(1).unwrap_or(color);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let angle_deg = dxf.pattern_angle.to_degrees() as f32;
|
2026-07-21 12:51:08 +03:00
|
|
|
|
let (kind, invert) =
|
|
|
|
|
|
model::hatch_model::GradientKind::from_name(&dxf.gradient_color.name);
|
2026-07-18 22:01:22 +03:00
|
|
|
|
model::hatch_model::HatchPattern::Gradient {
|
|
|
|
|
|
angle_deg,
|
|
|
|
|
|
color2,
|
2026-07-21 12:51:08 +03:00
|
|
|
|
kind,
|
|
|
|
|
|
invert,
|
2026-07-18 22:01:22 +03:00
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
} else if dxf.is_solid {
|
|
|
|
|
|
model::hatch_model::HatchPattern::Solid
|
fix(plot): correct hatch rendering in PDF / plot export
Hatched content — especially fills nested inside a block INSERT, such as a
title-block logo — plotted incorrectly: missing, mis-spaced, or with phantom
bars. Five related fixes make the PDF/plot output match AutoCAD.
1. Export block-internal hatch fills.
The export collected only hatches owned directly by the layout block, so a
hatch nested in a block INSERT was dropped and printed as bare monochrome
outlines. The insert-explosion the viewport already does is extracted into a
shared `exploded_insert_hatch_models()` and called from both the viewport
(`synced_hatch_models`) and the export (`paper_canvas_hatches`), so a plot
draws block-internal hatches identically to the screen.
2. Honour the hatch's own stored pattern-line spacing.
`hatch_model_from_dxf` re-derived pattern spacing from the name-matched
catalog entry x pattern_scale, ignoring the resolved line geometry the DWG
stores on the hatch. When a drawing was authored against a different base
spacing (imperial 0.125 vs the catalog's metric 3.175 for ANSI31), lines
came out up to ~25x too coarse and a dense fill collapsed to a few stray
lines. When the hatch carries its own line geometry it is now used directly
(identity scale/angle); the catalog path remains the fallback.
3. Fill far-from-origin pattern hatches.
`pattern_segments` clamped the ABSOLUTE scan-line index to
+/-MAX_LINES_PER_FAMILY. A fine-spaced hatch far from the pattern origin has
large-magnitude indices at both ends but a small span, so the clamp inverted
the range and emitted nothing — silently dropping the fill. Cap the line
count (span) instead of the absolute index.
4. Skip TEXTBOX boundary paths.
TEXTBOX boundary paths (flag bit 3) are text bounding-boxes AutoCAD derives
for island detection; they are never drawn or filled. Treating one as a fill
boundary painted its rectangle solid — a phantom bar. It is now skipped when
building the fill boundary.
5. Print white/ACI-7 hatch fills black on paper.
Hatch fills arrive adapted to the dark screen background, so a white/ACI-7
fill would vanish white-on-white on the sheet. Mirror the wire pass:
near-white/near-yellow -> black, near-cyan -> dark blue, matching AutoCAD's
colour-7-on-white plotting. Wipeouts keep their paper-white mask.
Adds `tests/block_hatch_export.rs` covering: block-internal hatch reaches the
export set, stored-line spacing is honoured, far-from-origin fills are not
dropped, and TEXTBOX paths are not filled.
2026-07-06 12:24:10 +02:00
|
|
|
|
} else if prebaked {
|
|
|
|
|
|
model::hatch_model::HatchPattern::Pattern(
|
|
|
|
|
|
dxf.pattern.lines.iter().map(family_from_stored_line).collect(),
|
|
|
|
|
|
)
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
let pat_name = &dxf.pattern.name;
|
|
|
|
|
|
if let Some(entry) = crate::scene::model::hatch_patterns::find(pat_name) {
|
|
|
|
|
|
entry.gpu.clone()
|
2026-07-05 11:58:44 +03:00
|
|
|
|
} else if matches!(
|
|
|
|
|
|
dxf.pattern_type,
|
|
|
|
|
|
acadrust::entities::hatch::HatchPatternType::UserDefined
|
|
|
|
|
|
) {
|
|
|
|
|
|
// User-defined hatch: parallel lines at `pattern_angle`, spaced
|
|
|
|
|
|
// `pattern_scale` apart, plus a perpendicular set when
|
|
|
|
|
|
// `is_double`. Its name ("_USER") is not a catalog pattern.
|
|
|
|
|
|
// Build BASE families (angle 0, and 90 for the cross set) with
|
|
|
|
|
|
// unit perpendicular spacing; the HatchModel's angle_offset
|
|
|
|
|
|
// (= pattern_angle) and scale (= pattern_scale) below rotate and
|
|
|
|
|
|
// space them — exactly as a predefined .PAT pattern is applied —
|
|
|
|
|
|
// so the angle/scale is applied once, not doubled. Replaces the
|
|
|
|
|
|
// old fallback that forced every user-defined hatch to flat
|
|
|
|
|
|
// horizontal lines at the wrong spacing (#278).
|
|
|
|
|
|
let fam = |angle_deg: f32| model::hatch_model::PatFamily {
|
|
|
|
|
|
angle_deg,
|
|
|
|
|
|
x0: 0.0,
|
|
|
|
|
|
y0: 0.0,
|
|
|
|
|
|
dx: 0.0,
|
|
|
|
|
|
dy: 1.0,
|
|
|
|
|
|
dashes: vec![],
|
|
|
|
|
|
};
|
|
|
|
|
|
let mut fams = vec![fam(0.0)];
|
|
|
|
|
|
if dxf.is_double {
|
|
|
|
|
|
fams.push(fam(90.0));
|
|
|
|
|
|
}
|
|
|
|
|
|
model::hatch_model::HatchPattern::Pattern(fams)
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
model::hatch_model::HatchPattern::Pattern(vec![model::hatch_model::PatFamily {
|
|
|
|
|
|
angle_deg: 0.0,
|
|
|
|
|
|
x0: 0.0,
|
|
|
|
|
|
y0: 0.0,
|
|
|
|
|
|
dx: 0.0,
|
|
|
|
|
|
dy: 5.0 * dxf.pattern_scale as f32,
|
|
|
|
|
|
dashes: vec![],
|
|
|
|
|
|
}])
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let name = if dxf.gradient_color.is_enabled() {
|
|
|
|
|
|
dxf.gradient_color.name.clone()
|
|
|
|
|
|
} else if dxf.is_solid {
|
|
|
|
|
|
"SOLID".into()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
dxf.pattern.name.clone()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Precision-preserving cast f64 → f32: pick an `world_origin`
|
|
|
|
|
|
// anchor (boundary AABB centre in f64) and store every vertex
|
|
|
|
|
|
// as a small f32 offset from it. NaN separators are preserved
|
|
|
|
|
|
// so the in_polygon ray-cast still sees the path breaks.
|
|
|
|
|
|
let mut min_x = f64::INFINITY;
|
|
|
|
|
|
let mut min_y = f64::INFINITY;
|
|
|
|
|
|
let mut max_x = f64::NEG_INFINITY;
|
|
|
|
|
|
let mut max_y = f64::NEG_INFINITY;
|
|
|
|
|
|
for &[x, y] in &boundary {
|
|
|
|
|
|
if x.is_finite() && y.is_finite() {
|
|
|
|
|
|
if x < min_x { min_x = x; }
|
|
|
|
|
|
if y < min_y { min_y = y; }
|
|
|
|
|
|
if x > max_x { max_x = x; }
|
|
|
|
|
|
if y > max_y { max_y = y; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let world_origin = if min_x.is_finite() && min_y.is_finite() {
|
|
|
|
|
|
[(min_x + max_x) * 0.5, (min_y + max_y) * 0.5]
|
|
|
|
|
|
} else {
|
|
|
|
|
|
[0.0, 0.0]
|
|
|
|
|
|
};
|
2026-07-08 22:48:33 +03:00
|
|
|
|
|
fix(hatch): anchor prebaked pattern origin near the geometry
Prebaked hatch fills anchored each pattern family at its authored
base point minus world_origin. On UTM-scale drawings that offset is
~1e6, which broke the fill two ways:
- the shader consumes x0/y0 as f32, so ~0.5 m quantization shredded
the pattern phase; and
- it evaluated the pattern that far from its origin, where the
multi-family aggregate patterns (AR-CONC, AR-SAND, GRAVEL, ...) —
effectively quasi-periodic — dissolve from their interlocking
stones into scattered dashes.
Boundaries were unaffected throughout; they ride the double-single
relative-to-eye path.
Fold the origin's offset from world_origin down to a small,
coherence-safe remainder on a `spacing * 64` grid, applied as one
common fold so every family keeps the relative phase that forms the
stones. Because the origin grip / Origin X/Y edit shifts every base
point by the same delta, the sub-grid remainder tracks it 1:1, so the
grip still moves the fill; a grid crossing needs a full `spacing * 64`
drag and is never hit by realistic alignment drags.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 20:05:15 +03:00
|
|
|
|
// Anchor the prebaked families near the geometry, tracking the pattern
|
|
|
|
|
|
// origin. The DWG base points sit at the pattern's *authored* origin,
|
|
|
|
|
|
// which on a UTM drawing can be ~1e6 units from the hatch. Handing the
|
|
|
|
|
|
// shader `base - world_origin` (~1e6) is wrong two ways: it consumes
|
|
|
|
|
|
// `x0/y0` as f32 (so ~0.5 m quantization shreds the phase — boundaries
|
|
|
|
|
|
// are unaffected, they ride the double-single relative-to-eye path), AND
|
|
|
|
|
|
// it evaluates the pattern that far from its origin. Multi-family
|
|
|
|
|
|
// aggregate patterns (AR-CONC, AR-SAND, GRAVEL, …) are effectively
|
|
|
|
|
|
// quasi-periodic: their families only cohere into the intended
|
|
|
|
|
|
// stones/grains near their shared origin and dissolve into scattered
|
|
|
|
|
|
// dashes at large offsets.
|
|
|
|
|
|
//
|
|
|
|
|
|
// So fold the origin's offset from `world_origin` down to a small,
|
|
|
|
|
|
// coherence-safe remainder on a grid of `spacing * 64` (many multiples
|
|
|
|
|
|
// of the pattern spacing — well inside the coherence range yet far
|
|
|
|
|
|
// larger than any realistic grip drag). Applying ONE common fold (from
|
|
|
|
|
|
// the reference line) preserves each family's relative phase, which is
|
|
|
|
|
|
// what forms the stones. Because the origin grip / Origin X/Y edit
|
|
|
|
|
|
// shifts every base point by the same delta, the sub-grid remainder
|
|
|
|
|
|
// tracks that delta 1:1, so the grip still moves the fill; a grid
|
|
|
|
|
|
// crossing (a whole `spacing * 64` drag) is never hit in practice.
|
2026-07-08 22:48:33 +03:00
|
|
|
|
if prebaked {
|
|
|
|
|
|
if let model::hatch_model::HatchPattern::Pattern(fams) = &mut pattern {
|
fix(hatch): anchor prebaked pattern origin near the geometry
Prebaked hatch fills anchored each pattern family at its authored
base point minus world_origin. On UTM-scale drawings that offset is
~1e6, which broke the fill two ways:
- the shader consumes x0/y0 as f32, so ~0.5 m quantization shredded
the pattern phase; and
- it evaluated the pattern that far from its origin, where the
multi-family aggregate patterns (AR-CONC, AR-SAND, GRAVEL, ...) —
effectively quasi-periodic — dissolve from their interlocking
stones into scattered dashes.
Boundaries were unaffected throughout; they ride the double-single
relative-to-eye path.
Fold the origin's offset from world_origin down to a small,
coherence-safe remainder on a `spacing * 64` grid, applied as one
common fold so every family keeps the relative phase that forms the
stones. Because the origin grip / Origin X/Y edit shifts every base
point by the same delta, the sub-grid remainder tracks it 1:1, so the
grip still moves the fill; a grid crossing needs a full `spacing * 64`
drag and is never hit by realistic alignment drags.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 20:05:15 +03:00
|
|
|
|
if let Some(rb) = dxf.pattern.lines.first().map(|l| l.base_point) {
|
|
|
|
|
|
let spacing = dxf
|
|
|
|
|
|
.pattern
|
|
|
|
|
|
.lines
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|l| l.offset.length())
|
|
|
|
|
|
.fold(0.0_f64, f64::max)
|
|
|
|
|
|
.max(1e-6);
|
|
|
|
|
|
let cell = spacing * 64.0;
|
|
|
|
|
|
let fold = |v: f64, o: f64| -> f64 {
|
|
|
|
|
|
let d = v - o;
|
|
|
|
|
|
d - (d / cell).round() * cell
|
|
|
|
|
|
};
|
|
|
|
|
|
let ox = fold(rb.x, world_origin[0]);
|
|
|
|
|
|
let oy = fold(rb.y, world_origin[1]);
|
|
|
|
|
|
for (fam, ln) in fams.iter_mut().zip(dxf.pattern.lines.iter()) {
|
|
|
|
|
|
fam.x0 = (ln.base_point.x - rb.x + ox) as f32;
|
|
|
|
|
|
fam.y0 = (ln.base_point.y - rb.y + oy) as f32;
|
|
|
|
|
|
}
|
2026-07-08 22:48:33 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let boundary_f32: Vec<[f32; 2]> = boundary
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|&[x, y]| {
|
|
|
|
|
|
if x.is_finite() && y.is_finite() {
|
|
|
|
|
|
[(x - world_origin[0]) as f32, (y - world_origin[1]) as f32]
|
|
|
|
|
|
} else {
|
|
|
|
|
|
[f32::NAN, f32::NAN]
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
|
|
Some(HatchModel {
|
|
|
|
|
|
boundary: std::sync::Arc::new(boundary_f32),
|
2026-07-11 21:55:10 +03:00
|
|
|
|
boundary_wcs: None,
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
pattern,
|
|
|
|
|
|
name,
|
2026-07-18 22:01:22 +03:00
|
|
|
|
// A gradient starts from its first stop; other fills use the
|
|
|
|
|
|
// entity colour.
|
|
|
|
|
|
color: gradient_color1.unwrap_or(color),
|
fix(plot): correct hatch rendering in PDF / plot export
Hatched content — especially fills nested inside a block INSERT, such as a
title-block logo — plotted incorrectly: missing, mis-spaced, or with phantom
bars. Five related fixes make the PDF/plot output match AutoCAD.
1. Export block-internal hatch fills.
The export collected only hatches owned directly by the layout block, so a
hatch nested in a block INSERT was dropped and printed as bare monochrome
outlines. The insert-explosion the viewport already does is extracted into a
shared `exploded_insert_hatch_models()` and called from both the viewport
(`synced_hatch_models`) and the export (`paper_canvas_hatches`), so a plot
draws block-internal hatches identically to the screen.
2. Honour the hatch's own stored pattern-line spacing.
`hatch_model_from_dxf` re-derived pattern spacing from the name-matched
catalog entry x pattern_scale, ignoring the resolved line geometry the DWG
stores on the hatch. When a drawing was authored against a different base
spacing (imperial 0.125 vs the catalog's metric 3.175 for ANSI31), lines
came out up to ~25x too coarse and a dense fill collapsed to a few stray
lines. When the hatch carries its own line geometry it is now used directly
(identity scale/angle); the catalog path remains the fallback.
3. Fill far-from-origin pattern hatches.
`pattern_segments` clamped the ABSOLUTE scan-line index to
+/-MAX_LINES_PER_FAMILY. A fine-spaced hatch far from the pattern origin has
large-magnitude indices at both ends but a small span, so the clamp inverted
the range and emitted nothing — silently dropping the fill. Cap the line
count (span) instead of the absolute index.
4. Skip TEXTBOX boundary paths.
TEXTBOX boundary paths (flag bit 3) are text bounding-boxes AutoCAD derives
for island detection; they are never drawn or filled. Treating one as a fill
boundary painted its rectangle solid — a phantom bar. It is now skipped when
building the fill boundary.
5. Print white/ACI-7 hatch fills black on paper.
Hatch fills arrive adapted to the dark screen background, so a white/ACI-7
fill would vanish white-on-white on the sheet. Mirror the wire pass:
near-white/near-yellow -> black, near-cyan -> dark blue, matching AutoCAD's
colour-7-on-white plotting. Wipeouts keep their paper-white mask.
Adds `tests/block_hatch_export.rs` covering: block-internal hatch reaches the
export set, stored-line spacing is honoured, far-from-origin fills are not
dropped, and TEXTBOX paths are not filled.
2026-07-06 12:24:10 +02:00
|
|
|
|
angle_offset: if prebaked { 0.0 } else { dxf.pattern_angle as f32 },
|
|
|
|
|
|
scale: if prebaked { 1.0 } else { dxf.pattern_scale as f32 },
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
world_origin,
|
|
|
|
|
|
draw_depth: 0.0,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Decode and cache all RasterImage entities from the current document.
|
|
|
|
|
|
/// Silently skips images whose files cannot be read.
|
|
|
|
|
|
pub fn populate_images_from_document(&mut self) {
|
|
|
|
|
|
self.images.clear();
|
|
|
|
|
|
let entries: Vec<(Handle, acadrust::entities::RasterImage)> = self
|
|
|
|
|
|
.document
|
|
|
|
|
|
.entities()
|
|
|
|
|
|
.filter_map(|e| {
|
|
|
|
|
|
if let EntityType::RasterImage(img) = e {
|
|
|
|
|
|
Some((img.common.handle, img.clone()))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
for (handle, img) in entries {
|
|
|
|
|
|
if let Some(model) = ImageModel::from_raster_image(&img) {
|
|
|
|
|
|
self.images.insert(handle, model);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.bump_geometry();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-21 12:22:15 +03:00
|
|
|
|
/// Rebuild the cached fill model (hatch / DXF SOLID) for `handle` after
|
|
|
|
|
|
/// its document entity was edited in place. The fill models are prebuilt
|
|
|
|
|
|
/// at load, so a pattern-scale / background / boundary edit stays
|
|
|
|
|
|
/// invisible until the cached model is refreshed (#415).
|
|
|
|
|
|
pub fn refresh_fill_model(&mut self, handle: Handle) {
|
|
|
|
|
|
let new_model = match self.document.get_entity(handle) {
|
|
|
|
|
|
Some(EntityType::Hatch(dxf)) => {
|
|
|
|
|
|
let color = convert::tess_util::aci_to_rgba(&dxf.common.color);
|
|
|
|
|
|
Self::hatch_model_from_dxf(dxf, color)
|
|
|
|
|
|
}
|
|
|
|
|
|
Some(EntityType::Solid(s)) => {
|
|
|
|
|
|
let color = convert::tess_util::aci_to_rgba(&s.common.color);
|
|
|
|
|
|
Some(Self::solid_hatch_model(s, color))
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
};
|
|
|
|
|
|
if let Some(model) = new_model {
|
|
|
|
|
|
self.hatches.insert(handle, model);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
pub fn populate_hatches_from_document(&mut self) {
|
|
|
|
|
|
self.hatches.clear();
|
|
|
|
|
|
|
|
|
|
|
|
let entries: Vec<(Handle, EntityType)> = self
|
|
|
|
|
|
.document
|
|
|
|
|
|
.entities()
|
|
|
|
|
|
.filter_map(|e| match e {
|
|
|
|
|
|
EntityType::Hatch(h) => Some((h.common.handle, e.clone())),
|
|
|
|
|
|
EntityType::Solid(s) => Some((s.common.handle, e.clone())),
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
|
|
use crate::par::prelude::*;
|
|
|
|
|
|
self.hatches = entries
|
|
|
|
|
|
.into_par_iter()
|
|
|
|
|
|
.filter_map(|(handle, kind)| {
|
|
|
|
|
|
// Paper-space entities live in sheet coordinates — world_offset must not
|
|
|
|
|
|
let model = match &kind {
|
|
|
|
|
|
EntityType::Hatch(dxf) => {
|
|
|
|
|
|
let color = convert::tess_util::aci_to_rgba(&dxf.common.color);
|
|
|
|
|
|
Self::hatch_model_from_dxf(dxf, color)
|
|
|
|
|
|
}
|
|
|
|
|
|
EntityType::Solid(solid) => {
|
|
|
|
|
|
let color = convert::tess_util::aci_to_rgba(&solid.common.color);
|
|
|
|
|
|
Some(Self::solid_hatch_model(solid, color))
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
};
|
|
|
|
|
|
model.map(|m| (handle, m))
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
|
|
self.bump_geometry();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Tessellate all `Solid3D` entities in the current document into
|
|
|
|
|
|
/// GPU-ready `MeshModel`s and store them in `self.meshes`.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Called after loading a document or after undo/redo so that every
|
|
|
|
|
|
/// `Solid3D` entity is represented in the mesh cache.
|
|
|
|
|
|
pub fn populate_meshes_from_document(&mut self) {
|
2026-07-05 00:38:38 +03:00
|
|
|
|
self.populate_meshes_impl(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Like [`populate_meshes_from_document`] but tessellates only solids
|
|
|
|
|
|
/// whose handle is not already cached — the existing meshes are kept.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Used after an XREF merge: the host document's solids were already
|
|
|
|
|
|
/// tessellated by the background loader, and the merge assigns brand-new
|
|
|
|
|
|
/// handles to every imported xref entity (see `merge_xref_into_block`),
|
|
|
|
|
|
/// so cached handles are guaranteed to be host solids. This turns the
|
|
|
|
|
|
/// post-xref pass from "re-tessellate host + all xrefs" into "tessellate
|
|
|
|
|
|
/// only the newly merged xref solids" — the dominant cost when a drawing
|
|
|
|
|
|
/// attaches several large xrefs. (#203)
|
|
|
|
|
|
pub fn populate_missing_meshes_from_document(&mut self) {
|
|
|
|
|
|
self.populate_meshes_impl(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn populate_meshes_impl(&mut self, incremental: bool) {
|
|
|
|
|
|
if !incremental {
|
|
|
|
|
|
self.meshes.clear();
|
|
|
|
|
|
self.block_meshes.clear();
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
// BLOCK-entity handles of the layout (model + paper) blocks. A solid
|
|
|
|
|
|
// owned by one of these is top-level; anything else lives in a block
|
|
|
|
|
|
// definition and is instanced per INSERT instead. (#123)
|
|
|
|
|
|
let layout_blocks: std::collections::HashSet<Handle> = self
|
|
|
|
|
|
.document
|
|
|
|
|
|
.objects
|
|
|
|
|
|
.values()
|
|
|
|
|
|
.filter_map(|o| match o {
|
|
|
|
|
|
acadrust::objects::ObjectType::Layout(l) if !l.block_record.is_null() => {
|
|
|
|
|
|
Some(l.block_record)
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
// Resolve color through `render_style` so the same bg adaptation
|
|
|
|
|
|
// wires use kicks in (pure black on dark bg → white, pure white
|
|
|
|
|
|
// on light bg → black). Without this, ACIS meshes ignore
|
|
|
|
|
|
// `adapt_to_bg` and stay invisible against matching bg colours.
|
|
|
|
|
|
let entries: Vec<(Handle, EntityType, [f32; 4], bool)> = self
|
|
|
|
|
|
.document
|
|
|
|
|
|
.entities()
|
|
|
|
|
|
.filter_map(|e| match e {
|
|
|
|
|
|
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_) => {
|
2026-07-05 00:38:38 +03:00
|
|
|
|
let handle = e.common().handle;
|
|
|
|
|
|
// Incremental (post-xref) pass: leave already-tessellated
|
|
|
|
|
|
// host solids untouched, only build the newly merged ones.
|
|
|
|
|
|
if incremental
|
|
|
|
|
|
&& (self.meshes.contains_key(&handle) || self.block_meshes.contains_key(&handle))
|
|
|
|
|
|
{
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let color = self.render_style(e).0;
|
|
|
|
|
|
let top_level = layout_blocks.contains(&e.common().owner_handle);
|
2026-07-05 00:38:38 +03:00
|
|
|
|
Some((handle, e.clone(), color, top_level))
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
|
|
use crate::par::prelude::*;
|
|
|
|
|
|
let facet_res = self.document.header.facet_resolution;
|
2026-07-17 19:45:42 +03:00
|
|
|
|
let isolines = self.document.header.isolines.max(0) as usize;
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
// Top-level solids: offset into the render frame, drawn flat.
|
|
|
|
|
|
// Block-definition solids: keep block-local coords for per-INSERT
|
|
|
|
|
|
// instancing (no offset applied here).
|
|
|
|
|
|
let built: Vec<(Handle, MeshLodSet, bool)> = entries
|
|
|
|
|
|
.into_par_iter()
|
|
|
|
|
|
.filter_map(|(handle, entity, color, top_level)| {
|
2026-07-17 19:45:42 +03:00
|
|
|
|
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines).map(|m| {
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
let m = if top_level { offset_mesh_lod_set(m) } else { m };
|
|
|
|
|
|
(handle, m, top_level)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
for (handle, m, top_level) in built {
|
|
|
|
|
|
if top_level {
|
|
|
|
|
|
self.meshes.insert(handle, m);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.block_meshes.insert(handle, m);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.bump_geometry();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Rebuild hatch / image / mesh caches after the document is modified
|
|
|
|
|
|
/// outside the normal `add_entity` path (e.g. REFCLOSE SAVE).
|
|
|
|
|
|
pub fn rebuild_derived_caches(&mut self) {
|
|
|
|
|
|
self.populate_hatches_from_document();
|
|
|
|
|
|
self.populate_images_from_document();
|
|
|
|
|
|
self.populate_meshes_from_document();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Build a solid-fill HatchModel for a DXF Solid entity.
|
|
|
|
|
|
/// DXF SOLID corners are in "Z-order": p0-p1 top, p2-p3 bottom.
|
|
|
|
|
|
/// Visual quad is p0→p1→p3→p2 (closed).
|
|
|
|
|
|
pub(super) fn solid_hatch_model(solid: &DxfSolid, color: [f32; 4]) -> HatchModel {
|
2026-07-15 22:55:51 +03:00
|
|
|
|
// Keep the corners in f64 until the AABB centre is known, then store
|
|
|
|
|
|
// each as a small f32 offset from it — same precision-preserving anchor
|
|
|
|
|
|
// `hatch_model_from_dxf` uses. Casting the absolute WCS corner straight
|
|
|
|
|
|
// to f32 costs ~0.06 units of resolution at UTM magnitudes (~1e6), so
|
|
|
|
|
|
// the quad snapped to a grid and the fill drifted off its outline.
|
2026-07-21 17:58:09 +03:00
|
|
|
|
// SOLID corners are stored in OCS (like ARC/LWPOLYLINE) — map through
|
|
|
|
|
|
// the arbitrary axis so a mirrored solid (normal 0,0,-1) fills on the
|
|
|
|
|
|
// correct side of the drawing.
|
|
|
|
|
|
let n = (solid.normal.x, solid.normal.y, solid.normal.z);
|
|
|
|
|
|
let w = |v: &acadrust::types::Vector3| -> [f64; 2] {
|
|
|
|
|
|
let (x, y, _) =
|
|
|
|
|
|
crate::scene::view::transform::ocs_point_to_wcs((v.x, v.y, v.z), n);
|
|
|
|
|
|
[x, y]
|
|
|
|
|
|
};
|
2026-07-15 22:55:51 +03:00
|
|
|
|
let corners: [[f64; 2]; 4] = [
|
2026-07-21 17:58:09 +03:00
|
|
|
|
w(&solid.first_corner),
|
|
|
|
|
|
w(&solid.second_corner),
|
|
|
|
|
|
w(&solid.fourth_corner),
|
|
|
|
|
|
w(&solid.third_corner),
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
];
|
2026-07-15 22:55:51 +03:00
|
|
|
|
let mut min = [f64::INFINITY; 2];
|
|
|
|
|
|
let mut max = [f64::NEG_INFINITY; 2];
|
|
|
|
|
|
for c in &corners {
|
|
|
|
|
|
for i in 0..2 {
|
|
|
|
|
|
if c[i] < min[i] {
|
|
|
|
|
|
min[i] = c[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
if c[i] > max[i] {
|
|
|
|
|
|
max[i] = c[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let world_origin = if min[0].is_finite() && min[1].is_finite() {
|
|
|
|
|
|
[(min[0] + max[0]) * 0.5, (min[1] + max[1]) * 0.5]
|
|
|
|
|
|
} else {
|
|
|
|
|
|
[0.0, 0.0]
|
|
|
|
|
|
};
|
|
|
|
|
|
let boundary = corners
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|c| {
|
|
|
|
|
|
[
|
|
|
|
|
|
(c[0] - world_origin[0]) as f32,
|
|
|
|
|
|
(c[1] - world_origin[1]) as f32,
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
HatchModel {
|
|
|
|
|
|
boundary: std::sync::Arc::new(boundary),
|
2026-07-11 21:55:10 +03:00
|
|
|
|
boundary_wcs: None,
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
pattern: model::hatch_model::HatchPattern::Solid,
|
|
|
|
|
|
name: "SOLID".into(),
|
|
|
|
|
|
color,
|
|
|
|
|
|
angle_offset: 0.0,
|
|
|
|
|
|
scale: 1.0,
|
2026-07-15 22:55:51 +03:00
|
|
|
|
world_origin,
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
draw_depth: 0.0,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn add_hatch(&mut self, model: HatchModel) -> Handle {
|
|
|
|
|
|
let mut dxf = DxfHatch::new();
|
|
|
|
|
|
dxf.is_solid = matches!(
|
|
|
|
|
|
model.pattern,
|
|
|
|
|
|
crate::scene::model::hatch_model::HatchPattern::Solid
|
|
|
|
|
|
);
|
2026-07-11 21:55:10 +03:00
|
|
|
|
// Prefer the command-supplied exact f64 boundary so a typed vertex is
|
|
|
|
|
|
// persisted without f32 quantization (issue #311). Falling back to the
|
|
|
|
|
|
// render-side `boundary`, the points arrive in local render space
|
|
|
|
|
|
// (world_offset already subtracted), so add `world_origin` back —
|
|
|
|
|
|
// otherwise the boundary wire lands `world_offset` away from the fill.
|
2026-07-13 01:32:05 +01:00
|
|
|
|
// Build one DXF boundary path per ring. The command layer separates
|
|
|
|
|
|
// the outer boundary from its holes with NaN sentinels in
|
|
|
|
|
|
// `boundary_wcs`; split on those so nested hatches (e.g. a small
|
|
|
|
|
|
// rectangle inside a big one) persist with real holes instead of a
|
2026-07-13 05:55:54 +01:00
|
|
|
|
// single self-intersecting polyline. Only the FIRST ring carries the
|
|
|
|
|
|
// external / outermost flags — every later ring is a hole and must not
|
|
|
|
|
|
// be flagged external, or DXF/DWG consumers treat the inner loop as
|
|
|
|
|
|
// another outer island rather than a hole.
|
2026-07-13 01:32:05 +01:00
|
|
|
|
if let Some(wcs) = &model.boundary_wcs {
|
|
|
|
|
|
let mut ring: Vec<Vector2> = Vec::new();
|
2026-07-13 05:55:54 +01:00
|
|
|
|
let mut first = true;
|
|
|
|
|
|
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool| {
|
2026-07-13 01:32:05 +01:00
|
|
|
|
if !r.is_empty() {
|
|
|
|
|
|
let edge = PolylineEdge::new(std::mem::take(r), true);
|
2026-07-13 05:55:54 +01:00
|
|
|
|
let mut path = if is_outer {
|
|
|
|
|
|
let mut p = BoundaryPath::external();
|
|
|
|
|
|
p.flags = acadrust::entities::hatch::BoundaryPathFlags::from_bits(
|
|
|
|
|
|
p.flags.bits()
|
|
|
|
|
|
| acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits(),
|
|
|
|
|
|
);
|
|
|
|
|
|
p
|
|
|
|
|
|
} else {
|
|
|
|
|
|
BoundaryPath::new()
|
|
|
|
|
|
};
|
2026-07-13 01:32:05 +01:00
|
|
|
|
path.add_edge(BoundaryEdge::Polyline(edge));
|
|
|
|
|
|
dxf.paths.push(path);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
for &[x, y] in wcs.iter() {
|
|
|
|
|
|
if x.is_finite() && y.is_finite() {
|
|
|
|
|
|
ring.push(Vector2::new(x, y));
|
|
|
|
|
|
} else {
|
2026-07-13 05:55:54 +01:00
|
|
|
|
let is_outer = first;
|
|
|
|
|
|
first = false;
|
|
|
|
|
|
push_ring(&mut ring, is_outer);
|
2026-07-13 01:32:05 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-13 05:55:54 +01:00
|
|
|
|
push_ring(&mut ring, first);
|
2026-07-13 01:32:05 +01:00
|
|
|
|
}
|
|
|
|
|
|
if dxf.paths.is_empty() {
|
2026-07-11 21:55:10 +03:00
|
|
|
|
let wx = model.world_origin[0];
|
|
|
|
|
|
let wy = model.world_origin[1];
|
2026-07-13 01:32:05 +01:00
|
|
|
|
let verts: Vec<Vector2> = model
|
2026-07-11 21:55:10 +03:00
|
|
|
|
.boundary
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter(|v| v[0].is_finite() && v[1].is_finite())
|
|
|
|
|
|
.map(|&[x, y]| Vector2::new(x as f64 + wx, y as f64 + wy))
|
2026-07-13 01:32:05 +01:00
|
|
|
|
.collect();
|
|
|
|
|
|
let edge = PolylineEdge::new(verts, true);
|
|
|
|
|
|
let mut path = BoundaryPath::external();
|
|
|
|
|
|
path.add_edge(BoundaryEdge::Polyline(edge));
|
|
|
|
|
|
dxf.paths.push(path);
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
if let Some(entry) = crate::scene::model::hatch_patterns::find(&model.name) {
|
|
|
|
|
|
dxf.pattern = crate::scene::model::hatch_patterns::build_dxf_pattern(entry);
|
|
|
|
|
|
}
|
|
|
|
|
|
dxf.pattern_angle = model.angle_offset as f64;
|
|
|
|
|
|
dxf.pattern_scale = if model.scale.abs() > 1e-6 {
|
|
|
|
|
|
model.scale as f64
|
|
|
|
|
|
} else {
|
|
|
|
|
|
1.0
|
|
|
|
|
|
};
|
2026-07-21 12:51:08 +03:00
|
|
|
|
// A gradient fill must be encoded on the DXF entity itself: the render
|
|
|
|
|
|
// model is rebuilt from the entity below (`add_entity` →
|
|
|
|
|
|
// `hatch_model_from_dxf`), so a gradient kept only on the command's
|
|
|
|
|
|
// model silently degraded to a plain pattern hatch.
|
|
|
|
|
|
if let crate::scene::model::hatch_model::HatchPattern::Gradient {
|
|
|
|
|
|
angle_deg,
|
|
|
|
|
|
color2,
|
|
|
|
|
|
kind,
|
|
|
|
|
|
invert,
|
|
|
|
|
|
} = &model.pattern
|
|
|
|
|
|
{
|
|
|
|
|
|
let to_color = |c: [f32; 4]| acadrust::types::Color::Rgb {
|
|
|
|
|
|
r: (c[0] * 255.0).round().clamp(0.0, 255.0) as u8,
|
|
|
|
|
|
g: (c[1] * 255.0).round().clamp(0.0, 255.0) as u8,
|
|
|
|
|
|
b: (c[2] * 255.0).round().clamp(0.0, 255.0) as u8,
|
|
|
|
|
|
};
|
|
|
|
|
|
dxf.is_solid = true;
|
|
|
|
|
|
dxf.gradient_color.enabled = true;
|
|
|
|
|
|
dxf.gradient_color.name = kind.dxf_name(*invert).to_string();
|
|
|
|
|
|
// The render model reads the gradient angle from pattern_angle
|
|
|
|
|
|
// (radians); the gradient record keeps its own copy for the file.
|
|
|
|
|
|
dxf.pattern_angle = (*angle_deg as f64).to_radians();
|
|
|
|
|
|
dxf.gradient_color.angle = (*angle_deg as f64).to_radians();
|
|
|
|
|
|
dxf.gradient_color.is_single_color = false;
|
|
|
|
|
|
// Linear has no INV name in the standard set: persist an inverted
|
|
|
|
|
|
// linear by swapping the colour stops instead.
|
|
|
|
|
|
let (c0, c1) =
|
|
|
|
|
|
if *invert && matches!(kind, crate::scene::model::hatch_model::GradientKind::Linear)
|
|
|
|
|
|
{
|
|
|
|
|
|
(*color2, model.color)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
(model.color, *color2)
|
|
|
|
|
|
};
|
|
|
|
|
|
dxf.gradient_color.colors = vec![
|
|
|
|
|
|
acadrust::entities::hatch::GradientColorEntry {
|
|
|
|
|
|
value: 0.0,
|
|
|
|
|
|
color: to_color(c0),
|
|
|
|
|
|
},
|
|
|
|
|
|
acadrust::entities::hatch::GradientColorEntry {
|
|
|
|
|
|
value: 1.0,
|
|
|
|
|
|
color: to_color(c1),
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
|
|
|
|
|
|
// `add_entity` already builds the render model from the DXF entity via
|
|
|
|
|
|
// `hatch_model_from_dxf` and inserts it with a correct `world_origin`
|
|
|
|
|
|
// (AABB-centred) for the relative-to-eye fill. The command-built `model`
|
|
|
|
|
|
// carries `world_origin: [0, 0]`, which after the world_offset removal
|
|
|
|
|
|
// leaves the fill mis-placed and effectively invisible until a later
|
|
|
|
|
|
// edit rebuilds it from the DXF — so keep the seed, don't overwrite it.
|
|
|
|
|
|
self.add_entity(EntityType::Hatch(dxf))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn clear(&mut self) {
|
|
|
|
|
|
self.document = CadDocument::new();
|
|
|
|
|
|
self.selected = HashSet::default();
|
|
|
|
|
|
self.preview_wires = vec![];
|
2026-07-10 10:39:44 +03:00
|
|
|
|
self.preview_text = vec![];
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
self.current_layout = "Model".to_string();
|
|
|
|
|
|
self.hatches = HashMap::default();
|
|
|
|
|
|
self.meshes = HashMap::default();
|
|
|
|
|
|
*self.camera.borrow_mut() = Camera::default();
|
|
|
|
|
|
self.camera_generation += 1;
|
|
|
|
|
|
self.bump_geometry();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|