fix(hatch): apply pattern property edits

Closes #476
This commit is contained in:
Hakan Seven 2026-07-27 16:44:31 +03:00
commit 759f2316aa
3 changed files with 118 additions and 14 deletions

View file

@ -1279,7 +1279,34 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
if let Some(acadrust::EntityType::Hatch(dxf)) = if let Some(acadrust::EntityType::Hatch(dxf)) =
self.tabs[i].scene.document.get_entity_mut(handle) self.tabs[i].scene.document.get_entity_mut(handle)
{ {
dxf.pattern = hatch_patterns::build_dxf_pattern(entry); let old_origin = dxf.pattern.lines.first().map(|line| {
(line.base_point.x, line.base_point.y)
});
let mut pattern = hatch_patterns::build_dxf_pattern(entry);
// Stored pattern lines are final world-space
// geometry. Preserve the selected hatch's scale,
// angle and origin when replacing the catalog
// pattern.
crate::entities::hatch::scale_pattern_geometry(
&mut pattern,
dxf.pattern_scale,
);
crate::entities::hatch::rotate_pattern_geometry(
&mut pattern,
dxf.pattern_angle,
);
if let (Some((old_x, old_y)), Some(new_origin)) =
(old_origin, pattern.lines.first())
{
let dx = old_x - new_origin.base_point.x;
let dy = old_y - new_origin.base_point.y;
crate::entities::hatch::translate_pattern_geometry(
&mut pattern,
dx,
dy,
);
}
dxf.pattern = pattern;
dxf.is_solid = matches!( dxf.is_solid = matches!(
entry.gpu, entry.gpu,
crate::scene::model::hatch_model::HatchPattern::Solid crate::scene::model::hatch_model::HatchPattern::Solid

View file

@ -99,6 +99,67 @@ fn boundary_centroid(h: &Hatch) -> Option<(f64, f64)> {
(n > 0.0).then(|| (sx / n, sy / n)) (n > 0.0).then(|| (sx / n, sy / n))
} }
/// Scale the stored, final hatch-pattern geometry around the pattern origin.
/// Pattern lines loaded from DXF/DWG are rendered prebaked, so their metadata
/// scale is not applied again by the renderer.
pub(crate) fn scale_pattern_geometry(
pattern: &mut acadrust::entities::HatchPattern,
factor: f64,
) {
let (origin_x, origin_y) = pattern
.lines
.first()
.map(|line| (line.base_point.x, line.base_point.y))
.unwrap_or((0.0, 0.0));
for line in pattern.lines.iter_mut() {
line.base_point.x = origin_x + (line.base_point.x - origin_x) * factor;
line.base_point.y = origin_y + (line.base_point.y - origin_y) * factor;
line.offset.x *= factor;
line.offset.y *= factor;
for dash in line.dash_lengths.iter_mut() {
*dash *= factor;
}
}
}
/// Rotate the stored, final hatch-pattern geometry around the pattern origin.
/// This keeps the line angle, base point and world-space offset in sync.
pub(crate) fn rotate_pattern_geometry(
pattern: &mut acadrust::entities::HatchPattern,
angle: f64,
) {
let (sin, cos) = angle.sin_cos();
let (origin_x, origin_y) = pattern
.lines
.first()
.map(|line| (line.base_point.x, line.base_point.y))
.unwrap_or((0.0, 0.0));
for line in pattern.lines.iter_mut() {
line.angle += angle;
let (x, y) = (
line.base_point.x - origin_x,
line.base_point.y - origin_y,
);
line.base_point.x = origin_x + x * cos - y * sin;
line.base_point.y = origin_y + x * sin + y * cos;
let (x, y) = (line.offset.x, line.offset.y);
line.offset.x = x * cos - y * sin;
line.offset.y = x * sin + y * cos;
}
}
/// Move every stored line by the same amount, preserving the pattern phase.
pub(crate) fn translate_pattern_geometry(
pattern: &mut acadrust::entities::HatchPattern,
dx: f64,
dy: f64,
) {
for line in pattern.lines.iter_mut() {
line.base_point.x += dx;
line.base_point.y += dy;
}
}
/// Read the hatch background colour from its `HATCHBACKGROUNDCOLOR` extended /// Read the hatch background colour from its `HATCHBACKGROUNDCOLOR` extended
/// data (group 1071, packed true colour: high byte = colour method, low three = /// data (group 1071, packed true colour: high byte = colour method, low three =
/// RGB). `None` when unset or not a true-colour value. /// RGB). `None` when unset or not a true-colour value.
@ -483,8 +544,16 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
return; return;
}; };
match field { match field {
"pattern_angle" if h.gradient_color.enabled => h.gradient_color.angle = v.to_radians(), "pattern_angle" if h.gradient_color.enabled => {
"pattern_angle" => h.pattern_angle = v.to_radians(), h.gradient_color.angle = v.to_radians();
h.pattern_angle = v.to_radians();
}
"pattern_angle" => {
let angle = v.to_radians();
let delta = angle - h.pattern_angle;
rotate_pattern_geometry(&mut h.pattern, delta);
h.pattern_angle = angle;
}
// The stored pattern lines are the FINAL rendered geometry (offsets, // The stored pattern lines are the FINAL rendered geometry (offsets,
// base points and dashes in world units — see the prebaked path in // base points and dashes in world units — see the prebaked path in
// hatch_model_from_dxf), so changing the scale must rescale them by // hatch_model_from_dxf), so changing the scale must rescale them by
@ -493,15 +562,7 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
let old = h.pattern_scale; let old = h.pattern_scale;
if old > 1e-12 { if old > 1e-12 {
let k = v / old; let k = v / old;
for line in h.pattern.lines.iter_mut() { scale_pattern_geometry(&mut h.pattern, k);
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; h.pattern_scale = v;
} }
@ -817,10 +878,17 @@ impl Grippable for Hatch {
use crate::scene::model::object::GripMenuAction as A; use crate::scene::model::object::GripMenuAction as A;
match action { match action {
A::HatchAngle => { A::HatchAngle => {
self.pattern_angle = value.to_radians(); let angle = value.to_radians();
let delta = angle - self.pattern_angle;
rotate_pattern_geometry(&mut self.pattern, delta);
self.pattern_angle = angle;
} }
A::HatchScale => { A::HatchScale => {
if value > 0.0 { if value > 0.0 {
if self.pattern_scale > 1e-12 {
let factor = value / self.pattern_scale;
scale_pattern_geometry(&mut self.pattern, factor);
}
self.pattern_scale = value; self.pattern_scale = value;
} }
} }

View file

@ -870,9 +870,18 @@ impl PropertiesPanel {
} else { } else {
Some(current.to_string()) Some(current.to_string())
}; };
// iced hides `selected` while the combo has focus and shows its
// placeholder instead. Use the real pattern name for a single hatch;
// a fixed VARIES placeholder made a correct ANSI31 value look mixed as
// soon as the dropdown opened (#476).
let placeholder = if current == VARIES_LABEL {
VARIES_LABEL
} else {
current
};
let combo = combo_box( let combo = combo_box(
&self.hatch_pattern_combo, &self.hatch_pattern_combo,
VARIES_LABEL, placeholder,
selected.as_ref(), selected.as_ref(),
Message::PropHatchPatternChanged, Message::PropHatchPatternChanged,
) )