fix(hatch): pattern-scale edits take effect + background on/off

Property edits on a hatch were invisible: fills render from the
prebuilt model cache, which move/copy refreshed but the properties
path never did — so a pattern-scale change (which must also rescale
the stored final pattern lines by the ratio) changed nothing on
screen (#415). invalidate_property_targets now rebuilds the edited
fill's cached model via Scene::refresh_fill_model.

The background row gains an on/off box: off removes the
HATCHBACKGROUNDCOLOR record entirely (the colour could never be
reverted before), on seeds ByLayer. The codec covers the full
colour-method range (ByLayer / ByBlock / RGB / ACI) and the backdrop
resolves ByLayer / ByBlock through the style chain. "More Colors…" on
the background picker now opens the palette window targeting the
background colour — it used to just close the picker.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Fable 5 committed 2026-07-21 12:22:15 +03:00
1 parent a7fd9ecb45
commit 9bfcad18b9
6 files changed
+154 -16

No files matched your search

+2
View File
@@ -831,6 +831,8 @@ pub enum ColorPickTarget {
Table(u8, &'static str),
/// Selected entities' colour (left properties panel).
Properties,
/// Selected entities' background colour (hatch / MTEXT background row).
PropertiesBg,
/// Current creation colour (ribbon).
Ribbon,
/// A layer's colour, by panel row index.
+4
View File
@@ -1087,6 +1087,10 @@ impl OpenCADStudio {
pub(super) fn invalidate_property_targets(&mut self, i: usize, handles: &[Handle]) {
for &handle in handles {
self.tabs[i].scene.mark_entity_dirty(handle);
// Hatch / SOLID fills render from prebuilt cached models; rebuild
// them or pattern edits (scale, background, …) stay invisible
// (#415).
self.tabs[i].scene.refresh_fill_model(handle);
}
// Solid (ACIS) meshes bake their colour into the mesh, so a colour /
// layer change needs an explicit recolour — re-tessellating wires
+3
View File
@@ -981,6 +981,9 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
Some(crate::app::ColorPickTarget::Properties) => {
Some(Message::PropColorChanged(color))
}
Some(crate::app::ColorPickTarget::PropertiesBg) => {
Some(Message::PropBgColorChanged(color))
}
Some(crate::app::ColorPickTarget::MText) => {
Some(Message::MTextColorChanged(color))
}
+104 -14
View File
@@ -109,19 +109,32 @@ pub fn background_color(h: &Hatch) -> Option<acadrust::types::Color> {
_ => None,
})?;
match (raw >> 24) & 0xFF {
0xC2 | 0xC3 => Some(acadrust::types::Color::Rgb {
// Colour-method byte: C0 = ByLayer, C1 = ByBlock, C2 = true colour,
// C3 = ACI index.
0xC0 => Some(acadrust::types::Color::ByLayer),
0xC1 => Some(acadrust::types::Color::ByBlock),
0xC2 => Some(acadrust::types::Color::Rgb {
r: ((raw >> 16) & 0xFF) as u8,
g: ((raw >> 8) & 0xFF) as u8,
b: (raw & 0xFF) as u8,
}),
0xC3 => Some(acadrust::types::Color::Index((raw & 0xFF) as u8)),
_ => None,
}
}
/// Pack an RGB colour into a `HATCHBACKGROUNDCOLOR` group-1071 true-colour word.
pub fn pack_background_color(c: &acadrust::types::Color) -> i32 {
let (r, g, b) = c.rgb().unwrap_or((255, 255, 255));
(0xC2000000u32 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)) as i32
use acadrust::types::Color;
(match c {
Color::ByLayer => 0xC0000000u32,
Color::ByBlock => 0xC1000000u32,
Color::Index(i) => 0xC3000000u32 | (*i as u8 as u32),
_ => {
let (r, g, b) = c.rgb().unwrap_or((255, 255, 255));
0xC2000000u32 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
}
}) as i32
}
/// Replace (or insert) the hatch's `HATCHBACKGROUNDCOLOR` extended-data record,
@@ -146,6 +159,25 @@ pub fn set_background_color(h: &mut Hatch, c: &acadrust::types::Color) {
h.common.extended_data.add_record(rec);
}
/// Remove the hatch's `HATCHBACKGROUNDCOLOR` extended-data record (background
/// off), preserving every other application's records.
pub fn clear_background_color(h: &mut Hatch) {
use acadrust::xdata::ExtendedDataRecord;
const APP: &str = "HATCHBACKGROUNDCOLOR";
let kept: Vec<ExtendedDataRecord> = h
.common
.extended_data
.records()
.iter()
.filter(|r| r.application_name != APP)
.cloned()
.collect();
h.common.extended_data.clear();
for r in kept {
h.common.extended_data.add_record(r);
}
}
fn properties(h: &Hatch) -> Vec<PropSection> {
let pattern_type = match h.pattern_type {
acadrust::entities::HatchPatternType::Predefined => "Predefined",
@@ -162,13 +194,14 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
let default_col = acadrust::types::Color::Index(7);
let grad_c1 = g.colors.first().map(|e| e.color).unwrap_or(default_col);
let grad_c2 = g.colors.get(1).map(|e| e.color).unwrap_or(default_col);
let bg_on = background_color(h).is_some();
let bg_col = background_color(h).unwrap_or(default_col);
if g.enabled {
// ── Gradient fill ──────────────────────────────────────────────────
let grad_type = if g.is_single_color { "One color" } else { "Two color" };
let centered = if g.shift.abs() < 1e-9 { "Yes" } else { "No" };
return vec![
let mut sections = vec![
PropSection {
title: "Pattern".into(),
props: vec![
@@ -214,15 +247,29 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
ro("Annotative", "annotative", String::new()),
ro("Island detection style", "style", style),
Property {
label: "Background color".into(),
field: "background_color",
value: PropValue::ColorChoice(bg_col),
},
label: "Background".into(),
field: "bg_enabled",
value: PropValue::BoolToggle {
field: "bg_enabled",
value: bg_on,
},
},
],
},
];
if bg_on {
if let Some(sec) = sections.last_mut() {
sec.props.push(Property {
label: "Background color".into(),
field: "background_color",
value: PropValue::ColorChoice(bg_col),
});
}
}
return sections;
}
// ── Hatch (pattern / solid) ────────────────────────────────────────────
// "Type" = pattern definition source (Predefined / User Defined / Custom).
let type_row = Property {
@@ -279,7 +326,7 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
.map(|l| (l.base_point.x, l.base_point.y))
.unwrap_or((0.0, 0.0));
vec![
let mut sections = vec![
PropSection {
title: "Pattern".into(),
props: vec![
@@ -296,9 +343,12 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
associative_row,
island_row,
Property {
label: "Background color".into(),
field: "background_color",
value: PropValue::ColorChoice(bg_col),
label: "Background".into(),
field: "bg_enabled",
value: PropValue::BoolToggle {
field: "bg_enabled",
value: bg_on,
},
},
],
},
@@ -310,7 +360,17 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
ro("Cumulative area", "cumulative_area", format!("{:.4}", area)),
],
},
]
];
if bg_on {
if let Some(sec) = sections.first_mut() {
sec.props.push(Property {
label: "Background color".into(),
field: "background_color",
value: PropValue::ColorChoice(bg_col),
});
}
}
sections
}
fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
@@ -318,6 +378,17 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
// Non-numeric fields (bool toggles / enum choices) — handled before the
// f64 parse below, which would otherwise reject their string values.
match field {
// Background on/off (#415): off removes the HATCHBACKGROUNDCOLOR
// record entirely (no background), on seeds a white one for the
// colour picker to change.
"bg_enabled" => {
if background_color(h).is_some() {
clear_background_color(h);
} else {
set_background_color(h, &acadrust::types::Color::ByLayer);
}
return;
}
"double" => {
h.is_double = if value == "toggle" {
!h.is_double
@@ -364,7 +435,26 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
match field {
"pattern_angle" if h.gradient_color.enabled => h.gradient_color.angle = v.to_radians(),
"pattern_angle" => h.pattern_angle = v.to_radians(),
"pattern_scale" if v > 0.0 => h.pattern_scale = v,
// The stored pattern lines are the FINAL rendered geometry (offsets,
// base points and dashes in world units — see the prebaked path in
// hatch_model_from_dxf), so changing the scale must rescale them by
// the ratio or the edit has no visible effect (#415).
"pattern_scale" if v > 0.0 => {
let old = h.pattern_scale;
if old > 1e-12 {
let k = v / old;
for line in h.pattern.lines.iter_mut() {
line.base_point.x *= k;
line.base_point.y *= k;
line.offset.x *= k;
line.offset.y *= k;
for d in line.dash_lengths.iter_mut() {
*d *= k;
}
}
}
h.pattern_scale = v;
}
// Scale every pattern line's offset so the first line's spacing = v,
// preserving the relative spacing between lines.
"spacing" if v > 0.0 => {
+37 -1
View File
@@ -708,7 +708,22 @@ impl Scene {
if let Some(bg) = crate::entities::hatch::background_color(dxf) {
let mut b = m.clone();
b.pattern = model::hatch_model::HatchPattern::Solid;
b.color = crate::scene::convert::tess_util::aci_to_rgba(&bg);
// 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)
}
};
b.name = "SOLID".into();
backdrop = Some(b);
}
@@ -1585,6 +1600,27 @@ impl Scene {
self.bump_geometry();
}
/// 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);
}
}
pub fn populate_hatches_from_document(&mut self) {
self.hatches.clear();
+4 -1
View File
@@ -430,7 +430,10 @@ impl PropertiesPanel {
},
Message::PropBgColorChanged,
Message::PropBgColorPickerToggle,
Message::PropBgColorPickerToggle,
// "More Colors…" opens the full palette window targeting the
// background colour — this used to just close the picker
// (#415).
Message::OpenColorWindow(crate::app::ColorPickTarget::PropertiesBg),
);
return prop_row_widget(label, selector);
}