forked from 1b-agent/opencadstudio-upstream
feat(layers): add LAYTRANS to translate layers onto a standard
Drawings arrive from elsewhere carrying someone else's layer names, and folding them into a set of house layers meant renaming and merging by hand, one layer at a time. LAYTRANS reads a layer set out of another drawing and moves this one onto it. Given a file it maps every name the two already share and translates at once, which is all most incoming drawings need. Bare, it opens a translator: the drawing's layers on one side, the loaded set on the other, mappings built up between them and applied only on Translate, so a mapping can be reconsidered and the whole translation is a single undo step. Mappings can be saved and read back, objects can be forced to ByLayer so the target layer's properties are what actually shows, and the result can be written out as a log. The move itself is the one LAYMRG already made for a single pair, so LAYMRG now goes through it too rather than keeping its own copy. Three things fell out for free or nearly so. A standards file is a drawing, so .dws and .dwt need nothing but a place in the file filter. The entity list is flat and holds block definitions, so objects inside blocks travel with everything else instead of being stranded on a layer that no longer exists. And a target layer that this drawing already has is left alone: the drawing has an opinion about it, and restyling it would reach past what was asked for -- only newly created targets take the standard's properties. Mappings are saved beside the drawing rather than inside a DWS. That is where AutoCAD keeps them, but putting them there would mean inventing a place in the drawing that nothing else reads back. Closes #624 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f7590b1aa8
commit
36e328f7d7
10 files changed
+835
-8
No files matched your search
@@ -0,0 +1,226 @@
|
||||
// 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()
|
||||
}
|
||||
|
||||
/// The transaction log AutoCAD writes beside the drawing, as text.
|
||||
pub fn to_log(&self) -> String {
|
||||
let mut out = String::new();
|
||||
for (from, to, moved) in &self.translated {
|
||||
out.push_str(&format!("{from} -> {to} ({moved} object(s))\n"));
|
||||
}
|
||||
for (layer, reason) in &self.skipped {
|
||||
out.push_str(&format!("{layer}: skipped — {reason}\n"));
|
||||
}
|
||||
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("no layers found in that file".into());
|
||||
}
|
||||
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(), "layer \"0\" cannot be translated".into()));
|
||||
continue;
|
||||
}
|
||||
if mapping.from.eq_ignore_ascii_case(current_layer) {
|
||||
report.skipped.push((
|
||||
mapping.from.clone(),
|
||||
"it is the current layer".into(),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if !scene.document.layers.contains(&mapping.from) {
|
||||
report
|
||||
.skipped
|
||||
.push((mapping.from.clone(), "no such layer in this drawing".into()));
|
||||
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(), "target layer is not in the loaded set".into()));
|
||||
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.linetype = String::new();
|
||||
common.linetype_handle = None;
|
||||
common.line_weight = acadrust::LineWeight::ByLayer;
|
||||
}
|
||||
moved += 1;
|
||||
}
|
||||
scene.document.layers.remove(from);
|
||||
moved
|
||||
}
|
||||
@@ -8,5 +8,6 @@ pub(super) mod laythw;
|
||||
pub(super) mod layuniso;
|
||||
pub(super) mod layulk;
|
||||
pub(super) mod make_current;
|
||||
pub mod laytrans;
|
||||
pub mod match_layer;
|
||||
pub(super) mod panel;
|
||||
Reference in new issue
Block a user