forked from 1b-agent/opencadstudio-upstream
feat(hatch): editable gradient colors + background color
Gradient Type (One/Two color), Color 1 and Color 2 editable via a generic field-keyed colour picker path. Background colour editable and stored as HATCHBACKGROUNDCOLOR extended data (group 1071 true colour), rendered in OCS as a solid backdrop drawn behind the pattern. Hide Origin X/Y for gradients (pattern-only concept) and drop the redundant gradient rows from the pattern branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
22059e138c
commit
b5106c82b5
5 files changed
+190
-27
No files matched your search
@@ -1558,6 +1558,12 @@ pub enum Message {
|
|||||||
/// User picked an MTEXT background fill colour — sets `background_color`
|
/// User picked an MTEXT background fill colour — sets `background_color`
|
||||||
/// and switches the background mode to Fill so it shows immediately.
|
/// and switches the background mode to Fill so it shows immediately.
|
||||||
PropBgColorChanged(AcadColor),
|
PropBgColorChanged(AcadColor),
|
||||||
|
/// Toggle the generic per-field colour picker (e.g. a hatch gradient colour)
|
||||||
|
/// identified by its property field name.
|
||||||
|
PropColorFieldToggle(String),
|
||||||
|
/// User picked a colour for a generic per-field colour row (hatch gradient
|
||||||
|
/// `Color 1` / `Color 2`), routed by the field name.
|
||||||
|
PropColorFieldChanged { field: String, color: AcadColor },
|
||||||
/// Collapse the inline color picker dropdown. Fired when another
|
/// Collapse the inline color picker dropdown. Fired when another
|
||||||
/// properties-panel dropdown (a combo_box) opens, so at most one panel
|
/// properties-panel dropdown (a combo_box) opens, so at most one panel
|
||||||
/// dropdown is open at a time and they can't overlap. (#235)
|
/// dropdown is open at a time and they can't overlap. (#235)
|
||||||
|
|||||||
+60
-7
@@ -2279,13 +2279,18 @@ impl OpenCADStudio {
|
|||||||
if !handles.is_empty() {
|
if !handles.is_empty() {
|
||||||
self.push_undo_snapshot(i, "CHPROP");
|
self.push_undo_snapshot(i, "CHPROP");
|
||||||
for &handle in &handles {
|
for &handle in &handles {
|
||||||
if let Some(acadrust::EntityType::MText(m)) =
|
match self.tabs[i].scene.document.get_entity_mut(handle) {
|
||||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
Some(acadrust::EntityType::MText(m)) => {
|
||||||
{
|
m.background_color = color.clone();
|
||||||
m.background_color = color.clone();
|
// Picking a colour turns the background on in Fill
|
||||||
// Picking a colour turns the background on in Fill
|
// mode (specific colour), preserving the frame bit.
|
||||||
// mode (specific colour), preserving the frame bit.
|
m.background_fill_flags =
|
||||||
m.background_fill_flags = (m.background_fill_flags & !0x02) | 0x01;
|
(m.background_fill_flags & !0x02) | 0x01;
|
||||||
|
}
|
||||||
|
Some(acadrust::EntityType::Hatch(h)) => {
|
||||||
|
crate::entities::hatch::set_background_color(h, &color);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.invalidate_property_targets(i, &handles);
|
self.invalidate_property_targets(i, &handles);
|
||||||
@@ -2296,6 +2301,54 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::PropColorFieldToggle(field) => {
|
||||||
|
let i = self.active_tab;
|
||||||
|
let p = &mut self.tabs[i].properties;
|
||||||
|
p.open_color_field = if p.open_color_field.as_deref() == Some(field.as_str()) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(field)
|
||||||
|
};
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::PropColorFieldChanged { field, color } => {
|
||||||
|
let i = self.active_tab;
|
||||||
|
let handles = self.property_target_handles(i);
|
||||||
|
if !handles.is_empty() {
|
||||||
|
self.push_undo_snapshot(i, "CHPROP");
|
||||||
|
let idx = if field == "gradient_color_2" { 1 } else { 0 };
|
||||||
|
for &handle in &handles {
|
||||||
|
if let Some(acadrust::EntityType::Hatch(h)) =
|
||||||
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||||
|
{
|
||||||
|
while h.gradient_color.colors.len() <= idx {
|
||||||
|
let value = if h.gradient_color.colors.is_empty() {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
h.gradient_color.colors.push(
|
||||||
|
acadrust::entities::hatch::GradientColorEntry {
|
||||||
|
value,
|
||||||
|
color: acadrust::types::Color::Index(7),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
h.gradient_color.colors[idx].color = color.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Rebuild hatch seeds so the gradient fill picks up the new
|
||||||
|
// colour (synced_hatch_models only patches the main colour).
|
||||||
|
self.tabs[i].scene.populate_hatches_from_document();
|
||||||
|
self.invalidate_property_targets(i, &handles);
|
||||||
|
self.tabs[i].properties.open_color_field = None;
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.refresh_properties();
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
Message::PropColorPickerClose => {
|
Message::PropColorPickerClose => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.tabs[i].properties.color_picker_open = false;
|
self.tabs[i].properties.color_picker_open = false;
|
||||||
|
|||||||
+81
-17
@@ -99,9 +99,51 @@ fn boundary_centroid(h: &Hatch) -> Option<(f64, f64)> {
|
|||||||
(n > 0.0).then(|| (sx / n, sy / n))
|
(n > 0.0).then(|| (sx / n, sy / n))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format a color for a read-only row (`ByLayer` / `ByBlock` / index / RGB).
|
/// Read the hatch background colour from its `HATCHBACKGROUNDCOLOR` extended
|
||||||
fn color_str(c: acadrust::types::Color) -> String {
|
/// data (group 1071, packed true colour: high byte = colour method, low three =
|
||||||
c.to_string()
|
/// RGB). `None` when unset or not a true-colour value.
|
||||||
|
pub fn background_color(h: &Hatch) -> Option<acadrust::types::Color> {
|
||||||
|
let rec = h.common.extended_data.get_record("HATCHBACKGROUNDCOLOR")?;
|
||||||
|
let raw = rec.values.iter().find_map(|v| match v {
|
||||||
|
acadrust::xdata::XDataValue::Integer32(n) => Some(*n as u32),
|
||||||
|
_ => None,
|
||||||
|
})?;
|
||||||
|
match (raw >> 24) & 0xFF {
|
||||||
|
0xC2 | 0xC3 => Some(acadrust::types::Color::Rgb {
|
||||||
|
r: ((raw >> 16) & 0xFF) as u8,
|
||||||
|
g: ((raw >> 8) & 0xFF) as u8,
|
||||||
|
b: (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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace (or insert) the hatch's `HATCHBACKGROUNDCOLOR` extended-data record,
|
||||||
|
/// preserving every other application's records.
|
||||||
|
pub fn set_background_color(h: &mut Hatch, c: &acadrust::types::Color) {
|
||||||
|
use acadrust::xdata::{ExtendedDataRecord, XDataValue};
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
let mut rec = ExtendedDataRecord::new(APP);
|
||||||
|
rec.add_value(XDataValue::Integer32(pack_background_color(c)));
|
||||||
|
h.common.extended_data.add_record(rec);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn properties(h: &Hatch) -> Vec<PropSection> {
|
fn properties(h: &Hatch) -> Vec<PropSection> {
|
||||||
@@ -117,8 +159,10 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
|
|||||||
};
|
};
|
||||||
let area = boundary_area(h);
|
let area = boundary_area(h);
|
||||||
let g = &h.gradient_color;
|
let g = &h.gradient_color;
|
||||||
let grad_c1 = g.colors.first().map(|e| color_str(e.color)).unwrap_or_default();
|
let default_col = acadrust::types::Color::Index(7);
|
||||||
let grad_c2 = g.colors.get(1).map(|e| color_str(e.color)).unwrap_or_default();
|
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_col = background_color(h).unwrap_or(default_col);
|
||||||
|
|
||||||
if g.enabled {
|
if g.enabled {
|
||||||
// ── Gradient fill ──────────────────────────────────────────────────
|
// ── Gradient fill ──────────────────────────────────────────────────
|
||||||
@@ -128,10 +172,25 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
|
|||||||
PropSection {
|
PropSection {
|
||||||
title: "Pattern".into(),
|
title: "Pattern".into(),
|
||||||
props: vec![
|
props: vec![
|
||||||
ro("Type", "fill_type", grad_type),
|
Property {
|
||||||
|
label: "Type".into(),
|
||||||
|
field: "fill_type",
|
||||||
|
value: PropValue::Choice {
|
||||||
|
selected: grad_type.to_string(),
|
||||||
|
options: vec!["Two color".into(), "One color".into()],
|
||||||
|
},
|
||||||
|
},
|
||||||
ro("Gradient name", "gradient_name", g.name.clone()),
|
ro("Gradient name", "gradient_name", g.name.clone()),
|
||||||
ro("Color 1", "gradient_color_1", grad_c1),
|
Property {
|
||||||
ro("Color 2", "gradient_color_2", grad_c2),
|
label: "Color 1".into(),
|
||||||
|
field: "gradient_color_1",
|
||||||
|
value: PropValue::ColorChoice(grad_c1),
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
label: "Color 2".into(),
|
||||||
|
field: "gradient_color_2",
|
||||||
|
value: PropValue::ColorChoice(grad_c2),
|
||||||
|
},
|
||||||
edit("Angle", "pattern_angle", g.angle.to_degrees()),
|
edit("Angle", "pattern_angle", g.angle.to_degrees()),
|
||||||
ro("Centered", "gradient_centered", centered),
|
ro("Centered", "gradient_centered", centered),
|
||||||
],
|
],
|
||||||
@@ -140,8 +199,6 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
|
|||||||
title: "Geometry".into(),
|
title: "Geometry".into(),
|
||||||
props: vec![
|
props: vec![
|
||||||
edit("Elevation", "elevation", h.elevation),
|
edit("Elevation", "elevation", h.elevation),
|
||||||
ro("Origin X", "origin_x", String::new()),
|
|
||||||
ro("Origin Y", "origin_y", String::new()),
|
|
||||||
ro("Area", "area", format!("{:.4}", area)),
|
ro("Area", "area", format!("{:.4}", area)),
|
||||||
ro("Cumulative area", "cumulative_area", format!("{:.4}", area)),
|
ro("Cumulative area", "cumulative_area", format!("{:.4}", area)),
|
||||||
],
|
],
|
||||||
@@ -156,7 +213,11 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
|
|||||||
),
|
),
|
||||||
ro("Annotative", "annotative", String::new()),
|
ro("Annotative", "annotative", String::new()),
|
||||||
ro("Island detection style", "style", style),
|
ro("Island detection style", "style", style),
|
||||||
ro("Background color", "background_color", String::new()),
|
Property {
|
||||||
|
label: "Background color".into(),
|
||||||
|
field: "background_color",
|
||||||
|
value: PropValue::ColorChoice(bg_col),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -234,12 +295,11 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
|
|||||||
double_row,
|
double_row,
|
||||||
associative_row,
|
associative_row,
|
||||||
island_row,
|
island_row,
|
||||||
ro("Background color", "background_color", String::new()),
|
Property {
|
||||||
// ── Gradient info (read-only) ──────────────────────────────
|
label: "Background color".into(),
|
||||||
ro("Gradient colors", "gradient_colors", g.colors.len().to_string()),
|
field: "background_color",
|
||||||
ro("Gradient tint", "gradient_tint", format!("{:.4}", g.color_tint)),
|
value: PropValue::ColorChoice(bg_col),
|
||||||
ro("Gradient color 1", "gradient_color_1", grad_c1),
|
},
|
||||||
ro("Gradient color 2", "gradient_color_2", grad_c2),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
PropSection {
|
PropSection {
|
||||||
@@ -292,6 +352,10 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
|||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
"fill_type" => {
|
||||||
|
h.gradient_color.is_single_color = value == "One color";
|
||||||
|
return;
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
let Some(v) = parse_f64(value) else {
|
let Some(v) = parse_f64(value) else {
|
||||||
|
|||||||
+18
-3
@@ -483,12 +483,23 @@ impl Scene {
|
|||||||
self.model_space_block_handle(),
|
self.model_space_block_handle(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.map(|(&handle, model)| {
|
.flat_map(|(&handle, model)| {
|
||||||
let entity = self.document.get_entity(handle);
|
let entity = self.document.get_entity(handle);
|
||||||
let mut m = model.clone();
|
let mut m = model.clone();
|
||||||
|
// 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;
|
||||||
if let Some(e) = entity {
|
if let Some(e) = entity {
|
||||||
m.color = self.render_style(e).0;
|
m.color = self.render_style(e).0;
|
||||||
if let EntityType::Hatch(dxf) = e {
|
if let EntityType::Hatch(dxf) = e {
|
||||||
|
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);
|
||||||
|
b.name = "SOLID".into();
|
||||||
|
backdrop = Some(b);
|
||||||
|
}
|
||||||
match &mut m.pattern {
|
match &mut m.pattern {
|
||||||
// Pattern built from the hatch's own stored lines is
|
// Pattern built from the hatch's own stored lines is
|
||||||
// already final (scale 1 / angle 0) — don't re-apply
|
// already final (scale 1 / angle 0) — don't re-apply
|
||||||
@@ -516,8 +527,12 @@ impl Scene {
|
|||||||
if self.selected.contains(&handle) {
|
if self.selected.contains(&handle) {
|
||||||
m.color = [0.15, 0.55, 1.00, m.color[3]];
|
m.color = [0.15, 0.55, 1.00, m.color[3]];
|
||||||
}
|
}
|
||||||
m.draw_depth = depth_map.get(&handle.value()).copied().unwrap_or(0.0);
|
let d = depth_map.get(&handle.value()).copied().unwrap_or(0.0);
|
||||||
m
|
m.draw_depth = d;
|
||||||
|
if let Some(b) = &mut backdrop {
|
||||||
|
b.draw_depth = d;
|
||||||
|
}
|
||||||
|
backdrop.into_iter().chain(std::iter::once(m))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,9 @@ pub struct PropertiesPanel {
|
|||||||
/// from `color_picker_open` so the entity colour and the background colour
|
/// from `color_picker_open` so the entity colour and the background colour
|
||||||
/// pickers are independent.
|
/// pickers are independent.
|
||||||
pub bg_color_picker_open: bool,
|
pub bg_color_picker_open: bool,
|
||||||
|
/// Field name of the generic per-field colour picker currently open (e.g. a
|
||||||
|
/// hatch gradient colour), or `None`. Keeps each field's picker independent.
|
||||||
|
pub open_color_field: Option<String>,
|
||||||
/// Which vertex a multi-vertex entity (polyline) is focused on — driven by
|
/// Which vertex a multi-vertex entity (polyline) is focused on — driven by
|
||||||
/// the Current Vertex ◀ / ▶ stepper. Reset to 0 when the selection changes.
|
/// the Current Vertex ◀ / ▶ stepper. Reset to 0 when the selection changes.
|
||||||
pub prop_vertex: usize,
|
pub prop_vertex: usize,
|
||||||
@@ -173,6 +176,7 @@ impl Default for PropertiesPanel {
|
|||||||
color_picker_open: false,
|
color_picker_open: false,
|
||||||
color_palette_open: false,
|
color_palette_open: false,
|
||||||
bg_color_picker_open: false,
|
bg_color_picker_open: false,
|
||||||
|
open_color_field: None,
|
||||||
prop_vertex: 0,
|
prop_vertex: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,6 +434,27 @@ impl PropertiesPanel {
|
|||||||
);
|
);
|
||||||
return prop_row_widget(label, selector);
|
return prop_row_widget(label, selector);
|
||||||
}
|
}
|
||||||
|
// Generic per-field colour picker (hatch gradient colours) — routes to
|
||||||
|
// the field, not the entity's main colour.
|
||||||
|
if field == "gradient_color_1" || field == "gradient_color_2" {
|
||||||
|
let open = self.open_color_field.as_deref() == Some(field);
|
||||||
|
let fsel = field.to_string();
|
||||||
|
let selector = crate::ui::color_select::color_selector(
|
||||||
|
color,
|
||||||
|
open,
|
||||||
|
crate::ui::color_select::ColorExtras {
|
||||||
|
by_layer: false,
|
||||||
|
by_block: false,
|
||||||
|
},
|
||||||
|
move |c| Message::PropColorFieldChanged {
|
||||||
|
field: fsel.clone(),
|
||||||
|
color: c,
|
||||||
|
},
|
||||||
|
Message::PropColorFieldToggle(field.to_string()),
|
||||||
|
Message::PropColorFieldToggle(field.to_string()),
|
||||||
|
);
|
||||||
|
return prop_row_widget(label, selector);
|
||||||
|
}
|
||||||
let selector = crate::ui::color_select::color_selector(
|
let selector = crate::ui::color_select::color_selector(
|
||||||
color,
|
color,
|
||||||
self.color_picker_open,
|
self.color_picker_open,
|
||||||
|
|||||||
Reference in new issue
Block a user