Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/io/pdf_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,10 +381,29 @@ fn emit_hatch(ops: &mut Vec<Op>, hatch: &HatchModel, ox: f32, oy: f32) {
if hatch.boundary.is_empty() {
return;
}
let [r, g, b, a] = hatch.color;
let [mut r, mut g, mut b, a] = hatch.color;
if a < 0.01 {
return;
}
// Adapt hatch fills to the white sheet, mirroring the wire pass: colours
// arrive adapted to the (dark) screen background, so a white/ACI-7 fill
// would vanish white-on-white on paper. Force near-white/near-yellow → black
// and near-cyan → dark blue, matching AutoCAD's colour-7-on-white plotting.
// Genuine colours are untouched; WIPEOUTS keep their paper-white mask.
if hatch.name != "WIPEOUT_FILL" {
let is_light = r > 0.80 && g > 0.80 && b > 0.80;
let is_yellow = r > 0.80 && g > 0.70 && b < 0.30;
let is_cyan = r < 0.30 && g > 0.70 && b > 0.70;
if is_light || is_yellow {
r = 0.0;
g = 0.0;
b = 0.0;
} else if is_cyan {
r = 0.0;
g = 0.15;
b = 0.50;
}
}
let world_ox = hatch.world_origin[0] as f32;
let world_oy = hatch.world_origin[1] as f32;

Expand Down
193 changes: 143 additions & 50 deletions src/scene/entity.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
use super::*;

/// 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
/// the exact stored step. `x0/y0` stay 0: for an infinite line family the phase
/// is not observable, and the stored base point is in the source (possibly
/// block-local) frame anyway.
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(),
}
}

impl Scene {
// ── Entity management ─────────────────────────────────────────────────

Expand Down Expand Up @@ -465,7 +490,13 @@ impl Scene {
m.color = self.render_style(e).0;
if let EntityType::Hatch(dxf) = e {
match &mut m.pattern {
model::hatch_model::HatchPattern::Pattern(_) => {
// 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() =>
{
m.angle_offset = dxf.pattern_angle as f32;
let anno = if self.current_layout == "Model" {
self.annotation_scale
Expand All @@ -477,7 +508,8 @@ impl Scene {
model::hatch_model::HatchPattern::Gradient { angle_deg, .. } => {
*angle_deg = dxf.pattern_angle.to_degrees() as f32;
}
model::hatch_model::HatchPattern::Solid => {}
model::hatch_model::HatchPattern::Pattern(_)
| model::hatch_model::HatchPattern::Solid => {}
}
}
}
Expand All @@ -495,6 +527,87 @@ impl Scene {
} else {
self.bg_color
};
// 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.)
models.extend(self.exploded_insert_hatch_models(layout_block, hatch_bg, true));

// Wide LWPolyline and Polyline2D fills
for entity in self.document.entities() {
let (common, fill_origin, fills) = match entity {
EntityType::LwPolyline(pl) => {
let (o, f) = crate::entities::lwpolyline::wide_fills(pl);
(&pl.common, o, f)
}
EntityType::Polyline2D(pl) => {
let (o, f) = crate::entities::polyline::wide_fills(pl);
(&pl.common, o, f)
}
_ => continue,
};
if fills.is_empty() {
continue;
}
if common.invisible || layer_hidden(&common.layer) {
continue;
}
if !self.belongs_to_visible_block(common.handle, common.owner_handle, layout_block) {
continue;
}
let base_color = self.render_style(entity).0;
let selected = self.selected.contains(&common.handle);
let color = if selected {
[0.15, 0.55, 1.00, 1.0]
} else {
base_color
};
for boundary in fills {
models.push(HatchModel {
boundary: Arc::new(boundary),
pattern: model::hatch_model::HatchPattern::Solid,
name: "SOLID".into(),
color,
angle_offset: 0.0,
scale: 1.0,
world_origin: fill_origin,
vp_scissor: None,
draw_depth: depth_map.get(&common.handle.value()).copied().unwrap_or(0.0),
});
}
}

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,
) -> 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();
// 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
Expand Down Expand Up @@ -522,7 +635,7 @@ impl Scene {
if !self.block_has_hatch(&ins.block_name, &mut hatch_block_memo) {
continue;
}
let selected = self.selected.contains(&ins.common.handle);
let selected = tint_selected && self.selected.contains(&ins.common.handle);
// 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)
Expand Down Expand Up @@ -632,51 +745,6 @@ impl Scene {
}
}
}

// Wide LWPolyline and Polyline2D fills
for entity in self.document.entities() {
let (common, fill_origin, fills) = match entity {
EntityType::LwPolyline(pl) => {
let (o, f) = crate::entities::lwpolyline::wide_fills(pl);
(&pl.common, o, f)
}
EntityType::Polyline2D(pl) => {
let (o, f) = crate::entities::polyline::wide_fills(pl);
(&pl.common, o, f)
}
_ => continue,
};
if fills.is_empty() {
continue;
}
if common.invisible || layer_hidden(&common.layer) {
continue;
}
if !self.belongs_to_visible_block(common.handle, common.owner_handle, layout_block) {
continue;
}
let base_color = self.render_style(entity).0;
let selected = self.selected.contains(&common.handle);
let color = if selected {
[0.15, 0.55, 1.00, 1.0]
} else {
base_color
};
for boundary in fills {
models.push(HatchModel {
boundary: Arc::new(boundary),
pattern: model::hatch_model::HatchPattern::Solid,
name: "SOLID".into(),
color,
angle_offset: 0.0,
scale: 1.0,
world_origin: fill_origin,
vp_scissor: None,
draw_depth: depth_map.get(&common.handle.value()).copied().unwrap_or(0.0),
});
}
}

models
}

Expand Down Expand Up @@ -829,6 +897,13 @@ impl Scene {
let mut boundary: Vec<[f64; 2]> = Vec::new();

for path in &dxf.paths {
// 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;
}
let before_path = boundary.len();
if !boundary.is_empty() {
boundary.push([f64::NAN, f64::NAN]);
Expand Down Expand Up @@ -1076,6 +1151,20 @@ impl Scene {
boundary.truncate(cut);
}

// 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();

let pattern = if dxf.gradient_color.is_enabled() {
let color2 = dxf
.gradient_color
Expand All @@ -1088,6 +1177,10 @@ impl Scene {
model::hatch_model::HatchPattern::Gradient { angle_deg, color2 }
} else if dxf.is_solid {
model::hatch_model::HatchPattern::Solid
} else if prebaked {
model::hatch_model::HatchPattern::Pattern(
dxf.pattern.lines.iter().map(family_from_stored_line).collect(),
)
} else {
let pat_name = &dxf.pattern.name;
if let Some(entry) = crate::scene::model::hatch_patterns::find(pat_name) {
Expand Down Expand Up @@ -1176,8 +1269,8 @@ impl Scene {
pattern,
name,
color,
angle_offset: dxf.pattern_angle as f32,
scale: dxf.pattern_scale as f32,
angle_offset: if prebaked { 0.0 } else { dxf.pattern_angle as f32 },
scale: if prebaked { 1.0 } else { dxf.pattern_scale as f32 },
world_origin,
vp_scissor: None,
draw_depth: 0.0,
Expand Down
11 changes: 9 additions & 2 deletions src/scene/model/hatch_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,15 @@ impl HatchModel {
if k_lo > k_hi {
std::mem::swap(&mut k_lo, &mut k_hi);
}
k_lo = k_lo.max(-MAX_LINES_PER_FAMILY);
k_hi = k_hi.min(MAX_LINES_PER_FAMILY);
// Cap the line COUNT (span), not the absolute index. A hatch far
// from the pattern origin (0,0) — e.g. a fine-spaced fill at large
// drawing coordinates — has large-magnitude k at both ends but a
// small span. Clamping the absolute index to ±MAX_LINES_PER_FAMILY
// would invert the range (k_lo > k_hi) and emit nothing, silently
// dropping the whole fill.
if k_hi.saturating_sub(k_lo) > MAX_LINES_PER_FAMILY {
k_hi = k_lo.saturating_add(MAX_LINES_PER_FAMILY);
}

let period: f32 = family.dashes.iter().map(|d| d.abs()).sum::<f32>() * scale;
let has_dashes = !family.dashes.is_empty() && period > 1e-6;
Expand Down
22 changes: 20 additions & 2 deletions src/scene/paper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,16 +212,34 @@ impl Scene {
let mut m = model.clone();
m.color = self.render_style(entity).0;
if let EntityType::Hatch(dxf) = entity {
// Only re-apply pattern_scale/angle for catalog-derived patterns
// (empty stored lines). A pattern built from the hatch's own
// stored lines is already final (scale 1 / angle 0).
if let model::hatch_model::HatchPattern::Pattern(_) = &m.pattern {
m.angle_offset = dxf.pattern_angle as f32;
m.scale = dxf.pattern_scale as f32;
if dxf.pattern.lines.is_empty() {
m.angle_offset = dxf.pattern_angle as f32;
m.scale = dxf.pattern_scale as f32;
}
}
}
if self.selected.contains(&handle) {
m.color = [0.15, 0.55, 1.00, m.color[3]];
}
models.push(m);
}
// Hatch fills nested inside a block INSERT are owned by the block
// record, so the loop above — which only keeps hatches owned by
// `layout_block` — never sees them. Explode the layout's visible
// INSERTs and materialize their fills at world position, exactly as the
// viewport does, so the export carries the block's colours instead of
// bare outlines. (No selection tint on export.)
let hatch_bg = if self.current_layout != "Model" {
self.paper_bg_color
} else {
self.bg_color
};
let exploded = self.exploded_insert_hatch_models(layout_block, hatch_bg, false);
models.extend(exploded);
Arc::new(models)
}

Expand Down
Loading