forked from 1b-agent/opencadstudio-upstream
fix(matchprop): complete property matching
This commit is contained in:
1 parent
99dbc8254d
commit
a36e0d7fdc
5 files changed
+572
-41
No files matched your search
+404
-38
@@ -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<Option<Vec<acadrust::xdata::XDataValue>>> =
|
||||
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<Self> {
|
||||
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()
|
||||
}
|
||||
+111
-3
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<WireModel> {
|
||||
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
|
||||
|
||||
@@ -106,6 +106,10 @@ impl Scene {
|
||||
/// the normal render shows the fill, not this outline.
|
||||
fn hatch_outline_wire(&self, handle: Handle) -> Option<WireModel> {
|
||||
let m = self.hatches.get(&handle)?;
|
||||
Self::hatch_model_outline_wire(handle, m)
|
||||
}
|
||||
|
||||
fn hatch_model_outline_wire(handle: Handle, m: &HatchModel) -> Option<WireModel> {
|
||||
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<WireModel> {
|
||||
let mut wires: Vec<WireModel> = 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<WireModel> {
|
||||
|
||||
Reference in new issue
Block a user