forked from 1b-agent/opencadstudio-upstream
229 lines
8.2 KiB
Rust
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
|
|
}
|