diff --git a/src/app/mod.rs b/src/app/mod.rs index d19c61898..35249746f 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -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) diff --git a/src/app/mtext_editor.rs b/src/app/mtext_editor.rs index e2f6dc5b1..45abacb9c 100644 --- a/src/app/mtext_editor.rs +++ b/src/app/mtext_editor.rs @@ -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 = 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); diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 610403391..542fbdb51 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -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; diff --git a/src/entities/insert.rs b/src/entities/insert.rs index 582319b52..ace4d569a 100644 --- a/src/entities/insert.rs +++ b/src/entities/insert.rs @@ -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. diff --git a/src/entities/mtext.rs b/src/entities/mtext.rs index e44f1e38d..238e0d66f 100644 --- a/src/entities/mtext.rs +++ b/src/entities/mtext.rs @@ -204,7 +204,6 @@ fn properties(t: &MText, text_style_names: &[String]) -> Vec { // 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 { _ => "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 { diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index ebc7b94ae..8f86dbcfc 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -1071,6 +1071,7 @@ pub trait MultiLeaderTess { line_weight_px: f32, anno_scale: f32, world_per_pixel: Option, + bg_color: [f32; 4], ) -> Vec; } @@ -1084,6 +1085,7 @@ impl MultiLeaderTess for MultiLeader { line_weight_px: f32, anno_scale: f32, world_per_pixel: Option, + bg_color: [f32; 4], ) -> Vec { 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(); diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 6af8cc9b9..d013a37f9 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -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![]; diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 39e3c6880..19a1d1c58 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -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; diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index bb75a7ad6..d26dde6e3 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -86,6 +86,10 @@ pub fn tessellate( line_weight_px: f32, anno_scale: f32, world_per_pixel: Option, + // 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 { 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 diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 6621638ad..dcda84b58 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -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,