Files
cadcraft/crates/engine/src/cmd/file/compare/export.rs
T

685 lines
29 KiB
Rust

//! COMPAREEXPORT: the comparison as a new drawing (a snapshot). The drawings are compared again
//! when the command runs, so edits made since COMPARE count. The new drawing's model space holds
//! copies of the other drawing's objects that differ, then copies of the current drawing's
//! objects in the compared tab, as two groups, `COMPARE_COMPARED` and `COMPARE_CURRENT`. Objects
//! only in this drawing or changed in it are drawn in the overlay's green, objects only in the
//! other drawing or changed there in its red, and objects in both in gray (color 8). A block
//! reference, dimension or multileader refers to a copy of its block whose objects all have that
//! color (`COMPARE_CURRENT_<name>`, `COMPARE_COMPARED_<name>`, `COMPARE_COMMON_<name>`; for a
//! dimension's `*D` block, another `*D` block), and a table or tolerance uses a recolored copy
//! of its style; xrefs keep their own colors. The new
//! drawing has this drawing's layers, styles and blocks, plus the layers and styles of the other
//! drawing that this one lacks and the blocks its copied objects refer to; an anonymous block of
//! the other drawing whose name this one uses is copied under a free name. Relative xref, image
//! and data link paths become absolute paths from the folder of the drawing they came from. It
//! opens untitled as `compare_<this> vs <other>.dwg`, with unsaved changes; saving it asks for a
//! file name.
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use cadcraft_doc::color::{Color, Rgb};
use cadcraft_doc::{Block, CellStyle, DimStyle, Drawing, Entity, EntityKind, EntityStore, Group, Handle, MAX_BLOCK_DEPTH, TableStyle};
use serde_json::{Value, json};
use super::super::super::{CommandSpec, bad};
use super::{Comparison, MAX_OBJECTS};
use crate::{Result, Session};
/// Only in this drawing, or changed in it (the overlay's color).
const THIS: Color = Color::True(Rgb(0x4c, 0xd9, 0x7b));
/// Only in the other drawing, or changed there.
const OTHER: Color = Color::True(Rgb(0xff, 0x5c, 0x5c));
/// In both drawings.
const COMMON: Color = Color::Index(8);
/// Block definitions copied from the other drawing (the ones they reference included), and
/// recolored copies of blocks and styles, per export.
const MAX_BLOCKS: usize = 10_000;
/// Objects copied into block definitions (the other drawing's blocks and the recolored
/// copies), per export.
const MAX_COPIED: usize = 1_000_000;
pub(super) fn spec() -> CommandSpec {
CommandSpec::new("compareexport", "Export Comparison", run)
.params(
"{} → {index, title, current, compared: group names, counts: {current, compared}, truncated, message}: the comparison \
as a new drawing, opened untitled",
)
.noundo()
}
/// What [`snapshot`] made: the drawing, the two group names, how many objects each holds,
/// whether a drawing had more objects than were compared, whether the other drawing had more
/// blocks to copy than [`MAX_BLOCKS`], and whether a block was left in its own colors.
struct Snapshot {
drawing: Drawing,
current: String,
compared: String,
counts: (usize, usize),
truncated: bool,
blocks_left_out: bool,
uncolored: bool,
}
fn run(s: &mut Session, _p: &Value) -> Result<Value> {
let st = s.state()?;
let (uid, cur, title, cur_folder) = (st.uid, st.doc.clone(), st.title.clone(), folder_of(st.path.as_deref()));
let c = s.compare.as_ref().filter(|c| c.uid == uid).ok_or_else(|| bad("compareexport", "no comparison: run COMPARE first"))?;
let name = format!("compare_{} vs {}.dwg", stem(&title), stem(&c.other_name));
let folders = Folders { here: cur_folder, there: folder_of(c.other_path.as_deref()) };
let Snapshot { drawing, current, compared, counts, truncated, blocks_left_out, uncolored } = snapshot(&cur, c, &folders);
let i = s.open_drawing(drawing, &name, None);
// Not saved yet: closing it asks to save it under a name.
let st = s.state_mut()?;
st.saved = Arc::new((*st.doc).clone());
let mut notes = Vec::new();
if truncated {
notes.push(format!("only the first {MAX_OBJECTS} objects of each drawing were compared"));
}
if blocks_left_out {
notes.push("some blocks of the other drawing were too many or too large to copy".to_string());
}
if uncolored {
notes.push("some blocks keep their own colors".to_string());
}
let notes = if notes.is_empty() { String::new() } else { format!(" ({})", notes.join("; ")) };
Ok(json!({
"index": i,
"title": name,
"current": current,
"compared": compared,
"counts": { "current": counts.0, "compared": counts.1 },
"truncated": truncated || blocks_left_out || uncolored,
"message": format!("The comparison was exported to the new drawing {name}{notes}."),
}))
}
/// The file of the drawing COMPARE compares with: the `path` it was read from, or the open
/// drawing's own file. `None` for `{data}` and for an open drawing never saved.
pub(super) fn source_path(s: &Session, p: &Value, other: &Arc<Drawing>) -> Option<String> {
match p.get("path").and_then(Value::as_str) {
Some(path) if p.get("document").is_none_or(Value::is_null) => Some(path.to_string()),
_ => s.docs.iter().find(|d| Arc::ptr_eq(&d.doc, other)).and_then(|d| d.path.clone()),
}
}
/// The folders of the two drawings' files. The snapshot has no folder of its own, so the
/// relative file paths it copies (xrefs, images, table data links) become absolute paths from
/// the folder of the drawing they came from.
struct Folders {
here: Option<PathBuf>,
there: Option<PathBuf>,
}
/// The absolute folder of the file at `path`.
fn folder_of(path: Option<&str>) -> Option<PathBuf> {
let parent = Path::new(path?).parent()?;
let parent = if parent.as_os_str().is_empty() { Path::new(".") } else { parent };
Some(std::path::absolute(parent).unwrap_or_else(|_| parent.to_path_buf()))
}
/// `p` as an absolute path from `folder` when it is relative.
pub(super) fn rebase_path(p: &mut String, folder: Option<&Path>) {
if let Some(folder) = folder
&& !p.trim().is_empty()
&& Path::new(p.as_str()).is_relative()
{
let joined = folder.join(p.as_str());
*p = std::path::absolute(&joined).unwrap_or(joined).to_string_lossy().to_string();
}
}
/// Rebase the file an object refers to: an image's file, a table's data link.
pub(super) fn rebase(k: &mut EntityKind, folder: Option<&Path>) {
match k {
EntityKind::Image(i) => rebase_path(&mut i.path, folder),
EntityKind::Table(t) => {
if let Some(l) = t.data_link.as_mut() {
rebase_path(&mut l.path, folder);
}
}
_ => {}
}
}
/// Rebase a block's xref path and the files its objects refer to.
fn rebase_block(b: &mut Block, folder: Option<&Path>) {
if let Some(x) = b.xref_path.as_mut() {
rebase_path(x, folder);
}
let mut store = EntityStore::new();
for e in b.entities.iter() {
let mut e = (**e).clone();
rebase(&mut e.kind, folder);
store.push(e);
}
b.entities = store;
}
/// Whether a block holds a relative file path that [`rebase_block`] would change.
fn has_relative_paths(b: &Block) -> bool {
let rel = |p: &str| !p.trim().is_empty() && Path::new(p).is_relative();
b.xref_path.as_deref().is_some_and(rel)
|| b.entities.iter().any(|e| match &e.kind {
EntityKind::Image(i) => rel(&i.path),
EntityKind::Table(t) => t.data_link.as_ref().is_some_and(|l| rel(&l.path)),
_ => false,
})
}
/// A drawing title without its file extension.
fn stem(title: &str) -> String {
std::path::Path::new(title).file_stem().map(|n| n.to_string_lossy().to_string()).filter(|n| !n.is_empty()).unwrap_or_else(|| title.to_string())
}
fn snapshot(cur: &Drawing, c: &Comparison, folders: &Folders) -> Snapshot {
let other: &Drawing = &c.other;
let mut d = cur.clone();
if folders.here.is_some() {
for b in d.blocks.values_mut().filter(|b| has_relative_paths(b)) {
rebase_block(Arc::make_mut(b), folders.here.as_deref());
}
}
d.model = EntityStore::new();
for l in &mut d.layouts {
l.entities = EntityStore::new();
}
d.foreign = Arc::default();
d.groups.clear();
d.constraints.clear();
// The per-object constraint bar and dimension exceptions name objects the copy doesn't have.
d.parametric.settings.bar_exceptions.clear();
d.parametric.settings.dim_exceptions.clear();
d.hidden_objects.clear();
d.read_notes.clear();
d.refedit = None;
d.skipped_records.clear();
d.handseed = d.handseed.max(other.handseed);
merge(&mut d.layers, &other.layers, |t| &t.name);
merge(&mut d.linetypes, &other.linetypes, |t| &t.name);
merge(&mut d.text_styles, &other.text_styles, |t| &t.name);
merge(&mut d.dim_styles, &other.dim_styles, |t| &t.name);
merge(&mut d.mleader_styles, &other.mleader_styles, |t| &t.name);
merge(&mut d.table_styles, &other.table_styles, |t| &t.name);
merge(&mut d.mline_styles, &other.mline_styles, |t| &t.name);
// Compared again, so edits made since COMPARE ran are classified as they are now.
let diff = super::diff(cur, other, &c.space);
let ours: HashSet<Handle> = diff.added.iter().copied().chain(diff.changed.iter().map(|(a, ..)| *a)).collect();
let here: Vec<(&Entity, Color)> = cur
.space(&c.space)
.into_iter()
.flat_map(EntityStore::iter)
.filter(|e| !matches!(e.kind, EntityKind::Viewport(_)))
.take(MAX_OBJECTS)
.map(|e| (e.as_ref(), if ours.contains(&e.handle) { THIS } else { COMMON }))
.collect();
// In the other drawing's draw order.
let theirs: HashSet<Handle> = diff.removed.iter().copied().chain(diff.changed.iter().map(|(_, b, ..)| *b)).collect();
let there: Vec<(&Entity, Color)> = other
.space(&c.space)
.into_iter()
.flat_map(EntityStore::iter)
.filter(|e| theirs.contains(&e.handle) && !matches!(e.kind, EntityKind::Viewport(_)))
.map(|e| (e.as_ref(), OTHER))
.collect();
let mut tinted = Tinted::default();
let (renames, blocks_left_out) = copy_blocks(&mut d, other, there.iter().map(|(e, _)| *e), folders.there.as_deref(), &mut tinted);
// The other drawing's objects first, so this drawing's are drawn over them.
let compared = add_group(&mut d, "COMPARE_COMPARED", &there, &renames, folders.there.as_deref(), &mut tinted);
let current = add_group(&mut d, "COMPARE_CURRENT", &here, &Renames::new(), folders.here.as_deref(), &mut tinted);
let uncolored = tinted.uncolored;
Snapshot { drawing: d, current, compared, counts: (here.len(), there.len()), truncated: diff.truncated, blocks_left_out, uncolored }
}
/// Add the entries of `from` whose names `to` lacks (compared without letter case).
fn merge<T: Clone>(to: &mut Vec<T>, from: &[T], name: impl Fn(&T) -> &String) {
for t in from {
if !to.iter().any(|x| name(x).eq_ignore_ascii_case(name(t))) {
to.push(t.clone());
}
}
}
/// Add copies of `objs` to model space with new handles, their block references renamed by
/// `renames` and their files rebased on `folder`, each painted in its comparison color, and
/// group them as `name` (or `name_2`… when that group name is taken). Returns the group's name.
fn add_group(d: &mut Drawing, name: &str, objs: &[(&Entity, Color)], renames: &Renames, folder: Option<&Path>, tinted: &mut Tinted) -> String {
let mut members = Vec::new();
for (e, color) in objs {
d.ensure_layer(&e.common.layer);
let h = d.new_handle();
let mut copy = Entity { handle: h, ..(*e).clone() };
detach(&mut copy.kind);
rebase(&mut copy.kind, folder);
rename_refs(&mut copy.kind, renames);
paint(d, &mut copy, *color, tinted, 0);
d.model.push(copy);
members.push(h);
}
let name = free_name(name, d.groups.len(), |n| d.groups.iter().any(|g| g.name.eq_ignore_ascii_case(n)));
if !members.is_empty() {
d.groups.push(Group { name: name.clone(), description: String::new(), selectable: true, members });
}
name
}
/// Drop the links of an associative dimension or hatch to the objects it follows: the copies
/// have new handles, so the links would name objects the snapshot does not have.
fn detach(k: &mut EntityKind) {
match k {
EntityKind::Dimension(dm) => dm.assoc.clear(),
EntityKind::Hatch(h) => {
h.associative = false;
h.loops.iter_mut().for_each(|l| l.sources.clear());
}
_ => {}
}
}
/// The block names an object refers to.
fn block_refs(k: &EntityKind) -> Vec<String> {
match k {
EntityKind::Insert(i) => vec![i.block.clone()],
EntityKind::Dimension(dm) => dm.block.iter().cloned().collect(),
EntityKind::MLeader(m) => m.block.iter().map(|b| b.block.clone()).collect(),
_ => Vec::new(),
}
}
/// Call `f` on each block name an object refers to.
fn each_ref(k: &mut EntityKind, mut f: impl FnMut(&mut String)) {
match k {
EntityKind::Insert(i) => f(&mut i.block),
EntityKind::Dimension(dm) => {
if let Some(n) = dm.block.as_mut() {
f(n);
}
}
EntityKind::MLeader(m) => {
if let Some(b) = m.block.as_mut() {
f(&mut b.block);
}
}
_ => {}
}
}
/// Point the block references of an object at the names its blocks were copied under.
fn rename_refs(k: &mut EntityKind, renames: &Renames) {
each_ref(k, |n| {
if let Some(new) = renames.get(&n.to_ascii_uppercase()) {
n.clone_from(new);
}
});
}
/// Recolored copies of blocks and styles made so far, and whether a block was left in its own
/// colors because the nesting or copy limit was reached.
#[derive(Default)]
struct Tinted {
/// (comparison tag, "block", "table style" or "dimension style", upper-case name) → the
/// copy's name.
names: HashMap<(&'static str, &'static str, String), String>,
uncolored: bool,
/// Objects copied into block definitions so far, counted against [`MAX_COPIED`].
copied: usize,
next: NextFree,
/// The drawing's block names in upper case, filled on the first block copy. Only
/// [`tinted_block`] adds blocks after that, and it adds their names here too.
blocks: Option<HashSet<String>>,
}
/// The next index to try for each name base. Names are only added while a snapshot is built,
/// so an index found taken stays taken, and each search starts where the last one for the same
/// base stopped: a name costs O(1) amortised instead of a scan from 1.
#[derive(Default)]
struct NextFree(HashMap<(&'static str, String), usize>);
impl NextFree {
/// The first of `name(i)` for `i` from 1 that `taken` doesn't hold, skipping the indices
/// earlier searches for `base` in namespace `kind` passed. `n` is how many names there are,
/// so one of `name(1)`…`name(n + 1)` is free. The caller adds the returned name.
fn find(&mut self, kind: &'static str, base: &str, n: usize, name: impl Fn(usize) -> String, taken: impl Fn(&str) -> bool) -> String {
let key = (kind, base.to_ascii_uppercase());
let start = self.0.get(&key).copied().unwrap_or(1);
let i = (start..=n.saturating_add(1)).find(|&i| !taken(&name(i))).unwrap_or_else(|| n.saturating_add(2).max(start));
self.0.insert(key, i.saturating_add(1));
name(i)
}
}
/// `base` for 1, `base_2`, `base_3`… after it.
fn suffixed(base: &str, i: usize) -> String {
if i == 1 { base.to_string() } else { format!("{base}_{i}") }
}
/// The word a comparison color adds to the names of its recolored copies.
fn tag(color: Color) -> &'static str {
if color == THIS {
"CURRENT"
} else if color == OTHER {
"COMPARED"
} else {
"COMMON"
}
}
/// `base`, or `base_2`, `base_3`… when `taken` holds that name. `n` is how many names there are.
fn free_name(base: &str, n: usize, taken: impl Fn(&str) -> bool) -> String {
(1..=n + 1).map(|i| suffixed(base, i)).find(|c| !taken(c)).unwrap_or_else(|| suffixed(base, n + 2))
}
/// Give an object its comparison color: its own color, its attributes' colors, and, through
/// recolored copies of the blocks and styles it refers to, the color of everything they draw.
fn paint(d: &mut Drawing, e: &mut Entity, color: Color, tinted: &mut Tinted, depth: usize) {
e.common.color = color;
let attribs = match &mut e.kind {
EntityKind::Insert(i) => Some(&mut i.attribs),
EntityKind::MLeader(m) => m.block.as_mut().map(|b| &mut b.attribs),
_ => None,
};
for a in attribs.into_iter().flatten() {
a.props.color = Some(color);
if let Some(ml) = a.multiline.as_mut() {
ml.mtext.contents = without_color_codes(&ml.mtext.contents);
}
}
// Colors an object carries apart from its own: inline MTEXT colors (in multiline attributes
// too), a dimension style's DIMCLRD, DIMCLRE and DIMCLRT, gradient colors, multiline element
// colors, the style colors of a table or a tolerance (through a recolored copy of the style)
// and inline cell colors. Hatch backgrounds, multiline fills and table cell fills are left out.
match &mut e.kind {
EntityKind::MText(t) => t.contents = without_color_codes(&t.contents),
EntityKind::AttDef(a) => {
if let Some(ml) = a.multiline.as_mut() {
ml.mtext.contents = without_color_codes(&ml.mtext.contents);
}
}
EntityKind::MLeader(m) => {
if let Some(t) = m.text.as_mut() {
t.contents = without_color_codes(&t.contents);
}
}
EntityKind::Dimension(dm) => {
if let Ok(by_block) = serde_json::to_value(Color::ByBlock) {
for var in ["DIMCLRD", "DIMCLRE", "DIMCLRT"] {
dm.overrides.retain(|k, _| DimStyle::field_name(k) != DimStyle::field_name(var));
dm.overrides.insert(var.into(), by_block.clone());
}
}
}
EntityKind::Tolerance(t) => {
if let Some(style) = tinted_dim_style(d, &t.style, color, tinted) {
t.style = style;
}
}
EntityKind::Hatch(h) => {
if let Some(g) = h.gradient.as_mut() {
(g.color1, g.color2) = (color, color);
}
h.background = None;
}
EntityKind::MLine(m) => {
for el in &mut m.def.elements {
el.color = Color::ByBlock;
}
m.def.fill = None;
}
EntityKind::Table(t) => {
for cell in t.cells.iter_mut().flatten() {
cell.text = without_color_codes(&cell.text);
}
if let Some(style) = tinted_table_style(d, &t.style, color, tinted) {
t.style = style;
}
}
_ => {}
}
if let EntityKind::Dimension(dm) = &mut e.kind {
// A dimension whose block can't be copied (past the limits) is drawn again from its
// geometry: its DIMCLR* overrides are ByBlock, so the original block would keep its colors.
dm.block = dm.block.take().and_then(|n| tinted_block(d, &n, color, tinted, depth));
return;
}
each_ref(&mut e.kind, |n| {
if let Some(copy) = tinted_block(d, n, color, tinted, depth) {
*n = copy;
}
});
}
/// The name of a copy of dimension style `name` that draws lines and text in the object's
/// color, made on first use. `None` when the drawing has no such style.
fn tinted_dim_style(d: &mut Drawing, name: &str, color: Color, tinted: &mut Tinted) -> Option<String> {
let key = (tag(color), "dimension style", name.to_ascii_uppercase());
if let Some(n) = tinted.names.get(&key) {
return Some(n.clone());
}
let st = d.dim_styles.iter().find(|s| s.name.eq_ignore_ascii_case(name))?.clone();
let base = format!("COMPARE_{}_{}", tag(color), st.name);
let new = tinted.next.find(
"dimension style",
&base,
d.dim_styles.len(),
|i| suffixed(&base, i),
|n| d.dim_styles.iter().any(|s| s.name.eq_ignore_ascii_case(n)),
);
d.dim_styles.push(DimStyle {
name: new.clone(),
dim_line_color: Color::ByBlock,
ext_line_color: Color::ByBlock,
text_color: Color::ByBlock,
..st
});
tinted.names.insert(key, new.clone());
Some(new)
}
/// The name of a copy of table style `name` whose cell styles draw text and borders in the
/// table's color and fill no cells, made on first use. `None` when the drawing has no such style.
fn tinted_table_style(d: &mut Drawing, name: &str, color: Color, tinted: &mut Tinted) -> Option<String> {
let key = (tag(color), "table style", name.to_ascii_uppercase());
if let Some(n) = tinted.names.get(&key) {
return Some(n.clone());
}
let st = d.table_styles.iter().find(|s| s.name.eq_ignore_ascii_case(name))?.clone();
let base = format!("COMPARE_{}_{}", tag(color), st.name);
let new = tinted.next.find(
"table style",
&base,
d.table_styles.len(),
|i| suffixed(&base, i),
|n| d.table_styles.iter().any(|s| s.name.eq_ignore_ascii_case(n)),
);
let names =
[CellStyle::TITLE, CellStyle::HEADER, CellStyle::DATA].into_iter().map(str::to_string).chain(st.cell_styles.iter().map(|c| c.name.clone()));
let mut cell_styles: Vec<CellStyle> = Vec::new();
for n in names {
if cell_styles.iter().any(|c| c.name.eq_ignore_ascii_case(&n)) {
continue;
}
let mut cs = st.cell_style(&n);
cs.name = n;
cs.text_color = Color::ByBlock;
cs.fill = None;
let b = &mut cs.borders;
for border in [&mut b.top, &mut b.inside_h, &mut b.bottom, &mut b.left, &mut b.inside_v, &mut b.right] {
border.color = Color::ByBlock;
}
cell_styles.push(cs);
}
d.table_styles.push(TableStyle { name: new.clone(), cell_styles, ..st });
tinted.names.insert(key, new.clone());
Some(new)
}
/// MTEXT contents without inline color codes (`\C1;`, `\c255;`), so the text takes the
/// object's color. An escaped backslash (`\\`) stays.
fn without_color_codes(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut it = s.chars().peekable();
while let Some(c) = it.next() {
if c != '\\' {
out.push(c);
continue;
}
match it.peek().copied() {
Some('C' | 'c') => {
for d in it.by_ref() {
if d == ';' {
break;
}
}
}
Some(n) => {
out.push('\\');
out.push(n);
it.next();
}
None => out.push('\\'),
}
}
out
}
/// The name of a copy of block `name` whose objects all have `color`, made on first use. `None`
/// (the reference keeps the original block) for an xref, a missing block, or past the nesting
/// and copy limits.
fn tinted_block(d: &mut Drawing, name: &str, color: Color, tinted: &mut Tinted, depth: usize) -> Option<String> {
let key = (tag(color), "block", name.to_ascii_uppercase());
if let Some(n) = tinted.names.get(&key) {
return Some(n.clone());
}
let b = d.block(name)?.clone();
if b.xref_path.is_some() {
return None;
}
if depth >= MAX_BLOCK_DEPTH || tinted.names.len() >= MAX_BLOCKS || tinted.copied.saturating_add(b.entities.len()) > MAX_COPIED {
tinted.uncolored = true;
return None;
}
tinted.copied += b.entities.len();
// A dimension's block copy stays a `*D` block, so saving keeps its geometry instead of
// drawing the dimension again.
let dim_block = b.anonymous && b.name.get(..2).is_some_and(|p| p.eq_ignore_ascii_case("*D"));
let Tinted { next, blocks, .. } = tinted;
let taken = blocks.get_or_insert_with(|| d.blocks.keys().map(|k| k.to_ascii_uppercase()).collect());
let is_taken = |n: &str| taken.contains(&n.to_ascii_uppercase());
let new = if dim_block {
next.find("block", "*D", d.blocks.len(), |i| format!("*D{i}"), is_taken)
} else {
let base = format!("COMPARE_{}_{}", tag(color), b.name.trim_start_matches('*'));
next.find("block", &base, d.blocks.len(), |i| suffixed(&base, i), is_taken)
};
taken.insert(new.to_ascii_uppercase());
tinted.names.insert(key, new.clone());
// Hold the name while the nested copies pick theirs.
d.blocks.insert(new.clone(), Arc::new(Block::new(&new)));
let mut copy = Block { name: new.clone(), anonymous: dim_block, entities: EntityStore::new(), ..(*b).clone() };
for e in b.entities.iter() {
let h = d.new_handle();
let mut e = Entity { handle: h, ..(**e).clone() };
detach(&mut e.kind);
paint(d, &mut e, color, tinted, depth + 1);
copy.entities.push(e);
}
d.blocks.insert(new.clone(), Arc::new(copy));
Some(new)
}
/// The name each block copied from the other drawing has in the snapshot, by its upper-case
/// name there.
type Renames = HashMap<String, String>;
/// Copy into `d` the blocks of `other` that `objs` refer to, directly or through other blocks.
/// A named block `d` already has keeps `d`'s definition when it draws the same thing there (the
/// same objects, base point, units, flags and xref file, in it and in the blocks it refers to);
/// otherwise the other drawing's block is copied as `PART_2` (or `PART_3`…). An anonymous block
/// (`*D3`, `*U7`…) whose name `d` already uses is copied under the next free name with the same
/// prefix. Copied
/// objects get new handles, and relative file paths are rebased on `folder`. The flag is true
/// when more than [`MAX_BLOCKS`] blocks or [`MAX_COPIED`] objects were needed and the rest were
/// left out.
fn copy_blocks<'a>(
d: &mut Drawing,
other: &Drawing,
objs: impl Iterator<Item = &'a Entity>,
folder: Option<&Path>,
tinted: &mut Tinted,
) -> (Renames, bool) {
let mut frontier: Vec<String> = objs.flat_map(|e| block_refs(&e.kind)).collect();
let mut seen = HashSet::new();
let mut needed: Vec<Arc<Block>> = Vec::new();
let mut left_out = false;
// `d`'s relative paths were made absolute from its own folder before the copy.
let (mut mine, mut theirs) = (super::Signer::with_frames(d, None), super::Signer::with_frames(other, folder));
// Whether `d`'s block `n` draws what `other`'s block `b` draws. Past the signers' work limit
// the signatures no longer cover every object, so the blocks count as different.
let mut same = |n: &str, b: &Block| {
!b.anonymous
&& d.block(n).is_some()
&& mine.block(n, 0) == theirs.block(n, 0)
&& mine.work < super::MAX_BLOCK_WORK
&& theirs.work < super::MAX_BLOCK_WORK
};
while let Some(n) = frontier.pop() {
let key = n.to_ascii_uppercase();
if seen.contains(&key) {
continue;
}
if seen.len() >= MAX_BLOCKS {
left_out |= other.block(&n).is_some_and(|b| !same(&n, b));
continue;
}
seen.insert(key);
let Some(b) = other.block(&n) else { continue };
if same(&n, b) {
continue;
}
if tinted.copied.saturating_add(b.entities.len()) > MAX_COPIED {
left_out = true;
continue;
}
tinted.copied += b.entities.len();
frontier.extend(b.entities.iter().flat_map(|e| block_refs(&e.kind)));
needed.push(b.clone());
}
// Names `d` does not use stay; the others get free names, avoiding the kept ones.
let mut renames = Renames::new();
let mut taken: HashSet<String> = d.blocks.keys().map(|k| k.to_ascii_uppercase()).collect();
let mut next = NextFree::default();
for b in needed.iter().filter(|b| d.block(&b.name).is_none()) {
taken.insert(b.name.to_ascii_uppercase());
renames.insert(b.name.to_ascii_uppercase(), b.name.clone());
}
for b in needed.iter().filter(|b| d.block(&b.name).is_some()) {
let is_taken = |n: &str| taken.contains(&n.to_ascii_uppercase());
let free = if b.anonymous {
let prefix: String = b.name.chars().take_while(|c| *c == '*' || c.is_ascii_alphabetic()).collect();
next.find("anonymous block", &prefix, taken.len(), |i| format!("{prefix}{i}"), is_taken)
} else {
next.find("block", &b.name, taken.len(), |i| suffixed(&b.name, i), is_taken)
};
taken.insert(free.to_ascii_uppercase());
renames.insert(b.name.to_ascii_uppercase(), free);
}
for b in needed {
let Some(name) = renames.get(&b.name.to_ascii_uppercase()).cloned() else { continue };
let mut copy = Block { name: name.clone(), entities: EntityStore::new(), ..(*b).clone() };
for e in b.entities.iter() {
let h = d.new_handle();
let mut e = Entity { handle: h, ..(**e).clone() };
detach(&mut e.kind);
rename_refs(&mut e.kind, &renames);
copy.entities.push(e);
}
rebase_block(&mut copy, folder);
d.blocks.insert(name, Arc::new(copy));
}
(renames, left_out)
}