diff --git a/assets/icons/ui/info.svg b/assets/icons/ui/info.svg
new file mode 100644
index 000000000..212f70fce
--- /dev/null
+++ b/assets/icons/ui/info.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/ui/more.svg b/assets/icons/ui/more.svg
new file mode 100644
index 000000000..50d01007a
--- /dev/null
+++ b/assets/icons/ui/more.svg
@@ -0,0 +1 @@
+
diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl
index 5aeb8221c..1c01174f9 100644
--- a/locales/ar-SA/opencadstudio.ftl
+++ b/locales/ar-SA/opencadstudio.ftl
@@ -5939,3 +5939,41 @@ style-manager =
.center-of-text-overline = مركز النص مع خط علوي
.block-extents = نطاق الكتلة
.base-point = نقطة الأساس
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = تم العثور على __ocs_fmt_0__
+ .found-total = تم العثور على __ocs_fmt_0__، الإجمالي __ocs_fmt_1__
+ .field-only-in-count = ** الأمر COUNTFIELD متاح فقط أثناء العد. **
+ .area-active = منطقة العد نشطة.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (للقراءة فقط)
+ .requires-0-or-1 = يتطلب 0 أو 1 فقط.
+ .requires-int-range = يتطلب عددًا صحيحًا بين __ocs_fmt_0__ و __ocs_fmt_1__.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT نمط النص الحالي: "__ocs_fmt_0__" ارتفاع النص: __ocs_fmt_1__
+ .fields-found = تم العثور على __ocs_fmt_0__ من الحقول.
+ .fields-updated = تم تحديث __ocs_fmt_0__ من الحقول.
+ .invalid-keyword = كلمة مفتاحية للخيار غير صالحة.
+ .invalid-boundary = كائن حد منطقة العد غير صالح. حدد خطًا متعددًا مغلقًا يتكون من قطع مستقيمة ولا يتقاطع مع نفسه.
+ .block-not-found = تعذر العثور على الكتلة "__ocs_fmt_0__".
+ .invalid-point = نقطة غير صالحة.
diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl
index 07809df3b..fe2b0bf6c 100644
--- a/locales/bg-BG/opencadstudio.ftl
+++ b/locales/bg-BG/opencadstudio.ftl
@@ -5927,3 +5927,41 @@ style-manager =
.center-of-text-overline = Център на надчертан текст
.block-extents = Обхват на блок
.base-point = Базова точка
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = Намерени: __ocs_fmt_0__
+ .found-total = Намерени: __ocs_fmt_0__, общо: __ocs_fmt_1__
+ .field-only-in-count = ** Командата COUNTFIELD е достъпна само по време на преброяване. **
+ .area-active = Областта за преброяване е активна.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (само за четене)
+ .requires-0-or-1 = Изисква се само 0 или 1.
+ .requires-int-range = Изисква се цяло число между __ocs_fmt_0__ и __ocs_fmt_1__.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT Текущ текстов стил: "__ocs_fmt_0__" Височина на текста: __ocs_fmt_1__
+ .fields-found = Намерени полета: __ocs_fmt_0__.
+ .fields-updated = Обновени полета: __ocs_fmt_0__.
+ .invalid-keyword = Невалидна ключова дума на опция.
+ .invalid-boundary = Невалиден обект за граница на областта за преброяване. Изберете затворена полилиния от отсечки, която не се самопресича.
+ .block-not-found = Блокът "__ocs_fmt_0__" не може да бъде намерен.
+ .invalid-point = Невалидна точка.
diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl
index 45ca319b6..10cc0a116 100644
--- a/locales/cs-CZ/opencadstudio.ftl
+++ b/locales/cs-CZ/opencadstudio.ftl
@@ -5924,3 +5924,41 @@ style-manager =
.center-of-text-overline = Střed textu s přeškrtnutím
.block-extents = Blokovat rozsahy
.base-point = Základní bod
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = Nalezeno: __ocs_fmt_0__
+ .found-total = Nalezeno: __ocs_fmt_0__, celkem: __ocs_fmt_1__
+ .field-only-in-count = ** Příkaz COUNTFIELD je dostupný pouze během počítání. **
+ .area-active = Oblast počítání je aktivní.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (jen pro čtení)
+ .requires-0-or-1 = Vyžaduje pouze 0 nebo 1.
+ .requires-int-range = Vyžaduje celé číslo mezi __ocs_fmt_0__ a __ocs_fmt_1__.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT Aktuální styl textu: "__ocs_fmt_0__" Výška textu: __ocs_fmt_1__
+ .fields-found = Nalezeno polí: __ocs_fmt_0__.
+ .fields-updated = Aktualizováno polí: __ocs_fmt_0__.
+ .invalid-keyword = Neplatné klíčové slovo volby.
+ .invalid-boundary = Neplatný objekt hranice oblasti počítání. Vyberte uzavřenou křivku složenou z úseček, která neprotíná sama sebe.
+ .block-not-found = Blok "__ocs_fmt_0__" nelze najít.
+ .invalid-point = Neplatný bod.
diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl
index 0b211d9e9..605ccb174 100644
--- a/locales/de-DE/opencadstudio.ftl
+++ b/locales/de-DE/opencadstudio.ftl
@@ -5925,3 +5925,41 @@ style-manager =
.center-of-text-overline = Textzentrum mit Oberstrich
.block-extents = Blockgrenzen
.base-point = Basispunkt
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = __ocs_fmt_0__ gefunden
+ .found-total = __ocs_fmt_0__ gefunden, __ocs_fmt_1__ gesamt
+ .field-only-in-count = ** Der Befehl COUNTFIELD ist nur während der Zählung verfügbar. **
+ .area-active = Zählbereich ist aktiv.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (schreibgeschützt)
+ .requires-0-or-1 = Nur 0 oder 1 zulässig.
+ .requires-int-range = Erfordert eine ganze Zahl zwischen __ocs_fmt_0__ und __ocs_fmt_1__.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT Aktueller Textstil: "__ocs_fmt_0__" Texthöhe: __ocs_fmt_1__
+ .fields-found = __ocs_fmt_0__ Feld(er) gefunden.
+ .fields-updated = __ocs_fmt_0__ Feld(er) aktualisiert.
+ .invalid-keyword = Ungültiges Optionsschlüsselwort.
+ .invalid-boundary = Ungültiges Begrenzungsobjekt für den Zählbereich. Wählen Sie eine geschlossene Polylinie aus Liniensegmenten, die sich nicht selbst schneidet.
+ .block-not-found = Block "__ocs_fmt_0__" wurde nicht gefunden.
+ .invalid-point = Ungültiger Punkt.
diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl
index 5a9422483..433624525 100644
--- a/locales/el-GR/opencadstudio.ftl
+++ b/locales/el-GR/opencadstudio.ftl
@@ -5927,3 +5927,41 @@ style-manager =
.center-of-text-overline = Κέντρο του κειμένου με επιγράμμιση
.block-extents = Όρια μπλοκ
.base-point = Σημείο βάσης
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = Βρέθηκαν: __ocs_fmt_0__
+ .found-total = Βρέθηκαν: __ocs_fmt_0__, σύνολο: __ocs_fmt_1__
+ .field-only-in-count = ** Η εντολή COUNTFIELD είναι διαθέσιμη μόνο κατά την καταμέτρηση. **
+ .area-active = Η περιοχή καταμέτρησης είναι ενεργή.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (μόνο για ανάγνωση)
+ .requires-0-or-1 = Απαιτείται μόνο 0 ή 1.
+ .requires-int-range = Απαιτείται ακέραιος μεταξύ __ocs_fmt_0__ και __ocs_fmt_1__.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT Τρέχον στυλ κειμένου: "__ocs_fmt_0__" Ύψος κειμένου: __ocs_fmt_1__
+ .fields-found = Βρέθηκαν πεδία: __ocs_fmt_0__.
+ .fields-updated = Ενημερώθηκαν πεδία: __ocs_fmt_0__.
+ .invalid-keyword = Μη έγκυρη λέξη-κλειδί επιλογής.
+ .invalid-boundary = Μη έγκυρο αντικείμενο ορίου περιοχής καταμέτρησης. Επιλέξτε μια κλειστή πολυγραμμή από ευθύγραμμα τμήματα που δεν τέμνει τον εαυτό της.
+ .block-not-found = Το μπλοκ "__ocs_fmt_0__" δεν βρέθηκε.
+ .invalid-point = Μη έγκυρο σημείο.
diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl
index 2949f49ae..6f3f4730d 100644
--- a/locales/en-US/opencadstudio.ftl
+++ b/locales/en-US/opencadstudio.ftl
@@ -5928,3 +5928,41 @@ style-manager =
.center-of-text-overline = Center of text with overline
.block-extents = Block extents
.base-point = Base point
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = __ocs_fmt_0__ found
+ .found-total = __ocs_fmt_0__ found, __ocs_fmt_1__ total
+ .field-only-in-count = ** COUNTFIELD command only available during Count. **
+ .area-active = count area is active.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (read only)
+ .requires-0-or-1 = Requires 0 or 1 only.
+ .requires-int-range = Requires an integer between __ocs_fmt_0__ and __ocs_fmt_1__.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT Current text style: "__ocs_fmt_0__" Text height: __ocs_fmt_1__
+ .fields-found = __ocs_fmt_0__ field(s) found.
+ .fields-updated = __ocs_fmt_0__ field(s) updated.
+ .invalid-keyword = Invalid option keyword.
+ .invalid-boundary = Invalid count area boundary object. Select a closed polyline consisting of line segments and does not intersect itself.
+ .block-not-found = Block "__ocs_fmt_0__" cannot be found.
+ .invalid-point = Invalid point.
diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl
index 6235fc49f..4bb6669be 100644
--- a/locales/es-ES/opencadstudio.ftl
+++ b/locales/es-ES/opencadstudio.ftl
@@ -5927,3 +5927,41 @@ style-manager =
.center-of-text-overline = Centro del texto con suprarrayado
.block-extents = Extensión del bloque
.base-point = Punto base
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = __ocs_fmt_0__ encontrado(s)
+ .found-total = __ocs_fmt_0__ encontrado(s), __ocs_fmt_1__ en total
+ .field-only-in-count = ** El comando COUNTFIELD solo está disponible durante el recuento. **
+ .area-active = El área de recuento está activa.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (solo lectura)
+ .requires-0-or-1 = Solo admite 0 o 1.
+ .requires-int-range = Se requiere un entero entre __ocs_fmt_0__ y __ocs_fmt_1__.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT Estilo de texto actual: "__ocs_fmt_0__" Altura de texto: __ocs_fmt_1__
+ .fields-found = __ocs_fmt_0__ campo(s) encontrado(s).
+ .fields-updated = __ocs_fmt_0__ campo(s) actualizado(s).
+ .invalid-keyword = Palabra clave de opción no válida.
+ .invalid-boundary = Objeto de contorno de área de recuento no válido. Seleccione una polilínea cerrada formada por segmentos de línea que no se corte a sí misma.
+ .block-not-found = No se encuentra el bloque "__ocs_fmt_0__".
+ .invalid-point = Punto no válido.
diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl
index 377ab482f..e01e8ffca 100644
--- a/locales/fi-FI/opencadstudio.ftl
+++ b/locales/fi-FI/opencadstudio.ftl
@@ -5924,3 +5924,41 @@ style-manager =
.center-of-text-overline = Tekstin keskikohta yliviivauksella
.block-extents = Estä laajuudet
.base-point = Peruspiste
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = Löytyi __ocs_fmt_0__
+ .found-total = Löytyi __ocs_fmt_0__, yhteensä __ocs_fmt_1__
+ .field-only-in-count = ** COUNTFIELD-komento on käytettävissä vain laskennan aikana. **
+ .area-active = Laskenta-alue on aktiivinen.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (vain luku)
+ .requires-0-or-1 = Vain 0 tai 1 sallitaan.
+ .requires-int-range = Vaaditaan kokonaisluku väliltä __ocs_fmt_0__–__ocs_fmt_1__.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT Nykyinen tekstityyli: "__ocs_fmt_0__" Tekstin korkeus: __ocs_fmt_1__
+ .fields-found = Kenttiä löytyi: __ocs_fmt_0__.
+ .fields-updated = Kenttiä päivitetty: __ocs_fmt_0__.
+ .invalid-keyword = Virheellinen asetuksen avainsana.
+ .invalid-boundary = Virheellinen laskenta-alueen rajaobjekti. Valitse suljettu, janoista koostuva moniviiva, joka ei leikkaa itseään.
+ .block-not-found = Lohkoa "__ocs_fmt_0__" ei löydy.
+ .invalid-point = Virheellinen piste.
diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl
index 39881c6cd..ca2bda963 100644
--- a/locales/fr-FR/opencadstudio.ftl
+++ b/locales/fr-FR/opencadstudio.ftl
@@ -5926,3 +5926,41 @@ style-manager =
.center-of-text-overline = Centre du texte avec surlignement
.block-extents = Limites du bloc
.base-point = Point de base
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = __ocs_fmt_0__ trouvé(s)
+ .found-total = __ocs_fmt_0__ trouvé(s), __ocs_fmt_1__ au total
+ .field-only-in-count = ** La commande COUNTFIELD n'est disponible que pendant le comptage. **
+ .area-active = La zone de comptage est active.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (lecture seule)
+ .requires-0-or-1 = Nécessite 0 ou 1 uniquement.
+ .requires-int-range = Nécessite un entier compris entre __ocs_fmt_0__ et __ocs_fmt_1__.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT Style de texte courant : "__ocs_fmt_0__" Hauteur du texte : __ocs_fmt_1__
+ .fields-found = __ocs_fmt_0__ champ(s) trouvé(s).
+ .fields-updated = __ocs_fmt_0__ champ(s) mis à jour.
+ .invalid-keyword = Mot-clé d'option non valide.
+ .invalid-boundary = Objet de contour de zone de comptage non valide. Sélectionnez une polyligne fermée composée de segments de ligne et qui ne se recoupe pas.
+ .block-not-found = Le bloc "__ocs_fmt_0__" est introuvable.
+ .invalid-point = Point non valide.
diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl
index 25de21e6b..3585bacf9 100644
--- a/locales/hi-IN/opencadstudio.ftl
+++ b/locales/hi-IN/opencadstudio.ftl
@@ -5915,3 +5915,41 @@ style-manager =
.center-of-text-overline = ऊपरी रेखा सहित टेक्स्ट का केंद्र
.block-extents = ब्लॉक परिमिति
.base-point = आधार बिंदु
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = __ocs_fmt_0__ मिले
+ .found-total = __ocs_fmt_0__ मिले, कुल __ocs_fmt_1__
+ .field-only-in-count = ** COUNTFIELD कमांड केवल गणना के दौरान उपलब्ध है। **
+ .area-active = गणना क्षेत्र सक्रिय है।
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (केवल पढ़ने के लिए)
+ .requires-0-or-1 = केवल 0 या 1 आवश्यक है।
+ .requires-int-range = __ocs_fmt_0__ और __ocs_fmt_1__ के बीच पूर्णांक आवश्यक है।
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT वर्तमान टेक्स्ट शैली: "__ocs_fmt_0__" टेक्स्ट ऊँचाई: __ocs_fmt_1__
+ .fields-found = __ocs_fmt_0__ फ़ील्ड मिले।
+ .fields-updated = __ocs_fmt_0__ फ़ील्ड अपडेट किए गए।
+ .invalid-keyword = अमान्य विकल्प कीवर्ड।
+ .invalid-boundary = अमान्य गणना क्षेत्र सीमा ऑब्जेक्ट। रेखा खंडों से बनी ऐसी बंद पॉलीलाइन चुनें जो स्वयं को प्रतिच्छेद न करे।
+ .block-not-found = ब्लॉक "__ocs_fmt_0__" नहीं मिला।
+ .invalid-point = अमान्य बिंदु।
diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl
index 3ecb0b524..665907146 100644
--- a/locales/hu-HU/opencadstudio.ftl
+++ b/locales/hu-HU/opencadstudio.ftl
@@ -5926,3 +5926,41 @@ style-manager =
.center-of-text-overline = Szöveg közepe áthúzással
.block-extents = Kiterjedések blokkolása
.base-point = Alappont
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = __ocs_fmt_0__ található
+ .found-total = __ocs_fmt_0__ található, összesen __ocs_fmt_1__
+ .field-only-in-count = ** A COUNTFIELD parancs csak számlálás közben érhető el. **
+ .area-active = A számlálási terület aktív.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (csak olvasható)
+ .requires-0-or-1 = Csak 0 vagy 1 adható meg.
+ .requires-int-range = __ocs_fmt_0__ és __ocs_fmt_1__ közötti egész szám szükséges.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT Aktuális szövegstílus: "__ocs_fmt_0__" Szövegmagasság: __ocs_fmt_1__
+ .fields-found = __ocs_fmt_0__ mező található.
+ .fields-updated = __ocs_fmt_0__ mező frissítve.
+ .invalid-keyword = Érvénytelen opciókulcsszó.
+ .invalid-boundary = Érvénytelen számlálási terület határobjektum. Válasszon egy zárt, vonalszakaszokból álló, önmagát nem metsző vonalláncot.
+ .block-not-found = A(z) "__ocs_fmt_0__" blokk nem található.
+ .invalid-point = Érvénytelen pont.
diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl
index f2fce8f34..134de2c2b 100644
--- a/locales/it-IT/opencadstudio.ftl
+++ b/locales/it-IT/opencadstudio.ftl
@@ -5926,3 +5926,41 @@ style-manager =
.center-of-text-overline = Centro del testo con sopralineato
.block-extents = Blocca estensioni
.base-point = Punto base
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = __ocs_fmt_0__ trovati
+ .found-total = __ocs_fmt_0__ trovati, __ocs_fmt_1__ in totale
+ .field-only-in-count = ** Il comando COUNTFIELD è disponibile solo durante il conteggio. **
+ .area-active = L'area di conteggio è attiva.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (sola lettura)
+ .requires-0-or-1 = Richiede solo 0 o 1.
+ .requires-int-range = Richiede un numero intero compreso tra __ocs_fmt_0__ e __ocs_fmt_1__.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT Stile di testo corrente: "__ocs_fmt_0__" Altezza testo: __ocs_fmt_1__
+ .fields-found = __ocs_fmt_0__ campi trovati.
+ .fields-updated = __ocs_fmt_0__ campi aggiornati.
+ .invalid-keyword = Parola chiave dell'opzione non valida.
+ .invalid-boundary = Oggetto di contorno dell'area di conteggio non valido. Selezionare una polilinea chiusa composta da segmenti di linea che non si interseca.
+ .block-not-found = Impossibile trovare il blocco "__ocs_fmt_0__".
+ .invalid-point = Punto non valido.
diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl
index de4ee732e..77c7b54d9 100644
--- a/locales/ja-JP/opencadstudio.ftl
+++ b/locales/ja-JP/opencadstudio.ftl
@@ -5915,3 +5915,41 @@ style-manager =
.center-of-text-overline = 上線付きテキストの中心
.block-extents = ブロック範囲
.base-point = 基点
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = __ocs_fmt_0__ 個を検出
+ .found-total = __ocs_fmt_0__ 個を検出、合計 __ocs_fmt_1__ 個
+ .field-only-in-count = ** COUNTFIELD コマンドはカウント中のみ使用できます。 **
+ .area-active = カウント領域が有効です。
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (読み取り専用)
+ .requires-0-or-1 = 0 または 1 のみ指定できます。
+ .requires-int-range = __ocs_fmt_0__ から __ocs_fmt_1__ までの整数を指定してください。
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT 現在の文字スタイル: "__ocs_fmt_0__" 文字の高さ: __ocs_fmt_1__
+ .fields-found = __ocs_fmt_0__ 個のフィールドが見つかりました。
+ .fields-updated = __ocs_fmt_0__ 個のフィールドを更新しました。
+ .invalid-keyword = オプションのキーワードが無効です。
+ .invalid-boundary = カウント領域の境界オブジェクトが無効です。線分で構成され、自己交差しない閉じたポリラインを選択してください。
+ .block-not-found = ブロック "__ocs_fmt_0__" が見つかりません。
+ .invalid-point = 点が無効です。
diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl
index d2d20aa96..1d4e4ef29 100644
--- a/locales/ko-KR/opencadstudio.ftl
+++ b/locales/ko-KR/opencadstudio.ftl
@@ -5926,3 +5926,41 @@ style-manager =
.center-of-text-overline = 윗줄이 있는 텍스트 중앙
.block-extents = 블록 범위
.base-point = 기준점
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = __ocs_fmt_0__개 찾음
+ .found-total = __ocs_fmt_0__개 찾음, 총 __ocs_fmt_1__개
+ .field-only-in-count = ** COUNTFIELD 명령은 개수 세기 중에만 사용할 수 있습니다. **
+ .area-active = 개수 영역이 활성화되었습니다.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (읽기 전용)
+ .requires-0-or-1 = 0 또는 1만 입력할 수 있습니다.
+ .requires-int-range = __ocs_fmt_0__에서 __ocs_fmt_1__ 사이의 정수가 필요합니다.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT 현재 문자 스타일: "__ocs_fmt_0__" 문자 높이: __ocs_fmt_1__
+ .fields-found = 필드 __ocs_fmt_0__개를 찾았습니다.
+ .fields-updated = 필드 __ocs_fmt_0__개를 업데이트했습니다.
+ .invalid-keyword = 옵션 키워드가 잘못되었습니다.
+ .invalid-boundary = 개수 영역 경계 객체가 잘못되었습니다. 선분으로 구성되고 자체 교차하지 않는 닫힌 폴리선을 선택하십시오.
+ .block-not-found = 블록 "__ocs_fmt_0__"을(를) 찾을 수 없습니다.
+ .invalid-point = 점이 잘못되었습니다.
diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl
index 5b64e8180..8a7b29a64 100644
--- a/locales/nl-NL/opencadstudio.ftl
+++ b/locales/nl-NL/opencadstudio.ftl
@@ -5925,3 +5925,41 @@ style-manager =
.center-of-text-overline = Tekstmiddelpunt met bovenstreep
.block-extents = Symboolgrenzen
.base-point = Basispunt
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = __ocs_fmt_0__ gevonden
+ .found-total = __ocs_fmt_0__ gevonden, __ocs_fmt_1__ totaal
+ .field-only-in-count = ** De opdracht COUNTFIELD is alleen beschikbaar tijdens het tellen. **
+ .area-active = Telgebied is actief.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (alleen-lezen)
+ .requires-0-or-1 = Alleen 0 of 1 toegestaan.
+ .requires-int-range = Vereist een geheel getal tussen __ocs_fmt_0__ en __ocs_fmt_1__.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT Huidige tekststijl: "__ocs_fmt_0__" Teksthoogte: __ocs_fmt_1__
+ .fields-found = __ocs_fmt_0__ veld(en) gevonden.
+ .fields-updated = __ocs_fmt_0__ veld(en) bijgewerkt.
+ .invalid-keyword = Ongeldig optiesleutelwoord.
+ .invalid-boundary = Ongeldig grensobject voor het telgebied. Selecteer een gesloten polylijn die uit lijnsegmenten bestaat en zichzelf niet snijdt.
+ .block-not-found = Blok "__ocs_fmt_0__" kan niet worden gevonden.
+ .invalid-point = Ongeldig punt.
diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl
index 84e340728..b425dee41 100644
--- a/locales/pl-PL/opencadstudio.ftl
+++ b/locales/pl-PL/opencadstudio.ftl
@@ -5926,3 +5926,41 @@ style-manager =
.center-of-text-overline = Środek tekstu z podkreśleniem
.block-extents = Zasięg bloku
.base-point = Punkt bazowy
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = Znaleziono: __ocs_fmt_0__
+ .found-total = Znaleziono: __ocs_fmt_0__, łącznie: __ocs_fmt_1__
+ .field-only-in-count = ** Polecenie COUNTFIELD jest dostępne tylko podczas liczenia. **
+ .area-active = Obszar liczenia jest aktywny.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (tylko do odczytu)
+ .requires-0-or-1 = Wymaga wyłącznie 0 lub 1.
+ .requires-int-range = Wymaga liczby całkowitej z zakresu od __ocs_fmt_0__ do __ocs_fmt_1__.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT Bieżący styl tekstu: "__ocs_fmt_0__" Wysokość tekstu: __ocs_fmt_1__
+ .fields-found = Znaleziono pól: __ocs_fmt_0__.
+ .fields-updated = Zaktualizowano pól: __ocs_fmt_0__.
+ .invalid-keyword = Nieprawidłowe słowo kluczowe opcji.
+ .invalid-boundary = Nieprawidłowy obiekt obwiedni obszaru liczenia. Wybierz zamkniętą polilinię złożoną z odcinków, która nie przecina samej siebie.
+ .block-not-found = Nie można znaleźć bloku "__ocs_fmt_0__".
+ .invalid-point = Nieprawidłowy punkt.
diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl
index fcaf2f718..4f1c800b8 100644
--- a/locales/pt-BR/opencadstudio.ftl
+++ b/locales/pt-BR/opencadstudio.ftl
@@ -5927,3 +5927,41 @@ style-manager =
.center-of-text-overline = Centro do texto com sobrelinha
.block-extents = Extensões do bloco
.base-point = Ponto base
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = __ocs_fmt_0__ encontrado(s)
+ .found-total = __ocs_fmt_0__ encontrado(s), __ocs_fmt_1__ no total
+ .field-only-in-count = ** O comando COUNTFIELD só está disponível durante a contagem. **
+ .area-active = A área de contagem está ativa.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (somente leitura)
+ .requires-0-or-1 = Requer apenas 0 ou 1.
+ .requires-int-range = Requer um número inteiro entre __ocs_fmt_0__ e __ocs_fmt_1__.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT Estilo de texto atual: "__ocs_fmt_0__" Altura do texto: __ocs_fmt_1__
+ .fields-found = __ocs_fmt_0__ campo(s) encontrado(s).
+ .fields-updated = __ocs_fmt_0__ campo(s) atualizado(s).
+ .invalid-keyword = Palavra-chave de opção inválida.
+ .invalid-boundary = Objeto de contorno da área de contagem inválido. Selecione uma polilinha fechada composta de segmentos de linha e que não se cruze.
+ .block-not-found = O bloco "__ocs_fmt_0__" não foi encontrado.
+ .invalid-point = Ponto inválido.
diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl
index a7db84262..3c8c71a8e 100644
--- a/locales/ru-RU/opencadstudio.ftl
+++ b/locales/ru-RU/opencadstudio.ftl
@@ -5928,3 +5928,41 @@ style-manager =
.center-of-text-overline = Центр текста с надчёркиванием
.block-extents = Габариты блока
.base-point = Базовая точка
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = Найдено: __ocs_fmt_0__
+ .found-total = Найдено: __ocs_fmt_0__, всего: __ocs_fmt_1__
+ .field-only-in-count = ** Команда COUNTFIELD доступна только в режиме подсчёта. **
+ .area-active = Область подсчёта активна.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (только чтение)
+ .requires-0-or-1 = Допускается только 0 или 1.
+ .requires-int-range = Требуется целое число от __ocs_fmt_0__ до __ocs_fmt_1__.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT Текущий текстовый стиль: "__ocs_fmt_0__" Высота текста: __ocs_fmt_1__
+ .fields-found = Найдено полей: __ocs_fmt_0__.
+ .fields-updated = Обновлено полей: __ocs_fmt_0__.
+ .invalid-keyword = Недопустимое ключевое слово параметра.
+ .invalid-boundary = Недопустимый объект границы области подсчёта. Выберите замкнутую полилинию из отрезков, которая не пересекает саму себя.
+ .block-not-found = Блок "__ocs_fmt_0__" не найден.
+ .invalid-point = Недопустимая точка.
diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl
index 0d4587ec8..43908e04f 100644
--- a/locales/tr-TR/opencadstudio.ftl
+++ b/locales/tr-TR/opencadstudio.ftl
@@ -5850,3 +5850,41 @@ style-manager =
.center-of-text-overline = Üst çizgili metin merkezi
.block-extents = Blok sınırları
.base-point = Taban noktası
+count-feature =
+ .selection = Seçimi say
+ .review = Sayım ayrıntılarını incele
+ .insert-field = Sayım alanı ekle
+ .expand-by = GENİŞLET
+ .mirror-state = Ayna durumu
+ .area = Belirli bir alanda say
+ .create-table = Tablo Oluştur
+ .back = Listeye dön
+ .match-scale = Ölçeği Eşleştir
+ .match-mirror = Ayna Durumunu Eşleştir
+ .error-report = Sayım hata raporu: __ocs_fmt_0__
+ .toolbar-count = Sayım: __ocs_fmt_0__
+ .overlap = Üst üste binen nesne
+ .show-instances = Örnekleri göster
+ .geometries = Geometriler: __ocs_fmt_0__
+ .invalid-title = Sayım - Geçersiz Alan
+ .invalid-message = Sayım sınırındaki değişiklikler geçersiz bir sayım alanıyla sonuçlandı. Ne yapmak istersiniz?
+ .invalid-undo = Sayım sınırındaki değişiklikleri geri al
+ .invalid-undo-detail = Son işlem geri alınır, sınır ve sayım alanı eski haline döner.
+ .invalid-continue = Devam et ve tüm model alanını say
+ .invalid-continue-detail = Sayım alanı kalkar; sayım tüm model alanında sürer.
+ .invalid-always = Geçerli seçimimi her zaman uygula
+ .found = __ocs_fmt_0__ bulundu
+ .found-total = __ocs_fmt_0__ bulundu, toplam __ocs_fmt_1__
+ .field-only-in-count = ** COUNTFIELD komutu yalnızca sayım sırasında kullanılabilir. **
+ .area-active = Sayım alanı etkin.
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__ (salt okunur)
+ .requires-0-or-1 = Yalnızca 0 veya 1 girilebilir.
+ .requires-int-range = __ocs_fmt_0__ ile __ocs_fmt_1__ arasında bir tamsayı gerekir.
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT Geçerli metin stili: "__ocs_fmt_0__" Metin yüksekliği: __ocs_fmt_1__
+ .fields-found = __ocs_fmt_0__ alan bulundu.
+ .fields-updated = __ocs_fmt_0__ alan güncellendi.
+ .invalid-keyword = Geçersiz seçenek anahtar sözcüğü.
+ .invalid-boundary = Geçersiz sayım alanı sınır nesnesi. Doğru parçalarından oluşan, kendisiyle kesişmeyen kapalı bir çoklu çizgi seçin.
+ .block-not-found = "__ocs_fmt_0__" bloğu bulunamadı.
+ .invalid-point = Geçersiz nokta.
diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl
index 086345d66..0249f7cfa 100644
--- a/locales/zh-CN/opencadstudio.ftl
+++ b/locales/zh-CN/opencadstudio.ftl
@@ -5910,3 +5910,41 @@ style-manager =
.center-of-text-overline = 带上划线的文字中心
.block-extents = 块范围
.base-point = 基点
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = 找到 __ocs_fmt_0__ 个
+ .found-total = 找到 __ocs_fmt_0__ 个,总计 __ocs_fmt_1__ 个
+ .field-only-in-count = ** COUNTFIELD 命令仅在计数期间可用。 **
+ .area-active = 计数区域已激活。
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__(只读)
+ .requires-0-or-1 = 只能为 0 或 1。
+ .requires-int-range = 需要介于 __ocs_fmt_0__ 和 __ocs_fmt_1__ 之间的整数。
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT 当前文字样式: "__ocs_fmt_0__" 文字高度: __ocs_fmt_1__
+ .fields-found = 找到 __ocs_fmt_0__ 个字段。
+ .fields-updated = 已更新 __ocs_fmt_0__ 个字段。
+ .invalid-keyword = 无效的选项关键字。
+ .invalid-boundary = 无效的计数区域边界对象。请选择由直线段组成且不自交的闭合多段线。
+ .block-not-found = 找不到块 "__ocs_fmt_0__"。
+ .invalid-point = 无效的点。
diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl
index 5a043837f..354f9442d 100644
--- a/locales/zh-TW/opencadstudio.ftl
+++ b/locales/zh-TW/opencadstudio.ftl
@@ -5922,3 +5922,41 @@ style-manager =
.center-of-text-overline = 帶上划線的文字中心
.block-extents = 圖塊範圍
.base-point = 基點
+count-feature =
+ .selection = Count Selection
+ .review = Review Count Details
+ .insert-field = Insert Count Field
+ .expand-by = EXPAND BY
+ .mirror-state = Mirror State
+ .area = Count in a specified area
+ .create-table = Create Table
+ .back = Back to List
+ .match-scale = Match Scale
+ .match-mirror = Match Mirror State
+ .error-report = Count Error Report: __ocs_fmt_0__
+ .toolbar-count = Count: __ocs_fmt_0__
+ .overlap = Overlapping object
+ .show-instances = Show Count Instances
+ .geometries = Geometries: __ocs_fmt_0__
+ .invalid-title = Count - Invalid Area
+ .invalid-message = The changes to count boundary have resulted to an invalid count area. What do you want to do?
+ .invalid-undo = Undo the changes to the count boundary
+ .invalid-undo-detail = The last change is undone; the boundary and the count area come back.
+ .invalid-continue = Continue and count the entire model space
+ .invalid-continue-detail = The count area is dropped; counting goes on in all of model space.
+ .invalid-always = Always perform my current choice
+ .found = 找到 __ocs_fmt_0__ 個
+ .found-total = 找到 __ocs_fmt_0__ 個,總計 __ocs_fmt_1__ 個
+ .field-only-in-count = ** COUNTFIELD 指令僅在計數期間可用。 **
+ .area-active = 計數區域已啟用。
+ .read-only-value = __ocs_fmt_0__ = __ocs_fmt_1__(唯讀)
+ .requires-0-or-1 = 只能為 0 或 1。
+ .requires-int-range = 需要介於 __ocs_fmt_0__ 與 __ocs_fmt_1__ 之間的整數。
+ .target-count = __ocs_fmt_0__ ...... __ocs_fmt_1__
+ .mtext-current-style = MTEXT 目前文字型式: "__ocs_fmt_0__" 文字高度: __ocs_fmt_1__
+ .fields-found = 找到 __ocs_fmt_0__ 個欄位。
+ .fields-updated = 已更新 __ocs_fmt_0__ 個欄位。
+ .invalid-keyword = 無效的選項關鍵字。
+ .invalid-boundary = 無效的計數區域邊界物件。請選取由線段組成且不自我相交的封閉聚合線。
+ .block-not-found = 找不到圖塊 "__ocs_fmt_0__"。
+ .invalid-point = 無效的點。
diff --git a/src/app/command_driver/viewport.rs b/src/app/command_driver/viewport.rs
index 35ceafa46..f8eadd485 100644
--- a/src/app/command_driver/viewport.rs
+++ b/src/app/command_driver/viewport.rs
@@ -418,7 +418,7 @@ impl OpenCADStudio {
}
}
- pub(super) fn handle_zoom_to_window(&mut self, p1: glam::DVec3, p2: glam::DVec3) {
+ pub(in crate::app) fn handle_zoom_to_window(&mut self, p1: glam::DVec3, p2: glam::DVec3) {
let i = self.active_tab;
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
diff --git a/src/app/commands/count.rs b/src/app/commands/count.rs
new file mode 100644
index 000000000..06e7f90ea
--- /dev/null
+++ b/src/app/commands/count.rs
@@ -0,0 +1,862 @@
+//! COUNT and its family: count mode (COUNT, COUNTAREA, COUNTCLOSE), the
+//! Count palette (COUNTLIST, COUNTLISTCLOSE), count fields (COUNTFIELD,
+//! UPDATEFIELD), count tables (COUNTTABLE) and the COUNT* variables.
+
+use crate::app::{Message, OpenCADStudio};
+use crate::command::CadCommand;
+use crate::modules::annotate::count_cmd::{CountCommand, CountTableCommand, UpdateFieldCommand};
+use crate::modules::annotate::field_cmd::FieldPlaceCommand;
+use crate::ui::window::count_palette::{aci_rgba, CountMode, CountMsg, CountTarget, RowAction, AREA_LAYER};
+use codec::count::{area_json, block_json, single_json, CountKey};
+use codec::{EntityType, Handle};
+use iced::Task;
+
+/// The variables this family answers for.
+pub(super) const COUNT_SYSVARS: &[&str] =
+ &["COUNTNUMBER", "COUNTSERVICE", "COUNTPALETTESTATE", "COUNTCOLOR", "COUNTERRORCOLOR"];
+
+fn hex(h: Handle) -> String {
+ format!("{:X}", h.value())
+}
+
+fn parse_handle(s: &str) -> Option {
+ u64::from_str_radix(s.trim(), 16).ok().map(Handle::new)
+}
+
+fn parse_xy(s: &str) -> Option<[f64; 2]> {
+ let (x, y) = s.split_once(',')?;
+ Some([x.trim().parse().ok()?, y.trim().parse().ok()?])
+}
+
+/// A field referring to `boundary` keeps the count area's polyline.
+fn boundary_in_use(doc: &codec::CadDocument, boundary: Handle) -> bool {
+ let needle = format!("\"boundaryObjectHandle\":\"{}\"", hex(boundary));
+ doc.fields.values().any(|f| f.code.contains(&needle))
+}
+
+impl OpenCADStudio {
+ pub(super) fn dispatch_count(&mut self, cmd: &str, i: usize) -> Option> {
+ let cmd = cmd.trim();
+ let (verb, rest) = cmd.split_once(char::is_whitespace).unwrap_or((cmd, ""));
+ let verb = verb.to_ascii_uppercase();
+ let rest = rest.trim();
+ if verb == "SETVAR" {
+ let (name, value) = rest.split_once(char::is_whitespace).unwrap_or((rest, ""));
+ let name = name.to_ascii_uppercase();
+ if !COUNT_SYSVARS.contains(&name.as_str()) {
+ return None;
+ }
+ self.count_sysvar(i, &name, Some(value.trim()).filter(|v| !v.is_empty()));
+ return Some(Task::none());
+ }
+ if COUNT_SYSVARS.contains(&verb.as_str()) {
+ self.count_sysvar(i, &verb, Some(rest).filter(|v| !v.is_empty()));
+ return Some(Task::none());
+ }
+ match verb.as_str() {
+ "COUNT" => {
+ let preselected = self.tabs[i].scene.selected_handles_in_order();
+ let current = matches!(rest.trim_start_matches('_').to_ascii_uppercase().as_str(), "C");
+ if current && !preselected.is_empty() {
+ // Count Selection: the selection is the target.
+ let found = preselected.len();
+ self.command_line.push_output(&crate::tf!("{found} found"));
+ let handles: Vec = preselected.iter().map(|h| hex(*h)).collect();
+ self.tabs[i].scene.deselect_all();
+ return Some(self.dispatch_command(&format!("_COUNTRUN C T {}", handles.join(","))));
+ }
+ // Plain COUNT asks for its targets itself.
+ self.tabs[i].scene.deselect_all();
+ let command = if current { CountCommand::with_area("C") } else { CountCommand::new(false) };
+ Some(self.start_count_command(i, Box::new(command)))
+ }
+ "COUNTAREA" => Some(self.start_count_command(i, Box::new(CountCommand::new(true)))),
+ "COUNTCLOSE" => {
+ self.close_count(i);
+ Some(Task::none())
+ }
+ "COUNTLIST" => {
+ self.set_count_palette(true);
+ Some(Task::none())
+ }
+ "COUNTLISTCLOSE" => {
+ self.set_count_palette(false);
+ Some(Task::none())
+ }
+ "_COUNTLISTTOGGLE" => {
+ let open = !self.count_palette.show;
+ self.set_count_palette(open);
+ Some(Task::none())
+ }
+ "COUNTFIELD" => {
+ match self.count_field_code(i) {
+ Some(code) => return Some(self.start_count_field(i, code)),
+ None if self.tabs[i].count.is_some() => {}
+ None => self
+ .command_line
+ .push_error(&crate::t!("** COUNTFIELD command only available during Count. **")),
+ }
+ Some(Task::none())
+ }
+ "COUNTTABLE" => {
+ let doc = &self.tabs[i].scene.document;
+ let mut blocks = Vec::new();
+ let mut others = [0usize; 3];
+ for br in doc.block_records.iter() {
+ let upper = br.name.to_ascii_uppercase();
+ if upper == "*MODEL_SPACE" || upper.starts_with("*PAPER_SPACE") {
+ continue;
+ }
+ if br.flags.is_xref || br.flags.is_xref_overlay {
+ others[0] += 1;
+ } else if br.name.contains('|') {
+ others[1] += 1;
+ } else if br.name.starts_with('*') {
+ others[2] += 1;
+ } else {
+ blocks.push(br.name.clone());
+ }
+ }
+ blocks.sort_by_key(|b| b.to_ascii_uppercase());
+ Some(self.start_count_command(i, Box::new(CountTableCommand::new(blocks, others))))
+ }
+ "UPDATEFIELD" => {
+ let preselected = self.tabs[i].scene.selected_handles_in_order();
+ if !preselected.is_empty() {
+ self.update_fields(i, &preselected);
+ return Some(Task::none());
+ }
+ Some(self.start_count_command(i, Box::new(UpdateFieldCommand::new())))
+ }
+ "_UPDATEFIELDRUN" => {
+ let handles: Vec = rest.split(',').filter_map(parse_handle).collect();
+ self.tabs[i].scene.deselect_all();
+ self.update_fields(i, &handles);
+ Some(Task::none())
+ }
+ "_COUNTRUN" => {
+ self.count_run(i, rest);
+ Some(Task::none())
+ }
+ "_COUNTAREASET" => {
+ let tokens: Vec<&str> = rest.split_whitespace().collect();
+ if let Some((area, boundary)) = self.resolve_count_area(i, &tokens) {
+ let mut mode = self.tabs[i].count.take().unwrap_or_default();
+ if mode.boundary != boundary {
+ self.drop_count_boundary(i, mode.boundary);
+ }
+ mode.area = area;
+ mode.boundary = boundary;
+ mode.cursor = None;
+ self.tabs[i].count = Some(mode);
+ self.apply_count_display(i);
+ self.command_line.push_output(&crate::t!("count area is active."));
+ }
+ Some(Task::none())
+ }
+ "_COUNTTABLEPLACE" => {
+ let (point, names) = rest.split_once(' ').unwrap_or((rest, ""));
+ let p: Vec = point.split(',').filter_map(|v| v.parse().ok()).collect();
+ if p.len() == 3 {
+ let names: Vec = names.split('|').filter(|n| !n.is_empty()).map(str::to_string).collect();
+ self.place_count_table(i, [p[0], p[1], p[2]], names);
+ }
+ Some(Task::none())
+ }
+ _ => None,
+ }
+ }
+
+ fn start_count_command(&mut self, i: usize, command: Box) -> Task {
+ self.reset_command_start_state(i);
+ self.command_line.push_info(&command.prompt());
+ self.tabs[i].active_cmd = Some(command);
+ self.push_ucs_to_cmd(i);
+ self.sync_dyn_fields();
+ self.focus_cmd_input()
+ }
+
+ /// COUNTNUMBER / COUNTPALETTESTATE are read-only; COUNTSERVICE,
+ /// COUNTCOLOR and COUNTERRORCOLOR take a value (asked for when none is
+ /// given).
+ fn count_sysvar(&mut self, i: usize, name: &str, value: Option<&str>) {
+ let read_only = match name {
+ "COUNTNUMBER" => Some(
+ self.tabs[i]
+ .count
+ .as_ref()
+ .filter(|m| m.target.is_some())
+ .map(|m| m.count(&self.tabs[i].scene.document))
+ .unwrap_or(0),
+ ),
+ "COUNTPALETTESTATE" => Some(self.count_palette.show as usize),
+ _ => None,
+ };
+ if let Some(v) = read_only {
+ self.command_line.push_output(&crate::tf!("{name} = {v} (read only)"));
+ return;
+ }
+ let (current, min, max) = match name {
+ "COUNTSERVICE" => (self.count_palette.service as i32, 0, 1),
+ "COUNTCOLOR" => (self.count_palette.color as i32, 1, 255),
+ _ => (self.count_palette.error_color as i32, 1, 255),
+ };
+ let ask = |app: &mut Self| {
+ app.command_line.push_output(&crate::tf!("Enter new value for {name} <{current}>:"));
+ app.pending_setvar = Some(name.to_string());
+ };
+ let Some(value) = value else {
+ ask(self);
+ return;
+ };
+ match value.parse::() {
+ Ok(n) if (min..=max).contains(&n) => {
+ match name {
+ "COUNTSERVICE" => self.count_palette.service = n == 1,
+ "COUNTCOLOR" => self.count_palette.color = n as i16,
+ _ => self.count_palette.error_color = n as i16,
+ }
+ self.persist_settings_if_changed();
+ self.apply_count_display(i);
+ }
+ _ => {
+ self.command_line.push_error(&if max == 1 {
+ crate::t!("Requires 0 or 1 only.")
+ } else {
+ crate::tf!("Requires an integer between {min} and {max}.")
+ });
+ ask(self);
+ }
+ }
+ }
+
+ /// Open or close the Count palette (docked on the right, expanded).
+ pub(in crate::app) fn set_count_palette(&mut self, open: bool) {
+ let id = crate::ui::dock::PanelId::Count;
+ self.count_palette.show = open;
+ self.ribbon.set_count_palette(open);
+ if open {
+ if self.dock.location(id).is_none() {
+ self.dock.dock(id, crate::app::config::DockSide::Right, usize::MAX);
+ }
+ self.dock_expanded = Some(id);
+ } else if self.dock_expanded == Some(id) {
+ self.dock_expanded = None;
+ }
+ }
+
+ /// Recolour the drawing for count mode: counted references, duplicates,
+ /// the rest faded (nothing changes without a target).
+ pub(in crate::app) fn apply_count_display(&mut self, i: usize) {
+ let computed = self.tabs[i].count.as_ref().map(|m| m.compute(&self.tabs[i].scene.document));
+ if let (Some(mode), Some(c)) = (self.tabs[i].count.as_mut(), computed) {
+ mode.cache = Some(c);
+ }
+ let display = self.tabs[i].count.as_ref().filter(|m| m.target.is_some()).map(|m| {
+ let r = m.result(&self.tabs[i].scene.document);
+ crate::scene::CountDisplay {
+ counted: r.counted.iter().copied().collect(),
+ errors: r.errors.iter().chain(&r.overlapped).copied().collect(),
+ color: aci_rgba(self.count_palette.color),
+ error_color: aci_rgba(self.count_palette.error_color),
+ }
+ });
+ self.tabs[i].scene.set_count_display(display);
+ let epoch = self.tabs[i].scene.geometry_epoch;
+ if let Some(mode) = self.tabs[i].count.as_mut() {
+ mode.epoch = epoch;
+ }
+ }
+
+ /// After the drawing changes, count mode colours its references again.
+ pub(in crate::app) fn refresh_count_if_stale(&mut self) {
+ let i = self.active_tab;
+ let Some(mode) = self.tabs[i].count.as_ref().filter(|m| m.epoch != self.tabs[i].scene.geometry_epoch) else {
+ return;
+ };
+ if let Some((h, _)) = mode.boundary {
+ match self.tabs[i].scene.document.get_entity(h) {
+ // The boundary is gone (erased, undone): ask, or do what was
+ // chosen for good.
+ None => {
+ if self.active_modal == Some(crate::app::ModalKind::CountInvalidArea) {
+ return;
+ }
+ match self.count_palette.invalid_choice {
+ 1 => self.count_invalid_undo(i),
+ 2 => self.count_invalid_continue(i),
+ _ => {
+ self.count_palette.invalid_always = false;
+ self.active_modal = Some(crate::app::ModalKind::CountInvalidArea);
+ }
+ }
+ return;
+ }
+ // An edited boundary moves the area with it.
+ Some(EntityType::LwPolyline(pl)) => {
+ let ring: Vec<[f64; 2]> = pl.vertices.iter().map(|v| [v.location.x, v.location.y]).collect();
+ if let Some(mode) = self.tabs[i].count.as_mut() {
+ mode.area = Some(ring);
+ }
+ }
+ Some(_) => {}
+ }
+ }
+ self.apply_count_display(i);
+ }
+
+ /// Invalid Area › Undo (also ✕ and Cancel): the change that took the
+ /// boundary away is reverted — redone when an undo took it, undone
+ /// otherwise. When that does not bring the boundary back, the history
+ /// step is put back and the area is dropped as with Continue.
+ fn count_invalid_undo(&mut self, i: usize) {
+ let Some((boundary, _)) = self.tabs[i].count.as_ref().and_then(|m| m.boundary) else {
+ return;
+ };
+ // An open step (the edit that just erased the boundary) goes on the
+ // undo stack first; after that, something to redo means the last
+ // history step was an undo.
+ self.finish_pending_history(i);
+ let redo = !self.tabs[i].history.redo_stack.is_empty();
+ if redo || !self.tabs[i].history.undo_stack.is_empty() {
+ if redo {
+ self.redo_active_tab();
+ } else {
+ self.undo_active_tab();
+ }
+ if self.tabs[i].scene.document.get_entity(boundary).is_some() {
+ self.apply_count_display(i);
+ return;
+ }
+ if redo {
+ self.undo_active_tab();
+ } else {
+ self.redo_active_tab();
+ }
+ }
+ self.count_invalid_continue(i);
+ }
+
+ /// Invalid Area › Continue: the area goes, the count covers all of model space.
+ fn count_invalid_continue(&mut self, i: usize) {
+ if let Some(mode) = self.tabs[i].count.as_mut() {
+ mode.area = None;
+ mode.boundary = None;
+ }
+ self.apply_count_display(i);
+ }
+
+ fn close_invalid_area(&mut self, choice: u8) {
+ if self.active_modal == Some(crate::app::ModalKind::CountInvalidArea) {
+ self.close_active_modal();
+ }
+ if self.count_palette.invalid_always {
+ self.count_palette.invalid_choice = choice;
+ self.persist_settings_if_changed();
+ }
+ }
+
+ /// COUNTCLOSE: leave count mode; the area polyline count mode drew goes
+ /// unless a field counts in it.
+ pub(in crate::app) fn close_count(&mut self, i: usize) {
+ if let Some(mode) = self.tabs[i].count.take() {
+ self.drop_count_boundary(i, mode.boundary);
+ }
+ self.tabs[i].scene.set_count_display(None);
+ }
+
+ fn drop_count_boundary(&mut self, i: usize, boundary: Option<(Handle, bool)>) {
+ if let Some((h, true)) = boundary {
+ let doc = &self.tabs[i].scene.document;
+ if doc.get_entity(h).is_some() && !boundary_in_use(doc, h) {
+ // Count mode removes its own polyline (the area layer is
+ // locked) outside the history: an open step must not take the
+ // removal in, or undoing it would bring back an orphan area.
+ self.finish_pending_history(i);
+ self.tabs[i].scene.rollback_new_entities(&[h]);
+ self.tabs[i].dirty = true;
+ }
+ }
+ }
+
+ /// The count area of `C`, `E`, `K`, `R a b`, `P a b c …` or `O h`, and
+ /// its boundary polyline (rectangles and polygons are drawn on the
+ /// `0-CountArea` layer). `None` when the area cannot be had.
+ fn resolve_count_area(
+ &mut self,
+ i: usize,
+ tokens: &[&str],
+ ) -> Option<(Option>, Option<(Handle, bool)>)> {
+ let kind = tokens.first().copied().unwrap_or("E");
+ let points = || tokens[1..].iter().map_while(|t| parse_xy(t)).collect::>();
+ match kind {
+ "E" => Some((None, None)),
+ "K" => {
+ let mode = self.tabs[i].count.as_ref();
+ Some((mode.and_then(|m| m.area.clone()), mode.and_then(|m| m.boundary)))
+ }
+ // Current area counts all of model space (the view and an area
+ // in use take no part).
+ "C" => Some((None, None)),
+ "R" => {
+ let p = points();
+ let (a, b) = (p.first()?, p.get(1)?);
+ let ring = vec![[a[0], a[1]], [b[0], a[1]], [b[0], b[1]], [a[0], b[1]]];
+ let h = self.draw_count_area(i, &ring)?;
+ Some((Some(ring), Some((h, true))))
+ }
+ "P" => {
+ let ring = points();
+ (ring.len() >= 3).then_some(())?;
+ let h = self.draw_count_area(i, &ring)?;
+ Some((Some(ring), Some((h, true))))
+ }
+ "O" => {
+ let h = parse_handle(tokens.get(1)?)?;
+ let ring = codec::count::boundary_polygon(&self.tabs[i].scene.document, h)?;
+ Some((Some(ring), Some((h, false))))
+ }
+ _ => None,
+ }
+ }
+
+ /// A closed polyline on `0-CountArea` (locked, colour 152, not plotted,
+ /// Continuous), whatever the current layer.
+ fn draw_count_area(&mut self, i: usize, ring: &[[f64; 2]]) -> Option {
+ self.push_undo_snapshot(i, "COUNT");
+ let doc = &mut self.tabs[i].scene.document;
+ if !doc.layers.contains(AREA_LAYER) {
+ let mut layer = codec::tables::Layer::new(AREA_LAYER);
+ layer.handle = doc.allocate_handle();
+ layer.color = codec::types::Color::from_index(152);
+ layer.is_plottable = false;
+ layer.flags.locked = true;
+ layer.line_type = "Continuous".into();
+ let _ = doc.layers.add(layer);
+ }
+ let mut pl = codec::entities::LwPolyline::new();
+ pl.vertices = ring
+ .iter()
+ .map(|p| codec::entities::LwVertex::new(codec::types::Vector2::new(p[0], p[1])))
+ .collect();
+ pl.is_closed = true;
+ pl.common.layer = AREA_LAYER.into();
+ let h = self.commit_entity_handle_preserve_layer(EntityType::LwPolyline(pl));
+ self.tabs[i].dirty = true;
+ h
+ }
+
+ /// `_COUNTRUN L` opens the palette; `_COUNTRUN T `
+ /// counts the targets and enters count mode.
+ fn count_run(&mut self, i: usize, rest: &str) {
+ let tokens: Vec<&str> = rest.split_whitespace().collect();
+ let split = tokens.iter().position(|t| *t == "L" || *t == "T").unwrap_or(tokens.len());
+ let (area_tokens, tail) = tokens.split_at(split);
+ let targets: Vec = match tail {
+ ["T", handles, ..] => handles
+ .split(',')
+ .filter_map(parse_handle)
+ .filter(|h| self.tabs[i].scene.document.get_entity(*h).is_some())
+ .collect(),
+ _ => Vec::new(),
+ };
+ // List all blocks: the palette, counting in the area when one was drawn.
+ if targets.is_empty() {
+ self.set_count_palette(true);
+ if matches!(area_tokens.first(), Some(&"C") | Some(&"E") | None) {
+ return;
+ }
+ }
+ let Some((area, boundary)) = self.resolve_count_area(i, area_tokens) else {
+ return;
+ };
+ let doc = &self.tabs[i].scene.document;
+ let blocks: Vec<&str> = targets
+ .iter()
+ .filter_map(|h| match doc.get_entity(*h) {
+ Some(EntityType::Insert(ins)) => Some(ins.block_name.as_str()),
+ _ => None,
+ })
+ .collect();
+ let target = match blocks.first() {
+ Some(first) if blocks.len() == targets.len() && blocks.iter().all(|b| b.eq_ignore_ascii_case(first)) => {
+ let name = doc.block_records.get(first).map(|b| b.name.clone()).unwrap_or_else(|| first.to_string());
+ Some(CountTarget::Block { name, reference: targets.first().copied(), matching: [false; 3], picked: true })
+ }
+ None if targets.is_empty() => None,
+ _ => Some(CountTarget::Group(targets.clone())),
+ };
+ // The picked targets do not stay selected.
+ self.tabs[i].scene.deselect_all();
+ self.refresh_properties();
+ if let Some(old) = self.tabs[i].count.take().filter(|o| o.boundary != boundary) {
+ self.drop_count_boundary(i, old.boundary);
+ }
+ let mode = CountMode { area, boundary, target, ..CountMode::default() };
+ if let Some(name) = mode.target_name() {
+ let n = mode.count(&self.tabs[i].scene.document);
+ self.command_line.push_output(&crate::tf!("{name} ...... {n}"));
+ }
+ self.tabs[i].count = Some(mode);
+ self.apply_count_display(i);
+ }
+
+ /// The field COUNTFIELD places for the current count: the picked
+ /// reference (`single`), or the block with the match options (in the
+ /// area's polyline when there is one).
+ fn count_field_code(&self, i: usize) -> Option {
+ let mode = self.tabs[i].count.as_ref()?;
+ match mode.target.as_ref()? {
+ CountTarget::Group(handles) => Some(format!("\\AcCount {}", single_json(handles))),
+ CountTarget::Block { reference: Some(r), matching: [false, false, false], picked: true, .. }
+ if mode.boundary.is_none() =>
+ {
+ Some(format!("\\AcCount {}", single_json(&[*r])))
+ }
+ CountTarget::Block { name, .. } => {
+ let instances = mode.instances(&self.tabs[i].scene.document);
+ Some(self.block_field_code(i, name, &mode.key(&instances)))
+ }
+ }
+ }
+
+ /// `\AcCount` of a block and key, `\AcCount2` inside count mode's area polyline.
+ fn block_field_code(&self, i: usize, name: &str, key: &CountKey) -> String {
+ match self.tabs[i].count.as_ref().and_then(|m| m.boundary) {
+ Some((boundary, _)) => format!("\\AcCount2 {}", area_json(name, key, boundary)),
+ None => format!("\\AcCount {}", block_json(name, key)),
+ }
+ }
+
+ /// Place a count field as multi-line text (`Specify start point or [Height/Justify]:`).
+ pub(in crate::app) fn start_count_field(&mut self, i: usize, code: String) -> Task {
+ let doc = &self.tabs[i].scene.document;
+ let value = crate::entities::field::evaluate(doc, &code, &[], None).unwrap_or_else(|| "####".into());
+ let defaults = crate::scene::creation_style::current_text_defaults(doc);
+ self.command_line.push_output(&crate::tf!(
+ "MTEXT Current text style: \"{}\" Text height: {:.4}",
+ defaults.style_name, defaults.height
+ ));
+ let command = FieldPlaceCommand::new(value, (code, Vec::new()), defaults.style_name, defaults.height).named("MTEXT");
+ self.start_count_command(i, Box::new(command))
+ }
+
+ /// UPDATEFIELD: every field the objects host is evaluated again and its
+ /// text written back. A table's fields are hosted by the cell texts of its
+ /// drawing (its `*T` block); fields a table keeps without such a text are
+ /// evaluated when drawn and are not counted here.
+ fn update_fields(&mut self, i: usize, handles: &[Handle]) {
+ let doc = &self.tabs[i].scene.document;
+ let mut found = 0;
+ let mut updates: Vec<(Handle, String)> = Vec::new();
+ for h in handles {
+ let Some(entity) = doc.get_entity(*h) else { continue };
+ let hosts: Vec = match entity {
+ EntityType::Table(t) => t
+ .block_record_handle
+ .and_then(|r| doc.block_records.iter().find(|b| b.handle == r))
+ .or_else(|| doc.block_records.get(&t.block_name).filter(|_| !t.block_name.is_empty()))
+ .map(|record| record.entity_handles.clone())
+ .unwrap_or_default(),
+ _ => vec![*h],
+ };
+ for host in hosts {
+ if !doc.get_entity(host).is_some_and(|e| crate::entities::field::hosts_field(doc, e)) {
+ continue;
+ }
+ found += 1;
+ if let Some(text) = crate::entities::field::resolve(doc, host) {
+ updates.push((host, text));
+ }
+ }
+ }
+ self.command_line.push_output(&crate::tf!("{found} field(s) found."));
+ if found == 0 {
+ return;
+ }
+ self.push_undo_snapshot(i, "UPDATEFIELD");
+ let mut changed = Vec::new();
+ for (h, text) in updates {
+ if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(h) {
+ match entity {
+ EntityType::MText(m) => m.value = text,
+ EntityType::Text(t) => t.value = text,
+ EntityType::AttributeDefinition(a) => a.default_value = text,
+ _ => continue,
+ }
+ changed.push((h, crate::scene::ChangeKind::Modified));
+ }
+ }
+ let updated = changed.len();
+ // Table cell texts live in a block definition.
+ let scene = &mut self.tabs[i].scene;
+ if scene.changes_touch_block_definition(&changed) {
+ scene.bump_geometry();
+ } else {
+ scene.bump_entities(&changed);
+ }
+ self.tabs[i].dirty = true;
+ self.command_line.push_output(&crate::tf!("{updated} field(s) updated."));
+ }
+
+ /// A count table at `point`: `Item` | `Count`, one row per block (all
+ /// counted blocks when `names` is empty), sorted by name; the counts are
+ /// fields.
+ fn place_count_table(&mut self, i: usize, point: [f64; 3], names: Vec) {
+ let doc = &self.tabs[i].scene.document;
+ let area = self.tabs[i].count.as_ref().and_then(|m| m.area.clone());
+ let instances = codec::count::block_instances(doc, area.as_deref());
+ let mut names: Vec = if names.is_empty() {
+ codec::count::block_counts(&instances).into_iter().map(|c| c.name).collect()
+ } else {
+ names
+ .iter()
+ .map(|n| doc.block_records.get(n).map(|b| b.name.clone()).unwrap_or_else(|| n.clone()))
+ .collect()
+ };
+ names.sort_by_key(|n| n.to_ascii_uppercase());
+ names.dedup_by_key(|n| n.to_ascii_uppercase());
+ let mut table = codec::entities::TableBuilder::new(names.len() + 1, 2)
+ .at(codec::types::Vector3::new(point[0], point[1], point[2]))
+ .row_height(0.36)
+ .column_width(3.6)
+ .build();
+ table.set_column_width(1, 1.08);
+ // As the reference writes it: no title row, data rows centred
+ // (table and cell overrides), header cells without overrides.
+ table.value_flags = 22;
+ table.legacy_style_override = Some(codec::entities::table::LegacyTableStyleOverride {
+ flags: 0x10001,
+ title_suppressed: Some(true),
+ row_alignments: vec![5],
+ ..Default::default()
+ });
+ table.set_cell_text(0, 0, "Item");
+ table.set_cell_text(0, 1, "Count");
+ for (r, name) in names.iter().enumerate() {
+ table.set_cell_text(r + 1, 0, name);
+ if let Some(cell) = table.cell_mut(r + 1, 1) {
+ // The count comes from the field; the cell keeps no text.
+ let mut content = codec::entities::table::CellContent::text("");
+ content.value.flags = 1;
+ cell.contents = vec![content];
+ cell.cell_type = codec::entities::table::CellType::Text;
+ }
+ }
+ for (r, row) in table.rows.iter_mut().enumerate() {
+ for cell in &mut row.cells {
+ if let Some(content) = cell.contents.first_mut().filter(|c| c.value.flags != 1) {
+ content.value.flags = 6;
+ }
+ if r == 0 {
+ cell.flag = 0x40000;
+ } else {
+ cell.flag = 0x40001;
+ let mut style = codec::entities::CellStyle::new();
+ style.override_flags = 0x01;
+ style.alignment = 5;
+ cell.style = Some(style);
+ }
+ }
+ }
+ let codes: Vec =
+ names.iter().map(|n| self.block_field_code(i, n, &CountKey::default())).collect();
+ self.push_undo_snapshot(i, "COUNTTABLE");
+ let Some(handle) = self.commit_entity_handle(EntityType::Table(Box::new(table))) else {
+ return;
+ };
+ // The table's graphics block holds one MTEXT per cell; each count
+ // cell's MTEXT owns its field, which the cell refers to.
+ let doc = &mut self.tabs[i].scene.document;
+ let fields = codes
+ .iter()
+ .enumerate()
+ .map(|(r, code)| {
+ let value = crate::entities::field::evaluate(doc, code, &[], None).unwrap_or_else(|| "####".into());
+ (r + 1, 1, r"%<\_FldIdx 0>%".to_string(), vec![codec::fields::NewField::new(code.clone(), value)])
+ })
+ .collect();
+ doc.build_table_block(handle, fields);
+ self.tabs[i].scene.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
+ self.tabs[i].dirty = true;
+ }
+
+ /// Zoom to a counted object (← / →, an error of the report).
+ fn zoom_to_insert(&mut self, i: usize, h: Handle) {
+ let doc = &self.tabs[i].scene.document;
+ let Some(entity) = doc.get_entity(h) else { return };
+ let outline = codec::count::entity_outline(doc, entity);
+ if outline.is_empty() {
+ return;
+ }
+ let (mut lo, mut hi) = (glam::DVec3::splat(f64::MAX), glam::DVec3::splat(f64::MIN));
+ for c in outline.iter().flat_map(codec::count::Piece::points) {
+ let p = glam::DVec3::new(c.x, c.y, c.z);
+ lo = lo.min(p);
+ hi = hi.max(p);
+ }
+ let pad = (hi - lo) * 1.5;
+ self.handle_zoom_to_window(lo - pad, hi + pad);
+ }
+
+ fn open_count_target(&mut self, i: usize, name: String, key: CountKey) {
+ let mut mode = self.tabs[i].count.take().unwrap_or_default();
+ let instances = mode.instances(&self.tabs[i].scene.document);
+ let reference = instances
+ .iter()
+ .find(|inst| inst.duplicate_of.is_none() && inst.name.eq_ignore_ascii_case(&name) && key.matches(inst))
+ .map(|inst| inst.handle);
+ mode.target = Some(CountTarget::Block {
+ name,
+ reference,
+ matching: [key.layer.is_some(), key.scale.is_some(), key.mirror_state.is_some()],
+ picked: false,
+ });
+ mode.cursor = None;
+ self.tabs[i].count = Some(mode);
+ self.apply_count_display(i);
+ }
+
+ /// The Field dialog's Show Count Instances: count mode on what the
+ /// expression counts.
+ pub(in crate::app) fn show_count_instances(&mut self, i: usize, json: &str) {
+ match codec::count::parse_query(json) {
+ Some(codec::count::CountQuery::Block { name, key, boundary }) => {
+ if let Some(b) = boundary {
+ if let Some(ring) = codec::count::boundary_polygon(&self.tabs[i].scene.document, b) {
+ let mut mode = self.tabs[i].count.take().unwrap_or_default();
+ if mode.boundary.map(|(h, _)| h) != Some(b) {
+ self.drop_count_boundary(i, mode.boundary);
+ mode.boundary = Some((b, false));
+ }
+ mode.area = Some(ring);
+ self.tabs[i].count = Some(mode);
+ }
+ }
+ self.open_count_target(i, name, key);
+ }
+ Some(codec::count::CountQuery::Single(handles)) if !handles.is_empty() => {
+ let hexes: Vec = handles.iter().map(|h| hex(*h)).collect();
+ let area = if self.tabs[i].count.is_some() { "K" } else { "E" };
+ self.count_run(i, &format!("{area} T {}", hexes.join(",")));
+ }
+ _ => {}
+ }
+ }
+
+ pub(in crate::app) fn on_count(&mut self, m: CountMsg) -> Task {
+ let i = self.active_tab;
+ if self.tabs[i].is_start {
+ return Task::none();
+ }
+ match m {
+ CountMsg::Search(s) => self.count_palette.search = s,
+ CountMsg::Sort(by_count) => {
+ if self.count_palette.by_count == by_count {
+ self.count_palette.descending = !self.count_palette.descending;
+ } else {
+ self.count_palette.by_count = by_count;
+ self.count_palette.descending = false;
+ }
+ }
+ CountMsg::Open(name, key) | CountMsg::Menu(name, key, RowAction::Review) => {
+ if self.count_palette.table.is_none() {
+ self.open_count_target(i, name, key);
+ }
+ }
+ CountMsg::Menu(name, key, RowAction::Field) => {
+ let code = self.block_field_code(i, &name, &key);
+ return self.start_count_field(i, code);
+ }
+ CountMsg::Menu(name, _, RowAction::Expand(k)) => {
+ let upper = name.to_ascii_uppercase();
+ let list = &mut self.count_palette.expanded;
+ match list.iter_mut().find(|(n, _)| *n == upper) {
+ Some((_, e)) => e[k] = !e[k],
+ None => {
+ let mut e = [false; 3];
+ e[k] = true;
+ list.push((upper, e));
+ }
+ }
+ }
+ CountMsg::CreateTable => self.count_palette.table = Some(Vec::new()),
+ CountMsg::TableCheck(name, on) => {
+ if let Some(checked) = self.count_palette.table.as_mut() {
+ let upper = name.to_ascii_uppercase();
+ checked.retain(|n| *n != upper);
+ if on {
+ checked.push(upper);
+ }
+ }
+ }
+ CountMsg::TableAll(on) => {
+ let names: Vec = if on {
+ let area = self.tabs[i].count.as_ref().and_then(|m| m.area.clone());
+ let instances = codec::count::block_instances(&self.tabs[i].scene.document, area.as_deref());
+ crate::ui::window::count_palette::rows(&instances, &self.count_palette)
+ .into_iter()
+ .map(|r| r.name.to_ascii_uppercase())
+ .collect()
+ } else {
+ Vec::new()
+ };
+ self.count_palette.table = Some(names);
+ }
+ CountMsg::TableCancel => self.count_palette.table = None,
+ CountMsg::TableInsert => {
+ let names = self.count_palette.table.take().unwrap_or_default();
+ if names.is_empty() {
+ return Task::none();
+ }
+ return self.start_count_command(i, Box::new(CountTableCommand::placing(names)));
+ }
+ CountMsg::Back | CountMsg::Close => self.close_count(i),
+ CountMsg::Match(k, on) => {
+ if let Some(CountMode { target: Some(CountTarget::Block { matching, .. }), .. }) = self.tabs[i].count.as_mut() {
+ matching[k] = on;
+ }
+ self.apply_count_display(i);
+ }
+ CountMsg::ToggleDetails => self.count_palette.details_closed = !self.count_palette.details_closed,
+ CountMsg::ToggleErrors => self.count_palette.errors_closed = !self.count_palette.errors_closed,
+ CountMsg::ShowError(h) => self.zoom_to_insert(i, h),
+ CountMsg::Prev | CountMsg::Next => {
+ let Some(mode) = self.tabs[i].count.as_ref() else { return Task::none() };
+ let result = mode.result(&self.tabs[i].scene.document);
+ let n = result.counted.len();
+ if n == 0 {
+ return Task::none();
+ }
+ let next = match (mode.cursor, matches!(m, CountMsg::Next)) {
+ (None, true) => 0,
+ (None, false) => n - 1,
+ (Some(c), true) => (c + 1) % n,
+ (Some(c), false) => (c + n - 1) % n,
+ };
+ let target = result.counted[next];
+ if let Some(mode) = self.tabs[i].count.as_mut() {
+ mode.cursor = Some(next);
+ }
+ self.zoom_to_insert(i, target);
+ }
+ CountMsg::Area => return self.dispatch_command("COUNTAREA"),
+ CountMsg::Select => {
+ self.tabs[i].scene.deselect_all();
+ let command = CountCommand::with_area(if self.tabs[i].count.is_some() { "K" } else { "C" });
+ return self.start_count_command(i, Box::new(command));
+ }
+ CountMsg::Field => return self.dispatch_command("COUNTFIELD"),
+ CountMsg::InvalidAlways(v) => self.count_palette.invalid_always = v,
+ CountMsg::InvalidUndo => {
+ self.close_invalid_area(1);
+ self.count_invalid_undo(i);
+ }
+ CountMsg::InvalidContinue => {
+ self.close_invalid_area(2);
+ self.count_invalid_continue(i);
+ }
+ }
+ Task::none()
+ }
+}
diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs
index b979fec69..16925118b 100644
--- a/src/app/commands/display.rs
+++ b/src/app/commands/display.rs
@@ -179,6 +179,8 @@ impl OpenCADStudio {
else {
continue;
};
+ // `changed` tracks this table while the action runs.
+ let changed_before = std::mem::take(&mut changed);
match &action {
TableAction::Cell(row, column, text) => {
if let Some(cell) = table.cell_mut(*row, *column) {
@@ -321,6 +323,12 @@ impl OpenCADStudio {
}
_ => {}
}
+ if changed {
+ // The stored picture no longer shows the cells: draw
+ // from them, and redraw the picture.
+ table.block_record_handle = None;
+ }
+ changed |= changed_before;
}
if changed {
self.invalidate_property_targets(i, &selected_handles);
diff --git a/src/app/commands/field.rs b/src/app/commands/field.rs
index 422e51bcb..3f0ee8a93 100644
--- a/src/app/commands/field.rs
+++ b/src/app/commands/field.rs
@@ -222,6 +222,15 @@ impl OpenCADStudio {
);
}
FieldDialogMsg::PlotScale(k) => state.plot_scale = k,
+ FieldDialogMsg::CountExpression(v) => state.count_expression = v,
+ FieldDialogMsg::ShowCountInstances => {
+ // The dialog closes and count mode shows what the expression counts.
+ let json = state.count_expression.clone();
+ self.field_dialog = None;
+ self.close_active_modal();
+ self.show_count_instances(i, &json);
+ return Task::none();
+ }
FieldDialogMsg::PlaceholderProperty(k) => {
state.placeholder_property = k;
state.text_case = 0;
diff --git a/src/app/commands/inquiry.rs b/src/app/commands/inquiry.rs
index e13a454d4..dda3f01b1 100644
--- a/src/app/commands/inquiry.rs
+++ b/src/app/commands/inquiry.rs
@@ -1216,45 +1216,6 @@ impl OpenCADStudio {
}
}
- // ── COUNT — entity statistics ─────────────────────────────────────
- "COUNT" => {
- use crate::command::KeywordCommand;
- let c = KeywordCommand::new(
- "COUNT",
- "COUNT tally [All (by type) / by Layer]:",
- vec![("All", "TYPE", None), ("By layer", "LAYER", None)],
- );
- self.command_line.push_info(&c.prompt());
- self.tabs[self.active_tab].active_cmd = Some(Box::new(c));
- }
- cmd if cmd.starts_with("COUNT ") => {
- let filter = cmd.split_once(' ').map(|(_, r)| r.trim().to_uppercase());
- let mut counts: std::collections::BTreeMap = Default::default();
- for e in self.tabs[i].scene.document.entities() {
- let layer = &e.common().layer;
- let type_name = crate::entities::names::dxf_name(e);
- let key = match &filter {
- Some(f) if f == "LAYER" => layer.clone(),
- Some(f) if f == "TYPE" => type_name.to_string(),
- Some(f) => {
- // Filter by layer name
- if layer.to_uppercase() != *f {
- continue;
- }
- type_name.to_string()
- }
- None => type_name.to_string(),
- };
- *counts.entry(key).or_default() += 1;
- }
- let total: usize = counts.values().sum();
- for (k, n) in &counts {
- self.command_line.push_output(crate::tf!(" {k}: {n}").as_ref());
- }
- self.command_line
- .push_output(crate::tf!("COUNT: {total} entity(ies) total.").as_ref());
- }
-
"DATAEXTRACTION" | "EATTEXT" | "ATTEXT" => {
self.open_data_extraction();
}
diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs
index b17c9eee6..5bca869e3 100644
--- a/src/app/commands/mod.rs
+++ b/src/app/commands/mod.rs
@@ -6,6 +6,7 @@ use std::path::PathBuf;
mod attdef;
mod field;
+mod count;
mod blocks;
mod xref_attach;
mod dim;
@@ -324,6 +325,9 @@ impl OpenCADStudio {
if let Some(t) = self.dispatch_field(cmd, i) {
return Some(t);
}
+ if let Some(t) = self.dispatch_count(cmd, i) {
+ return Some(t);
+ }
if let Some(t) = self.dispatch_draw(cmd, i) {
return Some(t);
}
diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs
index 951c0ef1d..974939311 100644
--- a/src/app/control/actions.rs
+++ b/src/app/control/actions.rs
@@ -73,6 +73,7 @@ pub(super) const NAMES: &[&str] = &[
"pdf_layer_toggle",
"pc_manager_toggle",
"pc_manager",
+ "count_palette",
"blocks_palette",
"pdf_page_select",
"pc_colormap",
@@ -925,6 +926,78 @@ impl OpenCADStudio {
};
Message::PcManager(PcManagerMsg::Toggle(handle, row))
}
+ // Count palette and count mode: "search=", "sort=name|count",
+ // "open=NAME", "child=NAME:" (an expanded row), "menu=:NAME", "create_table", "check=NAME:1",
+ // "all=1", "table_cancel", "table_insert", "back", "match=<0|1|2>:1",
+ // "details", "errors", "error=", "area", "prev", "next", "select",
+ // "field", "close", "invalid_undo", "invalid_continue", "invalid_always=1".
+ "count_palette" => {
+ use crate::ui::window::count_palette::{CountMsg as M, RowAction};
+ let value = string(req, "value")?;
+ let (key, arg) = value.split_once('=').unwrap_or((value, ""));
+ let flag = |s: &str| s == "1" || s.eq_ignore_ascii_case("true");
+ let bad = || failure("invalid_value", "Unknown count palette edit");
+ Message::Count(match key {
+ "search" => M::Search(arg.into()),
+ "sort" => M::Sort(arg == "count"),
+ "open" => M::Open(arg.into(), codec::count::CountKey::default()),
+ "child" => {
+ let (name, n) = arg.rsplit_once(':').ok_or_else(bad)?;
+ let n: usize = n.parse().map_err(|_| bad())?;
+ let [l, s, m] = self.count_palette.expansion(name).ok_or_else(bad)?;
+ let i = self.active_tab;
+ let area = self.tabs[i].count.as_ref().and_then(|c| c.area.clone());
+ let instances = codec::count::block_instances(&self.tabs[i].scene.document, area.as_deref());
+ let (k, _) = codec::count::expanded_counts(&instances, name, l, s, m).into_iter().nth(n).ok_or_else(bad)?;
+ M::Open(name.into(), k)
+ }
+ "menu" => {
+ let (action, name) = arg.split_once(':').ok_or_else(bad)?;
+ let action = match action {
+ "review" => RowAction::Review,
+ "field" => RowAction::Field,
+ "layer" => RowAction::Expand(0),
+ "scale" => RowAction::Expand(1),
+ "mirror" => RowAction::Expand(2),
+ _ => return Err(bad()),
+ };
+ M::Menu(name.into(), codec::count::CountKey::default(), action)
+ }
+ "create_table" => M::CreateTable,
+ "check" => {
+ let (name, on) = arg.rsplit_once(':').unwrap_or((arg, "1"));
+ M::TableCheck(name.into(), flag(on))
+ }
+ "all" => M::TableAll(flag(arg)),
+ "table_cancel" => M::TableCancel,
+ "table_insert" => M::TableInsert,
+ "back" => M::Back,
+ "match" => {
+ let (k, on) = arg.split_once(':').unwrap_or((arg, "1"));
+ M::Match(k.parse::().ok().filter(|k| *k < 3).ok_or_else(bad)?, flag(on))
+ }
+ "details" => M::ToggleDetails,
+ "errors" => M::ToggleErrors,
+ "error" => {
+ let i = self.active_tab;
+ let n: usize = arg.parse().map_err(|_| bad())?;
+ let mode = self.tabs[i].count.as_ref().ok_or_else(bad)?;
+ M::ShowError(*mode.result(&self.tabs[i].scene.document).errors.get(n).ok_or_else(bad)?)
+ }
+ "area" => M::Area,
+ "prev" => M::Prev,
+ "next" => M::Next,
+ "select" => M::Select,
+ "field" => M::Field,
+ "close" => M::Close,
+ // The Invalid Area dialog.
+ "invalid_undo" => M::InvalidUndo,
+ "invalid_continue" => M::InvalidContinue,
+ "invalid_always" => M::InvalidAlways(arg == "1"),
+ _ => return Err(bad()),
+ })
+ }
// Point Cloud Manager tree: "search=", "collapse", "expand",
// "toggle_node=" or "select=".
"pc_manager" => {
@@ -1133,6 +1206,8 @@ impl OpenCADStudio {
"link_text" => F::HyperlinkText(v.into()),
"link_url" => F::HyperlinkUrl(v.into()),
"plot_scale" => F::PlotScale(index(codec::fields::PLOT_SCALE_FORMATS.len())?),
+ "count" => F::CountExpression(v.into()),
+ "show_instances" => F::ShowCountInstances,
"placeholder" => F::PlaceholderProperty(index(codec::fields::BLOCK_PLACEHOLDER_PROPERTIES.len())?),
"table_function" => F::TableFunction(
["Average", "Sum", "Count", "Cell"]
diff --git a/src/app/document.rs b/src/app/document.rs
index 9e147db7e..605f13d8e 100644
--- a/src/app/document.rs
+++ b/src/app/document.rs
@@ -221,6 +221,8 @@ pub(super) struct DocumentTab {
pub(super) paper_bg_color: Option<[f32; 4]>,
/// Active REFEDIT session, if any.
pub(super) refedit_session: Option,
+ /// COUNT mode, while it is on.
+ pub(crate) count: Option,
/// Open BEDIT block tabs. Definitions are edited live; each tab owns its
/// entry snapshot and camera so nested blocks can remain open independently.
pub(super) block_edits: Vec,
@@ -651,6 +653,7 @@ impl DocumentTab {
bg_color: None,
paper_bg_color: None,
refedit_session: None,
+ count: None,
block_edits: Vec::new(),
active_block_edit: None,
active_mleader_style: "Standard".to_string(),
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 807f5a46e..0257df35a 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -818,6 +818,7 @@ pub(super) struct OpenCADStudio {
pub(crate) show_browser: bool,
/// Point Cloud Manager palette (POINTCLOUDMANAGER).
pub(crate) pc_manager: crate::ui::window::pc_manager::PcManager,
+ pub(crate) count_palette: crate::ui::window::count_palette::CountPalette,
/// Which viewport background the colour wheel is editing, or `None` when
/// it is closed. One slot, because only one wheel can be open at a time.
pub(crate) bg_picker: Option,
@@ -1942,6 +1943,7 @@ pub enum ModalKind {
GeometricTolerance,
AttDef,
AttDefEdit,
+ CountInvalidArea,
Field,
DraftingSettings,
AutoConstrainSettings,
@@ -3899,6 +3901,7 @@ pub enum Message {
FieldDialog(crate::ui::window::field_dialog::FieldDialogMsg),
/// A click or search in the Point Cloud Manager.
PcManager(crate::ui::window::pc_manager::PcManagerMsg),
+ Count(crate::ui::window::count_palette::CountMsg),
PdfImportPickResult(Result<(std::path::PathBuf, std::sync::Arc>), String>),
/// Result of the PDFATTACH file picker.
PdfAttachPickResult(Result<(std::path::PathBuf, std::sync::Arc>), String>),
@@ -4225,6 +4228,7 @@ impl OpenCADStudio {
show_external_references: false,
show_browser: false,
pc_manager: Default::default(),
+ count_palette: Default::default(),
bg_picker: None,
block_palette: Default::default(),
xref_manager: Default::default(),
diff --git a/src/app/settings.rs b/src/app/settings.rs
index 52907b2bf..89e1728ae 100644
--- a/src/app/settings.rs
+++ b/src/app/settings.rs
@@ -613,6 +613,18 @@ pub struct UserSettings {
/// BLOCKNAVIGATE (default ".").
#[serde(default = "default_block_navigate")]
pub block_navigate: String,
+ /// COUNTCOLOR (default 3).
+ #[serde(default = "default_count_color")]
+ pub count_color: i16,
+ /// COUNTERRORCOLOR (default 1).
+ #[serde(default = "default_count_error_color")]
+ pub count_error_color: i16,
+ /// COUNTSERVICE (default on).
+ #[serde(default = "default_true")]
+ pub count_service: bool,
+ /// Count - Invalid Area: 0 ask, 1 undo, 2 count all of model space.
+ #[serde(default)]
+ pub count_invalid_area: u8,
}
/// A block the Blocks palette lists from outside the current drawing (a
@@ -676,6 +688,14 @@ fn default_one_u8() -> u8 {
1
}
+fn default_count_color() -> i16 {
+ 3
+}
+
+fn default_count_error_color() -> i16 {
+ 1
+}
+
fn default_block_navigate() -> String {
".".to_string()
}
@@ -853,6 +873,10 @@ impl Default for UserSettings {
block_mru_list: 50,
block_redefine_mode: 1,
block_navigate: ".".to_string(),
+ count_color: 3,
+ count_error_color: 1,
+ count_service: true,
+ count_invalid_area: 0,
}
}
}
diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs
index 197091807..65a04a0d1 100644
--- a/src/app/update/dialog.rs
+++ b/src/app/update/dialog.rs
@@ -590,6 +590,10 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
PanelId::PointCloudManager => {
self.pc_manager.show = false;
}
+ PanelId::Count => {
+ self.count_palette.show = false;
+ self.ribbon.set_count_palette(false);
+ }
PanelId::Properties => {
self.show_properties = false;
self.ribbon.set_properties(false);
@@ -695,6 +699,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
PanelId::Browser => self.show_browser,
PanelId::NodeGraph => self.show_node_graph,
PanelId::PointCloudManager => self.pc_manager.show,
+ PanelId::Count => self.count_palette.show,
}
}
diff --git a/src/app/update/file.rs b/src/app/update/file.rs
index fe0e4d946..7938f71d1 100644
--- a/src/app/update/file.rs
+++ b/src/app/update/file.rs
@@ -744,6 +744,10 @@ impl OpenCADStudio {
block_mru_list: self.block_mru_list,
block_redefine_mode: self.block_redefine_mode,
block_navigate: self.block_navigate.clone(),
+ count_color: self.count_palette.color,
+ count_error_color: self.count_palette.error_color,
+ count_service: self.count_palette.service,
+ count_invalid_area: self.count_palette.invalid_choice,
}
}
@@ -875,6 +879,10 @@ impl OpenCADStudio {
self.block_mru_list = s.block_mru_list.min(100);
self.block_redefine_mode = s.block_redefine_mode.min(2);
self.block_navigate = s.block_navigate.clone();
+ self.count_palette.color = s.count_color;
+ self.count_palette.error_color = s.count_error_color;
+ self.count_palette.service = s.count_service;
+ self.count_palette.invalid_choice = s.count_invalid_area.min(2);
self.block_palette.recent = s.block_recent.clone();
self.block_palette.favorites = s.block_favorites.clone();
self.block_palette.libraries = s.block_libraries.clone();
diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs
index 9d97758cb..5fa43abe4 100644
--- a/src/app/update/mod.rs
+++ b/src/app/update/mod.rs
@@ -493,6 +493,8 @@ impl OpenCADStudio {
// The Reference Manager watches the active drawing and re-scans when
// the palette is open on another tab's entries.
self.refresh_xref_manager_if_stale();
+ // Count mode recolours its references after the drawing changes.
+ self.refresh_count_if_stale();
// Let V4 plugins observe selection changes that happened while handling
// this message (picking, window select, QSELECT, SELECTALL, grip edits,
// and plugin request draining).
@@ -1819,6 +1821,7 @@ impl OpenCADStudio {
Message::AttdefDialog(message) => self.on_attdef_dialog(message),
Message::FieldDialog(message) => self.on_field_dialog(message),
Message::PcManager(message) => self.update_pc_manager(message),
+ Message::Count(message) => self.on_count(message),
Message::RibbonSelectTab(idx) => {
self.ribbon.select(idx);
Task::none()
@@ -8548,6 +8551,10 @@ impl OpenCADStudio {
}
Message::CloseModal => {
+ // The Invalid Area dialog's ✕ undoes the boundary change, as its Cancel does.
+ if self.active_modal == Some(super::ModalKind::CountInvalidArea) {
+ return self.update(Message::Count(crate::ui::window::count_palette::CountMsg::InvalidUndo));
+ }
if self.active_modal == Some(super::ModalKind::Field) {
self.close_field_dialog();
return Task::none();
diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs
index 71a8a72bc..021102307 100644
--- a/src/app/view/mod.rs
+++ b/src/app/view/mod.rs
@@ -1669,6 +1669,13 @@ bg={bg_ms:.1}ms n={view_count}"
}
}
+ // Count mode: a blue frame round the drawing and the count toolbar.
+ if let (Some(mode), false) = (tab.count.as_ref(), tab.is_start) {
+ viewport_stack = viewport_stack.push(crate::ui::window::count_palette::border());
+ viewport_stack = viewport_stack
+ .push(crate::ui::window::count_palette::toolbar(mode, &tab.scene.document));
+ }
+
// Reserve the overlaid command line when placing cursor-anchored panels.
mark("chrome");
let command_line_inset = if self.command_line.history_open {
@@ -1888,6 +1895,7 @@ bg={bg_ms:.1}ms n={view_count}"
crate::ui::dock::PanelId::Browser => self.show_browser,
crate::ui::dock::PanelId::NodeGraph => self.show_node_graph,
crate::ui::dock::PanelId::PointCloudManager => self.pc_manager.show,
+ crate::ui::dock::PanelId::Count => self.count_palette.show,
}
};
let edge_stack = |side: crate::app::config::DockSide| -> Option> {
@@ -3022,6 +3030,15 @@ impl OpenCADStudio {
auto_collapse,
),
crate::ui::dock::PanelId::NodeGraph => tab.graph.panel(width, auto_collapse),
+ crate::ui::dock::PanelId::Count => crate::ui::window::count_palette::view(
+ &self.count_palette,
+ tab.count.as_ref(),
+ &tab.scene.document,
+ tab.id,
+ tab.scene.geometry_epoch,
+ width,
+ auto_collapse,
+ ),
crate::ui::dock::PanelId::PointCloudManager => crate::ui::window::pc_manager::view(
&self.pc_manager,
&tab.scene.document,
diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs
index d5f81eae6..5f693e757 100644
--- a/src/app/view/modal.rs
+++ b/src/app/view/modal.rs
@@ -40,6 +40,7 @@ impl OpenCADStudio {
Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(),
Some(K::AttDef) => crate::t!("Attribute Definition").into_owned(),
Some(K::AttDefEdit) => crate::t!("Edit Attribute Definition").into_owned(),
+ Some(K::CountInvalidArea) => crate::t!("Count - Invalid Area").into_owned(),
Some(K::Field) => crate::t!("Field").into_owned(),
Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(),
Some(K::AutoConstrainSettings) => crate::t!("Constraint Settings").into_owned(),
@@ -584,6 +585,9 @@ impl OpenCADStudio {
let state = self.field_dialog.as_ref()?;
sized_flow(ex, 760, 660, |flow| crate::ui::window::field_dialog::view(state, flow))
}
+ super::super::ModalKind::CountInvalidArea => {
+ sized_flow(ex, 520, 260, |flow| crate::ui::window::count_palette::invalid_area_view(&self.count_palette, flow))
+ }
super::super::ModalKind::AttDefEdit => {
let state = self.attdef_edit.as_ref()?;
sized_flow(ex, 460, 220, |flow| crate::ui::window::attdef_dialog::view_edit(state, flow))
diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs
index 3fe005fee..f657dec83 100644
--- a/src/locale_catalog.rs
+++ b/src/locale_catalog.rs
@@ -5530,6 +5530,43 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"US Survey Feet" => Some(("common", "us-survey-feet")),
"to layout" => Some(("common", "to-layout")),
"{count} objects selected" => Some(("common", "count-objects-selected")),
+ "Count Selection" => Some(("count-feature", "selection")),
+ "Review Count Details" => Some(("count-feature", "review")),
+ "Insert Count Field" => Some(("count-feature", "insert-field")),
+ "EXPAND BY" => Some(("count-feature", "expand-by")),
+ "Mirror State" => Some(("count-feature", "mirror-state")),
+ "Count in a specified area" => Some(("count-feature", "area")),
+ "Create Table" => Some(("count-feature", "create-table")),
+ "Back to List" => Some(("count-feature", "back")),
+ "Match Scale" => Some(("count-feature", "match-scale")),
+ "Match Mirror State" => Some(("count-feature", "match-mirror")),
+ "Count Error Report: {}" => Some(("count-feature", "error-report")),
+ "Count: {}" => Some(("count-feature", "toolbar-count")),
+ "Overlapping object" => Some(("count-feature", "overlap")),
+ "Show Count Instances" => Some(("count-feature", "show-instances")),
+ "Geometries: {}" => Some(("count-feature", "geometries")),
+ "Count - Invalid Area" => Some(("count-feature", "invalid-title")),
+ "The changes to count boundary have resulted to an invalid count area. What do you want to do?" => Some(("count-feature", "invalid-message")),
+ "Undo the changes to the count boundary" => Some(("count-feature", "invalid-undo")),
+ "The last change is undone; the boundary and the count area come back." => Some(("count-feature", "invalid-undo-detail")),
+ "Continue and count the entire model space" => Some(("count-feature", "invalid-continue")),
+ "The count area is dropped; counting goes on in all of model space." => Some(("count-feature", "invalid-continue-detail")),
+ "Always perform my current choice" => Some(("count-feature", "invalid-always")),
+ "{found} found" => Some(("count-feature", "found")),
+ "{found} found, {total} total" => Some(("count-feature", "found-total")),
+ "** COUNTFIELD command only available during Count. **" => Some(("count-feature", "field-only-in-count")),
+ "count area is active." => Some(("count-feature", "area-active")),
+ "{name} = {v} (read only)" => Some(("count-feature", "read-only-value")),
+ "Requires 0 or 1 only." => Some(("count-feature", "requires-0-or-1")),
+ "Requires an integer between {min} and {max}." => Some(("count-feature", "requires-int-range")),
+ "{name} ...... {n}" => Some(("count-feature", "target-count")),
+ "MTEXT Current text style: \"{}\" Text height: {:.4}" => Some(("count-feature", "mtext-current-style")),
+ "{found} field(s) found." => Some(("count-feature", "fields-found")),
+ "{updated} field(s) updated." => Some(("count-feature", "fields-updated")),
+ "Invalid option keyword." => Some(("count-feature", "invalid-keyword")),
+ "Invalid count area boundary object. Select a closed polyline consisting of line segments and does not intersect itself." => Some(("count-feature", "invalid-boundary")),
+ "Block \"{part}\" cannot be found." => Some(("count-feature", "block-not-found")),
+ "Invalid point." => Some(("count-feature", "invalid-point")),
_ => None,
}
}
diff --git a/src/modules/annotate/count_cmd.rs b/src/modules/annotate/count_cmd.rs
new file mode 100644
index 000000000..9ac6c3d3f
--- /dev/null
+++ b/src/modules/annotate/count_cmd.rs
@@ -0,0 +1,447 @@
+// COUNT / COUNTAREA, COUNTTABLE and UPDATEFIELD.
+//
+// Specify first corner point of the count area or
+// [Current area/Entire model space/Object/Polygonal] :
+// Specify opposite corner: (Polygonal: Specify start point: /
+// Specify next point; Object:
+// Select object as count area boundary:)
+// Select target objects or [List all blocks] : (COUNT)
+//
+// The command gathers its answers and hands them to the application as
+// `_COUNTRUN L|T ` (COUNTAREA: `_COUNTAREASET `), the
+// area being `C` (current area, which counts all of model space), `E`
+// (entire model space), `R x,y x,y`, `P x,y x,y …` or `O `.
+
+use codec::types::Handle;
+use codec::EntityType;
+use glam::DVec3;
+
+use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
+use crate::scene::model::wire_model::WireModel;
+
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+enum Step {
+ Area,
+ Corner,
+ PolyStart,
+ PolyNext,
+ Object,
+ Targets,
+}
+
+pub struct CountCommand {
+ /// COUNTAREA: only the area is asked.
+ area_only: bool,
+ step: Step,
+ area: String,
+ points: Vec,
+ targets: Vec,
+ picked: Option,
+}
+
+fn xy(p: DVec3) -> String {
+ format!("{:?},{:?}", p.x, p.y)
+}
+
+impl CountCommand {
+ pub fn new(area_only: bool) -> Self {
+ Self {
+ area_only,
+ step: Step::Area,
+ area: String::new(),
+ points: Vec::new(),
+ targets: Vec::new(),
+ picked: None,
+ }
+ }
+
+ /// COUNT whose area is already answered (`COUNT _C`).
+ pub fn with_area(area: &str) -> Self {
+ Self { area: area.to_string(), step: Step::Targets, ..Self::new(false) }
+ }
+
+ fn area_done(&mut self, area: String) -> CmdResult {
+ self.area = area;
+ if self.area_only {
+ return CmdResult::Dispatch(format!("_COUNTAREASET {}", self.area));
+ }
+ self.step = Step::Targets;
+ CmdResult::NeedPoint
+ }
+
+ fn area_keyword(&mut self, text: &str) -> CmdResult {
+ match text.trim().trim_start_matches('_').to_ascii_uppercase().as_str() {
+ "" | "C" | "CURRENT" | "CURRENT AREA" => self.area_done("C".into()),
+ "E" | "ENTIRE" | "ENTIRE MODEL SPACE" => self.area_done("E".into()),
+ "O" | "OBJECT" => {
+ self.step = Step::Object;
+ CmdResult::NeedPoint
+ }
+ "P" | "POLYGONAL" => {
+ self.points.clear();
+ self.step = Step::PolyStart;
+ CmdResult::NeedPoint
+ }
+ _ => CmdResult::ReportError(crate::t!("Invalid option keyword.").into_owned()),
+ }
+ }
+
+ fn finish_targets(&self) -> CmdResult {
+ if self.targets.is_empty() {
+ return CmdResult::Dispatch(format!("_COUNTRUN {} L", self.area));
+ }
+ let handles: Vec = self.targets.iter().map(|h| format!("{:X}", h.value())).collect();
+ CmdResult::Dispatch(format!("_COUNTRUN {} T {}", self.area, handles.join(",")))
+ }
+}
+
+impl CadCommand for CountCommand {
+ fn name(&self) -> &'static str {
+ if self.area_only {
+ "COUNTAREA"
+ } else {
+ "COUNT"
+ }
+ }
+
+ fn prompt(&self) -> String {
+ match self.step {
+ Step::Area => "Specify first corner point of the count area or [Current area/Entire model space/Object/Polygonal] :".into(),
+ Step::Corner => "Specify opposite corner:".into(),
+ Step::PolyStart => "Specify start point:".into(),
+ // The reference shows this one without a colon.
+ Step::PolyNext => "Specify next point".into(),
+ Step::Object => "Select object as count area boundary:".into(),
+ Step::Targets => "Select target objects or [List all blocks] :".into(),
+ }
+ }
+
+ fn options(&self) -> Vec {
+ match self.step {
+ Step::Area => vec![
+ CmdOption::new("Current area", "C"),
+ CmdOption::new("Entire model space", "E"),
+ CmdOption::new("Object", "O"),
+ CmdOption::new("Polygonal", "P"),
+ ],
+ Step::Targets => vec![CmdOption::new("List all blocks", "L")],
+ _ => Vec::new(),
+ }
+ }
+
+ fn input_kind(&self) -> InputKind {
+ match self.step {
+ Step::Targets => InputKind::SingleToken,
+ _ => InputKind::Point,
+ }
+ }
+
+ fn point_step_accepts_keywords(&self) -> bool {
+ self.step == Step::Area
+ }
+
+ fn is_selection_gathering(&self) -> bool {
+ self.step == Step::Targets
+ }
+
+ fn on_selection_complete(&mut self, handles: Vec) -> CmdResult {
+ // The selection arrives whole after every pick: `1 found`, then
+ // `1 found, 2 total`.
+ let found = handles.iter().filter(|h| !self.targets.contains(h)).count();
+ let first = self.targets.is_empty();
+ self.targets = handles;
+ let total = self.targets.len();
+ CmdResult::ReportMeasurement(if first {
+ crate::tf!("{found} found").into_owned()
+ } else {
+ crate::tf!("{found} found, {total} total").into_owned()
+ })
+ }
+
+ fn needs_entity_pick(&self) -> bool {
+ self.step == Step::Object
+ }
+
+ fn inject_before_entity_pick(&self) -> bool {
+ true
+ }
+
+ fn inject_picked_entity(&mut self, entity: EntityType) {
+ self.picked = Some(entity);
+ }
+
+ fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
+ match self.picked.take() {
+ Some(EntityType::LwPolyline(pl)) if codec::count::valid_boundary(&pl) => {
+ self.area_done(format!("O {:X}", handle.value()))
+ }
+ _ => CmdResult::CancelWithMessage(
+ crate::t!(
+ "Invalid count area boundary object. Select a closed polyline consisting of line segments and does not intersect itself."
+ )
+ .into_owned(),
+ ),
+ }
+ }
+
+ fn on_point(&mut self, pt: DVec3) -> CmdResult {
+ match self.step {
+ Step::Area => {
+ self.points = vec![pt];
+ self.step = Step::Corner;
+ CmdResult::NeedPoint
+ }
+ Step::Corner => {
+ let area = format!("R {} {}", xy(self.points[0]), xy(pt));
+ self.area_done(area)
+ }
+ Step::PolyStart | Step::PolyNext => {
+ self.points.push(pt);
+ self.step = Step::PolyNext;
+ CmdResult::NeedPoint
+ }
+ _ => CmdResult::NeedPoint,
+ }
+ }
+
+ fn on_text_input(&mut self, text: &str) -> Option {
+ match self.step {
+ Step::Area => Some(self.area_keyword(text)),
+ Step::Targets => match text.trim().trim_start_matches('_').to_ascii_uppercase().as_str() {
+ "L" | "LIST" | "LIST ALL BLOCKS" => {
+ self.targets.clear();
+ Some(self.finish_targets())
+ }
+ _ => None,
+ },
+ _ => None,
+ }
+ }
+
+ fn on_enter(&mut self) -> CmdResult {
+ match self.step {
+ Step::Area => self.area_keyword(""),
+ Step::PolyNext if self.points.len() >= 3 => {
+ let ring: Vec = self.points.iter().map(|p| xy(*p)).collect();
+ let area = format!("P {}", ring.join(" "));
+ self.area_done(area)
+ }
+ Step::Targets => self.finish_targets(),
+ _ => CmdResult::NeedPoint,
+ }
+ }
+
+ fn on_mouse_move(&mut self, pt: DVec3) -> Option {
+ let ring: Vec = match self.step {
+ Step::Corner => {
+ let a = self.points[0];
+ vec![a, DVec3::new(pt.x, a.y, a.z), pt, DVec3::new(a.x, pt.y, a.z)]
+ }
+ Step::PolyNext => {
+ let mut ring = self.points.clone();
+ ring.push(pt);
+ ring
+ }
+ _ => return None,
+ };
+ let mut points: Vec<[f64; 3]> = ring.iter().map(|p| p.to_array()).collect();
+ points.push(points[0]);
+ Some(WireModel::solid_f64("count_area".into(), points, WireModel::CYAN, false))
+ }
+}
+
+/// COUNTTABLE: the blocks to list, then where the table goes.
+pub struct CountTableCommand {
+ step: TableStep,
+ /// Defined user blocks (sorted), and the listing's other counts:
+ /// external references, dependent and unnamed blocks.
+ blocks: Vec,
+ others: [usize; 3],
+ names: Vec,
+ /// Listing lines still to show after `Press ENTER to continue:`.
+ pending: Vec,
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum TableStep {
+ Names,
+ List,
+ Point,
+ Page,
+}
+
+impl CountTableCommand {
+ pub fn new(blocks: Vec, others: [usize; 3]) -> Self {
+ Self { step: TableStep::Names, blocks, others, names: Vec::new(), pending: Vec::new() }
+ }
+
+ /// The palette's Create Table: the blocks are chosen, only the point is asked.
+ pub fn placing(names: Vec) -> Self {
+ Self { step: TableStep::Point, blocks: Vec::new(), others: [0; 3], names, pending: Vec::new() }
+ }
+
+ /// `?`: the defined blocks matching the pattern, paged by 22 lines
+ /// (`Press ENTER to continue:` in between), then the summary.
+ fn listing(&mut self, pattern: &str) -> CmdResult {
+ let pattern = if pattern.trim().is_empty() { "*" } else { pattern.trim() };
+ let mut names: Vec<&String> = self
+ .blocks
+ .iter()
+ .filter(|name| pattern.split(',').any(|p| crate::io::xref_model::wildcard_match(name, p.trim())))
+ .collect();
+ names.sort_by_key(|name| list_order(name));
+ self.pending = std::iter::once(String::new())
+ .chain(std::iter::once("Defined blocks.".to_string()))
+ .chain(names.iter().map(|name| format!(" {:<31}", format!("\"{name}\""))))
+ .collect();
+ self.next_page()
+ }
+
+ fn next_page(&mut self) -> CmdResult {
+ let page: Vec = self.pending.drain(..self.pending.len().min(22)).collect();
+ if !self.pending.is_empty() {
+ self.step = TableStep::Page;
+ return CmdResult::ReportMeasurement(page.join("\n"));
+ }
+ CmdResult::Measurement(format!(
+ "{}\n\nUser External Dependent Unnamed\nBlocks References Blocks Blocks\n{:>5}{:>10}{:>12}{:>12}\n",
+ page.join("\n"),
+ self.blocks.len(),
+ self.others[0],
+ self.others[1],
+ self.others[2]
+ ))
+ }
+}
+
+/// Block listing order: `_` first, then `-`, space and other punctuation,
+/// digits, and letters without case.
+fn list_order(name: &str) -> Vec<(u32, u32)> {
+ name.chars()
+ .map(|c| match c {
+ '_' => (0, 0),
+ '-' => (1, 0),
+ ' ' => (2, 0),
+ '0'..='9' => (4, c as u32),
+ c if c.is_alphabetic() => (5, c.to_lowercase().next().unwrap_or(c) as u32),
+ c => (3, c as u32),
+ })
+ .collect()
+}
+
+impl CadCommand for CountTableCommand {
+ fn name(&self) -> &'static str {
+ "COUNTTABLE"
+ }
+
+ fn prompt(&self) -> String {
+ match self.step {
+ TableStep::Names => "Enter block name(s) to include or [?] :".into(),
+ TableStep::List => "Enter block(s) to list <*>:".into(),
+ TableStep::Point => "Specify insertion point:".into(),
+ TableStep::Page => "Press ENTER to continue:".into(),
+ }
+ }
+
+ fn input_kind(&self) -> InputKind {
+ match self.step {
+ TableStep::Point => InputKind::Point,
+ _ => InputKind::FreeText,
+ }
+ }
+
+ fn on_text_input(&mut self, text: &str) -> Option {
+ let text = text.trim();
+ Some(match self.step {
+ TableStep::Names if text == "?" => {
+ self.step = TableStep::List;
+ CmdResult::NeedPoint
+ }
+ TableStep::Names if text.is_empty() => {
+ self.names.clear();
+ self.step = TableStep::Point;
+ CmdResult::NeedPoint
+ }
+ TableStep::Names => {
+ let mut names = Vec::new();
+ for part in text.split(',').map(str::trim).filter(|p| !p.is_empty()) {
+ match self.blocks.iter().find(|b| b.eq_ignore_ascii_case(part)) {
+ Some(b) => names.push(b.clone()),
+ None => {
+ return Some(CmdResult::CancelWithMessage(
+ crate::tf!("Block \"{part}\" cannot be found.").into_owned(),
+ ))
+ }
+ }
+ }
+ self.names = names;
+ self.step = TableStep::Point;
+ CmdResult::NeedPoint
+ }
+ TableStep::List => self.listing(text),
+ TableStep::Page => self.next_page(),
+ TableStep::Point => return None,
+ })
+ }
+
+ fn on_point(&mut self, pt: DVec3) -> CmdResult {
+ if self.step != TableStep::Point {
+ return CmdResult::NeedPoint;
+ }
+ CmdResult::Dispatch(format!("_COUNTTABLEPLACE {:?},{:?},{:?} {}", pt.x, pt.y, pt.z, self.names.join("|")))
+ }
+
+ fn on_enter(&mut self) -> CmdResult {
+ match self.step {
+ TableStep::Point => CmdResult::ReportError(crate::t!("Invalid point.").into_owned()),
+ _ => self.on_text_input("").unwrap_or(CmdResult::NeedPoint),
+ }
+ }
+}
+
+/// UPDATEFIELD: `Select objects:`, then the fields of the selection are
+/// evaluated again.
+pub struct UpdateFieldCommand {
+ selected: Vec,
+}
+
+impl UpdateFieldCommand {
+ pub fn new() -> Self {
+ Self { selected: Vec::new() }
+ }
+}
+
+impl CadCommand for UpdateFieldCommand {
+ fn name(&self) -> &'static str {
+ "UPDATEFIELD"
+ }
+
+ fn prompt(&self) -> String {
+ "Select objects:".into()
+ }
+
+ fn is_selection_gathering(&self) -> bool {
+ true
+ }
+
+ fn on_selection_complete(&mut self, handles: Vec) -> CmdResult {
+ self.selected = handles;
+ CmdResult::NeedPoint
+ }
+
+ fn on_point(&mut self, _pt: DVec3) -> CmdResult {
+ CmdResult::NeedPoint
+ }
+
+ fn on_enter(&mut self) -> CmdResult {
+ if self.selected.is_empty() {
+ return CmdResult::Cancel;
+ }
+ let handles: Vec = self.selected.iter().map(|h| format!("{:X}", h.value())).collect();
+ CmdResult::Dispatch(format!("_UPDATEFIELDRUN {}", handles.join(",")))
+ }
+}
+
+inventory::submit!(crate::command::CommandRegistration {
+ names: &["COUNT", "COUNTAREA", "COUNTCLOSE", "COUNTFIELD", "COUNTLIST", "COUNTLISTCLOSE", "COUNTTABLE", "UPDATEFIELD"]
+});
diff --git a/src/modules/annotate/field_cmd.rs b/src/modules/annotate/field_cmd.rs
index 73cf7d02d..953b362d1 100644
--- a/src/modules/annotate/field_cmd.rs
+++ b/src/modules/annotate/field_cmd.rs
@@ -29,6 +29,7 @@ pub struct FieldPlaceCommand {
height: f64,
attachment: AttachmentPoint,
plane: WorkingPlane,
+ name: &'static str,
}
impl FieldPlaceCommand {
@@ -41,8 +42,16 @@ impl FieldPlaceCommand {
height,
attachment: AttachmentPoint::TopLeft,
plane: WorkingPlane::default(),
+ name: "FIELD",
}
}
+
+ /// The command name shown with the prompts (COUNTFIELD places its field
+ /// as MTEXT).
+ pub fn named(mut self, name: &'static str) -> Self {
+ self.name = name;
+ self
+ }
}
fn short(v: f64) -> String {
@@ -52,7 +61,7 @@ fn short(v: f64) -> String {
impl CadCommand for FieldPlaceCommand {
fn name(&self) -> &'static str {
- "FIELD"
+ self.name
}
fn set_working_plane(&mut self, plane: WorkingPlane) {
diff --git a/src/modules/annotate/mod.rs b/src/modules/annotate/mod.rs
index 5a882d148..8e9f8ba39 100644
--- a/src/modules/annotate/mod.rs
+++ b/src/modules/annotate/mod.rs
@@ -8,6 +8,7 @@ pub mod data_extract;
pub mod data_link;
pub mod ddedit;
pub mod field_cmd;
+pub mod count_cmd;
pub mod diameter_dim;
pub mod dim_baseline;
pub mod dim_continue;
diff --git a/src/modules/view/count_palette.rs b/src/modules/view/count_palette.rs
new file mode 100644
index 000000000..97321536f
--- /dev/null
+++ b/src/modules/view/count_palette.rs
@@ -0,0 +1,11 @@
+use crate::modules::{IconKind, ModuleEvent, ToolDef};
+pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/data_extract.svg"));
+/// View › Palettes › Count: opens or closes the Count palette.
+pub fn tool() -> ToolDef {
+ ToolDef {
+ id: "COUNTLIST",
+ label: "Count",
+ icon: ICON,
+ event: ModuleEvent::Command("_COUNTLISTTOGGLE".to_string()),
+ }
+}
diff --git a/src/modules/view/mod.rs b/src/modules/view/mod.rs
index d29f1826d..e11bbd383 100644
--- a/src/modules/view/mod.rs
+++ b/src/modules/view/mod.rs
@@ -12,6 +12,7 @@ pub mod plot_window;
pub mod quick_print;
mod properties_palette;
mod sheetset;
+mod count_palette;
mod tile_horiz;
mod tile_vert;
pub mod visual_style;
@@ -120,6 +121,7 @@ impl CadModule for ViewModule {
tools: vec![
RibbonItem::LargeTool(tool_palettes::tool()),
RibbonItem::LargeTool(properties_palette::tool()),
+ RibbonItem::LargeTool(count_palette::tool()),
RibbonItem::LargeTool(sheetset::tool()),
],
},
diff --git a/src/scene/entity.rs b/src/scene/entity.rs
index 9c22246bb..3a5cb2094 100644
--- a/src/scene/entity.rs
+++ b/src/scene/entity.rs
@@ -1451,6 +1451,10 @@ impl Scene {
}
}
}
+ m.color = self.count_color(handle, m.color, self.current_bg());
+ if let Some(b) = &mut backdrop {
+ b.color = self.count_color(handle, b.color, self.current_bg());
+ }
if tint_selected && self.selected.contains(&handle) {
m.color = [0.15, 0.55, 1.00, m.color[3]];
}
@@ -1752,6 +1756,7 @@ impl Scene {
)
});
}
+ model.color = self.count_color(context.root_handle, model.color, hatch_bg);
if tint_selected && self.selected.contains(&context.root_handle) {
model.color = [0.15, 0.55, 1.00, model.color[3]];
}
diff --git a/src/scene/mod.rs b/src/scene/mod.rs
index 67335f4bb..6134a7955 100644
--- a/src/scene/mod.rs
+++ b/src/scene/mod.rs
@@ -1973,6 +1973,15 @@ pub enum ViewportRefreshScope {
/// Entity membership in document order, keyed by geometry epoch.
type BlockMembers = (u64, HashMap>);
+/// What count mode colours: see [`Scene::count_display`].
+#[derive(Debug, Clone, PartialEq, Default)]
+pub struct CountDisplay {
+ pub counted: HashSet,
+ pub errors: HashSet,
+ pub color: [f32; 4],
+ pub error_color: [f32; 4],
+}
+
pub struct Scene {
pub camera: Rc>,
/// View saved immediately before the latest navigation operation. ZOOM
@@ -2061,6 +2070,10 @@ pub struct Scene {
/// edited geometry stands out while the surrounding drawing stays visible
/// for context. `None` = not editing. (#136)
pub refedit_keep: Option>,
+ /// COUNT mode colours: counted references in COUNTCOLOR, overlapping
+ /// duplicates in COUNTERRORCOLOR, everything else faded. `None` outside
+ /// count mode.
+ pub count_display: Option,
/// Entity drawn with the selection-highlight colour without being part
/// of the real selection — used to preview a row in the cycling list box.
pub hover_highlight: Option,
@@ -2658,6 +2671,7 @@ impl Scene {
preview_hidden: HashSet::default(),
command_preview_hidden: HashSet::default(),
refedit_keep: None,
+ count_display: None,
hover_highlight: None,
constraint_hover_highlights: HashSet::default(),
constraint_hover_refs: Vec::new(),
@@ -3497,6 +3511,23 @@ impl Scene {
self.associative_hatch_source_cache.borrow_mut().take();
}
let mut changes = changes.to_vec();
+ // A table whose content this app changed (its picture reference
+ // dropped by the edit, or a new table without one) has its drawing
+ // (the *T block) redrawn from the cells. Tables keeping their picture
+ // (moved, restyled, opened from a file) are never redrawn: the block
+ // may hold authored content the redraw does not reproduce.
+ if !appearance_only {
+ for (handle, kind) in &changes {
+ if !matches!(kind, ChangeKind::Removed)
+ && matches!(
+ self.document.get_entity(*handle),
+ Some(EntityType::Table(t)) if t.block_record_handle.is_none()
+ )
+ {
+ self.document.refresh_table_block(*handle);
+ }
+ }
+ }
// A restyle (layer on/off, colour, text or dimension style) moves no
// geometry, so nothing measured from it is re-resolved: an
// association that disagrees with its dimension would otherwise
@@ -4036,6 +4067,9 @@ impl Scene {
);
}
}
+ if self.meshes.contains_key(&h) {
+ material.diffuse = self.count_color(h, material.diffuse, bg);
+ }
(h, material)
})
})
@@ -4085,6 +4119,9 @@ impl Scene {
material.diffuse =
crate::scene::cache::block_cache::fade_toward_bg(material.diffuse, bg);
}
+ if self.meshes.contains_key(&handle) {
+ material.diffuse = self.count_color(handle, material.diffuse, bg);
+ }
(handle, material)
})
})
@@ -4134,10 +4171,77 @@ impl Scene {
self.bump_geometry_no_blocks();
}
+ /// Enter / leave the COUNT colouring; a no-op when nothing changed.
+ /// Entering or leaving fades or restores the whole drawing; a change
+ /// inside count mode recolours only the objects whose colour changed.
+ pub fn set_count_display(&mut self, display: Option) {
+ if self.count_display == display {
+ return;
+ }
+ let old = std::mem::replace(&mut self.count_display, display);
+ let (Some(old), Some(new)) = (old, self.count_display.as_ref()) else {
+ self.hatch_cache.borrow_mut().clear();
+ self.recolor_meshes();
+ self.bump_geometry_no_blocks();
+ return;
+ };
+ let recoloured = (old.color, old.error_color) != (new.color, new.error_color);
+ let class = |d: &CountDisplay, h: &Handle| (d.errors.contains(h), d.counted.contains(h));
+ let changed: Vec = old
+ .counted
+ .iter()
+ .chain(&old.errors)
+ .chain(&new.counted)
+ .chain(&new.errors)
+ .copied()
+ .collect::>()
+ .into_iter()
+ .filter(|h| recoloured || class(&old, h) != class(new, h))
+ .collect();
+ self.recolor_meshes_for_handles(&changed);
+ let changes: Vec<(Handle, ChangeKind)> = changed.into_iter().map(|h| (h, ChangeKind::Modified)).collect();
+ self.bump_entities_restyled(&changes);
+ }
+
+ /// The colour count mode gives an object drawn as part of `handle` (a
+ /// top-level entity): the count colours, else faded; unchanged outside
+ /// count mode.
+ pub(crate) fn count_color(&self, handle: Handle, color: [f32; 4], bg: [f32; 4]) -> [f32; 4] {
+ let Some(count) = &self.count_display else {
+ return color;
+ };
+ let [r, g, b, _] = if count.errors.contains(&handle) {
+ count.error_color
+ } else if count.counted.contains(&handle) {
+ count.color
+ } else {
+ return crate::scene::cache::block_cache::fade_toward_bg(color, bg);
+ };
+ [r, g, b, color[3]]
+ }
+
/// Fade the colours of wires that belong to entities outside the REFEDIT
/// keep set (no-op when not editing). The geometry is untouched, so
- /// hit-testing still works on faded entities.
+ /// hit-testing still works on faded entities. In count mode the counted
+ /// references and the duplicates take the count colours instead.
fn apply_refedit_fade(&self, wires: &mut [WireModel], bg: [f32; 4]) {
+ if let Some(count) = &self.count_display {
+ for w in wires.iter_mut() {
+ let (color, class) = match Self::handle_from_wire_name(&w.name) {
+ Some(h) if count.errors.contains(&h) => (count.error_color, 2u64),
+ Some(h) if count.counted.contains(&h) => (count.color, 1),
+ _ => (crate::scene::cache::block_cache::fade_toward_bg(w.color, bg), 0),
+ };
+ w.color = color;
+ // Instances of one definition draw in one colour: counted and
+ // duplicate references get instance groups of their own.
+ // ponytail: tags the two top bits of the source id, which the
+ // id counter never reaches.
+ if let Some(instance) = w.render_instance.as_mut() {
+ instance.source_id |= class << 62;
+ }
+ }
+ }
let Some(keep) = &self.refedit_keep else {
return;
};
@@ -8777,6 +8881,14 @@ impl Scene {
self.material_base_dir.as_deref(),
);
}
+ if self.count_display.is_some() {
+ let own = transformed.instance_color.or(set.display_color()).unwrap_or([1.0; 4]);
+ let color = self.count_color(context.root_handle, own, self.current_bg());
+ transformed.instance_color = Some(color);
+ if let Some(material) = transformed.material.as_mut() {
+ material.diffuse = color;
+ }
+ }
transformed.instance_handle = Some(context.root_handle);
out.push(transformed);
},
diff --git a/src/ui/dock.rs b/src/ui/dock.rs
index 1e62a58cf..9cab6440c 100644
--- a/src/ui/dock.rs
+++ b/src/ui/dock.rs
@@ -48,6 +48,8 @@ pub enum PanelId {
NodeGraph,
/// Regions and scans of the drawing's point clouds.
PointCloudManager,
+ /// Block counts (COUNTLIST).
+ Count,
}
impl PanelId {
@@ -60,6 +62,7 @@ impl PanelId {
PanelId::Browser => "Browser",
PanelId::NodeGraph => "Node Graph",
PanelId::PointCloudManager => "Point Cloud Manager",
+ PanelId::Count => "Count",
}
}
@@ -72,6 +75,7 @@ impl PanelId {
PanelId::Browser => 230.0,
PanelId::NodeGraph => 220.0,
PanelId::PointCloudManager => 280.0,
+ PanelId::Count => 280.0,
}
}
@@ -162,6 +166,7 @@ impl DockState {
PanelId::Browser,
PanelId::NodeGraph,
PanelId::PointCloudManager,
+ PanelId::Count,
] {
self.panels.entry(id).or_insert_with(|| DockPanel::for_id(id));
}
diff --git a/src/ui/icons.rs b/src/ui/icons.rs
index 7a2385fb4..a2b5af4e7 100644
--- a/src/ui/icons.rs
+++ b/src/ui/icons.rs
@@ -83,6 +83,8 @@ pub static GEAR: &[u8] = include_bytes!("../../assets/icons/ui/gear.svg");
pub static DOT: &[u8] = include_bytes!("../../assets/icons/ui/dot.svg");
pub static DIRTY_DOT: &[u8] = include_bytes!("../../assets/icons/ui/dirty_dot.svg");
pub static ARROW_LONG_RIGHT: &[u8] = include_bytes!("../../assets/icons/ui/arrow_long_right.svg");
+pub static MORE: &[u8] = include_bytes!("../../assets/icons/ui/more.svg");
+pub static INFO: &[u8] = include_bytes!("../../assets/icons/ui/info.svg");
// ── Status-bar toggle icons (issue #216) ──────────────────────────────────
pub static ST_ORTHO: &[u8] = include_bytes!("../../assets/icons/status/ortho.svg");
diff --git a/src/ui/popup/context_menu.rs b/src/ui/popup/context_menu.rs
index 4c68f01ae..595184c8d 100644
--- a/src/ui/popup/context_menu.rs
+++ b/src/ui/popup/context_menu.rs
@@ -722,6 +722,12 @@ fn idle_rows(
t!("Quick Select...").into_owned(),
MenuAction::QuickSelect,
)));
+ // Count the selected objects, or start a count.
+ rows.push(MenuRow::Item(if has_selection {
+ cmd(t!("Count Selection").into_owned(), "COUNT _C")
+ } else {
+ cmd(t!("Count").into_owned(), "COUNT")
+ }));
rows.push(MenuRow::Separator);
// ── Panels ─────────────────────────────────────────────────────────
diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs
index 0cc1e04ee..c98ae83a5 100644
--- a/src/ui/ribbon/mod.rs
+++ b/src/ui/ribbon/mod.rs
@@ -58,6 +58,8 @@ pub struct Ribbon {
pub show_ucs_icon: bool,
/// Properties panel (PROPERTIES) visibility — drives the Properties button highlight.
pub show_properties: bool,
+ /// The Count palette is open (View › Palettes › Count).
+ pub show_count_palette: bool,
/// File tabs (FILETAB) visibility — drives the File Tabs button highlight.
pub show_file_tabs: bool,
/// Layout tabs (LAYOUTTAB) visibility — drives the Layout Tabs button highlight.
@@ -174,6 +176,7 @@ impl Ribbon {
show_viewcube: true,
show_ucs_icon: true,
show_properties: true,
+ show_count_palette: false,
show_file_tabs: true,
show_layout_tabs: true,
open_dropdown: None,
@@ -335,6 +338,9 @@ impl Ribbon {
pub fn set_properties(&mut self, on: bool) {
self.show_properties = on;
}
+ pub fn set_count_palette(&mut self, on: bool) {
+ self.show_count_palette = on;
+ }
pub fn set_file_tabs(&mut self, on: bool) {
self.show_file_tabs = on;
}
@@ -352,6 +358,7 @@ impl Ribbon {
show_viewcube: self.show_viewcube,
show_ucs_icon: self.show_ucs_icon,
show_properties: self.show_properties,
+ show_count_palette: self.show_count_palette,
show_block_palette,
show_file_tabs: self.show_file_tabs,
show_layout_tabs: self.show_layout_tabs,
diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs
index 74e7d9393..89bb1de2f 100644
--- a/src/ui/ribbon/widgets.rs
+++ b/src/ui/ribbon/widgets.rs
@@ -37,6 +37,7 @@ pub(super) struct ToggleState {
pub show_viewcube: bool,
pub show_ucs_icon: bool,
pub show_properties: bool,
+ pub show_count_palette: bool,
pub show_block_palette: bool,
pub show_file_tabs: bool,
pub show_layout_tabs: bool,
@@ -379,6 +380,7 @@ pub(super) fn is_active_tool(
"NAVVCUBE" => state.show_viewcube,
"UCSICON" => state.show_ucs_icon,
"PROPERTIES" => state.show_properties,
+ "COUNTLIST" => state.show_count_palette,
"BLOCKPALETTE" => state.show_block_palette,
"FILETAB" => state.show_file_tabs,
"LAYOUTTAB" => state.show_layout_tabs,
diff --git a/src/ui/window/count_palette.rs b/src/ui/window/count_palette.rs
new file mode 100644
index 000000000..8f8e08d70
--- /dev/null
+++ b/src/ui/window/count_palette.rs
@@ -0,0 +1,675 @@
+//! The Count palette (COUNTLIST) and count mode (COUNT): the drawing's
+//! blocks with how many references of each are counted, and — once a block
+//! is chosen — its counted references coloured in the drawing with a
+//! floating toolbar above it. Counting itself is `codec::count`.
+
+use crate::app::Message;
+use crate::ui::dock::PanelId;
+use crate::ui::style::common::muted_style;
+use crate::ui::style::form::{button_style, field_style};
+use codec::count::{BlockInstance, CountKey};
+use codec::{CadDocument, Handle};
+use iced::widget::{button, checkbox, column, container, mouse_area, row, scrollable, text, text_input, tooltip, Space};
+use iced::{Background, Border, Color, Element, Fill, Length, Theme};
+
+const BLOCK_ICON: &[u8] = include_bytes!("../../../assets/icons/blocks/block.svg");
+const AREA_ICON: &[u8] = include_bytes!("../../../assets/icons/shapes/rect.svg");
+const SELECT_ICON: &[u8] = include_bytes!("../../../assets/icons/blocks/select_objects.svg");
+const TABLE_ICON: &[u8] = include_bytes!("../../../assets/icons/table.svg");
+const WARNING_ICON: &[u8] = include_bytes!("../../../assets/icons/ui/warning_triangle.svg");
+const CHECK_ICON: &[u8] = include_bytes!("../../../assets/icons/ui/check.svg");
+
+/// The name of the layer count areas are drawn on.
+pub const AREA_LAYER: &str = "0-CountArea";
+
+/// What a count counts.
+#[derive(Debug, Clone, PartialEq)]
+pub enum CountTarget {
+ /// The references of a block. `reference` is the instance the match
+ /// options compare with; `picked` when it came from a COUNT selection.
+ Block {
+ name: String,
+ reference: Option,
+ matching: [bool; 3],
+ picked: bool,
+ },
+ /// Picked objects (several, or one that is not a reference): the copies
+ /// of the group are counted.
+ Group(Vec),
+}
+
+/// Count mode of a drawing.
+#[derive(Debug, Clone, Default)]
+pub struct CountMode {
+ /// The count area (a closed WCS polygon); `None` = all of model space.
+ pub area: Option>,
+ /// The closed polyline marking the area, and whether count mode drew it
+ /// (then COUNTCLOSE erases it unless a field refers to it).
+ pub boundary: Option<(Handle, bool)>,
+ pub target: Option,
+ /// The counted reference ← / → last showed.
+ pub cursor: Option,
+ /// The scene geometry epoch the colouring was computed at.
+ pub epoch: u64,
+ /// The result and count at `epoch` (recomputed when the drawing changes).
+ pub cache: Option<(CountResult, usize)>,
+}
+
+/// A count mode's result: the counted references and the overlapping
+/// duplicates (errors).
+#[derive(Debug, Clone, Default, PartialEq)]
+pub struct CountResult {
+ pub counted: Vec,
+ pub errors: Vec,
+ /// The counted references a duplicate overlaps (drawn in the error colour too).
+ pub overlapped: Vec,
+}
+
+impl CountMode {
+ pub fn instances(&self, doc: &CadDocument) -> Vec {
+ codec::count::block_instances(doc, self.area.as_deref())
+ }
+
+ /// The key the match options build from the reference instance.
+ pub fn key(&self, instances: &[BlockInstance]) -> CountKey {
+ match &self.target {
+ Some(CountTarget::Block { reference: Some(r), matching, .. }) => instances
+ .iter()
+ .find(|i| i.handle == *r)
+ .map(|i| CountKey::of(i, matching[0], matching[1], matching[2]))
+ .unwrap_or_default(),
+ _ => CountKey::default(),
+ }
+ }
+
+ /// The cached result, or one worked out from the drawing when there is none.
+ pub fn result(&self, doc: &CadDocument) -> std::borrow::Cow<'_, CountResult> {
+ match &self.cache {
+ Some((result, _)) => std::borrow::Cow::Borrowed(result),
+ None => std::borrow::Cow::Owned(self.compute(doc).0),
+ }
+ }
+
+ /// What the count shows: the counted references, or the copies of a group.
+ pub fn count(&self, doc: &CadDocument) -> usize {
+ match &self.cache {
+ Some((_, n)) => *n,
+ None => self.compute(doc).1,
+ }
+ }
+
+ /// The result and the count, worked out from the drawing.
+ pub fn compute(&self, doc: &CadDocument) -> (CountResult, usize) {
+ if let Some(CountTarget::Group(handles)) = &self.target {
+ let groups = codec::count::group_matches(doc, handles, self.area.as_deref());
+ let result = CountResult { counted: groups.concat(), errors: Vec::new(), overlapped: Vec::new() };
+ return (result, groups.len());
+ }
+ let result = self.block_result(doc);
+ let n = result.counted.len();
+ (result, n)
+ }
+
+ fn block_result(&self, doc: &CadDocument) -> CountResult {
+ match &self.target {
+ None | Some(CountTarget::Group(_)) => CountResult::default(),
+ Some(CountTarget::Block { name, .. }) => {
+ let instances = self.instances(doc);
+ let key = self.key(&instances);
+ let mine = |i: &&BlockInstance| i.name.eq_ignore_ascii_case(name) && key.matches(i);
+ CountResult {
+ counted: instances.iter().filter(mine).filter(|i| i.duplicate_of.is_none()).map(|i| i.handle).collect(),
+ errors: instances.iter().filter(mine).filter(|i| i.duplicate_of.is_some()).map(|i| i.handle).collect(),
+ overlapped: instances.iter().filter(mine).filter_map(|i| i.duplicate_of).collect(),
+ }
+ }
+ }
+ }
+
+ /// The name the toolbar and the command line show for the target.
+ pub fn target_name(&self) -> Option {
+ match &self.target {
+ Some(CountTarget::Block { name, .. }) => Some(name.clone()),
+ Some(CountTarget::Group(_)) => Some("group".into()),
+ None => None,
+ }
+ }
+}
+
+/// Palette state that is not in the drawing, and the profile variables
+/// COUNTCOLOR, COUNTERRORCOLOR and COUNTSERVICE.
+#[derive(Debug)]
+pub struct CountPalette {
+ pub show: bool,
+ pub search: String,
+ /// Sorted by the Count column (else by Name), and descending.
+ pub by_count: bool,
+ pub descending: bool,
+ /// Expanded rows: block name (upper case) → expand by layer, scale, mirror state.
+ pub expanded: Vec<(String, [bool; 3])>,
+ /// Create Table selection mode: the checked block names (upper case).
+ pub table: Option>,
+ pub details_closed: bool,
+ pub errors_closed: bool,
+ pub color: i16,
+ pub error_color: i16,
+ pub service: bool,
+ /// The Invalid Area dialog's "always" box while it is open.
+ pub invalid_always: bool,
+ /// What a lost count boundary does without asking: 0 ask, 1 undo, 2 go
+ /// on with all of model space (kept with the user settings).
+ pub invalid_choice: u8,
+ /// The list's references for (document tab id, geometry epoch, area).
+ pub list_cache: std::cell::RefCell