Files
OneCAD-Draft/src/app/command_driver/property_match.rs
T
Karim Jerbi 7052016dce refactor(kernels): extract reusable clipboard and matchprop kernels
Publishes app-free domain logic for shared use: match_special_props and PolylineMatchStyle move to entities::match_props; xdict trio usable headlessly; new match_layer, copy_to_clipboard, paste_entities and match_properties kernels over CadDocument. Handlers stay thin shells keeping undo, dirty flags, selection and command-line echo.

Adds cross-document copy, paste and property-match tests plus a headless proof driving copy, paste and match with no app instance.
2026-09-24 21:47:14 +01:00

168 lines
7.6 KiB
Rust

use super::*;
impl OpenCADStudio {
pub(super) fn handle_match_entity_layer(&mut self, dest: Vec<Handle>, src: Handle) {
let i = self.active_tab;
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
let src_layer = self.tabs[i]
.scene
.document
.get_entity(src)
.map(|e| e.common().layer.clone());
let dest: Vec<_> = dest
.into_iter()
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if dest.is_empty() {
self.command_line
.push_info(crate::t!("No editable objects selected.").as_ref());
} else if let Some(layer) = src_layer {
self.push_undo_snapshot(i, "LAYMATCH");
// Pure mutation lives in the shared kernel; lock-filter (above)
// plus undo, invalidate, dirty and echo stay caller-side.
let _ = crate::entities::match_props::match_layer_kernel(
&mut self.tabs[i].scene.document,
&dest,
src,
);
// New layer changes the baked by-layer colour/linetype/
// lineweight — re-tessellate the moved entities so they
// repaint immediately (issue #231 class).
self.invalidate_property_targets(i, &dest);
self.tabs[i].dirty = true;
self.command_line
.push_info(crate::tf!("Layer matched to \"{layer}\".").as_ref());
self.sync_ribbon_layers();
} else {
self.command_line
.push_error(crate::t!("Source object not found.").as_ref());
}
}
pub(super) fn handle_match_properties(
&mut self,
mut dest: Vec<Handle>,
src: Handle,
) -> Option<Task<Message>> {
let i = self.active_tab;
dest.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
if dest.is_empty() {
return Some(Task::none());
}
// The command stays active after each apply so more targets
// can keep being picked; Enter / Esc ends it (#362).
let src_clone = self.tabs[i].scene.document.get_entity(src).cloned();
// 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).
let dstyle_xdata: Option<Vec<(i16, acadrust::xdata::XDataValue)>> = src_clone
.as_ref()
.filter(|e| {
matches!(
e,
acadrust::EntityType::Dimension(_) | acadrust::EntityType::Leader(_)
)
})
.map(|e| crate::entities::dim_override::pairs(&e.common().extended_data));
if src_clone.is_some() {
// Entity-level transfer lives in the shared kernel so headless
// callers share it; the `&mut Document` xdata paths, undo,
// tessellation, selection and echo stay caller-side here.
// `all()` preserves the interactive behaviour; the xdata paths
// below honour the matching flags for future narrow callers.
let match_opts = crate::entities::match_props::MatchOpts::all();
self.apply_property_op(i, "MATCHPROP", &dest, |app, handle| {
let mut is_dim = false;
let mut is_hatch = false;
if let Some(se) = &src_clone {
if let Some(e) = app.tabs[i].scene.document.get_entity_mut(handle) {
is_dim = matches!(e, acadrust::EntityType::Dimension(_));
is_hatch = matches!(e, acadrust::EntityType::Hatch(_));
crate::entities::match_props::match_properties_kernel(se, e, &match_opts);
}
}
if is_hatch && match_opts.copy_hatch_background {
if let Some(values) = &hatch_background_xdata {
crate::scene::view::dispatch::set_entity_xdata(
&mut app.tabs[i].scene.document,
handle,
"HATCHBACKGROUNDCOLOR",
values.clone(),
);
}
}
// Dim-style overrides follow the style for dimension /
// leader destinations — through set_entity_xdata so no
// stale raw record survives. Caller-side: needs `&mut Document`.
// (Collapsed: the old outer `dstyle_xdata.is_some() || dst-is-dim`
// gate is subsumed — the replace only ever ran when both dst
// and src are dim/leader.)
if match_opts.copy_dim_overrides
&& matches!(
app.tabs[i].scene.document.get_entity(handle),
Some(acadrust::EntityType::Dimension(_) | acadrust::EntityType::Leader(_))
)
&& matches!(
src_clone,
Some(acadrust::EntityType::Dimension(_) | acadrust::EntityType::Leader(_))
)
{
crate::entities::dim_override::replace(
&mut app.tabs[i].scene.document,
handle,
dstyle_xdata.clone().unwrap_or_default(),
);
}
// A restyled dimension renders from its baked *D block —
// drop the stale block so the new style shows (#398).
if is_dim {
app.tabs[i].scene.invalidate_dim_block_recorded(handle);
}
// Hatch fills render from a prebuilt model (#415).
app.tabs[i].scene.refresh_fill_model(handle);
});
// Color / linetype / lineweight are baked into the cached
// wires at tessellation time; re-tessellate only the matched
// objects instead of rebuilding a large drawing.
let changes: Vec<_> = dest
.iter()
.copied()
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.command_line
.push_info(crate::tf!("Properties matched to {} object(s).", dest.len()).as_ref());
// Clear the consumed target selection and keep prompting.
self.tabs[i].scene.deselect_all();
if let Some(cmd) = &self.tabs[i].active_cmd {
self.command_line.push_info(&cmd.prompt());
}
} else {
self.command_line
.push_error(crate::t!("Source object not found.").as_ref());
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
}
None
}
}