Files
OneCAD-Draft/src/modules/draw/layers/laytrans.rs
T

229 lines
8.2 KiB
Rust

// Layer translation — map this drawing's layers onto a set taken from another
// drawing, then move everything across.
//
// Drawings arrive from outside with someone else's layer names. Translating
// them is three separate jobs that the command, the dialog and the saved
// mappings all share: reading a target set out of another file, deciding which
// source layer becomes which target, and performing the move. Only the third
// touches the drawing, and it is the same move LAYMRG makes for a single pair,
// so both go through `merge_layer`. (#624)
use std::collections::BTreeMap;
use std::path::Path;
use acadrust::tables::Layer;
use crate::scene::Scene;
/// A target layer read out of another drawing, with the properties the
/// translated layer should end up carrying.
#[derive(Clone, Debug)]
pub struct TargetLayer {
pub name: String,
pub layer: Layer,
}
/// One source layer and the target it becomes.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Mapping {
pub from: String,
pub to: String,
}
/// What a translation should do beyond moving the objects.
#[derive(Clone, Copy, Debug, Default)]
pub struct Options {
/// Reset each moved object's own colour and linetype to ByLayer, so the
/// target layer's properties are what actually shows. Objects that override
/// their layer would otherwise survive the translation looking unchanged.
pub force_bylayer: bool,
}
/// What a translation did, for the command line and the log.
#[derive(Clone, Debug, Default)]
pub struct Report {
/// Source layer, target layer, objects moved.
pub translated: Vec<(String, String, usize)>,
/// Mappings that could not run, with the reason.
pub skipped: Vec<(String, String)>,
}
impl Report {
pub fn objects(&self) -> usize {
self.translated.iter().map(|(_, _, n)| n).sum()
}
/// Layer translation log stored beside the drawing.
pub fn to_log(&self) -> String {
let mut out = String::new();
for (from, to, moved) in &self.translated {
out.push_str(&crate::tf!("{from} -> {to} ({moved} object(s))\n").into_owned());
}
for (layer, reason) in &self.skipped {
out.push_str(&crate::tf!("{layer}: skipped — {reason}\n").into_owned());
}
out
}
}
/// The layers another drawing defines, as translation targets.
///
/// Any drawing serves: a standards file, a template, or an already-correct
/// drawing. They are all the same format, so nothing here cares which.
pub fn load_targets(path: &Path) -> Result<Vec<TargetLayer>, String> {
let document = crate::io::load_file(path)?;
let mut targets: Vec<TargetLayer> = document
.layers
.iter()
// A layer the source drawing itself borrowed from a reference is not
// part of the standard it is offering.
.filter(|layer| !layer.name.contains('|') && !layer.flags.xref_dependent)
.map(|layer| TargetLayer {
name: layer.name.clone(),
layer: layer.clone(),
})
.collect();
targets.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
if targets.is_empty() {
return Err(crate::t!("no layers found in that file").into_owned());
}
Ok(targets)
}
/// Layers of `scene` that a translation could move, current layer excluded —
/// the drawing has to keep drawing somewhere.
pub fn source_layers(scene: &Scene, current: &str) -> Vec<String> {
let mut names: Vec<String> = scene
.document
.layers
.names()
.filter(|name| *name != "0" && !name.eq_ignore_ascii_case(current))
.map(|name| name.to_string())
.collect();
names.sort_by_key(|name| name.to_lowercase());
names
}
/// Pair up every source layer that a target names as well.
///
/// This is the mapping most translations want and all of them start from: the
/// two drawings already agree on those names, so the only thing to settle is
/// the properties.
pub fn map_same(sources: &[String], targets: &[TargetLayer]) -> Vec<Mapping> {
let by_name: BTreeMap<String, &TargetLayer> = targets
.iter()
.map(|target| (target.name.to_lowercase(), target))
.collect();
sources
.iter()
.filter_map(|source| {
by_name.get(&source.to_lowercase()).map(|target| Mapping {
from: source.clone(),
to: target.name.clone(),
})
})
.collect()
}
/// Move everything on each mapped layer onto its target and drop the layer it
/// left. The target is created, with the properties the target drawing gave it,
/// when this drawing has no layer by that name.
pub fn translate(
scene: &mut Scene,
mappings: &[Mapping],
targets: &[TargetLayer],
current_layer: &str,
options: Options,
) -> Report {
let mut report = Report::default();
for mapping in mappings {
if mapping.from.eq_ignore_ascii_case(&mapping.to) {
continue;
}
if mapping.from == "0" {
report
.skipped
.push((mapping.from.clone(), crate::t!("layer \"0\" cannot be translated").into_owned()));
continue;
}
if mapping.from.eq_ignore_ascii_case(current_layer) {
report.skipped.push((
mapping.from.clone(),
crate::t!("it is the current layer").into_owned(),
));
continue;
}
if !scene.document.layers.contains(&mapping.from) {
report
.skipped
.push((mapping.from.clone(), crate::t!("no such layer in this drawing").into_owned()));
continue;
}
// Bring the target in with the standard's own properties. An existing
// layer of that name is left as it is: the drawing already has an
// opinion about it, and silently restyling it would reach past what the
// translation was asked to do.
if !scene.document.layers.contains(&mapping.to) {
let Some(target) = targets
.iter()
.find(|target| target.name.eq_ignore_ascii_case(&mapping.to))
else {
report
.skipped
.push((mapping.from.clone(), crate::t!("target layer is not in the loaded set").into_owned()));
continue;
};
let mut layer = target.layer.clone();
layer.name = target.name.clone();
layer.handle = scene.document.allocate_handle();
// Whatever reference the target drawing held it through does not
// come with it.
layer.flags.xref_dependent = false;
layer.xref_block_record_handle = acadrust::Handle::NULL;
let _ = scene.document.layers.add(layer);
}
let moved = merge_layer(scene, &mapping.from, &mapping.to, options.force_bylayer);
report
.translated
.push((mapping.from.clone(), mapping.to.clone(), moved));
}
if !report.translated.is_empty() {
let touched: Vec<String> = report
.translated
.iter()
.map(|(_, to, _)| to.clone())
.collect();
scene.invalidate_dependency_index();
scene.invalidate_layer_dependencies(&touched);
}
report
}
/// Move every object off `from` onto `to` and drop `from`. Returns how many
/// objects moved.
///
/// The entity list is flat and holds block definitions too, so objects inside
/// blocks travel with the rest rather than being left behind on a layer that no
/// longer exists.
pub fn merge_layer(scene: &mut Scene, from: &str, to: &str, force_bylayer: bool) -> usize {
let mut moved = 0usize;
for entity in scene.document.entities_mut() {
if entity.common().layer != from {
continue;
}
let common = entity.common_mut();
common.layer = to.to_string();
if force_bylayer {
common.color = acadrust::Color::ByLayer;
common.color_name = None;
common.color_book_handle = None;
common.linetype = String::new();
common.linetype_handle = None;
common.line_weight = acadrust::LineWeight::ByLayer;
}
moved += 1;
}
scene.document.layers.remove(from);
moved
}