From 58b2f22c093c4a2a9d1db2960a118c0575c565a1 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 13 Oct 2026 21:31:46 +0300 Subject: [PATCH] feat(compare): COMPARE overlays what differs from another drawing COMPARE picks a drawing (or takes its path) and matches its objects with this drawing's by what they draw: type, layer and tessellated geometry, so an unchanged copy matches whatever its handle. Objects only here are outlined in green, objects only in the other drawing in red, over the current drawing, and Next / Previous zoom from one difference to the next with a count in the prompt. A moved or edited object shows as one of each. eXit (or Esc) clears the overlay. Refs #970 Co-Authored-By: Claude Opus 5.5 --- locales/ar-SA/opencadstudio.ftl | 3 + locales/bg-BG/opencadstudio.ftl | 3 + locales/cs-CZ/opencadstudio.ftl | 3 + locales/de-DE/opencadstudio.ftl | 3 + locales/el-GR/opencadstudio.ftl | 3 + locales/en-US/opencadstudio.ftl | 3 + locales/es-ES/opencadstudio.ftl | 3 + locales/fi-FI/opencadstudio.ftl | 3 + locales/fr-FR/opencadstudio.ftl | 3 + locales/hi-IN/opencadstudio.ftl | 3 + locales/hu-HU/opencadstudio.ftl | 3 + locales/it-IT/opencadstudio.ftl | 3 + locales/ja-JP/opencadstudio.ftl | 3 + locales/ko-KR/opencadstudio.ftl | 3 + locales/lo-LA/opencadstudio.ftl | 3 + locales/nl-NL/opencadstudio.ftl | 3 + locales/pl-PL/opencadstudio.ftl | 3 + locales/pt-BR/opencadstudio.ftl | 3 + locales/ru-RU/opencadstudio.ftl | 3 + locales/tr-TR/opencadstudio.ftl | 3 + locales/zh-CN/opencadstudio.ftl | 3 + locales/zh-TW/opencadstudio.ftl | 3 + src/app/command_driver/mod.rs | 5 ++ src/app/commands/styleprops.rs | 36 ++++++++ src/command.rs | 5 ++ src/locale_catalog.rs | 3 + src/modules/manage/compare.rs | 104 ++++++++++++++++++++++ src/modules/manage/mod.rs | 1 + src/scene/compare.rs | 151 ++++++++++++++++++++++++++++++++ src/scene/mod.rs | 1 + 30 files changed, 372 insertions(+) create mode 100644 src/modules/manage/compare.rs create mode 100644 src/scene/compare.rs diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index d7f98e422..eea138efa 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -295,6 +295,9 @@ command-copy = .mode = COPY أدخل خيار وضع النسخ [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ هنا فقط (أخضر)، __ocs_fmt_1__ في الرسم الآخر فقط (أحمر)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = مقارنة مع رسم .label-associative-array-created = __ocs_fmt_0__: تم إنشاء مصفوفة ترابطية. .assoc-array-array-path = مصفوفة (مسار) .assoc-array-array-polar = مصفوفة (قطبية) diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index cb7085f4d..feee17bdc 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -282,6 +282,9 @@ command-copy = .displacement = COPY Задайте преместване: .mode = COPY Въведете режим на копиране [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ само тук (зелено), __ocs_fmt_1__ само в другия чертеж (червено)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Сравняване с чертеж .label-associative-array-created = __ocs_fmt_0__: създаден е асоциативен масив. .assoc-array-array-path = Масив (по път) .assoc-array-array-polar = Масив (полярен) diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 71bf17c6f..a32db3547 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -282,6 +282,9 @@ command-copy = .mode = COPY Zadejte režim kopírování [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ jen zde (zeleně), __ocs_fmt_1__ jen v druhém výkresu (červeně)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Porovnat s výkresem .label-associative-array-created = __ocs_fmt_0__: vytvořeno asociativní pole. .assoc-array-array-path = Pole (podél cesty) .assoc-array-array-polar = Pole (kruhové) diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 6ef673520..42ce8800d 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -283,6 +283,9 @@ command-copy = .mode = COPY Kopiermodus eingeben [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ nur hier (grün), __ocs_fmt_1__ nur in der anderen Zeichnung (rot)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Mit Zeichnung vergleichen .label-associative-array-created = __ocs_fmt_0__: assoziative Reihe erstellt. .assoc-array-array-path = Reihe (Pfad) .assoc-array-array-polar = Reihe (polar) diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 48f3a6273..f5646e118 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -282,6 +282,9 @@ command-copy = .mode = COPY Επιλέξτε λειτουργία αντιγραφής [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ μόνο εδώ (πράσινο), __ocs_fmt_1__ μόνο στο άλλο σχέδιο (κόκκινο)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Σύγκριση με σχέδιο .label-associative-array-created = __ocs_fmt_0__: δημιουργήθηκε συσχετιστικός πίνακας. .assoc-array-array-path = Πίνακας (διαδρομή) .assoc-array-array-polar = Πίνακας (πολικός) diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index d1781a219..f2539ea74 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -282,6 +282,9 @@ command-copy = .mode = COPY Enter a copy mode option [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ only here (green), __ocs_fmt_1__ only in the other drawing (red)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Compare with drawing .label-associative-array-created = __ocs_fmt_0__: associative array created. .assoc-array-array-path = Array (Path) .assoc-array-array-polar = Array (Polar) diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 96d19d420..47ec91dcf 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -283,6 +283,9 @@ command-copy = .mode = COPY Indique modo de copia [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ solo aquí (verde), __ocs_fmt_1__ solo en el otro dibujo (rojo)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Comparar con dibujo .label-associative-array-created = __ocs_fmt_0__: se ha creado una matriz asociativa. .assoc-array-array-path = Matriz (trayectoria) .assoc-array-array-polar = Matriz (polar) diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 672a1be0e..1eb2acba2 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -282,6 +282,9 @@ command-copy = .mode = COPY Valitse kopiointitila [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ vain täällä (vihreä), __ocs_fmt_1__ vain toisessa piirustuksessa (punainen)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Vertaa piirustukseen .label-associative-array-created = __ocs_fmt_0__: assosiatiivinen taulukko luotu. .assoc-array-array-path = Taulukko (polku) .assoc-array-array-polar = Taulukko (polaarinen) diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index eb9f59748..1193628d7 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -283,6 +283,9 @@ command-copy = .mode = COPY Entrez le mode de copie [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ seulement ici (vert), __ocs_fmt_1__ seulement dans l’autre dessin (rouge)__ocs_fmt_2__ [Next/Previous/eXit] : + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Comparer avec un dessin .label-associative-array-created = __ocs_fmt_0__ : réseau associatif créé. .assoc-array-array-path = Réseau (trajectoire) .assoc-array-array-polar = Réseau (polaire) diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 6c58c7b57..d3a7193ef 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -271,6 +271,9 @@ command-copy = .mode = COPY कॉपी मोड विकल्प दर्ज करें [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ केवल यहाँ (हरा), __ocs_fmt_1__ केवल दूसरी ड्रॉइंग में (लाल)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = ड्रॉइंग से तुलना करें .label-associative-array-created = __ocs_fmt_0__: सहयोगी ऐरे बनाया गया। .assoc-array-array-path = ऐरे (पथ) .assoc-array-array-polar = ऐरे (ध्रुवीय) diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 75d445a98..a5e19f84a 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -281,6 +281,9 @@ command-copy = .mode = COPY Adja meg a másolási módot [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ csak itt (zöld), __ocs_fmt_1__ csak a másik rajzban (piros)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Összehasonlítás rajzzal .label-associative-array-created = __ocs_fmt_0__: asszociatív tömb létrehozva. .assoc-array-array-path = Tömb (útvonal) .assoc-array-array-polar = Tömb (poláris) diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index fc91c30db..ee88209fc 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -282,6 +282,9 @@ command-copy = .mode = COPY Specificare modalità di copia [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ solo qui (verde), __ocs_fmt_1__ solo nell'altro disegno (rosso)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Confronta con disegno .label-associative-array-created = __ocs_fmt_0__: serie associativa creata. .assoc-array-array-path = Serie (percorso) .assoc-array-array-polar = Serie (polare) diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 5fbb8a6e6..c404bf79d 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -273,6 +273,9 @@ command-copy = .mode = COPY コピーモードを入力 [Single/Multiple] : common = + .compare-prompt = COMPARE ここのみ __ocs_fmt_0__ (緑)、もう一方の図面のみ __ocs_fmt_1__ (赤)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = 図面と比較 .label-associative-array-created = __ocs_fmt_0__: 自動調整配列複写を作成しました。 .assoc-array-array-path = 配列複写 (パス) .assoc-array-array-polar = 配列複写 (円形状) diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 8d25f85c4..9b090c1b2 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -282,6 +282,9 @@ command-copy = .mode = COPY 복사 모드 옵션 입력 [Single/Multiple] : common = + .compare-prompt = COMPARE 여기에만 __ocs_fmt_0__ (녹색), 다른 도면에만 __ocs_fmt_1__ (빨간색)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = 도면과 비교 .label-associative-array-created = __ocs_fmt_0__: 연관 배열을 만들었습니다. .assoc-array-array-path = 배열 (경로) .assoc-array-array-polar = 배열 (원형) diff --git a/locales/lo-LA/opencadstudio.ftl b/locales/lo-LA/opencadstudio.ftl index c188faa06..daa8e1ac4 100644 --- a/locales/lo-LA/opencadstudio.ftl +++ b/locales/lo-LA/opencadstudio.ftl @@ -282,6 +282,9 @@ command-copy = .mode = COPY ປ້ອນຕົວເລືອກໂໝດການຄັດລອກ [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ ຢູ່ນີ້ເທົ່ານັ້ນ (ຂຽວ), __ocs_fmt_1__ ຢູ່ໃນແບບອື່ນເທົ່ານັ້ນ (ແດງ)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = ປຽບທຽບກັບແບບ .label-associative-array-created = __ocs_fmt_0__: ສ້າງອາເຣແບບເຊື່ອມໂຍງແລ້ວ. .assoc-array-array-path = ອາເຣ (ເສັ້ນທາງ) .assoc-array-array-polar = ອາເຣ (ວົງມົນ) diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index f621a55f9..aa2947764 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -283,6 +283,9 @@ command-copy = .mode = COPY Geef kopieermodus op [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ alleen hier (groen), __ocs_fmt_1__ alleen in de andere tekening (rood)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Vergelijken met tekening .label-associative-array-created = __ocs_fmt_0__: associatieve reeks gemaakt. .assoc-array-array-path = Reeks (pad) .assoc-array-array-polar = Reeks (polair) diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 648827533..e14539fb9 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -282,6 +282,9 @@ command-copy = .mode = COPY Podaj tryb kopiowania [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ tylko tutaj (zielone), __ocs_fmt_1__ tylko w drugim rysunku (czerwone)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Porównaj z rysunkiem .label-associative-array-created = __ocs_fmt_0__: utworzono szyk skojarzony. .assoc-array-array-path = Szyk (ścieżka) .assoc-array-array-polar = Szyk (biegunowy) diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index eacb449da..a27c55e6f 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -283,6 +283,9 @@ command-copy = .mode = COPY Informe o modo de cópia [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ só aqui (verde), __ocs_fmt_1__ só no outro desenho (vermelho)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Comparar com desenho .label-associative-array-created = __ocs_fmt_0__: matriz associativa criada. .assoc-array-array-path = Matriz (caminho) .assoc-array-array-polar = Matriz (polar) diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index eac8d2f50..8ce95ea49 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -286,6 +286,9 @@ command-copy = .mode = COPY Задайте режим копирования [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ только здесь (зелёные), __ocs_fmt_1__ только в другом чертеже (красные)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Сравнить с чертежом .label-associative-array-created = __ocs_fmt_0__: создан ассоциативный массив. .assoc-array-array-path = Массив (по траектории) .assoc-array-array-polar = Массив (круговой) diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index de6964037..5230c586a 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -268,6 +268,9 @@ command-copy = .mode = COPY Kopyalama modu seçin [Single/Multiple] : common = + .compare-prompt = COMPARE __ocs_fmt_0__ yalnızca burada (yeşil), __ocs_fmt_1__ yalnızca diğer çizimde (kırmızı)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = Çizimle karşılaştır .label-associative-array-created = __ocs_fmt_0__: ilişkisel dizi oluşturuldu. .assoc-array-array-path = Dizi (Yol) .assoc-array-array-polar = Dizi (Kutupsal) diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index c1c21d60e..87f586862 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -270,6 +270,9 @@ command-copy = .mode = COPY 输入复制模式选项 [Single/Multiple] : common = + .compare-prompt = COMPARE 仅此处 __ocs_fmt_0__ (绿色),仅另一图形 __ocs_fmt_1__ (红色)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = 与图形比较 .label-associative-array-created = __ocs_fmt_0__: 已创建关联阵列。 .assoc-array-array-path = 阵列 (路径) .assoc-array-array-polar = 阵列 (环形) diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 11dd5b6ee..57e1801ad 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -282,6 +282,9 @@ command-copy = .mode = COPY 輸入複製模式選項 [Single/Multiple] : common = + .compare-prompt = COMPARE 僅此處 __ocs_fmt_0__ (綠色),僅另一圖面 __ocs_fmt_1__ (紅色)__ocs_fmt_2__ [Next/Previous/eXit]: + .compare-error = COMPARE: __ocs_fmt_0__ + .compare-with-drawing = 與圖面比較 .label-associative-array-created = __ocs_fmt_0__: 已建立關聯陣列。 .assoc-array-array-path = 陣列 (路徑) .assoc-array-array-polar = 陣列 (環形) diff --git a/src/app/command_driver/mod.rs b/src/app/command_driver/mod.rs index 7c4a40807..9300ab80e 100644 --- a/src/app/command_driver/mod.rs +++ b/src/app/command_driver/mod.rs @@ -885,6 +885,11 @@ impl OpenCADStudio { CmdResult::ZoomToWindow { p1, p2 } => { self.handle_zoom_to_window(p1, p2); } + CmdResult::Show { p1, p2 } => { + let i = self.active_tab; + self.tabs[i].scene.remember_current_view(); + self.tabs[i].scene.zoom_to_window(p1.as_vec3(), p2.as_vec3()); + } CmdResult::CommitAndZoom { entity, p1, p2 } => { self.handle_commit_entities_and_exit(vec![entity], false, false); self.handle_zoom_to_window(p1, p2); diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index fcf75ec7d..430256eaf 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -3335,6 +3335,42 @@ impl OpenCADStudio { // Read-only scan: flags entities on undefined layers and block // references to undefined block definitions. Reports only; it does // not auto-repair (so it can never make the drawing worse). + // COMPARE — overlay what differs from another drawing. Bare picks + // the drawing; with a path it compares at once. + "COMPARE" => { + return Some(Task::perform( + async { + crate::sys::file_dialog() + .set_title(crate::t!("Compare with drawing").as_ref()) + .add_filter(crate::t!("Drawing").as_ref(), &["dwg", "dxf"]) + .pick_file() + .await + .map(|handle| crate::sys::handle_path(&handle)) + }, + |path| match path { + Some(path) => Message::Command(format!("COMPARE {}", path.display())), + None => Message::Noop, + }, + )); + } + cmd if cmd.starts_with("COMPARE ") => { + let path = std::path::PathBuf::from(cmd.trim_start_matches("COMPARE").trim()); + match crate::io::load_file(&path) { + Ok(other) => { + let comparison = + crate::scene::compare::compare(&self.tabs[i].scene, other); + // The overlay shows at once, before the cursor moves. + self.tabs[i].scene.set_preview_wires(comparison.overlay.clone()); + let command = + crate::modules::manage::compare::CompareCommand::new(comparison); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + Err(error) => self + .command_line + .push_error(crate::tf!("COMPARE: {error}").as_ref()), + } + } "AUDIT" => { use std::collections::BTreeSet; let mut undefined_layers: BTreeSet = BTreeSet::new(); diff --git a/src/command.rs b/src/command.rs index b3b3a9b1c..6cd30c03c 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1948,6 +1948,11 @@ pub enum CmdResult { p1: DVec3, p2: DVec3, }, + /// Zoom to the given corner points; the command stays active. + Show { + p1: DVec3, + p2: DVec3, + }, /// Commit `entity`, then zoom to the given corner points; end command. CommitAndZoom { entity: EntityType, diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 9bb77b77b..4a4745e31 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -4227,6 +4227,9 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "UCS requires origin, X-axis point and XY-plane point." => Some(("common", "ucs-requires-origin-x-axis-point-and-xy-plane-point")), "DWGUNITS current unit: {current}" => Some(("common", "dwgunits-current-unit")), "LAYTRANS: {why}." => Some(("common", "laytrans")), + "Compare with drawing" => Some(("common", "compare-with-drawing")), + "COMPARE: {error}" => Some(("common", "compare-error")), + "COMPARE {added} only here (green), {removed} only in the other drawing (red){position} [Next/Previous/eXit]:" => Some(("common", "compare-prompt")), "LAYTRANS: no layer names in common — use LAYTRANS with no file to map them." => Some(("common", "laytrans-no-layer-names-in-common-use-laytrans-with-no-file-to-map-them")), "DWGUNITS: not a unit this drawing can use." => Some(("common", "dwgunits-not-a-unit-this-drawing-can-use")), "DWGUNITS: already in {label}." => Some(("common", "dwgunits-already-in")), diff --git a/src/modules/manage/compare.rs b/src/modules/manage/compare.rs new file mode 100644 index 000000000..45c4c05bd --- /dev/null +++ b/src/modules/manage/compare.rs @@ -0,0 +1,104 @@ +// COMPARE — show what differs from another drawing and step through it. +// +// COMPARE only here (green), only in the other drawing +// (red) [Next/Previous/eXit]: + +use crate::command::{CadCommand, CmdOption, CmdResult}; +use crate::scene::compare::Comparison; +use crate::scene::WireModel; +use glam::DVec3; + +pub struct CompareCommand { + comparison: Comparison, + /// The difference shown last; `None` before the first step. + current: Option, +} + +impl CompareCommand { + pub fn new(comparison: Comparison) -> Self { + Self { + comparison, + current: None, + } + } + + fn show(&mut self, forward: bool) -> CmdResult { + let count = self.comparison.spots.len(); + if count == 0 { + return CmdResult::NeedPoint; + } + let next = match (self.current, forward) { + (None, true) => 0, + (None, false) => count - 1, + (Some(index), true) => (index + 1) % count, + (Some(index), false) => (index + count - 1) % count, + }; + self.current = Some(next); + let (min, max) = self.comparison.spots[next]; + // A margin around the difference so it is seen in its context. + let margin = ((max - min).length() * 0.5).max(1.0); + CmdResult::Show { + p1: min - DVec3::splat(margin), + p2: max + DVec3::splat(margin), + } + } +} + +impl CadCommand for CompareCommand { + fn name(&self) -> &'static str { + "COMPARE" + } + + fn prompt(&self) -> String { + let (added, removed) = (self.comparison.added, self.comparison.removed); + let position = match self.current { + Some(index) => format!(" {}/{}", index + 1, self.comparison.spots.len()), + None => String::new(), + }; + crate::tf!( + "COMPARE {added} only here (green), {removed} only in the other drawing (red){position} [Next/Previous/eXit]:" + ) + .into_owned() + } + + fn options(&self) -> Vec { + vec![ + CmdOption::new("Next", "N"), + CmdOption::new("Previous", "P"), + CmdOption::new("eXit", "X"), + ] + } + + fn wants_text_input(&self) -> bool { + true + } + + fn on_text_input(&mut self, text: &str) -> Option { + match text.trim().trim_start_matches('_').to_ascii_uppercase().as_str() { + "" | "N" | "NEXT" => Some(self.show(true)), + "P" | "PREVIOUS" => Some(self.show(false)), + "X" | "EXIT" => Some(CmdResult::Cancel), + _ => None, + } + } + + fn on_point(&mut self, _pt: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + self.show(true) + } + + fn on_escape(&mut self) -> CmdResult { + CmdResult::Cancel + } + + fn on_preview_wires(&mut self, _pt: DVec3) -> Vec { + // ponytail: the overlay is cloned per frame; cache it in the scene if + // comparisons of very large drawings stutter. + self.comparison.overlay.clone() + } +} + +inventory::submit!(crate::command::CommandRegistration { names: &["COMPARE"] }); diff --git a/src/modules/manage/mod.rs b/src/modules/manage/mod.rs index 48fda655e..7c7f3a7f0 100644 --- a/src/modules/manage/mod.rs +++ b/src/modules/manage/mod.rs @@ -2,6 +2,7 @@ mod about; mod audit; +pub mod compare; mod cui_export; mod cui_import; mod find_nonpurgeable; diff --git a/src/scene/compare.rs b/src/scene/compare.rs new file mode 100644 index 000000000..dd18ef101 --- /dev/null +++ b/src/scene/compare.rs @@ -0,0 +1,151 @@ +//! COMPARE: what differs between this drawing and another one. Objects are +//! matched by what they draw — type, layer and their tessellated geometry — +//! so a copy that kept its geometry matches whatever its handle, and an +//! object that moved or changed shows as removed in one place and added in +//! the other. + +use codec::{CadDocument, Handle}; +use glam::DVec3; +use rustc_hash::FxHashMap; + +use crate::scene::{Scene, WireModel}; + +/// The result of a comparison, ready to draw over the current drawing. +pub struct Comparison { + /// Objects only this drawing has. + pub added: usize, + /// Objects only the other drawing has. + pub removed: usize, + /// Their outlines: added in green, removed in red. + pub overlay: Vec, + /// Each difference's extent, in reading order, to step through. + pub spots: Vec<(DVec3, DVec3)>, +} + +const ADDED: [f32; 4] = [0.25, 0.85, 0.35, 1.0]; +const REMOVED: [f32; 4] = [0.95, 0.3, 0.25, 1.0]; + +fn wires_by_entity(scene: &Scene) -> FxHashMap> { + let mut map: FxHashMap> = FxHashMap::default(); + for wire in scene.entity_wires().iter() { + if let Some(handle) = Scene::handle_from_wire_name(&wire.name) { + map.entry(handle).or_default().push(wire.clone()); + } + } + map +} + +fn point(wire: &WireModel, index: usize) -> DVec3 { + let high = wire.points[index]; + let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]); + DVec3::new( + f64::from(high[0]) + f64::from(low[0]), + f64::from(high[1]) + f64::from(low[1]), + f64::from(high[2]) + f64::from(low[2]), + ) +} + +/// What an object draws, as a comparable key: its type, its layer and its +/// outline points rounded to 1e-4 drawing units. +fn signature(document: &CadDocument, handle: Handle, wires: &[WireModel]) -> String { + let mut key = match document.get_entity(handle) { + Some(entity) => format!( + "{}|{}", + crate::entities::names::ui_name(entity), + entity.common().layer + ), + None => String::new(), + }; + for wire in wires { + key.push('#'); + for index in 0..wire.points.len() { + let p = point(wire, index); + if !p.is_finite() { + key.push('/'); + continue; + } + let q = (p * 1e4).round(); + key.push_str(&format!("{},{},{};", q.x, q.y, q.z)); + } + } + key +} + +fn extent(wires: &[WireModel]) -> Option<(DVec3, DVec3)> { + let mut bounds: Option<(DVec3, DVec3)> = None; + for wire in wires { + for index in 0..wire.points.len() { + let p = point(wire, index); + if !p.is_finite() { + continue; + } + bounds = Some(match bounds { + None => (p, p), + Some((min, max)) => (min.min(p), max.max(p)), + }); + } + } + bounds +} + +/// Compare `current` with the drawing `other`, in the space `current` +/// shows. +pub fn compare(current: &Scene, other: CadDocument) -> Comparison { + let mut other_scene = Scene::new(); + other_scene.document = other; + other_scene.bump_geometry(); + let here = wires_by_entity(current); + let there = wires_by_entity(&other_scene); + + let mut unmatched: FxHashMap> = FxHashMap::default(); + for (handle, wires) in &there { + unmatched + .entry(signature(&other_scene.document, *handle, wires)) + .or_default() + .push(*handle); + } + let mut overlay = Vec::new(); + let mut spots = Vec::new(); + let mut added = 0; + let mut mark = |wires: &[WireModel], color: [f32; 4]| { + if let Some(bounds) = extent(wires) { + spots.push(bounds); + } + overlay.extend(wires.iter().cloned().map(|mut wire| { + wire.color = color; + wire.selected = false; + wire.line_weight_px = wire.line_weight_px.max(2.0); + wire + })); + }; + for (handle, wires) in &here { + let key = signature(¤t.document, *handle, wires); + match unmatched.get_mut(&key).and_then(Vec::pop) { + Some(_) => {} + None => { + added += 1; + mark(wires, ADDED); + } + } + } + let mut removed = 0; + for handle in unmatched.into_values().flatten() { + if let Some(wires) = there.get(&handle) { + removed += 1; + mark(wires, REMOVED); + } + } + // Reading order: top to bottom, then left to right. + spots.sort_by(|a, b| { + b.1.y + .partial_cmp(&a.1.y) + .unwrap_or(std::cmp::Ordering::Equal) + .then(a.0.x.partial_cmp(&b.0.x).unwrap_or(std::cmp::Ordering::Equal)) + }); + Comparison { + added, + removed, + overlay, + spots, + } +} diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 4546c5fe7..8f19519e2 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -7,6 +7,7 @@ // cache — block-definition and property caches pub mod annotative; pub mod assoc_array; +pub mod compare; pub mod block_service; pub mod cache; pub mod convert;