Merge pull request #1614 from ramox81/codex/count-feature

Count: COUNT / COUNTAREA count mode, Count palette, count fields and tables
This commit is contained in:
Hakan Seven committed 2026-10-06 09:16:57 +03:00
commit 6da9675c55
51 files changed
+2828 -43

No files matched your search

Generated
+1 -1
View File
@@ -4206,7 +4206,7 @@ dependencies = [
[[package]]
name = "opencadcodec"
version = "0.5.5"
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=fe69506#fe69506cb99dea6f4c4a73b690a27fdf04403ea0"
source = "git+https://github.com/ramox81/cadcodec.git?rev=196a5ae#196a5aeabc1f22a526259d6788efcf0df9ad24e9"
dependencies = [
"ahash 0.8.12",
"anyhow",
+3
View File
@@ -90,6 +90,9 @@ objc2-app-kit = { version = "0.2", features = ["NSApplication", "NSResponder", "
block2 = "0.5"
objc2-foundation = { version = "0.2", features = ["NSArray", "NSURL", "NSString", "NSObject", "NSNotification", "NSThread", "NSEnumerator"] }
[patch."https://github.com/HakanSeven12/opencadcodec.git"]
opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "196a5ae" }
[patch.crates-io]
iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
+25
View File
@@ -5939,3 +5939,28 @@ style-manager =
.center-of-text-overline = مركز النص مع خط علوي
.block-extents = نطاق الكتلة
.base-point = نقطة الأساس
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5927,3 +5927,28 @@ style-manager =
.center-of-text-overline = Център на надчертан текст
.block-extents = Обхват на блок
.base-point = Базова точка
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5924,3 +5924,28 @@ 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 =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5925,3 +5925,28 @@ style-manager =
.center-of-text-overline = Textzentrum mit Oberstrich
.block-extents = Blockgrenzen
.base-point = Basispunkt
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5927,3 +5927,28 @@ style-manager =
.center-of-text-overline = Κέντρο του κειμένου με επιγράμμιση
.block-extents = Όρια μπλοκ
.base-point = Σημείο βάσης
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5928,3 +5928,28 @@ style-manager =
.center-of-text-overline = Center of text with overline
.block-extents = Block extents
.base-point = Base point
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5927,3 +5927,28 @@ style-manager =
.center-of-text-overline = Centro del texto con suprarrayado
.block-extents = Extensión del bloque
.base-point = Punto base
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5924,3 +5924,28 @@ style-manager =
.center-of-text-overline = Tekstin keskikohta yliviivauksella
.block-extents = Estä laajuudet
.base-point = Peruspiste
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5926,3 +5926,28 @@ style-manager =
.center-of-text-overline = Centre du texte avec surlignement
.block-extents = Limites du bloc
.base-point = Point de base
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5915,3 +5915,28 @@ style-manager =
.center-of-text-overline = ऊपरी रेखा सहित टेक्स्ट का केंद्र
.block-extents = ब्लॉक परिमिति
.base-point = आधार बिंदु
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5926,3 +5926,28 @@ style-manager =
.center-of-text-overline = Szöveg közepe áthúzással
.block-extents = Kiterjedések blokkolása
.base-point = Alappont
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5926,3 +5926,28 @@ style-manager =
.center-of-text-overline = Centro del testo con sopralineato
.block-extents = Blocca estensioni
.base-point = Punto base
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5915,3 +5915,28 @@ style-manager =
.center-of-text-overline = 上線付きテキストの中心
.block-extents = ブロック範囲
.base-point = 基点
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5926,3 +5926,28 @@ style-manager =
.center-of-text-overline = 윗줄이 있는 텍스트 중앙
.block-extents = 블록 범위
.base-point = 기준점
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5925,3 +5925,28 @@ style-manager =
.center-of-text-overline = Tekstmiddelpunt met bovenstreep
.block-extents = Symboolgrenzen
.base-point = Basispunt
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5926,3 +5926,28 @@ style-manager =
.center-of-text-overline = Środek tekstu z podkreśleniem
.block-extents = Zasięg bloku
.base-point = Punkt bazowy
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5927,3 +5927,28 @@ style-manager =
.center-of-text-overline = Centro do texto com sobrelinha
.block-extents = Extensões do bloco
.base-point = Ponto base
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5928,3 +5928,28 @@ style-manager =
.center-of-text-overline = Центр текста с надчёркиванием
.block-extents = Габариты блока
.base-point = Базовая точка
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5850,3 +5850,28 @@ style-manager =
.center-of-text-overline = Üst çizgili metin merkezi
.block-extents = Blok sınırları
.base-point = Taban noktası
count-feature =
.palette = Sayım
.menu = Say
.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
+25
View File
@@ -5910,3 +5910,28 @@ style-manager =
.center-of-text-overline = 带上划线的文字中心
.block-extents = 块范围
.base-point = 基点
count-feature =
.palette = Count
.menu = Count
.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
+25
View File
@@ -5922,3 +5922,28 @@ style-manager =
.center-of-text-overline = 帶上划線的文字中心
.block-extents = 圖塊範圍
.base-point = 基點
count-feature =
.palette = Count
.menu = Count
.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
+825
View File
@@ -0,0 +1,825 @@
//! 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<Handle> {
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<Task<Message>> {
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.
self.command_line.push_output(&format!("{} found", preselected.len()));
let handles: Vec<String> = 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), "COUNT"))
}
"COUNTAREA" => Some(self.start_count_command(i, Box::new(CountCommand::new(true)), "COUNTAREA")),
"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("** 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)), "COUNTTABLE"))
}
"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()), "UPDATEFIELD"))
}
"_UPDATEFIELDRUN" => {
let handles: Vec<Handle> = 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("count area is active.");
}
Some(Task::none())
}
"_COUNTTABLEPLACE" => {
let (point, names) = rest.split_once(' ').unwrap_or((rest, ""));
let p: Vec<f64> = point.split(',').filter_map(|v| v.parse().ok()).collect();
if p.len() == 3 {
let names: Vec<String> = 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<dyn CadCommand>, _name: &str) -> Task<Message> {
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(&format!("{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(&format!("Enter new value for {name} <{current}>:"));
app.pending_setvar = Some(name.to_string());
};
let Some(value) = value else {
ask(self);
return;
};
match value.parse::<i32>() {
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 {
"Requires 0 or 1 only.".to_string()
} else {
format!("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.into_iter().collect(),
errors: r.errors.into_iter().chain(r.overlapped).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 undone.
fn count_invalid_undo(&mut self, i: usize) {
self.undo_active_tab();
self.apply_count_display(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) {
self.push_undo_snapshot(i, "COUNT");
// The area layer is locked; count mode removes its own polyline.
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<Vec<[f64; 2]>>, Option<(Handle, bool)>)> {
let kind = tokens.first().copied().unwrap_or("E");
let points = || tokens[1..].iter().map_while(|t| parse_xy(t)).collect::<Vec<_>>();
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<Handle> {
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 <area> L` opens the palette; `_COUNTRUN <area> T <handles>`
/// 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<Handle> = 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(&format!("{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<String> {
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<Message> {
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(&format!(
"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), "COUNTFIELD")
}
/// UPDATEFIELD: every field the objects host is evaluated again and its
/// text written back.
fn update_fields(&mut self, i: usize, handles: &[Handle]) {
let doc = &self.tabs[i].scene.document;
let mut updates: Vec<(Handle, String)> = Vec::new();
let mut found = 0;
for h in handles {
let Some(entity) = doc.get_entity(*h) else { continue };
if matches!(entity, EntityType::Table(_)) {
// Table cells evaluate their fields when drawn.
if let EntityType::Table(t) = entity {
found += t.field_handles.len();
}
continue;
}
if !crate::entities::field::hosts_field(doc, entity) {
continue;
}
found += 1;
if let Some(text) = crate::entities::field::resolve(doc, *h) {
updates.push((*h, text));
}
}
self.command_line.push_output(&format!("{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 table_handles: Vec<(Handle, crate::scene::ChangeKind)> = handles
.iter()
.filter(|h| matches!(self.tabs[i].scene.document.get_entity(**h), Some(EntityType::Table(_))))
.map(|h| (*h, crate::scene::ChangeKind::Modified))
.collect();
changed.extend(table_handles);
self.tabs[i].scene.bump_entities(&changed);
self.tabs[i].dirty = true;
self.command_line.push_output(&format!("{found} 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<String>) {
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<String> = 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<String> =
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::Vec3::splat(f32::MAX), glam::Vec3::splat(f32::MIN));
for c in outline.iter().flat_map(codec::count::Piece::points) {
let p = glam::Vec3::new(c.x as f32, c.y as f32, c.z as f32);
lo = lo.min(p);
hi = hi.max(p);
}
let pad = (hi - lo) * 1.5;
self.tabs[i].scene.remember_current_view();
self.tabs[i].scene.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<String> = 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<Message> {
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<String> = 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)), "COUNTTABLE");
}
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 counted = mode.result(&self.tabs[i].scene.document).counted;
if counted.is_empty() {
return Task::none();
}
let n = counted.len();
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,
};
if let Some(mode) = self.tabs[i].count.as_mut() {
mode.cursor = Some(next);
}
self.zoom_to_insert(i, counted[next]);
}
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), "COUNT");
}
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()
}
}
+9
View File
@@ -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;
-39
View File
@@ -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<String, usize> = 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();
}
+4
View File
@@ -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);
}
+75
View File
@@ -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",
@@ -927,6 +928,78 @@ impl OpenCADStudio {
}
// Point Cloud Manager tree: "search=<text>", "collapse", "expand",
// "toggle_node=<key>" or "select=<key>".
// Count palette and count mode: "search=<text>", "sort=name|count",
// "open=NAME", "child=NAME:<n>" (an expanded row), "menu=<review|field|
// layer|scale|mirror>:NAME", "create_table", "check=NAME:1",
// "all=1", "table_cancel", "table_insert", "back", "match=<0|1|2>:1",
// "details", "errors", "error=<n>", "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::<usize>().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()),
})
}
"pc_manager" => {
use crate::ui::window::pc_manager::PcManagerMsg as M;
let value = string(req, "value")?;
@@ -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"]
+3
View File
@@ -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<RefEditSession>,
/// COUNT mode, while it is on.
pub(crate) count: Option<crate::ui::window::count_palette::CountMode>,
/// 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<BlockEditSession>,
@@ -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(),
+4
View File
@@ -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<BgTarget>,
@@ -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<Vec<u8>>), String>),
/// Result of the PDFATTACH file picker.
PdfAttachPickResult(Result<(std::path::PathBuf, std::sync::Arc<Vec<u8>>), 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(),
+24
View File
@@ -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,
}
}
}
+5
View File
@@ -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,
}
}
+8
View File
@@ -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();
+7
View File
@@ -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();
+16
View File
@@ -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<Element<'_, Message>> {
@@ -3022,6 +3030,14 @@ 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.scene.geometry_epoch,
width,
auto_collapse,
),
crate::ui::dock::PanelId::PointCloudManager => crate::ui::window::pc_manager::view(
&self.pc_manager,
&tab.scene.document,
+4
View File
@@ -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))
+24
View File
@@ -5530,6 +5530,30 @@ 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 palette" => Some(("count-feature", "palette")),
"Count menu" => Some(("count-feature", "menu")),
"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")),
_ => None,
}
}
+447
View File
@@ -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] <Current area>:
// Specify opposite corner: (Polygonal: Specify start point: /
// Specify next point; Object:
// Select object as count area boundary:)
// Select target objects or [List all blocks] <List all blocks>: (COUNT)
//
// The command gathers its answers and hands them to the application as
// `_COUNTRUN <area> L|T <handles>` (COUNTAREA: `_COUNTAREASET <area>`), the
// area being `C` (current view), `E` (entire model space), `R x,y x,y`,
// `P x,y x,y …` or `O <handle>`.
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<DVec3>,
targets: Vec<Handle>,
picked: Option<EntityType>,
}
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("Invalid option keyword.".into()),
}
}
fn finish_targets(&self) -> CmdResult {
if self.targets.is_empty() {
return CmdResult::Dispatch(format!("_COUNTRUN {} L", self.area));
}
let handles: Vec<String> = 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] <Current area>:".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] <List all blocks>:".into(),
}
}
fn options(&self) -> Vec<CmdOption> {
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<Handle>) -> 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;
CmdResult::ReportMeasurement(if first {
format!("{found} found")
} else {
format!("{found} found, {} total", self.targets.len())
})
}
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(
"Invalid count area boundary object. Select a closed polyline consisting of line segments and does not intersect itself."
.into(),
),
}
}
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<CmdResult> {
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<String> = 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<WireModel> {
let ring: Vec<DVec3> = 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<String>,
others: [usize; 3],
names: Vec<String>,
/// Listing lines still to show after `Press ENTER to continue:`.
pending: Vec<String>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum TableStep {
Names,
List,
Point,
Page,
}
impl CountTableCommand {
pub fn new(blocks: Vec<String>, 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<String>) -> 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<String> = self.pending.drain(..self.pending.len().min(22)).collect();
if !self.pending.is_empty() {
self.step = TableStep::Page;
return CmdResult::ReportMeasurement(page.join("
"));
}
CmdResult::Measurement(format!(
"{}
User External Dependent Unnamed
Blocks References Blocks Blocks
{:>5}{:>10}{:>12}{:>12}
",
page.join("
"),
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 [?] <all blocks>:".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<CmdResult> {
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(format!("Block \"{part}\" cannot be found."))),
}
}
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("Invalid point.".into()),
_ => 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<Handle>,
}
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<Handle>) -> 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<String> = 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"]
});
+10 -1
View File
@@ -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) {
+1
View File
@@ -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;
+11
View File
@@ -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 palette",
icon: ICON,
event: ModuleEvent::Command("_COUNTLISTTOGGLE".to_string()),
}
}
+2
View File
@@ -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()),
],
},
+2
View File
@@ -1451,6 +1451,7 @@ impl Scene {
}
}
}
m.color = self.count_color(handle, m.color, self.current_bg());
if tint_selected && self.selected.contains(&handle) {
m.color = [0.15, 0.55, 1.00, m.color[3]];
}
@@ -1752,6 +1753,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]];
}
+75 -1
View File
@@ -1973,6 +1973,15 @@ pub enum ViewportRefreshScope {
/// Entity membership in document order, keyed by geometry epoch.
type BlockMembers = (u64, HashMap<Handle, Vec<Handle>>);
/// What count mode colours: see [`Scene::count_display`].
#[derive(Debug, Clone, PartialEq, Default)]
pub struct CountDisplay {
pub counted: HashSet<Handle>,
pub errors: HashSet<Handle>,
pub color: [f32; 4],
pub error_color: [f32; 4],
}
pub struct Scene {
pub camera: Rc<RefCell<Camera>>,
/// 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<HashSet<Handle>>,
/// COUNT mode colours: counted references in COUNTCOLOR, overlapping
/// duplicates in COUNTERRORCOLOR, everything else faded. `None` outside
/// count mode.
pub count_display: Option<CountDisplay>,
/// 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<Handle>,
@@ -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,15 @@ impl Scene {
self.associative_hatch_source_cache.borrow_mut().take();
}
let mut changes = changes.to_vec();
// A table's drawing (its *T block) follows the table's cells, sizes
// and styles; it is left alone while it already matches.
for (handle, kind) in &changes {
if !matches!(kind, ChangeKind::Removed)
&& matches!(self.document.get_entity(*handle), Some(EntityType::Table(_)))
{
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 +4059,9 @@ impl Scene {
);
}
}
if self.meshes.contains_key(&h) {
material.diffuse = self.count_color(h, material.diffuse, bg);
}
(h, material)
})
})
@@ -4134,10 +4160,50 @@ impl Scene {
self.bump_geometry_no_blocks();
}
/// Enter / leave the COUNT colouring; a no-op when nothing changed.
pub fn set_count_display(&mut self, display: Option<CountDisplay>) {
if self.count_display == display {
return;
}
self.count_display = display;
self.hatch_cache.borrow_mut().clear();
self.recolor_meshes();
self.bump_geometry_no_blocks();
}
/// 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() {
w.color = match Self::handle_from_wire_name(&w.name) {
Some(h) if count.errors.contains(&h) => count.error_color,
Some(h) if count.counted.contains(&h) => count.color,
_ => crate::scene::cache::block_cache::fade_toward_bg(w.color, bg),
};
// Shared instance geometry keeps its own colour; draw these wires on their own.
w.render_instance = None;
}
}
let Some(keep) = &self.refedit_keep else {
return;
};
@@ -8777,6 +8843,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);
},
+5
View File
@@ -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));
}
+6
View File
@@ -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 menu").into_owned(), "COUNT")
}));
rows.push(MenuRow::Separator);
// ── Panels ─────────────────────────────────────────────────────────
+7
View File
@@ -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,
+2
View File
@@ -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,
+678
View File
@@ -0,0 +1,678 @@
//! 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<Handle>,
matching: [bool; 3],
picked: bool,
},
/// Picked objects (several, or one that is not a reference): the copies
/// of the group are counted.
Group(Vec<Handle>),
}
/// 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<Vec<[f64; 2]>>,
/// 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<CountTarget>,
/// The counted reference ← / → last showed.
pub cursor: Option<usize>,
/// 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<Handle>,
pub errors: Vec<Handle>,
/// The counted references a duplicate overlaps (drawn in the error colour too).
pub overlapped: Vec<Handle>,
}
impl CountMode {
pub fn instances(&self, doc: &CadDocument) -> Vec<BlockInstance> {
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(),
}
}
pub fn result(&self, doc: &CadDocument) -> CountResult {
match &self.cache {
Some((result, _)) => result.clone(),
None => 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<String> {
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<Vec<String>>,
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, geometry epoch, area).
pub list_cache: std::cell::RefCell<Option<(usize, u64, Option<Vec<[f64; 2]>>, std::rc::Rc<Vec<BlockInstance>>)>>,
}
impl Default for CountPalette {
fn default() -> Self {
Self {
show: false,
search: String::new(),
by_count: false,
descending: false,
expanded: Vec::new(),
table: None,
details_closed: false,
errors_closed: false,
color: 3,
error_color: 1,
service: true,
invalid_always: false,
invalid_choice: 0,
list_cache: Default::default(),
}
}
}
impl CountPalette {
pub fn expansion(&self, name: &str) -> Option<[bool; 3]> {
let key = name.to_ascii_uppercase();
self.expanded.iter().find(|(n, _)| *n == key).map(|(_, e)| *e)
}
}
/// What a row's context menu does.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RowAction {
Review,
Field,
/// Expand by layer (0), scale (1) or mirror state (2).
Expand(usize),
}
#[derive(Debug, Clone)]
pub enum CountMsg {
Search(String),
/// Sort by the Count column (`true`) or the Name column.
Sort(bool),
/// A row clicked: count mode on its block (and key, for an expanded row).
Open(String, CountKey),
Menu(String, CountKey, RowAction),
CreateTable,
TableCheck(String, bool),
TableAll(bool),
TableCancel,
TableInsert,
Back,
Match(usize, bool),
ToggleDetails,
ToggleErrors,
/// Show an overlapping duplicate.
ShowError(Handle),
Area,
Prev,
Next,
Select,
Field,
Close,
/// The Invalid Area dialog: undo the boundary change (also its ✕ and
/// Cancel), or go on counting all of model space; `InvalidAlways` is its
/// "always perform my current choice" box.
InvalidUndo,
InvalidContinue,
InvalidAlways(bool),
}
fn msg(m: CountMsg) -> Message {
Message::Count(m)
}
/// One block's row: counted references and duplicates.
pub struct Row {
pub name: String,
pub count: usize,
pub errors: usize,
}
/// The rows of the list: blocks whose name contains the search text
/// (case-insensitively), sorted by the chosen column.
pub fn rows(instances: &[BlockInstance], palette: &CountPalette) -> Vec<Row> {
let query = palette.search.trim().to_lowercase();
let mut rows: Vec<Row> = codec::count::block_counts(instances)
.into_iter()
.filter(|c| query.is_empty() || c.name.to_lowercase().contains(&query))
.map(|c| Row { count: c.counted.len(), errors: c.duplicates.len(), name: c.name })
.collect();
if palette.by_count {
rows.sort_by(|a, b| a.count.cmp(&b.count).then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase())));
}
if palette.descending {
rows.reverse();
}
rows
}
fn small_icon<'a>(bytes: &'static [u8], size: f32) -> Element<'a, Message> {
crate::ui::icons::semantic(bytes, size)
}
fn row_menu(name: String, key: CountKey, expandable: bool) -> Element<'static, Message> {
let entry = |label: String, action: RowAction| -> Element<'static, Message> {
button(text(label).size(12))
.width(Fill)
.padding([5, 12])
.on_press(msg(CountMsg::Menu(name.clone(), key.clone(), action)))
.style(|theme: &Theme, status| {
let mut s = button::text(theme, status);
if matches!(status, button::Status::Hovered) {
s.background = Some(Background::Color(theme.palette().primary.base.color));
s.text_color = theme.palette().primary.base.text;
}
s
})
.into()
};
let mut items = column![
entry(crate::t!("Review Count Details").into_owned(), RowAction::Review),
entry(crate::t!("Insert Count Field").into_owned(), RowAction::Field),
]
.spacing(1);
if expandable {
items = items
.push(container(Space::new()).height(1).width(Fill).style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
}))
.push(container(text(crate::t!("EXPAND BY")).size(10).style(muted_style)).padding([3, 12]))
.push(entry(crate::t!("Layer").into_owned(), RowAction::Expand(0)))
.push(entry(crate::t!("Scale").into_owned(), RowAction::Expand(1)))
.push(entry(crate::t!("Mirror State").into_owned(), RowAction::Expand(2)));
}
container(items)
.padding(4)
.width(Length::Fixed(200.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() },
..Default::default()
})
.into()
}
fn list_row<'a>(
palette: &CountPalette,
name: &str,
key: CountKey,
label: String,
count: String,
errors: bool,
depth: u16,
expanded: Option<bool>,
) -> Element<'a, Message> {
let lead: Element<'a, Message> = if let Some(checked) = palette.table.as_ref().filter(|_| depth == 0) {
let on = checked.contains(&name.to_ascii_uppercase());
let n = name.to_string();
checkbox(on).size(14).on_toggle(move |v| msg(CountMsg::TableCheck(n.clone(), v))).into()
} else if expanded == Some(true) {
container(text("−").size(13)).width(Length::Fixed(14.0)).into()
} else {
Space::new().width(Length::Fixed(14.0)).into()
};
let mut cells = row![Space::new().width(Length::Fixed(f32::from(depth) * 18.0)), lead];
if depth == 0 {
cells = cells.push(small_icon(BLOCK_ICON, 14.0));
}
cells = cells.push(text(label).size(12).width(Fill));
if errors {
cells = cells.push(crate::ui::icons::themed_warning(WARNING_ICON, 12.0));
}
cells = cells.push(text(count).size(12));
let body = container(cells.spacing(6).align_y(iced::Center)).width(Fill).padding([4, 6]);
let area = mouse_area(body)
.on_press(msg(CountMsg::Open(name.to_string(), key.clone())))
.interaction(iced::mouse::Interaction::Pointer);
let (n, expandable) = (name.to_string(), depth == 0);
iced_aw::ContextMenu::new(area, move || row_menu(n.clone(), key.clone(), expandable)).into()
}
fn header_button<'a>(label: String, active: bool, descending: bool, m: CountMsg) -> Element<'a, Message> {
let arrow = if !active {
""
} else if descending {
" ▼"
} else {
" ▲"
};
button(text(format!("{label}{arrow}")).size(11))
.on_press(msg(m))
.style(button::text)
.padding([2, 4])
.into()
}
/// The list: search, area button, Name / Count columns, rows, Create Table.
fn list_view<'a>(palette: &'a CountPalette, instances: &[BlockInstance]) -> Element<'a, Message> {
let search = text_input(&crate::t!("Search"), &palette.search)
.on_input(|v| msg(CountMsg::Search(v)))
.padding([4, 8])
.size(12)
.style(field_style);
let top = row![
search.width(Fill),
crate::ui::dock::tool_button(
small_icon(AREA_ICON, crate::ui::dock::TOOL_H),
crate::t!("Count in a specified area").into_owned(),
msg(CountMsg::Area),
),
]
.spacing(4)
.align_y(iced::Center);
let header = row![
header_button(crate::t!("Name").into_owned(), !palette.by_count, palette.descending, CountMsg::Sort(false)),
Space::new().width(Fill),
header_button(crate::t!("Count").into_owned(), palette.by_count, palette.descending, CountMsg::Sort(true)),
]
.align_y(iced::Center);
let mut list = column![].spacing(1);
for r in rows(instances, palette) {
let expansion = palette.expansion(&r.name).filter(|e| e.iter().any(|v| *v));
let count = if expansion.is_some() { "•••".to_string() } else { r.count.to_string() };
list = list.push(list_row(
palette,
&r.name,
CountKey::default(),
r.name.clone(),
count,
r.errors > 0,
0,
Some(expansion.is_some()),
));
if let Some([l, s, m]) = expansion {
for (key, n) in codec::count::expanded_counts(instances, &r.name, l, s, m) {
let label = key.label();
list = list.push(list_row(palette, &r.name, key, label, n.to_string(), false, 1, None));
}
}
}
let footer: Element<'a, Message> = match &palette.table {
None => row![
Space::new().width(Fill),
button(text(crate::t!("Create Table")).size(12))
.on_press(msg(CountMsg::CreateTable))
.style(button_style(false))
.padding([5, 12]),
]
.into(),
Some(checked) => {
let all = rows(instances, palette);
let every = !all.is_empty() && all.iter().all(|r| checked.contains(&r.name.to_ascii_uppercase()));
let mut insert = button(text(crate::t!("Insert")).size(12)).style(button_style(true)).padding([5, 12]);
if !checked.is_empty() {
insert = insert.on_press(msg(CountMsg::TableInsert));
}
row![
checkbox(every).size(14).on_toggle(|v| msg(CountMsg::TableAll(v))),
Space::new().width(Fill),
button(text(crate::t!("Cancel")).size(12))
.on_press(msg(CountMsg::TableCancel))
.style(button_style(false))
.padding([5, 12]),
insert,
]
.spacing(6)
.align_y(iced::Center)
.into()
}
};
column![
top,
container(column![header, scrollable(list).height(Fill)].spacing(2))
.padding(4)
.height(Fill)
.style(card),
footer,
]
.spacing(6)
.into()
}
fn card(theme: &Theme) -> container::Style {
container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { radius: 4.0.into(), ..Default::default() },
..Default::default()
}
}
/// Count mode in the palette: back to the list, the counted block with the
/// match options, and the error report.
fn mode_view<'a>(palette: &'a CountPalette, mode: &'a CountMode, doc: &CadDocument) -> Element<'a, Message> {
let back = button(row![crate::ui::icons::themed_arrow_left(10.0), text(crate::t!("Back to List")).size(12)].spacing(4).align_y(iced::Center))
.on_press(msg(CountMsg::Back))
.style(button::text)
.padding([2, 2]);
let result = mode.result(doc);
let mut body = column![back].spacing(8);
if let Some(name) = mode.target_name() {
let mut details = column![button(
row![
crate::ui::icons::themed_success(CHECK_ICON, 12.0),
text(match mode.target {
Some(CountTarget::Group(_)) => crate::tf!("Geometries: {}", mode.count(doc)).into_owned(),
_ => format!("{name}: {}", mode.count(doc)),
})
.size(12)
.width(Fill),
]
.spacing(6)
.align_y(iced::Center),
)
.on_press(msg(CountMsg::ToggleDetails))
.style(button::text)
.padding(0)]
.spacing(6);
if let (false, Some(CountTarget::Block { matching, .. })) = (palette.details_closed, &mode.target) {
for (k, label) in ["Match Layer", "Match Scale", "Match Mirror State"].iter().enumerate() {
details = details.push(
checkbox(matching[k])
.label(crate::t!(*label).into_owned())
.text_size(12)
.size(14)
.on_toggle(move |v| msg(CountMsg::Match(k, v))),
);
}
}
body = body.push(container(details).padding(8).width(Fill).style(card));
}
if !result.errors.is_empty() {
let mut report = column![button(
row![
crate::ui::icons::themed_warning(WARNING_ICON, 12.0),
text(crate::tf!("Count Error Report: {}", result.errors.len())).size(12).width(Fill),
]
.spacing(6)
.align_y(iced::Center),
)
.on_press(msg(CountMsg::ToggleErrors))
.style(button::text)
.padding(0)]
.spacing(4);
if !palette.errors_closed {
for h in &result.errors {
report = report.push(
button(text(crate::t!("Overlapping object")).size(12))
.on_press(msg(CountMsg::ShowError(*h)))
.style(button::text)
.padding([2, 18]),
);
}
}
body = body.push(container(report).padding(8).width(Fill).style(card));
}
body.into()
}
pub fn view<'a>(
palette: &'a CountPalette,
mode: Option<&'a CountMode>,
doc: &'a CadDocument,
epoch: u64,
width: f32,
auto_collapse: bool,
) -> Element<'a, Message> {
let title_bar = crate::ui::dock::title_bar(PanelId::Count, crate::t!("Count palette").into_owned(), auto_collapse);
let body = match mode {
Some(mode) if mode.target.is_some() => mode_view(palette, mode, doc),
_ => {
let area = mode.and_then(|m| m.area.clone());
let key = (doc as *const CadDocument as usize, epoch);
let mut cache = palette.list_cache.borrow_mut();
let instances = match cache.as_ref() {
Some((d, e, a, list)) if (*d, *e) == key && *a == area => list.clone(),
_ => {
let list = std::rc::Rc::new(codec::count::block_instances(doc, area.as_deref()));
*cache = Some((key.0, key.1, area, list.clone()));
list
}
};
list_view(palette, &instances)
}
};
crate::ui::dock::frame(column![title_bar, body].spacing(6), width)
}
fn bar_button<'a>(icon: Element<'a, Message>, tip: String, m: Option<CountMsg>) -> Element<'a, Message> {
let mut b = button(icon).style(button::text).padding([3, 5]);
if let Some(m) = m {
b = b.on_press(msg(m));
}
tooltip(b, text(tip).size(10), tooltip::Position::Bottom).gap(4).into()
}
/// The floating toolbar over the drawing in count mode: the count, the
/// status, previous / next reference, area, select, field and close.
pub fn toolbar<'a>(mode: &CountMode, doc: &CadDocument) -> Element<'a, Message> {
let has_target = mode.target.is_some();
let result = mode.result(doc);
let count = if has_target { mode.count(doc).to_string() } else { "-".to_string() };
let status: Element<'a, Message> = if !result.errors.is_empty() {
crate::ui::icons::themed_warning(WARNING_ICON, 14.0)
} else if has_target {
container(text("i").size(11).style(|theme: &Theme| text::Style { color: Some(theme.palette().primary.base.color) }))
.padding([0, 4])
.style(|theme: &Theme| container::Style {
border: Border { color: theme.palette().primary.base.color, width: 1.0, radius: 8.0.into() },
..Default::default()
})
.into()
} else {
crate::ui::icons::themed_disabled(WARNING_ICON, 14.0)
};
let on = |m: CountMsg| has_target.then_some(m);
let icon = |bytes: &'static [u8]| -> Element<'a, Message> {
if has_target {
crate::ui::icons::themed_success(bytes, 16.0)
} else {
crate::ui::icons::themed_disabled(bytes, 16.0)
}
};
let bar = row![
text(crate::tf!("Count: {}", count)).size(13),
status,
bar_button(crate::ui::icons::themed_arrow_left(12.0), crate::t!("Previous").into_owned(), on(CountMsg::Prev)),
bar_button(crate::ui::icons::themed_arrow_right(12.0), crate::t!("Next").into_owned(), on(CountMsg::Next)),
bar_button(crate::ui::icons::themed_success(AREA_ICON, 16.0), crate::t!("Count in a specified area").into_owned(), Some(CountMsg::Area)),
bar_button(crate::ui::icons::themed_success(SELECT_ICON, 16.0), crate::t!("Select objects").into_owned(), Some(CountMsg::Select)),
bar_button(icon(TABLE_ICON), crate::t!("Insert Count Field").into_owned(), on(CountMsg::Field)),
bar_button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0), crate::t!("Close").into_owned(), Some(CountMsg::Close)),
]
.spacing(6)
.align_y(iced::Center);
let bar = container(bar).padding([4, 10]).style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() },
..Default::default()
});
container(iced::widget::opaque(bar)).width(Fill).align_x(iced::Center).padding([10, 0]).into()
}
/// The blue frame around the drawing area in count mode.
pub fn border<'a>() -> Element<'a, Message> {
container(Space::new().width(Fill).height(Fill))
.width(Fill)
.height(Fill)
.style(|_: &Theme| container::Style {
border: Border { color: Color::from_rgb(0.18, 0.52, 0.95), width: 2.0, radius: 0.0.into() },
..Default::default()
})
.into()
}
/// A colour index as the display colour (`COUNTCOLOR` / `COUNTERRORCOLOR`).
pub fn aci_rgba(index: i16) -> [f32; 4] {
crate::scene::convert::tess_util::aci_to_rgba(&codec::types::Color::from_index(index))
}
/// The Invalid Area dialog, shown when the count area's boundary is gone.
pub fn invalid_area_view<'a>(palette: &CountPalette, sizing: crate::ui::modal::ModalSizing) -> Element<'a, Message> {
let action = |title: String, detail: String, m: CountMsg| -> Element<'a, Message> {
button(
row![
text("→").size(16).style(|theme: &Theme| text::Style { color: Some(theme.palette().primary.base.color) }),
column![text(title).size(13), text(detail).size(11).style(muted_style)].spacing(2),
]
.spacing(10)
.align_y(iced::Center),
)
.on_press(msg(m))
.width(Fill)
.padding([10, 12])
.style(button_style(false))
.into()
};
column![
row![
crate::ui::icons::themed_warning(WARNING_ICON, 20.0),
text(crate::t!("The changes to count boundary have resulted to an invalid count area. What do you want to do?"))
.size(14)
.width(Fill),
]
.spacing(12)
.align_y(iced::Center),
action(
crate::t!("Undo the changes to the count boundary").into_owned(),
crate::t!("The last change is undone; the boundary and the count area come back.").into_owned(),
CountMsg::InvalidUndo,
),
action(
crate::t!("Continue and count the entire model space").into_owned(),
crate::t!("The count area is dropped; counting goes on in all of model space.").into_owned(),
CountMsg::InvalidContinue,
),
row![
checkbox(palette.invalid_always)
.label(crate::t!("Always perform my current choice").into_owned())
.text_size(12)
.size(14)
.on_toggle(|v| msg(CountMsg::InvalidAlways(v))),
Space::new().width(Fill),
crate::ui::style::form::dialog_button(crate::t!("Cancel"), msg(CountMsg::InvalidUndo), false),
]
.spacing(8)
.align_y(iced::Center),
]
.spacing(10)
.padding([12, 14])
.width(sizing.width)
.into()
}
+44 -1
View File
@@ -32,7 +32,7 @@ pub const CATEGORIES: &[(&str, &[&str])] = &[
&["Author", "Comments", "Filename", "Filesize", "HyperlinkBase", "Keywords", "LastSavedBy", "Subject", "Title"],
),
("Linked", &["Hyperlink"]),
("Objects", &["BlockPlaceholder", "Formula", "NamedObject", "Object"]),
("Objects", &["BlockPlaceholder", "Count", "CountInArea", "Formula", "NamedObject", "Object"]),
("Other", &["DieselExpression", "SystemVariable"]),
(
"Plot",
@@ -77,6 +77,8 @@ pub enum FieldKind {
Formula,
Hyperlink,
PlotScale,
/// Count / CountInArea: a JSON expression.
Count,
BlockPlaceholder,
}
@@ -96,6 +98,7 @@ pub fn kind_of(name: &str) -> FieldKind {
"Formula" => FieldKind::Formula,
"Hyperlink" => FieldKind::Hyperlink,
"PlotScale" => FieldKind::PlotScale,
"Count" | "CountInArea" => FieldKind::Count,
"BlockPlaceholder" => FieldKind::BlockPlaceholder,
_ => FieldKind::Text,
}
@@ -130,6 +133,8 @@ pub struct FieldDialogState {
pub hyperlink_url: String,
/// Index into codec PLOT_SCALE_FORMATS.
pub plot_scale: usize,
/// The JSON expression of a Count / CountInArea field.
pub count_expression: String,
/// The block being edited in the block editor; block placeholders are
/// only offered there.
pub placeholder_block: Option<String>,
@@ -165,6 +170,7 @@ impl FieldDialogState {
hyperlink_text: String::new(),
hyperlink_url: String::new(),
plot_scale: 0,
count_expression: String::new(),
placeholder_block: None,
placeholder_property: 7,
preview: String::new(),
@@ -221,6 +227,13 @@ impl FieldDialogState {
.map_or_else(|| "\\AcVar PlotScale".to_string(), |f| f.1.to_string()),
vec![],
),
FieldKind::Count => {
let evaluator = if self.name == "CountInArea" { "AcCount2" } else { "AcCount" };
match self.count_expression.trim() {
"" => (format!("\\{evaluator}"), vec![]),
json => (format!("\\{evaluator} {json}"), vec![]),
}
}
FieldKind::BlockPlaceholder => {
let (_, property, format) = codec::fields::BLOCK_PLACEHOLDER_PROPERTIES[self.placeholder_property];
// Text properties take the chosen case; the others keep their own format.
@@ -330,6 +343,8 @@ pub enum FieldDialogMsg {
HyperlinkUrl(String),
BrowseHyperlink,
PlotScale(usize),
CountExpression(String),
ShowCountInstances,
Help,
Ok,
}
@@ -632,6 +647,7 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
]
.spacing(8)
.into(),
FieldKind::Count => column![].into(),
FieldKind::Object => column![
row![
text(t!("Object type")).size(12).width(Length::Fixed(96.0)),
@@ -727,6 +743,33 @@ pub fn view<'a>(
]
.spacing(10),
FieldKind::Hyperlink => column![card(t!("Hyperlink").into_owned(), format_panel(state)), preview].spacing(10),
FieldKind::Count => column![
card(
t!("Expression").into_owned(),
column![
text_input("", &state.count_expression)
.size(12)
.padding([8, 8])
.font(iced::Font::MONOSPACE)
.style(field_style)
.on_input(|v| msg(FieldDialogMsg::CountExpression(v))),
row![
button(text(t!("Evaluate")).size(12))
.on_press(msg(FieldDialogMsg::Evaluate))
.style(button_style(false))
.padding([5, 14]),
button(text(t!("Show Count Instances")).size(12))
.on_press(msg(FieldDialogMsg::ShowCountInstances))
.style(button_style(false))
.padding([5, 14]),
]
.spacing(8),
]
.spacing(8),
),
preview,
]
.spacing(10),
FieldKind::BlockPlaceholder => column![card(t!("Block placeholder").into_owned(), format_panel(state)), preview].spacing(10),
_ => column![card(t!("Format").into_owned(), format_panel(state)), preview].spacing(10),
};
+1
View File
@@ -27,6 +27,7 @@ pub mod alias_editor;
pub mod find_replace;
pub mod named_parameters;
pub mod pc_manager;
pub mod count_palette;
pub mod pdf_dialogs;
pub mod xref_attach;
pub mod xref_help;