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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5 committed 2026-08-07 16:51:15 +03:00
1 parent f7590b1aa8
commit 36e328f7d7
10 files changed
+835 -8

No files matched your search

+171 -8
View File
@@ -78,6 +78,18 @@ impl OpenCADStudio {
}
}
// LAYTRANS — translate this drawing's layers onto a set taken from
// another drawing. Bare opens the mapping dialog; with a file it
// maps every name the two drawings share and translates at once,
// which is what most drawings coming from outside need. (#624)
"LAYTRANS" => {
return Some(self.open_layer_translator(None));
}
cmd if cmd.starts_with("LAYTRANS ") => {
let path = std::path::PathBuf::from(cmd.trim_start_matches("LAYTRANS").trim());
return Some(self.layer_translate_same(&path));
}
// LAYDEL <name> — delete a layer and erase the objects on it.
"LAYDEL" => {
use crate::command::ValuePromptCommand;
@@ -192,14 +204,13 @@ impl OpenCADStudio {
return Some(Task::none());
}
self.push_undo_snapshot(i, "LAYMRG");
let mut moved = 0usize;
for e in self.tabs[i].scene.document.entities_mut() {
if e.common().layer == src {
e.common_mut().layer = dst.clone();
moved += 1;
}
}
self.tabs[i].scene.document.layers.remove(&src);
// The same move a translation makes for each of its pairs.
let moved = crate::modules::draw::layers::laytrans::merge_layer(
&mut self.tabs[i].scene,
&src,
&dst,
false,
);
self.tabs[i].scene.invalidate_dependency_index();
self.tabs[i]
.scene
@@ -666,3 +677,155 @@ impl OpenCADStudio {
Some(self.finish_dispatch(cmd))
}
}
impl OpenCADStudio {
/// Translate every layer the two drawings name alike, in one step.
///
/// The common case for a drawing arriving from outside: the names already
/// line up and only the properties are wrong, so there is nothing to sit in
/// a dialog and decide.
pub(in crate::app) fn layer_translate_same(&mut self, path: &std::path::Path) -> Task<Message> {
use crate::modules::draw::layers::laytrans;
let i = self.active_tab;
let targets = match laytrans::load_targets(path) {
Ok(targets) => targets,
Err(why) => {
self.command_line
.push_error(crate::tf!("LAYTRANS: {why}.").as_ref());
return Task::none();
}
};
let current = self.tabs[i].active_layer.clone();
let sources = laytrans::source_layers(&self.tabs[i].scene, &current);
let mappings = laytrans::map_same(&sources, &targets);
if mappings.is_empty() {
self.command_line.push_info(
crate::t!("LAYTRANS: no layer names in common — use LAYTRANS with no file to map them.")
.as_ref(),
);
return Task::none();
}
self.push_undo_snapshot(i, "LAYTRANS");
let report = laytrans::translate(
&mut self.tabs[i].scene,
&mappings,
&targets,
&current,
laytrans::Options::default(),
);
self.finish_layer_translation(i, report)
}
/// Open the translator, optionally with a standard already loaded.
pub(in crate::app) fn open_layer_translator(
&mut self,
preload: Option<std::path::PathBuf>,
) -> Task<Message> {
self.layer_translator
.get_or_insert_with(Default::default);
self.active_modal = Some(crate::app::ModalKind::LayerTranslator);
match preload {
Some(path) => Task::done(Message::LayerTranslatorLoaded(path)),
None => Task::none(),
}
}
/// Read or write the mapping list as plain `from -> to` lines.
///
/// A drawing standards file is where AutoCAD keeps these, but writing them
/// there would mean inventing a place inside the DWG for something no other
/// application reads back. A small text file beside the drawing carries the
/// same information without touching the drawing's own structure.
pub(in crate::app) fn layer_translator_mappings_file(&mut self, path: &std::path::Path, save: bool) {
use crate::modules::draw::layers::laytrans::Mapping;
if save {
let Some(state) = self.layer_translator.as_ref() else {
return;
};
let body: String = state
.mappings
.iter()
.map(|m| format!("{} -> {}\n", m.from, m.to))
.collect();
match std::fs::write(path, body) {
Ok(()) => self.command_line.push_output(
crate::tf!("LAYTRANS: saved {n} mapping(s).", n = state.mappings.len())
.as_ref(),
),
Err(why) => self
.command_line
.push_error(crate::tf!("LAYTRANS: could not save — {why}.").as_ref()),
}
return;
}
let text = match std::fs::read_to_string(path) {
Ok(text) => text,
Err(why) => {
self.command_line
.push_error(crate::tf!("LAYTRANS: could not read — {why}.").as_ref());
return;
}
};
let parsed: Vec<Mapping> = text
.lines()
.filter_map(|line| {
let (from, to) = line.split_once("->")?;
let (from, to) = (from.trim(), to.trim());
(!from.is_empty() && !to.is_empty()).then(|| Mapping {
from: from.to_string(),
to: to.to_string(),
})
})
.collect();
let count = parsed.len();
if let Some(state) = self.layer_translator.as_mut() {
state.mappings = parsed;
}
self.command_line
.push_output(crate::tf!("LAYTRANS: loaded {count} mapping(s).").as_ref());
}
/// Write what the last translation did beside the drawing.
pub(in crate::app) fn write_layer_translation_log(&mut self, i: usize) {
let Some(report) = self.last_layer_translation.as_ref() else {
return;
};
let base = self.tabs[i]
.current_path
.clone()
.unwrap_or_else(|| std::path::PathBuf::from(&self.tabs[i].tab_title));
let path = base.with_extension("laytrans.log");
match std::fs::write(&path, report.to_log()) {
Ok(()) => self
.command_line
.push_output(crate::tf!("LAYTRANS: log written to {}.", path.display()).as_ref()),
Err(why) => self
.command_line
.push_error(crate::tf!("LAYTRANS: could not write log — {why}.").as_ref()),
}
}
/// Apply a translation's result: mark the drawing, refresh the panel and
/// say what moved. Shared by the command and the dialog so both report the
/// same way.
pub(in crate::app) fn finish_layer_translation(
&mut self,
i: usize,
report: crate::modules::draw::layers::laytrans::Report,
) -> Task<Message> {
let layers = report.translated.len();
let objects = report.objects();
self.tabs[i].dirty = true;
self.refresh_layer_panel();
self.refresh_properties();
for (layer, reason) in &report.skipped {
self.command_line
.push_info(crate::tf!("LAYTRANS: skipped \"{layer}\" — {reason}.").as_ref());
}
self.command_line.push_output(
crate::tf!("LAYTRANS: translated {layers} layer(s), {objects} object(s).").as_ref(),
);
self.last_layer_translation = Some(report);
Task::none()
}
}
+1
View File
@@ -297,6 +297,7 @@ inventory::submit!(crate::command::CommandRegistration {
"DBLIST",
// Layer management.
"LAYDEL",
"LAYTRANS",
"LAYMRG",
"LAYERSTATE",
"LAS",
+29
View File
@@ -403,6 +403,11 @@ pub(super) struct OpenCADStudio {
/// a pick *replaces* the set, so borrowing it here would wipe everything
/// gathered so far rather than subtract one thing. (#596)
select_remove_mode: bool,
/// What the last layer translation did, kept so the log can be written
/// after the fact rather than forcing a path out of the user up front.
last_layer_translation: Option<crate::modules::draw::layers::laytrans::Report>,
/// The layer-translator dialog's working state while it is open.
layer_translator: Option<crate::ui::window::layer_translator::State>,
/// PICKDRAG (#226): false (default) = press-drag lassoes; true =
/// press-drag draws a rectangle marquee.
pick_drag_rect: bool,
@@ -1489,6 +1494,7 @@ pub enum ModalKind {
UpdateNotice,
Layers,
LayerStateManager,
LayerTranslator,
LayerStateEditor,
Plot,
PrintAll,
@@ -1765,6 +1771,27 @@ pub enum Message {
LanguageChanged(crate::i18n::Language),
/// Drop every entity from the active drawing.
ClearScene,
// ── Layer Translator (#624) ──────────────────────────────────────────
/// Pick the drawing whose layers become the translation targets.
LayerTranslatorLoad,
/// A target set was read from `path`.
LayerTranslatorLoaded(std::path::PathBuf),
LayerTranslatorSelectFrom(String),
LayerTranslatorSelectTo(String),
/// Pair the two selected layers.
LayerTranslatorMap,
/// Pair every layer the two drawings name alike.
LayerTranslatorMapSame,
/// Drop the mapping that starts at this layer.
LayerTranslatorUnmap(String),
LayerTranslatorForceByLayer(bool),
LayerTranslatorWriteLog(bool),
LayerTranslatorSaveMappings,
LayerTranslatorLoadMappings,
/// Path chosen for saving or loading mappings.
LayerTranslatorMappingsPath(std::path::PathBuf, bool),
/// Apply every mapping.
LayerTranslatorTranslate,
/// Set the active tab's render mode — one of the seven visual styles, and
/// the only way a style is ever set.
SetRenderMode(acadrust::entities::ViewportRenderMode),
@@ -2977,6 +3004,8 @@ impl OpenCADStudio {
selection_cycling: false,
pick_add: true,
select_remove_mode: false,
last_layer_translation: None,
layer_translator: None,
pick_drag_rect: false,
perf_hud: false,
cycle_candidates: None,
+136
View File
@@ -1858,6 +1858,142 @@ impl OpenCADStudio {
self.active_modal = Some(super::ModalKind::LayerStateManager);
Task::none()
}
// ── Layer Translator (#624) ──────────────────────────────────
Message::LayerTranslatorLoad => Task::perform(
crate::io::pick_layer_standard_path(),
|path| match path {
Some(path) => Message::LayerTranslatorLoaded(path),
None => Message::Noop,
},
),
Message::LayerTranslatorLoaded(path) => {
use crate::modules::draw::layers::laytrans;
match laytrans::load_targets(&path) {
Ok(targets) => {
let state = self.layer_translator.get_or_insert_with(Default::default);
state.source_file = path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default();
state.targets = targets;
// A target set that no longer contains a mapped name
// would translate onto nothing.
let names: Vec<String> =
state.targets.iter().map(|t| t.name.clone()).collect();
state
.mappings
.retain(|m| names.iter().any(|n| n.eq_ignore_ascii_case(&m.to)));
state.selected_to = None;
}
Err(why) => self
.command_line
.push_error(crate::tf!("LAYTRANS: {why}.").as_ref()),
}
Task::none()
}
Message::LayerTranslatorSelectFrom(name) => {
if let Some(state) = self.layer_translator.as_mut() {
state.selected_from = Some(name);
}
Task::none()
}
Message::LayerTranslatorSelectTo(name) => {
if let Some(state) = self.layer_translator.as_mut() {
state.selected_to = Some(name);
}
Task::none()
}
Message::LayerTranslatorMap => {
use crate::modules::draw::layers::laytrans::Mapping;
if let Some(state) = self.layer_translator.as_mut() {
if let (Some(from), Some(to)) =
(state.selected_from.take(), state.selected_to.clone())
{
state.mappings.retain(|m| !m.from.eq_ignore_ascii_case(&from));
state.mappings.push(Mapping { from, to });
}
}
Task::none()
}
Message::LayerTranslatorMapSame => {
use crate::modules::draw::layers::laytrans;
let i = self.active_tab;
let current = self.tabs[i].active_layer.clone();
let sources = laytrans::source_layers(&self.tabs[i].scene, &current);
if let Some(state) = self.layer_translator.as_mut() {
for mapping in laytrans::map_same(&sources, &state.targets) {
if !state
.mappings
.iter()
.any(|m| m.from.eq_ignore_ascii_case(&mapping.from))
{
state.mappings.push(mapping);
}
}
}
Task::none()
}
Message::LayerTranslatorUnmap(from) => {
if let Some(state) = self.layer_translator.as_mut() {
state.mappings.retain(|m| m.from != from);
}
Task::none()
}
Message::LayerTranslatorForceByLayer(value) => {
if let Some(state) = self.layer_translator.as_mut() {
state.force_bylayer = value;
}
Task::none()
}
Message::LayerTranslatorWriteLog(value) => {
if let Some(state) = self.layer_translator.as_mut() {
state.write_log = value;
}
Task::none()
}
Message::LayerTranslatorSaveMappings => Task::perform(
crate::io::pick_layer_mapping_path(true),
|path| match path {
Some(path) => Message::LayerTranslatorMappingsPath(path, true),
None => Message::Noop,
},
),
Message::LayerTranslatorLoadMappings => Task::perform(
crate::io::pick_layer_mapping_path(false),
|path| match path {
Some(path) => Message::LayerTranslatorMappingsPath(path, false),
None => Message::Noop,
},
),
Message::LayerTranslatorMappingsPath(path, save) => {
self.layer_translator_mappings_file(&path, save);
Task::none()
}
Message::LayerTranslatorTranslate => {
use crate::modules::draw::layers::laytrans;
let Some(state) = self.layer_translator.take() else {
return Task::none();
};
self.active_modal = None;
let i = self.active_tab;
let current = self.tabs[i].active_layer.clone();
self.push_undo_snapshot(i, "LAYTRANS");
let report = laytrans::translate(
&mut self.tabs[i].scene,
&state.mappings,
&state.targets,
&current,
laytrans::Options {
force_bylayer: state.force_bylayer,
},
);
let write_log = state.write_log;
let task = self.finish_layer_translation(i, report);
if write_log {
self.write_layer_translation_log(i);
}
task
}
Message::LayerStateManagerSelect(name) => {
self.load_layer_state_editor(Some(name));
Task::none()
+15
View File
@@ -19,6 +19,7 @@ impl OpenCADStudio {
Some(K::UpdateNotice) => crate::tr!("modal-update-available"),
Some(K::Layers) => crate::tr!("modal-layer-manager"),
Some(K::LayerStateManager) => crate::tr!("modal-layer-state-manager"),
Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(),
Some(K::LayerStateEditor) => crate::tr!("modal-edit-layer-state"),
Some(K::Plot) => crate::tr!("modal-plot"),
Some(K::PrintAll) => t!("Print All").into_owned(),
@@ -169,6 +170,20 @@ impl OpenCADStudio {
|flow| tab.layers.view_window(self.layer_name_col_w, flow),
)
}
super::super::ModalKind::LayerTranslator => {
use crate::modules::draw::layers::laytrans;
let i = self.active_tab;
let current = self.tabs[i].active_layer.clone();
let sources = laytrans::source_layers(&self.tabs[i].scene, &current);
let state = self.layer_translator.as_ref()?;
sized_flow(ex, 760, 460, |flow| {
crate::ui::window::layer_translator::view_window(
state,
sources.clone(),
flow,
)
})
}
super::super::ModalKind::LayerStateManager => {
let states = self.tabs[self.active_tab].scene.document.layer_states();
sized_flow(