forked from 1b-agent/opencadstudio-upstream
feat(text): MTEXT background fill / mask + text frame, editable in Properties
Render the MTEXT background (never drawn before — the flags were only shown read-only in the panel) and make it editable. - Text arm emits an opaque fill_tris rect behind the glyphs: flag 0x01 uses the entity background colour, 0x02 uses the canvas colour (a wipeout-style mask). Box = glyph bounds padded by (background_scale - 1) × text height. - Text frame (flag 0x10) draws a closed rectangle in the text colour around the same box. - Threaded `bg_color` into `tessellate()` (+ all callers and the MultiLeaderTess trait) so the mask can use the drawing-window colour. - Properties: "Background mask" is a Off / Fill / Mask dropdown, "Background color" is its own colour picker (independent of the entity colour — new PropBgColor* messages; picking a colour switches to Fill mode), and "Text frame" is a checkbox toggle. Edits re-tessellate so the change shows live. Renders via the face3d fill path, so the fill shows with FILLMODE on (same as MLEADER background fill); the frame is a stroke and always shows. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
110683f419
commit
cd7306f530
10 files changed
+223
-14
No files matched your search
@@ -1504,6 +1504,11 @@ pub enum Message {
|
||||
PropAttrCommit(String),
|
||||
/// Toggle the inline color picker dropdown open/closed.
|
||||
PropColorPickerToggle,
|
||||
/// Toggle the MTEXT background-colour picker dropdown open/closed.
|
||||
PropBgColorPickerToggle,
|
||||
/// User picked an MTEXT background fill colour — sets `background_color`
|
||||
/// and switches the background mode to Fill so it shows immediately.
|
||||
PropBgColorChanged(AcadColor),
|
||||
/// Collapse the inline color picker dropdown. Fired when another
|
||||
/// properties-panel dropdown (a combo_box) opens, so at most one panel
|
||||
/// dropdown is open at a time and they can't overlap. (#235)
|
||||
|
||||
@@ -396,6 +396,7 @@ impl super::OpenCADStudio {
|
||||
let mt = ed.build_mtext();
|
||||
let entity = EntityType::MText(mt.clone());
|
||||
let anno = self.tabs[i].scene.annotation_scale;
|
||||
let bg = self.tabs[i].scene.bg_color;
|
||||
let wires: Vec<WireModel> = tessellate::tessellate(
|
||||
&self.tabs[i].scene.document,
|
||||
ed.editing.unwrap_or(Handle::new(1)),
|
||||
@@ -407,6 +408,7 @@ impl super::OpenCADStudio {
|
||||
1.0,
|
||||
anno,
|
||||
None,
|
||||
bg,
|
||||
);
|
||||
// Per-character boxes share the preview wires' absolute coordinate frame.
|
||||
let boxes = crate::entities::mtext::glyph_boxes(&mt, &self.tabs[i].scene.document);
|
||||
|
||||
@@ -2214,6 +2214,36 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::PropBgColorPickerToggle => {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].properties.bg_color_picker_open =
|
||||
!self.tabs[i].properties.bg_color_picker_open;
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::PropBgColorChanged(color) => {
|
||||
let i = self.active_tab;
|
||||
let handles = self.property_target_handles(i);
|
||||
if !handles.is_empty() {
|
||||
self.push_undo_snapshot(i, "CHPROP");
|
||||
for &handle in &handles {
|
||||
if let Some(acadrust::EntityType::MText(m)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
m.background_color = color.clone();
|
||||
// Picking a colour turns the background on in Fill
|
||||
// mode (specific colour), preserving the frame bit.
|
||||
m.background_fill_flags = (m.background_fill_flags & !0x02) | 0x01;
|
||||
}
|
||||
}
|
||||
self.invalidate_property_targets(i, &handles);
|
||||
self.tabs[i].properties.bg_color_picker_open = false;
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::PropColorPickerClose => {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].properties.color_picker_open = false;
|
||||
|
||||
@@ -239,6 +239,7 @@ pub(crate) fn append_insert_attribute_wires(
|
||||
sub_lw_px,
|
||||
anno_scale,
|
||||
None,
|
||||
bg_color,
|
||||
);
|
||||
// Use the INSERT's handle so selection / picking groups attribute
|
||||
// text with the parent insert instead of treating it as a stray text.
|
||||
|
||||
+51
-11
@@ -204,7 +204,6 @@ fn properties(t: &MText, text_style_names: &[String]) -> Vec<PropSection> {
|
||||
// Absolute line-space distance for single spacing (~1.66 * height) scaled
|
||||
// by the line-spacing factor.
|
||||
let line_space_distance = t.height * 1.666_666_666_666_667 * t.line_spacing_factor;
|
||||
let bg_mask_on = (t.background_fill_flags & 0x03) != 0;
|
||||
let text_frame_on = (t.background_fill_flags & 0x10) != 0;
|
||||
vec![
|
||||
PropSection {
|
||||
@@ -280,18 +279,38 @@ fn properties(t: &MText, text_style_names: &[String]) -> Vec<PropSection> {
|
||||
_ => "At least",
|
||||
},
|
||||
),
|
||||
ro(
|
||||
"Background mask",
|
||||
"background_mask",
|
||||
if bg_mask_on { "On" } else { "Off" },
|
||||
),
|
||||
Property {
|
||||
label: "Background mask".into(),
|
||||
field: "background_mask",
|
||||
value: PropValue::Choice {
|
||||
selected: if t.background_fill_flags & 0x01 != 0 {
|
||||
"Fill".into()
|
||||
} else if t.background_fill_flags & 0x02 != 0 {
|
||||
"Mask".into()
|
||||
} else {
|
||||
"Off".into()
|
||||
},
|
||||
options: ["Off", "Fill", "Mask"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: "Background color".into(),
|
||||
field: "background_color",
|
||||
value: PropValue::ColorChoice(t.background_color.clone()),
|
||||
},
|
||||
edit("Defined width", "rect_w", t.rectangle_width),
|
||||
edit("Defined height", "rect_h", t.rectangle_height.unwrap_or(0.0)),
|
||||
ro(
|
||||
"Text frame",
|
||||
"text_frame",
|
||||
if text_frame_on { "On" } else { "Off" },
|
||||
),
|
||||
Property {
|
||||
label: "Text frame".into(),
|
||||
field: "text_frame",
|
||||
value: PropValue::BoolToggle {
|
||||
field: "text_frame",
|
||||
value: text_frame_on,
|
||||
},
|
||||
},
|
||||
ro(
|
||||
"Columns",
|
||||
"columns",
|
||||
@@ -336,6 +355,27 @@ fn apply_geom_prop(t: &mut MText, field: &str, value: &str) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
"background_mask" => {
|
||||
// Clear both fill bits, then set the chosen one. 0x01 = use the
|
||||
// background-fill colour, 0x02 = use the drawing-window colour (mask).
|
||||
t.background_fill_flags &= !0x03;
|
||||
match value {
|
||||
"Fill" => t.background_fill_flags |= 0x01,
|
||||
"Mask" => t.background_fill_flags |= 0x02,
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
"text_frame" => {
|
||||
// Rendered as a checkbox (BoolToggle) → the toggle sends "toggle";
|
||||
// flip the frame bit. Accept explicit "On"/"Off" too for safety.
|
||||
match value {
|
||||
"toggle" => t.background_fill_flags ^= 0x10,
|
||||
"On" => t.background_fill_flags |= 0x10,
|
||||
_ => t.background_fill_flags &= !0x10,
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let Some(v) = crate::entities::common::parse_f64(value) else {
|
||||
|
||||
@@ -1071,6 +1071,7 @@ pub trait MultiLeaderTess {
|
||||
line_weight_px: f32,
|
||||
anno_scale: f32,
|
||||
world_per_pixel: Option<f32>,
|
||||
bg_color: [f32; 4],
|
||||
) -> Vec<crate::scene::model::wire_model::WireModel>;
|
||||
}
|
||||
|
||||
@@ -1084,6 +1085,7 @@ impl MultiLeaderTess for MultiLeader {
|
||||
line_weight_px: f32,
|
||||
anno_scale: f32,
|
||||
world_per_pixel: Option<f32>,
|
||||
bg_color: [f32; 4],
|
||||
) -> Vec<crate::scene::model::wire_model::WireModel> {
|
||||
use crate::scene::convert::tessellate::{
|
||||
append_arrow, arrow_from_block, color_or_inherit, tessellate, ArrowKind, DimGeom,
|
||||
@@ -1354,6 +1356,7 @@ impl MultiLeaderTess for MultiLeader {
|
||||
leader_lw_px,
|
||||
1.0,
|
||||
None,
|
||||
bg_color,
|
||||
);
|
||||
for w in &mut sub_wires {
|
||||
w.name = name.clone();
|
||||
|
||||
Vendored
+1
-1
@@ -429,7 +429,7 @@ fn tessellate_sub_local(
|
||||
// before casting to f32 — same precision-preservation trick used for
|
||||
// top-level entities, applied per-defn.
|
||||
let wires_out = tessellate::tessellate(
|
||||
doc, h, sub, false, sub_color, pat_len, pat, lw_px, anno_scale, None,
|
||||
doc, h, sub, false, sub_color, pat_len, pat, lw_px, anno_scale, None, bg_color,
|
||||
);
|
||||
if wires_out.is_empty() {
|
||||
return vec![];
|
||||
|
||||
@@ -196,6 +196,7 @@ pub(crate) fn tessellate_entity(
|
||||
1.5,
|
||||
1.0,
|
||||
world_per_pixel,
|
||||
bg_color,
|
||||
);
|
||||
let ab = entity_aabb(e);
|
||||
for w in &mut wires {
|
||||
@@ -343,6 +344,7 @@ pub(crate) fn tessellate_entity(
|
||||
line_weight_px,
|
||||
anno_scale,
|
||||
world_per_pixel,
|
||||
bg_color,
|
||||
);
|
||||
for w in &mut wires {
|
||||
w.aci = aci;
|
||||
@@ -579,6 +581,7 @@ pub(crate) fn tessellate_entity(
|
||||
sub_line_weight_px,
|
||||
anno_scale,
|
||||
world_per_pixel,
|
||||
bg_color,
|
||||
);
|
||||
for w in &mut wires {
|
||||
w.name = h.value().to_string();
|
||||
@@ -753,6 +756,7 @@ pub(crate) fn tessellate_entity(
|
||||
line_weight_px,
|
||||
anno_scale,
|
||||
world_per_pixel,
|
||||
bg_color,
|
||||
);
|
||||
for b in &mut bases {
|
||||
b.aci = aci;
|
||||
|
||||
@@ -86,6 +86,10 @@ pub fn tessellate(
|
||||
line_weight_px: f32,
|
||||
anno_scale: f32,
|
||||
world_per_pixel: Option<f32>,
|
||||
// Canvas background colour — used for the MTEXT background *mask* fill
|
||||
// (flag 0x02, "use drawing window colour") so the mask erases geometry
|
||||
// behind the text the way a wipeout does.
|
||||
bg_color: [f32; 4],
|
||||
) -> Vec<WireModel> {
|
||||
let color = if selected {
|
||||
WireModel::SELECTED
|
||||
@@ -327,6 +331,100 @@ pub fn tessellate(
|
||||
} else {
|
||||
WireModel::UNBOUNDED_AABB
|
||||
};
|
||||
// MTEXT background fill / mask: an opaque rectangle behind
|
||||
// the glyphs, emitted first so it renders under the text.
|
||||
// Flag 0x01 → the entity's background-fill colour; 0x02 →
|
||||
// the drawing-window (canvas) colour, which masks geometry
|
||||
// behind the text like a wipeout. Box = glyph bounds padded
|
||||
// by (background_scale - 1) × text height.
|
||||
if sdf_text && text_aabb != WireModel::UNBOUNDED_AABB {
|
||||
if let EntityType::MText(m) = entity {
|
||||
let has_fill = m.background_fill_flags & 0x03 != 0;
|
||||
let has_frame = m.background_fill_flags & 0x10 != 0;
|
||||
if has_fill || has_frame {
|
||||
// Padded box shared by the fill and the frame.
|
||||
let th = (m.height * anno) as f32;
|
||||
let pad = ((m.background_scale as f32) - 1.0).max(0.0) * th;
|
||||
let [bnx, bny, bxx, bxy] = text_aabb;
|
||||
let (l, b, r, t) = (
|
||||
(bnx - pad) as f64,
|
||||
(bny - pad) as f64,
|
||||
(bxx + pad) as f64,
|
||||
(bxy + pad) as f64,
|
||||
);
|
||||
// Fill / mask — two triangles behind the glyphs.
|
||||
if has_fill {
|
||||
let fill_color = if m.background_fill_flags & 0x01 != 0 {
|
||||
color_or_inherit(&m.background_color, bg_color)
|
||||
} else {
|
||||
bg_color
|
||||
};
|
||||
let corners = [[l, b], [r, b], [r, t], [l, t]];
|
||||
let mut ft = Vec::with_capacity(6);
|
||||
let mut ftl = Vec::with_capacity(6);
|
||||
for &k in &[0usize, 1, 2, 0, 2, 3] {
|
||||
let (h, lo) =
|
||||
split_ds_xyz(corners[k][0], corners[k][1], elev_v);
|
||||
ft.push(h);
|
||||
ftl.push(lo);
|
||||
}
|
||||
wires.push(WireModel {
|
||||
text_verts: Vec::new(),
|
||||
name: name.clone(),
|
||||
points: vec![],
|
||||
points_low: Vec::new(),
|
||||
color: fill_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: ft,
|
||||
fill_tris_low: ftl,
|
||||
});
|
||||
}
|
||||
// Text frame — a closed rectangle in the text
|
||||
// colour around the same box.
|
||||
if has_frame {
|
||||
let loop_xy =
|
||||
[[l, b], [r, b], [r, t], [l, t], [l, b]];
|
||||
let mut fp = Vec::with_capacity(5);
|
||||
let mut fpl = Vec::with_capacity(5);
|
||||
for &[x, y] in &loop_xy {
|
||||
let (h, lo) = split_ds_xyz(x, y, elev_v);
|
||||
fp.push(h);
|
||||
fpl.push(lo);
|
||||
}
|
||||
wires.push(WireModel {
|
||||
text_verts: Vec::new(),
|
||||
name: name.clone(),
|
||||
points: fp,
|
||||
points_low: fpl,
|
||||
color: entity_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
fill_tris_low: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Debug (env OCS_TEXT_BOX): draw a rectangle around the glyph
|
||||
// bounds as a separate outline wire so the text box is
|
||||
// visible for testing. The empty text wire below is left
|
||||
|
||||
+28
-2
@@ -145,6 +145,10 @@ pub struct PropertiesPanel {
|
||||
pub color_picker_open: bool,
|
||||
/// Whether the full 16×16 ACI palette is expanded inside the color picker.
|
||||
pub color_palette_open: bool,
|
||||
/// Whether the MTEXT background-colour picker dropdown is open. Separate
|
||||
/// from `color_picker_open` so the entity colour and the background colour
|
||||
/// pickers are independent.
|
||||
pub bg_color_picker_open: bool,
|
||||
}
|
||||
|
||||
impl Default for PropertiesPanel {
|
||||
@@ -165,6 +169,7 @@ impl Default for PropertiesPanel {
|
||||
source_handles: vec![],
|
||||
color_picker_open: false,
|
||||
color_palette_open: false,
|
||||
bg_color_picker_open: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,7 +325,7 @@ impl PropertiesPanel {
|
||||
for prop in §ion.props {
|
||||
match &prop.value {
|
||||
PropValue::ColorChoice(color) => {
|
||||
col = col.push(self.render_color_row(&prop.label, *color));
|
||||
col = col.push(self.render_color_row(&prop.label, prop.field, *color));
|
||||
}
|
||||
PropValue::ColorVaries => {
|
||||
col = col.push(self.render_color_varies_row(&prop.label));
|
||||
@@ -396,7 +401,28 @@ impl PropertiesPanel {
|
||||
|
||||
// ── Color row (custom picker) ─────────────────────────────────────────
|
||||
|
||||
fn render_color_row<'a>(&'a self, label: &'a str, color: AcadColor) -> Element<'a, Message> {
|
||||
fn render_color_row<'a>(
|
||||
&'a self,
|
||||
label: &'a str,
|
||||
field: &'static str,
|
||||
color: AcadColor,
|
||||
) -> Element<'a, Message> {
|
||||
// MTEXT background colour uses its own picker state + messages so it
|
||||
// routes to `background_color`, not the entity's main colour.
|
||||
if field == "background_color" {
|
||||
let selector = crate::ui::color_select::color_selector(
|
||||
color,
|
||||
self.bg_color_picker_open,
|
||||
crate::ui::color_select::ColorExtras {
|
||||
by_layer: true,
|
||||
by_block: true,
|
||||
},
|
||||
Message::PropBgColorChanged,
|
||||
Message::PropBgColorPickerToggle,
|
||||
Message::PropBgColorPickerToggle,
|
||||
);
|
||||
return prop_row_widget(label, selector);
|
||||
}
|
||||
let selector = crate::ui::color_select::color_selector(
|
||||
color,
|
||||
self.color_picker_open,
|
||||
|
||||
Reference in new issue
Block a user