From a36e0d7fdc3937230d95615b8b4a4ab03d1cee63 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 28 Jul 2026 19:53:39 +0300 Subject: [PATCH] fix(matchprop): complete property matching --- src/app/command_driver.rs | 442 ++++++++++++++++++++-- src/app/update/viewport.rs | 114 +++++- src/command.rs | 15 + src/modules/draw/properties/match_prop.rs | 16 + src/scene/preview.rs | 26 ++ 5 files changed, 572 insertions(+), 41 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index b5792778a..2166ab9c6 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -419,6 +419,7 @@ impl OpenCADStudio { // command an ADDSELECTED launched, revert the template-property override // so CLAYER / CECOLOR / … are left unchanged (#239). No-op otherwise. if was_active && self.tabs[i].active_cmd.is_none() { + self.tabs[i].scene.set_hover_highlight(None); self.restore_add_selected_defaults(); } task @@ -444,6 +445,13 @@ impl OpenCADStudio { if let Some(p) = prompt { self.command_line.push_info(&p); } + if !self.tabs[i] + .active_cmd + .as_ref() + .is_some_and(|c| c.entity_pick_highlights_hover()) + { + self.tabs[i].scene.set_hover_highlight(None); + } // The command may have advanced to a step with a different // dynamic-input shape (e.g. FILLET object-pick → radius entry). // Rebuild the fields now so the matching box appears immediately @@ -1339,23 +1347,29 @@ impl OpenCADStudio { CmdResult::MatchProperties { dest, src } => { // The command stays active after each apply so more targets // can keep being picked; Enter / Esc ends it (#362). - let props = self.tabs[i].scene.document.get_entity(src).map(|e| { - let c = e.common(); - ( - c.layer.clone(), - c.color, - c.linetype.clone(), - c.linetype_scale, - c.line_weight, - ) - }); // Special (type-specific) properties travel like AutoCAD's // Special Properties: each is captured from the source when it // carries it and applied only to destinations that support it // (#281). Text formatting crosses TEXT ↔ MTEXT (#361); the dim // style crosses Dimension / Leader / Tolerance. let src_clone = self.tabs[i].scene.document.get_entity(src).cloned(); - let transparency = src_clone.as_ref().map(|e| e.common().transparency.clone()); + let src_common = src_clone.as_ref().map(|e| e.common().clone()); + let thickness = src_clone + .as_ref() + .and_then(crate::scene::view::dispatch::entity_thickness); + // This application record is the hatch background colour. + // Keep the outer Option to distinguish "source is not a + // hatch" from "source hatch has no background". + let hatch_background_xdata: Option>> = + match src_clone.as_ref() { + Some(acadrust::EntityType::Hatch(h)) => Some( + h.common + .extended_data + .get_record("HATCHBACKGROUNDCOLOR") + .map(|r| r.values.clone()), + ), + _ => None, + }; // Dimension-style overrides ride the ACAD record, identified // by a leading DSTYLE string. Matching replicates that payload // (or clears the destination when the source has none). @@ -1369,26 +1383,53 @@ impl OpenCADStudio { }) .map(|e| crate::entities::dim_override::pairs(&e.common().extended_data)); - if let Some((layer, color, linetype, lt_scale, lw)) = props { + if let Some(common) = src_common { self.push_undo_snapshot(i, "MATCHPROP"); - let mut any_dim = false; for h in &dest { + let mut is_dim = false; + let mut is_hatch = false; if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) { - e.as_entity_mut().set_layer(layer.clone()); - crate::scene::view::dispatch::apply_color(e, color); - crate::scene::view::dispatch::apply_line_weight(e, lw); - e.common_mut().linetype = linetype.clone(); - e.common_mut().linetype_scale = lt_scale; - if let Some(t) = &transparency { - e.common_mut().transparency = t.clone(); + e.as_entity_mut().set_layer(common.layer.clone()); + crate::scene::view::dispatch::apply_color(e, common.color); + crate::scene::view::dispatch::apply_line_weight(e, common.line_weight); + { + let dst_common = e.common_mut(); + dst_common.linetype = common.linetype.clone(); + dst_common.linetype_handle = common.linetype_handle; + dst_common.linetype_scale = common.linetype_scale; + dst_common.transparency = common.transparency.clone(); + dst_common.color_book_handle = common.color_book_handle; + dst_common.full_visual_style_handle = + common.full_visual_style_handle; + dst_common.face_visual_style_handle = + common.face_visual_style_handle; + dst_common.edge_visual_style_handle = + common.edge_visual_style_handle; + dst_common.material_flags = common.material_flags; + dst_common.material_handle = common.material_handle; + dst_common.shadow_flags = common.shadow_flags; + dst_common.plotstyle_flags = common.plotstyle_flags; + dst_common.plotstyle_handle = common.plotstyle_handle; + } + if let Some(value) = thickness { + crate::scene::view::dispatch::set_entity_thickness(e, value); } if let Some(se) = &src_clone { - if matches!(e, acadrust::EntityType::Dimension(_)) { - any_dim = true; - } + is_dim = matches!(e, acadrust::EntityType::Dimension(_)); + is_hatch = matches!(e, acadrust::EntityType::Hatch(_)); match_special_props(se, e); } } + if is_hatch { + if let Some(values) = &hatch_background_xdata { + crate::scene::view::dispatch::set_entity_xdata( + &mut self.tabs[i].scene.document, + *h, + "HATCHBACKGROUNDCOLOR", + values.clone(), + ); + } + } // Dim-style overrides follow the style for dimension / // leader destinations — through set_entity_xdata so no // stale raw record survives. @@ -1423,7 +1464,7 @@ impl OpenCADStudio { } // A restyled dimension renders from its baked *D block — // drop the stale block so the new style shows (#398). - if any_dim { + if is_dim { self.tabs[i].scene.invalidate_dim_block_recorded(*h); } // Hatch fills render from a prebuilt model (#415). @@ -3387,24 +3428,63 @@ fn match_special_props(src: &acadrust::EntityType, dst: &mut acadrust::EntityTyp // Text-ish source formatting. let text_fmt = match src { - E::Text(t) => Some((t.style.clone(), t.height, t.width_factor, t.oblique_angle)), - E::MText(m) => Some((m.style.clone(), m.height, 1.0, 0.0)), + E::Text(t) => Some(( + t.style.clone(), + t.height, + Some(t.width_factor), + Some(t.oblique_angle), + )), + E::MText(m) => Some((m.style.clone(), m.height, None, None)), + E::AttributeDefinition(a) => Some(( + a.text_style.clone(), + a.height, + Some(a.width_factor), + Some(a.oblique_angle), + )), + E::AttributeEntity(a) => Some(( + a.text_style.clone(), + a.height, + Some(a.width_factor), + Some(a.oblique_angle), + )), _ => None, }; if let Some((style, height, wf, ob)) = text_fmt { match dst { E::Text(t) => { - t.style = style; + t.style = style.clone(); t.height = height; - if matches!(src, E::Text(_)) { - t.width_factor = wf; - t.oblique_angle = ob; + if let Some(value) = wf { + t.width_factor = value; + } + if let Some(value) = ob { + t.oblique_angle = value; } } E::MText(m) => { - m.style = style; + m.style = style.clone(); m.height = height; } + E::AttributeDefinition(a) => { + a.text_style = style.clone(); + a.height = height; + if let Some(value) = wf { + a.width_factor = value; + } + if let Some(value) = ob { + a.oblique_angle = value; + } + } + E::AttributeEntity(a) => { + a.text_style = style; + a.height = height; + if let Some(value) = wf { + a.width_factor = value; + } + if let Some(value) = ob { + a.oblique_angle = value; + } + } _ => {} } } @@ -3442,16 +3522,81 @@ fn match_special_props(src: &acadrust::EntityType, dst: &mut acadrust::EntityTyp dh.pattern_angle = sh.pattern_angle; dh.pattern_scale = sh.pattern_scale; dh.is_solid = sh.is_solid; + dh.is_double = sh.is_double; + dh.style = sh.style; dh.gradient_color = sh.gradient_color.clone(); } - // Polyline display style: width + linetype generation. - if let (E::LwPolyline(sp), E::LwPolyline(dp)) = (src, dst as &mut E) { - dp.constant_width = sp.constant_width; - dp.plinegen = sp.plinegen; - for v in &mut dp.vertices { - v.start_width = sp.constant_width; - v.end_width = sp.constant_width; + // Polyline display style crosses lightweight and legacy 2D polylines. + // Per-vertex/tapered widths are resampled over the destination vertices; + // they must not be flattened into the source's constant-width field. + if let Some(style) = PolylineMatchStyle::from_entity(src) { + style.apply_to(dst); + } + + if let (E::Leader(sl), E::Leader(dl)) = (src, dst as &mut E) { + dl.arrow_enabled = sl.arrow_enabled; + dl.path_type = sl.path_type; + dl.hookline_direction = sl.hookline_direction; + dl.hookline_enabled = sl.hookline_enabled; + dl.override_color = sl.override_color; + dl.dimension_gap = sl.dimension_gap; + dl.arrowhead_type = sl.arrowhead_type; + dl.arrow_size = sl.arrow_size; + dl.byblock_color = sl.byblock_color; + } + if let (E::Tolerance(st), E::Tolerance(dt)) = (src, dst as &mut E) { + dt.dimension_style_handle = st.dimension_style_handle; + dt.text_height = st.text_height; + dt.dimension_gap = st.dimension_gap; + } + + // Paper-space geometry and view position stay; viewport display/plot + // styling and effective scale follow the source. + if let (E::Viewport(sv), E::Viewport(dv)) = (src, dst as &mut E) { + let scale = crate::scene::vp_effective_scale(sv.custom_scale, sv.view_height, sv.height); + dv.custom_scale = scale; + if scale.abs() > 1e-9 { + dv.view_height = dv.height / scale; + } + dv.status.locked = sv.status.locked; + dv.status.hide_plot = sv.status.hide_plot; + dv.render_mode = sv.render_mode; + dv.style_sheet = sv.style_sheet.clone(); + dv.shade_plot_mode = sv.shade_plot_mode; + dv.background_handle = sv.background_handle; + dv.shade_plot_handle = sv.shade_plot_handle; + dv.visual_style_handle = sv.visual_style_handle; + dv.default_lighting = sv.default_lighting; + dv.default_lighting_type = sv.default_lighting_type; + dv.brightness = sv.brightness; + dv.contrast = sv.contrast; + dv.ambient_color = sv.ambient_color; + } + + if let (E::Table(st), E::Table(dt)) = (src, dst as &mut E) { + dt.table_style_handle = st.table_style_handle; + dt.base_style = st.base_style.clone(); + dt.override_flag = st.override_flag; + dt.override_border_color = st.override_border_color; + dt.override_border_line_weight = st.override_border_line_weight; + dt.override_border_visibility = st.override_border_visibility; + dt.legacy_style_override = st.legacy_style_override.clone(); + dt.legacy_border_colors = st.legacy_border_colors.clone(); + dt.legacy_border_line_weights = st.legacy_border_line_weights.clone(); + dt.legacy_border_visibility = st.legacy_border_visibility.clone(); + } + + if let (E::MLine(sm), E::MLine(dm)) = (src, dst as &mut E) { + dm.style_handle = sm.style_handle; + dm.style_name = sm.style_name.clone(); + dm.style_element_count = sm.style_element_count; + dm.justification = sm.justification; + dm.scale_factor = sm.scale_factor; + // Stored segment offsets bake the old style into each vertex. Empty + // data deliberately selects the renderer's style-derived fallback. + for vertex in &mut dm.vertices { + vertex.segments.clear(); } } @@ -3472,7 +3617,228 @@ fn match_special_props(src: &acadrust::EntityType, dst: &mut acadrust::EntityTyp dm.text_color = sm.text_color; dm.text_frame = sm.text_frame; dm.text_height = sm.text_height; + dm.text_left_attachment = sm.text_left_attachment; + dm.text_right_attachment = sm.text_right_attachment; + dm.text_top_attachment = sm.text_top_attachment; + dm.text_bottom_attachment = sm.text_bottom_attachment; + dm.text_attachment_direction = sm.text_attachment_direction; + dm.text_attachment_point = sm.text_attachment_point; + dm.text_alignment = sm.text_alignment; + dm.text_angle_type = sm.text_angle_type; + dm.text_direction_negative = sm.text_direction_negative; + dm.text_align_in_ipe = sm.text_align_in_ipe; + dm.block_content_color = sm.block_content_color; + dm.block_connection_type = sm.block_connection_type; + dm.block_scale = sm.block_scale; dm.scale_factor = sm.scale_factor; dm.property_override_flags = sm.property_override_flags; + dm.enable_annotation_scale = sm.enable_annotation_scale; + dm.extend_leader_to_text = sm.extend_leader_to_text; + dm.arrowhead_overrides = sm.arrowhead_overrides.clone(); + + let sc = &sm.context; + let dc = &mut dm.context; + dc.scale_factor = sc.scale_factor; + dc.text_height = sc.text_height; + dc.text_width = sc.text_width; + dc.text_boundary_height = sc.text_boundary_height; + dc.line_spacing_factor = sc.line_spacing_factor; + dc.line_spacing_style = sc.line_spacing_style; + dc.text_color = sc.text_color; + dc.text_attachment_point = sc.text_attachment_point; + dc.text_flow_direction = sc.text_flow_direction; + dc.text_alignment = sc.text_alignment; + dc.text_left_attachment = sc.text_left_attachment; + dc.text_right_attachment = sc.text_right_attachment; + dc.text_top_attachment = sc.text_top_attachment; + dc.text_bottom_attachment = sc.text_bottom_attachment; + dc.text_height_automatic = sc.text_height_automatic; + dc.word_break = sc.word_break; + dc.text_style_handle = sc.text_style_handle; + dc.block_content_scale = sc.block_content_scale; + dc.block_content_color = sc.block_content_color; + dc.block_connection_type = sc.block_connection_type; + dc.column_type = sc.column_type; + dc.column_width = sc.column_width; + dc.column_gutter = sc.column_gutter; + dc.column_flow_reversed = sc.column_flow_reversed; + dc.column_sizes = sc.column_sizes.clone(); + dc.background_fill_enabled = sc.background_fill_enabled; + dc.background_mask_fill_on = sc.background_mask_fill_on; + dc.background_fill_color = sc.background_fill_color; + dc.background_scale_factor = sc.background_scale_factor; + dc.background_transparency = sc.background_transparency; + dc.arrowhead_size = sc.arrowhead_size; + dc.landing_gap = sc.landing_gap; + dc.scale_handle = sc.scale_handle; + } + + // External-reference identity, placement and clip geometry stay. Only + // display controls/appearance are matched. + if let (E::RasterImage(si), E::RasterImage(di)) = (src, dst as &mut E) { + di.flags = si.flags; + di.clipping_enabled = si.clipping_enabled; + di.brightness = si.brightness; + di.contrast = si.contrast; + di.fade = si.fade; + di.clip_boundary.clip_mode = si.clip_boundary.clip_mode; + } + if let (E::Wipeout(sw), E::Wipeout(dw)) = (src, dst as &mut E) { + dw.flags = sw.flags; + dw.clipping_enabled = sw.clipping_enabled; + dw.brightness = sw.brightness; + dw.contrast = sw.contrast; + dw.fade = sw.fade; + dw.clip_mode = sw.clip_mode; + } + if let (E::Underlay(su), E::Underlay(du)) = (src, dst as &mut E) { + du.flags = su.flags; + du.contrast = su.contrast; + du.fade = su.fade; + du.clip_inverted = su.clip_inverted; } } + +struct PolylineMatchStyle { + plinegen: bool, + widths: Vec<(f64, f64)>, +} + +impl PolylineMatchStyle { + fn from_entity(entity: &acadrust::EntityType) -> Option { + use acadrust::EntityType as E; + + match entity { + E::LwPolyline(poly) => { + let widths = if poly.vertices.is_empty() { + vec![(poly.constant_width, poly.constant_width)] + } else { + poly.vertices + .iter() + .map(|v| { + ( + width_or_default(v.start_width, poly.constant_width), + width_or_default(v.end_width, poly.constant_width), + ) + }) + .collect() + }; + Some(Self { + plinegen: poly.plinegen, + widths, + }) + } + E::Polyline2D(poly) => { + let widths = if poly.vertices.is_empty() { + vec![(poly.start_width, poly.end_width)] + } else { + poly.vertices + .iter() + .map(|v| { + ( + width_or_default(v.start_width, poly.start_width), + width_or_default(v.end_width, poly.end_width), + ) + }) + .collect() + }; + Some(Self { + plinegen: poly.flags.bits() + & acadrust::entities::PolylineFlags::LINETYPE_CONTINUOUS.bits() + != 0, + widths, + }) + } + _ => None, + } + } + + fn apply_to(&self, entity: &mut acadrust::EntityType) { + use acadrust::EntityType as E; + + match entity { + E::LwPolyline(poly) => { + poly.plinegen = self.plinegen; + let sampled = resample_widths(&self.widths, poly.vertices.len()); + let first = sampled.first().copied().unwrap_or((0.0, 0.0)); + let can_use_constant = widths_are_same(&sampled) && nearly_equal(first.0, first.1); + poly.constant_width = if can_use_constant { first.0 } else { 0.0 }; + for (vertex, (start, end)) in poly.vertices.iter_mut().zip(sampled) { + if can_use_constant { + vertex.start_width = 0.0; + vertex.end_width = 0.0; + } else { + vertex.start_width = start; + vertex.end_width = end; + } + } + } + E::Polyline2D(poly) => { + let mut bits = poly.flags.bits(); + let flag = acadrust::entities::PolylineFlags::LINETYPE_CONTINUOUS.bits(); + if self.plinegen { + bits |= flag; + } else { + bits &= !flag; + } + poly.flags = acadrust::entities::PolylineFlags::from_bits(bits); + let sampled = resample_widths(&self.widths, poly.vertices.len()); + let first = sampled.first().copied().unwrap_or((0.0, 0.0)); + let can_use_defaults = widths_are_same(&sampled); + if can_use_defaults { + poly.start_width = first.0; + poly.end_width = first.1; + } else { + poly.start_width = 0.0; + poly.end_width = 0.0; + } + for (vertex, (start, end)) in poly.vertices.iter_mut().zip(sampled) { + if can_use_defaults { + vertex.start_width = 0.0; + vertex.end_width = 0.0; + } else { + vertex.start_width = start; + vertex.end_width = end; + } + } + } + _ => {} + } + } +} + +fn width_or_default(value: f64, default: f64) -> f64 { + if value.abs() <= 1e-12 { + default + } else { + value + } +} + +fn nearly_equal(a: f64, b: f64) -> bool { + (a - b).abs() <= 1e-9 +} + +fn widths_are_same(widths: &[(f64, f64)]) -> bool { + let Some(first) = widths.first() else { + return true; + }; + widths + .iter() + .all(|value| nearly_equal(value.0, first.0) && nearly_equal(value.1, first.1)) +} + +fn resample_widths(source: &[(f64, f64)], count: usize) -> Vec<(f64, f64)> { + if count == 0 || source.is_empty() { + return Vec::new(); + } + if count == 1 || source.len() == 1 { + return vec![source[0]; count]; + } + (0..count) + .map(|index| { + let source_index = index.saturating_mul(source.len() - 1) / count.saturating_sub(1); + source[source_index] + }) + .collect() +} diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index bad9aef23..0b05d82a9 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1339,7 +1339,19 @@ impl OpenCADStudio { } p } else if needs_entity { - let hover_handle = scene::pick::hit_test::click_hit( + let include_fills = self.tabs[i] + .active_cmd + .as_ref() + .map(|c| c.entity_pick_includes_fills()) + .unwrap_or(false); + let candidate_handles = if include_fills { + self.tabs[i] + .scene + .interaction_candidate_handles(&snap_candidates) + } else { + None + }; + let hovered = scene::pick::hit_test::click_hit( p, &snap_candidates, view_rot, @@ -1348,7 +1360,53 @@ impl OpenCADStudio { self.tabs[i].scene.document.header.lineweight_display, ) .and_then(|s| Scene::handle_from_wire_name(s)) - .unwrap_or(acadrust::Handle::NULL); + .or_else(|| { + if !include_fills { + return None; + } + scene::pick::hit_test::click_hit_hatch( + p, + &self.tabs[i] + .scene + .visible_hatches_for_click(candidate_handles.as_ref()), + view_rot, + eye, + bounds, + candidate_handles.as_ref(), + ) + }) + .or_else(|| { + include_fills.then(|| { + scene::pick::hit_test::click_hit_insert_hatch( + p, + self.tabs[i].scene.insert_hatches_for_click().as_ref(), + view_rot, + eye, + bounds, + candidate_handles.as_ref(), + ) + })? + }) + .or_else(|| { + include_fills.then(|| { + self.tabs[i].scene.solid_hover_hit( + p, + view_rot, + eye, + bounds, + candidate_handles.as_ref(), + ) + })? + }); + let highlights_hover = self.tabs[i] + .active_cmd + .as_ref() + .map(|c| c.entity_pick_highlights_hover()) + .unwrap_or(false); + if highlights_hover { + self.tabs[i].scene.set_hover_highlight(hovered); + } + let hover_handle = hovered.unwrap_or(acadrust::Handle::NULL); let shift = self.shift_down; let mut p = self.tabs[i] .active_cmd @@ -2290,6 +2348,18 @@ impl OpenCADStudio { bounds, scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0, ); + let include_fills = self.tabs[i] + .active_cmd + .as_ref() + .map(|c| c.entity_pick_includes_fills()) + .unwrap_or(false); + let candidate_handles = if include_fills { + self.tabs[i] + .scene + .interaction_candidate_handles(&click_candidates) + } else { + None + }; let hit = scene::pick::hit_test::click_hit( p, &click_candidates, @@ -2298,7 +2368,45 @@ impl OpenCADStudio { bounds, self.tabs[i].scene.document.header.lineweight_display, ) - .and_then(|s| Scene::handle_from_wire_name(s)); + .and_then(|s| Scene::handle_from_wire_name(s)) + .or_else(|| { + if !include_fills { + return None; + } + scene::pick::hit_test::click_hit_hatch( + p, + &self.tabs[i] + .scene + .visible_hatches_for_click(candidate_handles.as_ref()), + view_rot2, + eye2, + bounds, + candidate_handles.as_ref(), + ) + }) + .or_else(|| { + include_fills.then(|| { + scene::pick::hit_test::click_hit_insert_hatch( + p, + self.tabs[i].scene.insert_hatches_for_click().as_ref(), + view_rot2, + eye2, + bounds, + candidate_handles.as_ref(), + ) + })? + }) + .or_else(|| { + include_fills.then(|| { + self.tabs[i].scene.solid_click_hit( + p, + view_rot2, + eye2, + bounds, + candidate_handles.as_ref(), + ) + })? + }); if let Some(handle) = hit { // Some commands (e.g. SS_CATCHMENT) need the entity // body before `on_entity_pick` can advance. diff --git a/src/command.rs b/src/command.rs index c6f0382e4..7f1e8ee8d 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1296,6 +1296,21 @@ pub trait CadCommand: Send { false } + /// Include filled hatch / DXF SOLID regions in the entity hit-test. + /// + /// Most entity-pick commands operate on curve geometry and intentionally + /// keep the cheaper wire-only path. Commands that accept fill entities + /// override this so clicking inside a fill resolves its entity handle. + fn entity_pick_includes_fills(&self) -> bool { + false + } + + /// Render the entity under the cursor through the normal rollover + /// highlight while this command is waiting for an entity pick. + fn entity_pick_highlights_hover(&self) -> bool { + false + } + /// Called when the text editor closes, either because the user committed or cancelled the edit. fn on_editor_closed(&mut self, _committed: bool) -> CmdResult { CmdResult::Cancel diff --git a/src/modules/draw/properties/match_prop.rs b/src/modules/draw/properties/match_prop.rs index cd839fdd5..9d48690a8 100644 --- a/src/modules/draw/properties/match_prop.rs +++ b/src/modules/draw/properties/match_prop.rs @@ -18,6 +18,7 @@ use glam::DVec3; use crate::command::{CadCommand, CmdResult}; use crate::scene::model::wire_model::WireModel; +use crate::scene::Scene; pub struct MatchPropCommand { /// Source handle; NULL until phase 1 is complete. @@ -53,6 +54,21 @@ impl CadCommand for MatchPropCommand { !self.phase1_done() } + fn entity_pick_includes_fills(&self) -> bool { + !self.phase1_done() + } + + fn entity_pick_highlights_hover(&self) -> bool { + !self.phase1_done() + } + + fn entity_pick_acquire_previews(&self, scene: &Scene, handle: Handle) -> Vec { + if self.phase1_done() || handle.is_null() { + return vec![]; + } + scene.fill_hover_preview_wires(handle) + } + fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult { if handle.is_null() { return CmdResult::NeedPoint; // nothing hit, keep waiting diff --git a/src/scene/preview.rs b/src/scene/preview.rs index 679f8a3bc..9084c7692 100644 --- a/src/scene/preview.rs +++ b/src/scene/preview.rs @@ -106,6 +106,10 @@ impl Scene { /// the normal render shows the fill, not this outline. fn hatch_outline_wire(&self, handle: Handle) -> Option { let m = self.hatches.get(&handle)?; + Self::hatch_model_outline_wire(handle, m) + } + + fn hatch_model_outline_wire(handle: Handle, m: &HatchModel) -> Option { let (wx, wy) = (m.world_origin[0], m.world_origin[1]); let pts: Vec<[f64; 3]> = m .boundary @@ -129,6 +133,28 @@ impl Scene { )) } + /// Fill-only geometry has no resident wire for the normal rollover xray. + /// Supply an orange boundary overlay while an entity-pick command previews + /// a top-level hatch or an Insert whose visible children contain hatches. + pub fn fill_hover_preview_wires(&self, handle: Handle) -> Vec { + let mut wires: Vec = match self.document.get_entity(handle) { + Some(EntityType::Hatch(_)) => self.hatch_outline_wire(handle).into_iter().collect(), + Some(EntityType::Insert(_)) => self + .insert_hatches_for_click() + .get(&handle) + .into_iter() + .flatten() + .filter_map(|model| Self::hatch_model_outline_wire(handle, model)) + .collect(), + _ => Vec::new(), + }; + for wire in &mut wires { + wire.color = WireModel::HOVER; + wire.line_weight_px = wire.line_weight_px.max(2.0); + } + wires + } + /// Build wire models for an arbitrary slice of entities (e.g. clipboard contents). /// Entities need not be in the document — they are tessellated directly. pub fn wires_for_entities(&self, entities: &[acadrust::EntityType]) -> Vec {