forked from 1b-agent/opencadstudio-upstream
DWF and DGN underlays: attach, display, snaps, frames and references
DWFATTACH / -DWFATTACH and DGNATTACH / -DGNATTACH attach a DWF / DWFx sheet or a DGN (V7 or V8) model with the reference's prompts; ATTACH routes the file types to them. The sheet or model is read into vector paths, drawn as a transparent underlay raster (contrast, fade, monochrome and background adjustment apply) and offers its geometry to object snaps. Definitions are kept in ACAD_DWFDEFINITIONS / ACAD_DGNDEFINITIONS, the External References palette lists and reloads them, and image textures get line-keeping mip levels so thin underlay lines stay visible when zoomed out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7f8f8457c7
commit
cb7860e50e
46 files changed
+2723
-129
No files matched your search
Generated
+3
@@ -53,12 +53,14 @@ dependencies = [
|
|||||||
"rayon",
|
"rayon",
|
||||||
"resvg",
|
"resvg",
|
||||||
"rfd",
|
"rfd",
|
||||||
|
"roxmltree",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"rustc-hash 2.1.3",
|
"rustc-hash 2.1.3",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
|
"tiny-skia",
|
||||||
"ttf-parser",
|
"ttf-parser",
|
||||||
"unicode-bidi",
|
"unicode-bidi",
|
||||||
"ureq",
|
"ureq",
|
||||||
@@ -68,6 +70,7 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
"winresource",
|
"winresource",
|
||||||
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ flate2 = "1"
|
|||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
hayro = "0.7.1"
|
hayro = "0.7.1"
|
||||||
kurbo = "0.13"
|
kurbo = "0.13"
|
||||||
|
# DWF/DWFx packages, their XPS pages and DWF/DGN underlay rasters.
|
||||||
|
zip = { version = "8", default-features = false, features = ["deflate"] }
|
||||||
|
roxmltree = "0.20"
|
||||||
|
tiny-skia = "0.11"
|
||||||
# Rasterises assets/logo.svg into the window icon at startup, so the taskbar
|
# Rasterises assets/logo.svg into the window icon at startup, so the taskbar
|
||||||
# icon comes from the same file every other build target does. Already in the
|
# icon comes from the same file every other build target does. Already in the
|
||||||
# tree behind iced's svg feature; named here only to use it directly.
|
# tree behind iced's svg feature; named here only to use it directly.
|
||||||
|
|||||||
@@ -1150,6 +1150,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = تم فتح الرسم، لكن تعذر تخزين النسخة الحديثة: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = تم فتح الرسم، لكن تعذر تخزين النسخة الحديثة: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1511,6 +1515,8 @@ common =
|
|||||||
.value-1-layout-selected = تم تحديد تخطيط واحد
|
.value-1-layout-selected = تم تحديد تخطيط واحد
|
||||||
.layouts-selected = تخطيطات محددة
|
.layouts-selected = تخطيطات محددة
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = حدد التخطيطات للإخراج. تُطبع كل صفحة في وضع التخطيط.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = حدد التخطيطات للإخراج. تُطبع كل صفحة في وضع التخطيط.
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1137,6 +1137,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = Чертежът е отворен, но скорошното копие не може да се съхрани: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = Чертежът е отворен, но скорошното копие не може да се съхрани: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1498,6 +1502,8 @@ common =
|
|||||||
.value-1-layout-selected = Избрано е 1 оформление
|
.value-1-layout-selected = Избрано е 1 оформление
|
||||||
.layouts-selected = избрани оформления
|
.layouts-selected = избрани оформления
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Изберете оформленията за извеждане. Всяка страница се печата в режим Оформление.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Изберете оформленията за извеждане. Всяка страница се печата в режим Оформление.
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1137,6 +1137,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = Výkres otevřen, ale nedávnou kopii nelze uložit: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = Výkres otevřen, ale nedávnou kopii nelze uložit: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1498,6 +1502,8 @@ common =
|
|||||||
.value-1-layout-selected = Vybráno 1 rozvržení
|
.value-1-layout-selected = Vybráno 1 rozvržení
|
||||||
.layouts-selected = vybraných rozvržení
|
.layouts-selected = vybraných rozvržení
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Vyberte rozvržení pro výstup. Každá stránka se tiskne v režimu Rozvržení.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Vyberte rozvržení pro výstup. Každá stránka se tiskne v režimu Rozvržení.
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1138,6 +1138,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = Zeichnung geöffnet, aber die Kopie für zuletzt verwendete Dateien konnte nicht gespeichert werden: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = Zeichnung geöffnet, aber die Kopie für zuletzt verwendete Dateien konnte nicht gespeichert werden: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1499,6 +1503,8 @@ common =
|
|||||||
.value-1-layout-selected = 1 Layout ausgewählt
|
.value-1-layout-selected = 1 Layout ausgewählt
|
||||||
.layouts-selected = Layouts ausgewählt
|
.layouts-selected = Layouts ausgewählt
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Auszugebende Layouts auswählen. Jede Seite wird im Layoutmodus gedruckt.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Auszugebende Layouts auswählen. Jede Seite wird im Layoutmodus gedruckt.
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1139,6 +1139,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = Το σχέδιο άνοιξε, αλλά το πρόσφατο αντίγραφο δεν αποθηκεύτηκε: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = Το σχέδιο άνοιξε, αλλά το πρόσφατο αντίγραφο δεν αποθηκεύτηκε: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1500,6 +1504,8 @@ common =
|
|||||||
.value-1-layout-selected = Επιλέχθηκε 1 διάταξη
|
.value-1-layout-selected = Επιλέχθηκε 1 διάταξη
|
||||||
.layouts-selected = επιλεγμένες διατάξεις
|
.layouts-selected = επιλεγμένες διατάξεις
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Επιλέξτε τις διατάξεις για έξοδο. Κάθε σελίδα εκτυπώνεται σε λειτουργία Διάταξης.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Επιλέξτε τις διατάξεις για έξοδο. Κάθε σελίδα εκτυπώνεται σε λειτουργία Διάταξης.
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1139,6 +1139,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = Opened drawing, but recent copy could not be stored: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = Opened drawing, but recent copy could not be stored: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = Select PDF Underlay
|
.select-pdf-underlay = Select PDF Underlay
|
||||||
.pdf-files = PDF Files
|
.pdf-files = PDF Files
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = Select External Reference File
|
.select-external-reference-file = Select External Reference File
|
||||||
.cad-files = CAD Files
|
.cad-files = CAD Files
|
||||||
@@ -1500,6 +1504,8 @@ common =
|
|||||||
.value-1-layout-selected = 1 layout selected
|
.value-1-layout-selected = 1 layout selected
|
||||||
.layouts-selected = layouts selected
|
.layouts-selected = layouts selected
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Select the layouts to output. Each page is printed in Layout mode.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Select the layouts to output. Each page is printed in Layout mode.
|
||||||
.select-all-1fc9a3 = Select all
|
.select-all-1fc9a3 = Select all
|
||||||
.select-none = Select none
|
.select-none = Select none
|
||||||
|
|||||||
@@ -1138,6 +1138,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = Se abrió el dibujo, pero no se pudo guardar la copia reciente: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = Se abrió el dibujo, pero no se pudo guardar la copia reciente: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1499,6 +1503,8 @@ common =
|
|||||||
.value-1-layout-selected = 1 presentación seleccionada
|
.value-1-layout-selected = 1 presentación seleccionada
|
||||||
.layouts-selected = presentaciones seleccionadas
|
.layouts-selected = presentaciones seleccionadas
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Seleccione las presentaciones de salida. Cada página se imprime en modo Presentación.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Seleccione las presentaciones de salida. Cada página se imprime en modo Presentación.
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1137,6 +1137,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = Piirustus avattu, mutta viimeksi käytetyn kopion tallennus epäonnistui: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = Piirustus avattu, mutta viimeksi käytetyn kopion tallennus epäonnistui: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1498,6 +1502,8 @@ common =
|
|||||||
.value-1-layout-selected = 1 asettelu valittu
|
.value-1-layout-selected = 1 asettelu valittu
|
||||||
.layouts-selected = asettelua valittu
|
.layouts-selected = asettelua valittu
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Valitse tulostettavat asettelut. Jokainen sivu tulostetaan asettelutilassa.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Valitse tulostettavat asettelut. Jokainen sivu tulostetaan asettelutilassa.
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1138,6 +1138,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = Dessin ouvert, mais sa copie récente n’a pas pu être conservée : __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = Dessin ouvert, mais sa copie récente n’a pas pu être conservée : __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1499,6 +1503,8 @@ common =
|
|||||||
.value-1-layout-selected = 1 présentation sélectionnée
|
.value-1-layout-selected = 1 présentation sélectionnée
|
||||||
.layouts-selected = présentations sélectionnées
|
.layouts-selected = présentations sélectionnées
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Sélectionnez les présentations à produire. Chaque page est imprimée en mode Présentation.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Sélectionnez les présentations à produire. Chaque page est imprimée en mode Présentation.
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1126,6 +1126,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = चित्र खुल गया, लेकिन हालिया प्रति सहेजी नहीं जा सकी: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = चित्र खुल गया, लेकिन हालिया प्रति सहेजी नहीं जा सकी: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1487,6 +1491,8 @@ common =
|
|||||||
.value-1-layout-selected = 1 लेआउट चयनित
|
.value-1-layout-selected = 1 लेआउट चयनित
|
||||||
.layouts-selected = लेआउट चयनित
|
.layouts-selected = लेआउट चयनित
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = आउटपुट के लिए लेआउट चुनें। हर पृष्ठ लेआउट मोड में छपता है।
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = आउटपुट के लिए लेआउट चुनें। हर पृष्ठ लेआउट मोड में छपता है।
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1137,6 +1137,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = A rajz megnyílt, de a legutóbbi másolat nem menthető: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = A rajz megnyílt, de a legutóbbi másolat nem menthető: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1498,6 +1502,8 @@ common =
|
|||||||
.value-1-layout-selected = 1 elrendezés kijelölve
|
.value-1-layout-selected = 1 elrendezés kijelölve
|
||||||
.layouts-selected = elrendezés kijelölve
|
.layouts-selected = elrendezés kijelölve
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Válassza ki a nyomtatandó elrendezéseket. Minden oldal Elrendezés módban nyomtatódik.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Válassza ki a nyomtatandó elrendezéseket. Minden oldal Elrendezés módban nyomtatódik.
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1137,6 +1137,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = Disegno aperto, ma impossibile conservare la copia recente: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = Disegno aperto, ma impossibile conservare la copia recente: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1498,6 +1502,8 @@ common =
|
|||||||
.value-1-layout-selected = 1 layout selezionato
|
.value-1-layout-selected = 1 layout selezionato
|
||||||
.layouts-selected = layout selezionati
|
.layouts-selected = layout selezionati
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Seleziona i layout da produrre. Ogni pagina viene stampata in modalità Layout.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Seleziona i layout da produrre. Ogni pagina viene stampata in modalità Layout.
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1128,6 +1128,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = 図面を開きましたが、最近使ったファイルのコピーを保存できませんでした: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = 図面を開きましたが、最近使ったファイルのコピーを保存できませんでした: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1489,6 +1493,8 @@ common =
|
|||||||
.value-1-layout-selected = 1個のレイアウトを選択
|
.value-1-layout-selected = 1個のレイアウトを選択
|
||||||
.layouts-selected = 個のレイアウトを選択
|
.layouts-selected = 個のレイアウトを選択
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = 出力するレイアウトを選択します。各ページはレイアウトモードで印刷されます。
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = 出力するレイアウトを選択します。各ページはレイアウトモードで印刷されます。
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1137,6 +1137,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = 도면은 열었지만 최근 사본을 저장할 수 없습니다: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = 도면은 열었지만 최근 사본을 저장할 수 없습니다: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1498,6 +1502,8 @@ common =
|
|||||||
.value-1-layout-selected = 배치 1개 선택됨
|
.value-1-layout-selected = 배치 1개 선택됨
|
||||||
.layouts-selected = 개 배치 선택됨
|
.layouts-selected = 개 배치 선택됨
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = 출력할 배치를 선택하세요. 각 페이지는 배치 모드로 인쇄됩니다.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = 출력할 배치를 선택하세요. 각 페이지는 배치 모드로 인쇄됩니다.
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1136,6 +1136,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = Tekening geopend, maar de recente kopie kon niet worden opgeslagen: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = Tekening geopend, maar de recente kopie kon niet worden opgeslagen: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1497,6 +1501,8 @@ common =
|
|||||||
.value-1-layout-selected = 1 lay-out geselecteerd
|
.value-1-layout-selected = 1 lay-out geselecteerd
|
||||||
.layouts-selected = lay-outs geselecteerd
|
.layouts-selected = lay-outs geselecteerd
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Selecteer de uit te voeren lay-outs. Elke pagina wordt in lay-outmodus afgedrukt.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Selecteer de uit te voeren lay-outs. Elke pagina wordt in lay-outmodus afgedrukt.
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1137,6 +1137,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = Otwarto rysunek, ale nie udało się zachować ostatniej kopii: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = Otwarto rysunek, ale nie udało się zachować ostatniej kopii: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1498,6 +1502,8 @@ common =
|
|||||||
.value-1-layout-selected = Wybrano 1 układ
|
.value-1-layout-selected = Wybrano 1 układ
|
||||||
.layouts-selected = wybranych układów
|
.layouts-selected = wybranych układów
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Wybierz układy do wydruku. Każda strona jest drukowana w trybie Układ.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Wybierz układy do wydruku. Każda strona jest drukowana w trybie Układ.
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1138,6 +1138,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = Desenho aberto, mas não foi possível armazenar a cópia recente: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = Desenho aberto, mas não foi possível armazenar a cópia recente: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1499,6 +1503,8 @@ common =
|
|||||||
.value-1-layout-selected = 1 layout selecionado
|
.value-1-layout-selected = 1 layout selecionado
|
||||||
.layouts-selected = layouts selecionados
|
.layouts-selected = layouts selecionados
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Selecione os layouts para saída. Cada página é impressa no modo Layout.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Selecione os layouts para saída. Cada página é impressa no modo Layout.
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1141,6 +1141,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = Чертёж открыт, но не удалось сохранить недавнюю копию: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = Чертёж открыт, но не удалось сохранить недавнюю копию: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1502,6 +1506,8 @@ common =
|
|||||||
.value-1-layout-selected = Выбран 1 лист
|
.value-1-layout-selected = Выбран 1 лист
|
||||||
.layouts-selected = выбранных листов
|
.layouts-selected = выбранных листов
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Выберите листы для вывода. Каждая страница печатается в режиме листа.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Выберите листы для вывода. Каждая страница печатается в режиме листа.
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1124,6 +1124,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = Çizim açıldı ancak son kullanılan kopyası saklanamadı: __ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = Çizim açıldı ancak son kullanılan kopyası saklanamadı: __ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Dosyaları
|
||||||
|
.dgn-files = DGN Dosyaları
|
||||||
|
.select-dwf-file = DWF Dosyası Seç
|
||||||
|
.select-dgn-file = DGN Dosyası Seç
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1485,6 +1489,8 @@ common =
|
|||||||
.value-1-layout-selected = 1 yerleşim seçili
|
.value-1-layout-selected = 1 yerleşim seçili
|
||||||
.layouts-selected = yerleşim seçili
|
.layouts-selected = yerleşim seçili
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Çıktı alınacak yerleşimleri seçin. Her sayfa Yerleşim modunda yazdırılır.
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = Çıktı alınacak yerleşimleri seçin. Her sayfa Yerleşim modunda yazdırılır.
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1125,6 +1125,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = 已打开图形,但无法保存最近副本:__ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = 已打开图形,但无法保存最近副本:__ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1486,6 +1490,8 @@ common =
|
|||||||
.value-1-layout-selected = 已选择 1 个布局
|
.value-1-layout-selected = 已选择 1 个布局
|
||||||
.layouts-selected = 个布局已选择
|
.layouts-selected = 个布局已选择
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = 选择要输出的布局。每页均以布局模式打印。
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = 选择要输出的布局。每页均以布局模式打印。
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -1137,6 +1137,10 @@ common =
|
|||||||
.opened-drawing-but-recent-copy-could-not-be-stored = 已開啟圖面,但無法儲存最近使用的副本:__ocs_fmt_0__
|
.opened-drawing-but-recent-copy-could-not-be-stored = 已開啟圖面,但無法儲存最近使用的副本:__ocs_fmt_0__
|
||||||
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
.select-pdf-underlay = { draw.select }: PDF · { properties.underlay }
|
||||||
.pdf-files = { common.files } (PDF)
|
.pdf-files = { common.files } (PDF)
|
||||||
|
.dwf-files = DWF Files
|
||||||
|
.dgn-files = DGN Files
|
||||||
|
.select-dwf-file = Select DWF File
|
||||||
|
.select-dgn-file = Select DGN File
|
||||||
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
.pdfattach = PDFATTACH: __ocs_fmt_0__
|
||||||
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
.select-external-reference-file = { draw.select } · { properties.external-reference } · { common.files }
|
||||||
.cad-files = { common.files } (CAD)
|
.cad-files = { common.files } (CAD)
|
||||||
@@ -1498,6 +1502,8 @@ common =
|
|||||||
.value-1-layout-selected = 已選取 1 個配置
|
.value-1-layout-selected = 已選取 1 個配置
|
||||||
.layouts-selected = 個配置已選取
|
.layouts-selected = 個配置已選取
|
||||||
.pdf = PDF
|
.pdf = PDF
|
||||||
|
.dwf = DWF
|
||||||
|
.dgn = DGN
|
||||||
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = 選取要輸出的配置。每頁均以配置模式列印。
|
.select-the-layouts-to-output-each-page-is-printed-in-layout-mode = 選取要輸出的配置。每頁均以配置模式列印。
|
||||||
.select-all-1fc9a3 = { common.select-all }
|
.select-all-1fc9a3 = { common.select-all }
|
||||||
.select-none = { draw.select }: { common.none-title-case }
|
.select-none = { draw.select }: { common.none-title-case }
|
||||||
|
|||||||
@@ -613,12 +613,12 @@ impl OpenCADStudio {
|
|||||||
crate::app::commands::pdf_import::PdfImportSource::File(import),
|
crate::app::commands::pdf_import::PdfImportSource::File(import),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
CmdResult::AttachPdfPages { path, pages } => {
|
CmdResult::AttachPdfPages { kind, path, pages } => {
|
||||||
let label = self.history_label_from_active_cmd(i, "PDFATTACH");
|
let label = self.history_label_from_active_cmd(i, "PDFATTACH");
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
self.attach_pdf_pages(i, label, &path, pages);
|
self.attach_pdf_pages(i, label, kind, &path, pages);
|
||||||
self.restore_pre_cmd_tangent();
|
self.restore_pre_cmd_tangent();
|
||||||
}
|
}
|
||||||
CmdResult::PdfImport(request) => {
|
CmdResult::PdfImport(request) => {
|
||||||
|
|||||||
@@ -218,14 +218,16 @@ impl OpenCADStudio {
|
|||||||
&mut self,
|
&mut self,
|
||||||
i: usize,
|
i: usize,
|
||||||
label: String,
|
label: String,
|
||||||
|
kind: codec::entities::UnderlayType,
|
||||||
path: &str,
|
path: &str,
|
||||||
pages: Vec<(String, codec::EntityType)>,
|
pages: Vec<(String, codec::EntityType)>,
|
||||||
) {
|
) {
|
||||||
let pending = self.begin_undo(i, label, pages.len(), false);
|
let pending = self.begin_undo(i, label, pages.len(), false);
|
||||||
|
|
||||||
for (page, mut entity) in pages {
|
for (page, mut entity) in pages {
|
||||||
let definition = crate::modules::insert::pdf_attach::ensure_pdf_definition(
|
let definition = crate::modules::insert::pdf_attach::ensure_underlay_definition(
|
||||||
&mut self.tabs[i].scene.document,
|
&mut self.tabs[i].scene.document,
|
||||||
|
kind,
|
||||||
path,
|
path,
|
||||||
&page,
|
&page,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1022,6 +1022,8 @@ impl OpenCADStudio {
|
|||||||
| "DWFFRAME"
|
| "DWFFRAME"
|
||||||
| "DGNFRAME"
|
| "DGNFRAME"
|
||||||
| "PDFOSNAP"
|
| "PDFOSNAP"
|
||||||
|
| "DWFOSNAP"
|
||||||
|
| "DGNOSNAP"
|
||||||
| "UOSNAP"
|
| "UOSNAP"
|
||||||
| "PDFIMPORTMODE"
|
| "PDFIMPORTMODE"
|
||||||
| "PDFIMPORTFILTER"
|
| "PDFIMPORTFILTER"
|
||||||
@@ -1181,7 +1183,9 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
return Some(self.finish_dispatch(cmd));
|
return Some(self.finish_dispatch(cmd));
|
||||||
}
|
}
|
||||||
if matches!(name.as_str(), "PDFOSNAP" | "UOSNAP") {
|
// ponytail: one flag for every underlay kind; split it if a drawing
|
||||||
|
// needs PDF snaps on while DWF/DGN snaps are off.
|
||||||
|
if matches!(name.as_str(), "PDFOSNAP" | "DWFOSNAP" | "DGNOSNAP" | "UOSNAP") {
|
||||||
let current = i16::from(crate::scene::model::pdf_vector::pdf_osnap());
|
let current = i16::from(crate::scene::model::pdf_vector::pdf_osnap());
|
||||||
if let Some(value) = &value {
|
if let Some(value) = &value {
|
||||||
match value.parse::<i16>().ok().filter(|value| (0..=1).contains(value)) {
|
match value.parse::<i16>().ok().filter(|value| (0..=1).contains(value)) {
|
||||||
|
|||||||
@@ -12,6 +12,26 @@ use crate::modules::insert::xref_cmd::display_path;
|
|||||||
use crate::ui::window::xref_attach::{XrefAttachMsg, XrefAttachState};
|
use crate::ui::window::xref_attach::{XrefAttachMsg, XrefAttachState};
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
|
/// DWFATTACH / DGNATTACH on the command line: with a file, from its
|
||||||
|
/// sheet / model prompt; without one, from the path prompt.
|
||||||
|
pub(super) fn start_underlay_attach(
|
||||||
|
&mut self,
|
||||||
|
i: usize,
|
||||||
|
kind: codec::entities::UnderlayType,
|
||||||
|
file: &str,
|
||||||
|
) {
|
||||||
|
use crate::command::CadCommand;
|
||||||
|
let insunits = self.tabs[i].scene.document.header.insertion_units;
|
||||||
|
let mut command = crate::modules::insert::pdf_attach::PdfAttachCommand::for_kind(kind, insunits);
|
||||||
|
if !file.trim().is_empty() {
|
||||||
|
if let Some(crate::command::CmdResult::ReportError(message)) = command.on_text_input(file) {
|
||||||
|
self.command_line.push_error(&message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.command_line.push_info(&command.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
|
}
|
||||||
|
|
||||||
/// Commands owned here; `None` defers to the other families.
|
/// Commands owned here; `None` defers to the other families.
|
||||||
pub(super) fn dispatch_xref_attach(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
pub(super) fn dispatch_xref_attach(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||||
let upper = cmd.to_ascii_uppercase();
|
let upper = cmd.to_ascii_uppercase();
|
||||||
@@ -21,6 +41,22 @@ impl OpenCADStudio {
|
|||||||
};
|
};
|
||||||
match (verb.as_str(), rest.is_empty()) {
|
match (verb.as_str(), rest.is_empty()) {
|
||||||
("ATTACH", true) => Some(Task::done(Message::AttachPick)),
|
("ATTACH", true) => Some(Task::done(Message::AttachPick)),
|
||||||
|
("DWFATTACH", true) => Some(Task::done(Message::UnderlayAttachPick(
|
||||||
|
codec::entities::UnderlayType::Dwf,
|
||||||
|
))),
|
||||||
|
("DGNATTACH", true) => Some(Task::done(Message::UnderlayAttachPick(
|
||||||
|
codec::entities::UnderlayType::Dgn,
|
||||||
|
))),
|
||||||
|
// A file after DWFATTACH / DGNATTACH, or the - forms, go on the
|
||||||
|
// command line.
|
||||||
|
("DWFATTACH" | "-DWFATTACH", _) => {
|
||||||
|
self.start_underlay_attach(i, codec::entities::UnderlayType::Dwf, rest);
|
||||||
|
Some(Task::none())
|
||||||
|
}
|
||||||
|
("DGNATTACH" | "-DGNATTACH", _) => {
|
||||||
|
self.start_underlay_attach(i, codec::entities::UnderlayType::Dgn, rest);
|
||||||
|
Some(Task::none())
|
||||||
|
}
|
||||||
("XATTACH", true) => Some(Task::done(Message::XAttachPick)),
|
("XATTACH", true) => Some(Task::done(Message::XAttachPick)),
|
||||||
// A file named on the command line skips the picker.
|
// A file named on the command line skips the picker.
|
||||||
// Handled at once, so the placement it starts belongs to this
|
// Handled at once, so the placement it starts belongs to this
|
||||||
@@ -299,6 +335,26 @@ impl OpenCADStudio {
|
|||||||
/// Messages of the ATTACH picker and the dialog.
|
/// Messages of the ATTACH picker and the dialog.
|
||||||
pub(in crate::app) fn update_xref_attach(&mut self, message: Message) -> Task<Message> {
|
pub(in crate::app) fn update_xref_attach(&mut self, message: Message) -> Task<Message> {
|
||||||
match message {
|
match message {
|
||||||
|
Message::UnderlayAttachPick(kind) => Task::perform(
|
||||||
|
async move {
|
||||||
|
let (title, filter, extensions): (&str, &str, &[&str]) = match kind {
|
||||||
|
codec::entities::UnderlayType::Dgn => {
|
||||||
|
("Select DGN File", "DGN Files", &["dgn"])
|
||||||
|
}
|
||||||
|
_ => ("Select DWF File", "DWF Files", &["dwf", "dwfx"]),
|
||||||
|
};
|
||||||
|
let handle = crate::sys::file_dialog()
|
||||||
|
.set_title(crate::t!(title).as_ref())
|
||||||
|
.add_filter(crate::t!(filter).as_ref(), extensions)
|
||||||
|
.pick_file()
|
||||||
|
.await;
|
||||||
|
match handle {
|
||||||
|
Some(h) => Ok(crate::sys::handle_path(&h)),
|
||||||
|
None => Err("Cancelled".to_string()),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Message::AttachPickResult,
|
||||||
|
),
|
||||||
Message::AttachPick => Task::perform(
|
Message::AttachPick => Task::perform(
|
||||||
async {
|
async {
|
||||||
let handle = crate::sys::file_dialog()
|
let handle = crate::sys::file_dialog()
|
||||||
@@ -306,11 +362,14 @@ impl OpenCADStudio {
|
|||||||
.add_filter(
|
.add_filter(
|
||||||
crate::t!("All Reference Files").as_ref(),
|
crate::t!("All Reference Files").as_ref(),
|
||||||
&[
|
&[
|
||||||
"dwg", "dxf", "pdf", "png", "jpg", "jpeg", "bmp", "tif", "tiff",
|
"dwg", "dxf", "pdf", "dwf", "dwfx", "dgn", "png", "jpg", "jpeg",
|
||||||
|
"bmp", "tif", "tiff",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.add_filter(crate::t!("Drawing").as_ref(), &["dwg", "dxf"])
|
.add_filter(crate::t!("Drawing").as_ref(), &["dwg", "dxf"])
|
||||||
.add_filter(crate::t!("PDF Files").as_ref(), &["pdf"])
|
.add_filter(crate::t!("PDF Files").as_ref(), &["pdf"])
|
||||||
|
.add_filter(crate::t!("DWF Files").as_ref(), &["dwf", "dwfx"])
|
||||||
|
.add_filter(crate::t!("DGN Files").as_ref(), &["dgn"])
|
||||||
.add_filter(
|
.add_filter(
|
||||||
crate::t!("Images").as_ref(),
|
crate::t!("Images").as_ref(),
|
||||||
&["png", "jpg", "jpeg", "bmp", "tif", "tiff"],
|
&["png", "jpg", "jpeg", "bmp", "tif", "tiff"],
|
||||||
@@ -337,6 +396,16 @@ impl OpenCADStudio {
|
|||||||
)))),
|
)))),
|
||||||
Err(e) => self.update(Message::PdfAttachPickResult(Err(e.to_string()))),
|
Err(e) => self.update(Message::PdfAttachPickResult(Err(e.to_string()))),
|
||||||
},
|
},
|
||||||
|
"dwf" | "dwfx" | "dgn" => {
|
||||||
|
let kind = if ext == "dgn" {
|
||||||
|
codec::entities::UnderlayType::Dgn
|
||||||
|
} else {
|
||||||
|
codec::entities::UnderlayType::Dwf
|
||||||
|
};
|
||||||
|
let i = self.active_tab;
|
||||||
|
self.start_underlay_attach(i, kind, &path.to_string_lossy());
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
"png" | "jpg" | "jpeg" | "bmp" | "tif" | "tiff" => {
|
"png" | "jpg" | "jpeg" | "bmp" | "tif" | "tiff" => {
|
||||||
let result = image::image_dimensions(&path)
|
let result = image::image_dimensions(&path)
|
||||||
.map(|(w, h)| (path.clone(), w, h))
|
.map(|(w, h)| (path.clone(), w, h))
|
||||||
|
|||||||
@@ -3851,6 +3851,8 @@ pub enum Message {
|
|||||||
XAttachPickResult(Result<std::path::PathBuf, String>),
|
XAttachPickResult(Result<std::path::PathBuf, String>),
|
||||||
/// ATTACH: pick a drawing, image or PDF to reference.
|
/// ATTACH: pick a drawing, image or PDF to reference.
|
||||||
AttachPick,
|
AttachPick,
|
||||||
|
/// DWFATTACH / DGNATTACH: pick the file (the result goes the ATTACH way).
|
||||||
|
UnderlayAttachPick(codec::entities::UnderlayType),
|
||||||
/// The Reference slide-out's xref fading: amount dragged, drag done,
|
/// The Reference slide-out's xref fading: amount dragged, drag done,
|
||||||
/// switch.
|
/// switch.
|
||||||
XrefFadeSlide(u8),
|
XrefFadeSlide(u8),
|
||||||
|
|||||||
@@ -867,8 +867,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
|||||||
Some(entity.common().handle)
|
Some(entity.common().handle)
|
||||||
}
|
}
|
||||||
codec::EntityType::Underlay(underlay)
|
codec::EntityType::Underlay(underlay)
|
||||||
if matches!(underlay.underlay_type, codec::entities::UnderlayType::Pdf)
|
if self.tabs[i].xref_unloaded.is_unloaded(underlay.definition_handle.value()) =>
|
||||||
&& self.tabs[i].xref_unloaded.is_unloaded(underlay.definition_handle.value()) =>
|
|
||||||
{
|
{
|
||||||
Some(entity.common().handle)
|
Some(entity.common().handle)
|
||||||
}
|
}
|
||||||
@@ -1021,8 +1020,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
|||||||
"XREF: cannot reload nested reference '{}'. Reload it in its host drawing.",
|
"XREF: cannot reload nested reference '{}'. Reload it in its host drawing.",
|
||||||
name
|
name
|
||||||
).as_ref());
|
).as_ref());
|
||||||
} else if *kind == crate::io::xref_model::RefKind::Pdf {
|
} else if *kind == crate::io::xref_model::RefKind::Underlay {
|
||||||
// A PDF reloads by clearing its definition's unloaded state.
|
// An underlay reloads by clearing its definition's unloaded state.
|
||||||
for (key, row_name, row_kind, _) in &picked {
|
for (key, row_name, row_kind, _) in &picked {
|
||||||
if row_kind != kind || row_name != name {
|
if row_kind != kind || row_name != name {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -1426,6 +1426,7 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
message @ (Message::AttachPick
|
message @ (Message::AttachPick
|
||||||
|
| Message::UnderlayAttachPick(_)
|
||||||
| Message::AttachPickResult(_)
|
| Message::AttachPickResult(_)
|
||||||
| Message::XrefAttach(_)
|
| Message::XrefAttach(_)
|
||||||
| Message::XrefAttachBrowseResult(_)) => self.update_xref_attach(message),
|
| Message::XrefAttachBrowseResult(_)) => self.update_xref_attach(message),
|
||||||
|
|||||||
@@ -1569,6 +1569,7 @@ pub enum CmdResult {
|
|||||||
/// page; the host creates or reuses the definitions and commits them all
|
/// page; the host creates or reuses the definitions and commits them all
|
||||||
/// in one undo step, then ends the command.
|
/// in one undo step, then ends the command.
|
||||||
AttachPdfPages {
|
AttachPdfPages {
|
||||||
|
kind: codec::entities::UnderlayType,
|
||||||
path: String,
|
path: String,
|
||||||
pages: Vec<(String, EntityType)>,
|
pages: Vec<(String, EntityType)>,
|
||||||
},
|
},
|
||||||
|
|||||||
+25
-15
@@ -47,23 +47,33 @@ pub(crate) fn definition<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Page number of a definition ("1" when unset).
|
/// Page number of a PDF definition ("1" when unset), or the sheet (DWF) or
|
||||||
|
/// model (DGN) name (empty: the file's first).
|
||||||
pub(crate) fn page_of(def: &codec::entities::UnderlayDefinition) -> &str {
|
pub(crate) fn page_of(def: &codec::entities::UnderlayDefinition) -> &str {
|
||||||
if def.page_name.trim().is_empty() {
|
if def.page_name.trim().is_empty() && matches!(def.underlay_type, codec::entities::UnderlayType::Pdf) {
|
||||||
"1"
|
"1"
|
||||||
} else {
|
} else {
|
||||||
def.page_name.trim()
|
def.page_name.trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Size of the referenced page in underlay units (page inches), when the
|
/// The referenced page (PDF, page inches from its lower-left corner) or
|
||||||
/// definition resolves and its PDF page can be read.
|
/// sheet / model (DWF, DGN, in its own units about its origin) as min x,
|
||||||
pub(crate) fn page_size(u: &Underlay, document: &codec::CadDocument) -> Option<(f64, f64)> {
|
/// min y, max x, max y in underlay units, when it can be read.
|
||||||
let def = definition(u, document)?;
|
pub(crate) fn definition_rect(def: &codec::entities::UnderlayDefinition) -> Option<[f64; 4]> {
|
||||||
if !matches!(def.underlay_type, codec::entities::UnderlayType::Pdf) {
|
match def.underlay_type {
|
||||||
return None;
|
codec::entities::UnderlayType::Pdf => {
|
||||||
|
crate::scene::model::pdf_raster::page_size_inches(&def.file_path, page_of(def))
|
||||||
|
.map(|(w, h)| [0.0, 0.0, w, h])
|
||||||
|
}
|
||||||
|
kind => crate::scene::model::underlay_vector::sheet(kind, &def.file_path, page_of(def))
|
||||||
|
.map(|sheet| sheet.rect),
|
||||||
}
|
}
|
||||||
crate::scene::model::pdf_raster::page_size_inches(&def.file_path, page_of(def))
|
}
|
||||||
|
|
||||||
|
/// The underlay's page rectangle (see [`definition_rect`]).
|
||||||
|
pub(crate) fn page_rect(u: &Underlay, document: &codec::CadDocument) -> Option<[f64; 4]> {
|
||||||
|
definition_rect(definition(u, document)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The name a definition shows: its own name, or "<file> - <page>" as the
|
/// The name a definition shows: its own name, or "<file> - <page>" as the
|
||||||
@@ -115,12 +125,12 @@ pub(crate) fn is_clipped(u: &Underlay) -> bool {
|
|||||||
|
|
||||||
/// Page frame in world space (CCW from the insertion).
|
/// Page frame in world space (CCW from the insertion).
|
||||||
fn page_quad(u: &Underlay, document: &codec::CadDocument) -> Option<[[f64; 3]; 4]> {
|
fn page_quad(u: &Underlay, document: &codec::CadDocument) -> Option<[[f64; 3]; 4]> {
|
||||||
let (w, h) = page_size(u, document)?;
|
let [x0, y0, x1, y1] = page_rect(u, document)?;
|
||||||
Some([
|
Some([
|
||||||
local_to_world(u, [0.0, 0.0]),
|
local_to_world(u, [x0, y0]),
|
||||||
local_to_world(u, [w, 0.0]),
|
local_to_world(u, [x1, y0]),
|
||||||
local_to_world(u, [w, h]),
|
local_to_world(u, [x1, y1]),
|
||||||
local_to_world(u, [0.0, h]),
|
local_to_world(u, [x0, y1]),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +146,7 @@ fn shown_local_extent(u: &Underlay, document: &codec::CadDocument) -> Option<(f6
|
|||||||
}
|
}
|
||||||
return Some((hi[0] - lo[0], hi[1] - lo[1]));
|
return Some((hi[0] - lo[0], hi[1] - lo[1]));
|
||||||
}
|
}
|
||||||
page_size(u, document)
|
page_rect(u, document).map(|r| (r[2] - r[0], r[3] - r[1]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Width and height the Properties panel shows: the shown extent times the
|
/// Width and height the Properties panel shows: the shown extent times the
|
||||||
|
|||||||
+1
-5
@@ -1121,7 +1121,6 @@ pub fn collect_entries_with_prev(
|
|||||||
unloaded: &std::collections::HashSet<crate::io::xref_model::UnloadKey>,
|
unloaded: &std::collections::HashSet<crate::io::xref_model::UnloadKey>,
|
||||||
prev: &std::collections::HashMap<u64, std::time::SystemTime>,
|
prev: &std::collections::HashMap<u64, std::time::SystemTime>,
|
||||||
) -> Vec<crate::io::xref_model::ReferenceEntry> {
|
) -> Vec<crate::io::xref_model::ReferenceEntry> {
|
||||||
use codec::entities::UnderlayType;
|
|
||||||
use codec::objects::ObjectType;
|
use codec::objects::ObjectType;
|
||||||
use crate::io::xref_model::{
|
use crate::io::xref_model::{
|
||||||
child_key, decide_status, normalize_lexical, RefKind, RefStatus, RefType,
|
child_key, decide_status, normalize_lexical, RefKind, RefStatus, RefType,
|
||||||
@@ -1252,16 +1251,13 @@ pub fn collect_entries_with_prev(
|
|||||||
let ObjectType::UnderlayDefinition(def) = obj else {
|
let ObjectType::UnderlayDefinition(def) = obj else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if def.underlay_type != UnderlayType::Pdf {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let key = handle.value();
|
let key = handle.value();
|
||||||
let name = if def.name.trim().is_empty() {
|
let name = if def.name.trim().is_empty() {
|
||||||
file_name_only(&def.file_path).to_owned()
|
file_name_only(&def.file_path).to_owned()
|
||||||
} else {
|
} else {
|
||||||
def.name.clone()
|
def.name.clone()
|
||||||
};
|
};
|
||||||
let mut entry = ReferenceEntry::new(key, name, RefKind::Pdf);
|
let mut entry = ReferenceEntry::new(key, name, RefKind::Underlay);
|
||||||
entry.saved_path = def.file_path.clone();
|
entry.saved_path = def.file_path.clone();
|
||||||
if def.unloaded || unloaded.contains(&UnloadKey::Direct(key)) {
|
if def.unloaded || unloaded.contains(&UnloadKey::Direct(key)) {
|
||||||
entry.status = RefStatus::Unloaded;
|
entry.status = RefStatus::Unloaded;
|
||||||
|
|||||||
@@ -303,7 +303,8 @@ pub fn wildcard_match(name: &str, pattern: &str) -> bool {
|
|||||||
pub enum RefKind {
|
pub enum RefKind {
|
||||||
DwgXref,
|
DwgXref,
|
||||||
Image,
|
Image,
|
||||||
Pdf,
|
/// A PDF, DWF or DGN underlay.
|
||||||
|
Underlay,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lifecycle state of a [`ReferenceEntry`].
|
/// Lifecycle state of a [`ReferenceEntry`].
|
||||||
|
|||||||
@@ -3771,6 +3771,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"Opened drawing, but recent copy could not be stored: {error}" => Some(("common", "opened-drawing-but-recent-copy-could-not-be-stored")),
|
"Opened drawing, but recent copy could not be stored: {error}" => Some(("common", "opened-drawing-but-recent-copy-could-not-be-stored")),
|
||||||
"Select PDF Underlay" => Some(("common", "select-pdf-underlay")),
|
"Select PDF Underlay" => Some(("common", "select-pdf-underlay")),
|
||||||
"PDF Files" => Some(("common", "pdf-files")),
|
"PDF Files" => Some(("common", "pdf-files")),
|
||||||
|
"DWF Files" => Some(("common", "dwf-files")),
|
||||||
|
"DGN Files" => Some(("common", "dgn-files")),
|
||||||
|
"Select DWF File" => Some(("common", "select-dwf-file")),
|
||||||
|
"Select DGN File" => Some(("common", "select-dgn-file")),
|
||||||
"PDFATTACH: {e}" => Some(("common", "pdfattach")),
|
"PDFATTACH: {e}" => Some(("common", "pdfattach")),
|
||||||
"Select External Reference File" => Some(("common", "select-external-reference-file")),
|
"Select External Reference File" => Some(("common", "select-external-reference-file")),
|
||||||
"CAD Files" => Some(("common", "cad-files")),
|
"CAD Files" => Some(("common", "cad-files")),
|
||||||
@@ -4495,6 +4499,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"1 layout selected" => Some(("common", "value-1-layout-selected")),
|
"1 layout selected" => Some(("common", "value-1-layout-selected")),
|
||||||
"layouts selected" => Some(("common", "layouts-selected")),
|
"layouts selected" => Some(("common", "layouts-selected")),
|
||||||
"PDF" => Some(("common", "pdf")),
|
"PDF" => Some(("common", "pdf")),
|
||||||
|
"DWF" => Some(("common", "dwf")),
|
||||||
|
"DGN" => Some(("common", "dgn")),
|
||||||
"Select the layouts to output. Each page is printed in Layout mode." => Some(("common", "select-the-layouts-to-output-each-page-is-printed-in-layout-mode")),
|
"Select the layouts to output. Each page is printed in Layout mode." => Some(("common", "select-the-layouts-to-output-each-page-is-printed-in-layout-mode")),
|
||||||
"Select all" => Some(("common", "select-all-1fc9a3")),
|
"Select all" => Some(("common", "select-all-1fc9a3")),
|
||||||
"Select none" => Some(("common", "select-none")),
|
"Select none" => Some(("common", "select-none")),
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
// PDFATTACH / -PDFATTACH — attach a PDF page as an underlay.
|
// PDFATTACH / -PDFATTACH — attach a PDF page as an underlay; DWFATTACH /
|
||||||
|
// DGNATTACH (and their - forms) attach a DWF sheet or a DGN model.
|
||||||
//
|
//
|
||||||
// Path to PDF file to attach: (-PDFATTACH only)
|
// Path to PDF file to attach: (-PDFATTACH only)
|
||||||
// Enter page number or [?] <1>:
|
// Enter page number or [?] <1>: (DWF: name of sheet, DGN: model)
|
||||||
|
// Specify conversion units [Master/Sub] <Master>: (DGN only)
|
||||||
// Specify insertion point:
|
// Specify insertion point:
|
||||||
// Base image size: Width: 7.8740, Height: 3.9369, Meters
|
// Base image size: Width: 7.8740, Height: 3.9369, Meters
|
||||||
// Specify scale factor or [Unit] <1>:
|
// Specify scale factor or [Unit] <1>:
|
||||||
// Specify rotation <0>:
|
// Specify rotation <last>:
|
||||||
//
|
//
|
||||||
// The definition (one per file and page, named "<file> - <page>" in the
|
// The definition (one per file and page, named "<file> - <page>" in the
|
||||||
// ACAD_PDFDEFINITIONS dictionary) is created with the underlay when it is
|
// ACAD_PDFDEFINITIONS, ACAD_DWFDEFINITIONS or ACAD_DGNDEFINITIONS dictionary) is created with the underlay when it is
|
||||||
// placed, so a cancelled attach leaves nothing and undo removes both.
|
// placed, so a cancelled attach leaves nothing and undo removes both.
|
||||||
|
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use codec::entities::{Underlay, UnderlayDisplayFlags};
|
use codec::entities::{Underlay, UnderlayDisplayFlags, UnderlayType};
|
||||||
use codec::objects::{Dictionary, ObjectType, UnderlayDefinition};
|
use codec::objects::{Dictionary, ObjectType, UnderlayDefinition};
|
||||||
use codec::types::{Handle, Vector3};
|
use codec::types::{Handle, Vector3};
|
||||||
use codec::{CadDocument, EntityType};
|
use codec::{CadDocument, EntityType};
|
||||||
@@ -51,6 +53,14 @@ const UNITS: [(&str, &str); 9] = [
|
|||||||
/// The unit last chosen with the Unit option; it stays the default.
|
/// The unit last chosen with the Unit option; it stays the default.
|
||||||
static LAST_UNIT: Mutex<Option<usize>> = Mutex::new(None);
|
static LAST_UNIT: Mutex<Option<usize>> = Mutex::new(None);
|
||||||
|
|
||||||
|
/// The rotation (degrees) of the last underlay attached; the next attach
|
||||||
|
/// offers it.
|
||||||
|
static LAST_ROTATION: Mutex<f64> = Mutex::new(0.0);
|
||||||
|
|
||||||
|
fn last_rotation() -> f64 {
|
||||||
|
LAST_ROTATION.lock().map(|r| *r).unwrap_or(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
/// The Unit option's index for the drawing's INSUNITS.
|
/// The Unit option's index for the drawing's INSUNITS.
|
||||||
fn unit_for_insunits(insunits: i16) -> usize {
|
fn unit_for_insunits(insunits: i16) -> usize {
|
||||||
match insunits {
|
match insunits {
|
||||||
@@ -93,6 +103,7 @@ enum Step {
|
|||||||
Path,
|
Path,
|
||||||
Page,
|
Page,
|
||||||
ListPages,
|
ListPages,
|
||||||
|
Conversion,
|
||||||
Insertion,
|
Insertion,
|
||||||
Scale,
|
Scale,
|
||||||
Unit,
|
Unit,
|
||||||
@@ -100,6 +111,7 @@ enum Step {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct PdfAttachCommand {
|
pub struct PdfAttachCommand {
|
||||||
|
kind: UnderlayType,
|
||||||
step: Step,
|
step: Step,
|
||||||
/// The file read for the page (absolute or registered source).
|
/// The file read for the page (absolute or registered source).
|
||||||
path: String,
|
path: String,
|
||||||
@@ -112,7 +124,14 @@ pub struct PdfAttachCommand {
|
|||||||
preset_rotation: Option<f64>,
|
preset_rotation: Option<f64>,
|
||||||
page: String,
|
page: String,
|
||||||
page_count: usize,
|
page_count: usize,
|
||||||
page_size: (f64, f64),
|
/// DWF sheet or DGN model names of the file.
|
||||||
|
items: Vec<String>,
|
||||||
|
/// The page (PDF inches from its corner) or sheet rectangle.
|
||||||
|
rect: [f64; 4],
|
||||||
|
/// DGN sub units per master unit (the Sub conversion's scale).
|
||||||
|
sub_per_master: f64,
|
||||||
|
/// Scale the scale prompt offers.
|
||||||
|
default_scale: f64,
|
||||||
insertion: DVec3,
|
insertion: DVec3,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
unit: usize,
|
unit: usize,
|
||||||
@@ -122,12 +141,18 @@ pub struct PdfAttachCommand {
|
|||||||
impl PdfAttachCommand {
|
impl PdfAttachCommand {
|
||||||
/// `-PDFATTACH`: starts by asking for the file.
|
/// `-PDFATTACH`: starts by asking for the file.
|
||||||
pub fn new(insunits: i16) -> Self {
|
pub fn new(insunits: i16) -> Self {
|
||||||
|
Self::for_kind(UnderlayType::Pdf, insunits)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `-DWFATTACH` / `-DGNATTACH` (or `-PDFATTACH`): asks for the file.
|
||||||
|
pub fn for_kind(kind: UnderlayType, insunits: i16) -> Self {
|
||||||
let unit = LAST_UNIT
|
let unit = LAST_UNIT
|
||||||
.lock()
|
.lock()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|last| *last)
|
.and_then(|last| *last)
|
||||||
.unwrap_or_else(|| unit_for_insunits(insunits));
|
.unwrap_or_else(|| unit_for_insunits(insunits));
|
||||||
Self {
|
Self {
|
||||||
|
kind,
|
||||||
step: Step::Path,
|
step: Step::Path,
|
||||||
path: String::new(),
|
path: String::new(),
|
||||||
stored_path: None,
|
stored_path: None,
|
||||||
@@ -136,7 +161,10 @@ impl PdfAttachCommand {
|
|||||||
preset_rotation: None,
|
preset_rotation: None,
|
||||||
page: "1".to_string(),
|
page: "1".to_string(),
|
||||||
page_count: 0,
|
page_count: 0,
|
||||||
page_size: (0.0, 0.0),
|
items: Vec::new(),
|
||||||
|
rect: [0.0; 4],
|
||||||
|
sub_per_master: 1.0,
|
||||||
|
default_scale: 1.0,
|
||||||
insertion: DVec3::ZERO,
|
insertion: DVec3::ZERO,
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
unit,
|
unit,
|
||||||
@@ -146,15 +174,48 @@ impl PdfAttachCommand {
|
|||||||
|
|
||||||
/// PDFATTACH with a file already chosen: starts at the page prompt.
|
/// PDFATTACH with a file already chosen: starts at the page prompt.
|
||||||
pub fn with_file(path: &str, insunits: i16) -> Self {
|
pub fn with_file(path: &str, insunits: i16) -> Self {
|
||||||
let mut command = Self::new(insunits);
|
Self::with_kind_file(UnderlayType::Pdf, path, insunits)
|
||||||
if let Some(count) = crate::scene::model::pdf_raster::page_count(path) {
|
}
|
||||||
command.path = path.to_string();
|
|
||||||
command.page_count = count;
|
/// DWFATTACH / DGNATTACH / PDFATTACH with the file chosen: starts at the
|
||||||
command.step = Step::Page;
|
/// page, sheet or model prompt.
|
||||||
}
|
pub fn with_kind_file(kind: UnderlayType, path: &str, insunits: i16) -> Self {
|
||||||
|
let mut command = Self::for_kind(kind, insunits);
|
||||||
|
command.open(path);
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads the file's pages or sheet / model names; false when it cannot.
|
||||||
|
fn open(&mut self, path: &str) -> bool {
|
||||||
|
let found = match self.kind {
|
||||||
|
UnderlayType::Pdf => crate::scene::model::pdf_raster::page_count(path).filter(|n| *n > 0).map(|n| {
|
||||||
|
self.page_count = n;
|
||||||
|
}),
|
||||||
|
kind => crate::scene::model::underlay_vector::item_names(kind, path)
|
||||||
|
.filter(|names| !names.is_empty())
|
||||||
|
.map(|names| {
|
||||||
|
self.page = names[0].clone();
|
||||||
|
self.items = names;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
if found.is_some() {
|
||||||
|
self.path = path.to_string();
|
||||||
|
self.step = Step::Page;
|
||||||
|
}
|
||||||
|
found.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn page_rect(&self, page: &str) -> [f64; 4] {
|
||||||
|
match self.kind {
|
||||||
|
UnderlayType::Pdf => crate::scene::model::pdf_raster::page_size_inches(&self.path, page)
|
||||||
|
.map(|(w, h)| [0.0, 0.0, w, h])
|
||||||
|
.unwrap_or([0.0; 4]),
|
||||||
|
kind => crate::scene::model::underlay_vector::sheet(kind, &self.path, page)
|
||||||
|
.map(|s| s.rect)
|
||||||
|
.unwrap_or([0.0; 4]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The dialog's attach with the insertion point asked on screen: pages
|
/// The dialog's attach with the insertion point asked on screen: pages
|
||||||
/// chosen, the path to store, and any scale / rotation already given.
|
/// chosen, the path to store, and any scale / rotation already given.
|
||||||
pub fn from_dialog(
|
pub fn from_dialog(
|
||||||
@@ -171,9 +232,7 @@ impl PdfAttachCommand {
|
|||||||
command.page = first.clone();
|
command.page = first.clone();
|
||||||
command.extra_pages = rest.to_vec();
|
command.extra_pages = rest.to_vec();
|
||||||
}
|
}
|
||||||
command.page_size =
|
command.rect = command.page_rect(&command.page);
|
||||||
crate::scene::model::pdf_raster::page_size_inches(read_path, &command.page)
|
|
||||||
.unwrap_or((0.0, 0.0));
|
|
||||||
command.preset_scale = scale;
|
command.preset_scale = scale;
|
||||||
if let Some(s) = scale {
|
if let Some(s) = scale {
|
||||||
command.scale = s;
|
command.scale = s;
|
||||||
@@ -198,7 +257,9 @@ impl PdfAttachCommand {
|
|||||||
fn base_image_size(&self) -> String {
|
fn base_image_size(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
"Base image size: Width: {:.4}, Height: {:.4}, {}",
|
"Base image size: Width: {:.4}, Height: {:.4}, {}",
|
||||||
self.page_size.0, self.page_size.1, UNITS[self.unit].1
|
self.rect[2] - self.rect[0],
|
||||||
|
self.rect[3] - self.rect[1],
|
||||||
|
UNITS[self.unit].1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,16 +269,15 @@ impl PdfAttachCommand {
|
|||||||
return CmdResult::NeedPoint;
|
return CmdResult::NeedPoint;
|
||||||
}
|
}
|
||||||
if std::path::Path::new(&path).extension().is_none() {
|
if std::path::Path::new(&path).extension().is_none() {
|
||||||
path.push_str(".pdf");
|
path.push_str(match self.kind {
|
||||||
|
UnderlayType::Pdf => ".pdf",
|
||||||
|
UnderlayType::Dwf => ".dwf",
|
||||||
|
UnderlayType::Dgn => ".dgn",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
match crate::scene::model::pdf_raster::page_count(&path) {
|
match self.open(&path) {
|
||||||
Some(count) if count > 0 => {
|
true => CmdResult::NeedPoint,
|
||||||
self.path = path;
|
false => CmdResult::ReportError(format!(
|
||||||
self.page_count = count;
|
|
||||||
self.step = Step::Page;
|
|
||||||
CmdResult::NeedPoint
|
|
||||||
}
|
|
||||||
_ => CmdResult::ReportError(format!(
|
|
||||||
"{} not found.",
|
"{} not found.",
|
||||||
crate::entities::underlay::display_path(&path)
|
crate::entities::underlay::display_path(&path)
|
||||||
)),
|
)),
|
||||||
@@ -230,12 +290,34 @@ impl PdfAttachCommand {
|
|||||||
self.step = Step::ListPages;
|
self.step = Step::ListPages;
|
||||||
return CmdResult::NeedPoint;
|
return CmdResult::NeedPoint;
|
||||||
}
|
}
|
||||||
|
if self.kind != UnderlayType::Pdf {
|
||||||
|
let name = if text.is_empty() { self.items[0].clone() } else { text.to_string() };
|
||||||
|
let Some(found) = self.items.iter().find(|n| n.eq_ignore_ascii_case(&name)) else {
|
||||||
|
// ponytail: the PDF wording; the reference's message for an
|
||||||
|
// unknown sheet / model name was not measured.
|
||||||
|
return CmdResult::ReportError(format!(
|
||||||
|
"There is no {name} in {}.",
|
||||||
|
crate::entities::underlay::display_path(&self.path)
|
||||||
|
));
|
||||||
|
};
|
||||||
|
self.page = found.clone();
|
||||||
|
self.rect = self.page_rect(&self.page);
|
||||||
|
if self.kind == UnderlayType::Dgn {
|
||||||
|
self.sub_per_master = crate::scene::model::underlay_vector::sheet(self.kind, &self.path, &self.page)
|
||||||
|
.map(|s| s.sub_per_master)
|
||||||
|
.filter(|v| *v > 0.0)
|
||||||
|
.unwrap_or(1.0);
|
||||||
|
self.step = Step::Conversion;
|
||||||
|
} else {
|
||||||
|
self.step = Step::Insertion;
|
||||||
|
}
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
let page = if text.is_empty() { "1" } else { text };
|
let page = if text.is_empty() { "1" } else { text };
|
||||||
match page.parse::<usize>() {
|
match page.parse::<usize>() {
|
||||||
Ok(n) if n >= 1 && n <= self.page_count => {
|
Ok(n) if n >= 1 && n <= self.page_count => {
|
||||||
self.page = n.to_string();
|
self.page = n.to_string();
|
||||||
self.page_size = crate::scene::model::pdf_raster::page_size_inches(&self.path, &self.page)
|
self.rect = self.page_rect(&self.page);
|
||||||
.unwrap_or((0.0, 0.0));
|
|
||||||
self.step = Step::Insertion;
|
self.step = Step::Insertion;
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
@@ -246,10 +328,30 @@ impl PdfAttachCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// DGN: Master keeps the model's master units, Sub offers the scale that
|
||||||
|
/// turns sub units into master units.
|
||||||
|
fn accept_conversion(&mut self, text: &str) -> CmdResult {
|
||||||
|
let t = text.trim().to_ascii_uppercase();
|
||||||
|
let sub = match t.as_str() {
|
||||||
|
"" => false,
|
||||||
|
_ if "MASTER".starts_with(&t) => false,
|
||||||
|
_ if "SUB".starts_with(&t) => true,
|
||||||
|
_ => return CmdResult::ReportError("Invalid option keyword.".to_string()),
|
||||||
|
};
|
||||||
|
self.default_scale = if sub { 1.0 / self.sub_per_master } else { 1.0 };
|
||||||
|
self.scale = self.default_scale;
|
||||||
|
self.step = Step::Insertion;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
fn list_pages(&mut self, pattern: &str) -> CmdResult {
|
fn list_pages(&mut self, pattern: &str) -> CmdResult {
|
||||||
let pattern = if pattern.trim().is_empty() { "*" } else { pattern.trim() };
|
let pattern = if pattern.trim().is_empty() { "*" } else { pattern.trim() };
|
||||||
let lines: Vec<String> = (1..=self.page_count)
|
let names: Vec<String> = match self.kind {
|
||||||
.map(|n| n.to_string())
|
UnderlayType::Pdf => (1..=self.page_count).map(|n| n.to_string()).collect(),
|
||||||
|
_ => self.items.clone(),
|
||||||
|
};
|
||||||
|
let lines: Vec<String> = names
|
||||||
|
.into_iter()
|
||||||
.filter(|n| crate::io::xref_model::wildcard_match(n, pattern))
|
.filter(|n| crate::io::xref_model::wildcard_match(n, pattern))
|
||||||
.collect();
|
.collect();
|
||||||
self.step = Step::Page;
|
self.step = Step::Page;
|
||||||
@@ -295,7 +397,7 @@ impl PdfAttachCommand {
|
|||||||
fn accept_rotation(&mut self, text: &str) -> CmdResult {
|
fn accept_rotation(&mut self, text: &str) -> CmdResult {
|
||||||
let text = text.trim();
|
let text = text.trim();
|
||||||
let degrees = if text.is_empty() {
|
let degrees = if text.is_empty() {
|
||||||
0.0
|
last_rotation()
|
||||||
} else {
|
} else {
|
||||||
match crate::entities::common::parse_f64(text) {
|
match crate::entities::common::parse_f64(text) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
@@ -306,6 +408,9 @@ impl PdfAttachCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if let Ok(mut last) = LAST_ROTATION.lock() {
|
||||||
|
*last = degrees;
|
||||||
|
}
|
||||||
self.commit(degrees.to_radians())
|
self.commit(degrees.to_radians())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +419,7 @@ impl PdfAttachCommand {
|
|||||||
.chain(self.extra_pages.iter().cloned())
|
.chain(self.extra_pages.iter().cloned())
|
||||||
.collect();
|
.collect();
|
||||||
let placed = underlays_for_pages(
|
let placed = underlays_for_pages(
|
||||||
|
self.kind,
|
||||||
&pages,
|
&pages,
|
||||||
self.plane.to_local(self.insertion),
|
self.plane.to_local(self.insertion),
|
||||||
self.scale,
|
self.scale,
|
||||||
@@ -323,16 +429,16 @@ impl PdfAttachCommand {
|
|||||||
.map(|(page, underlay)| (page, self.plane.place_entity(underlay)))
|
.map(|(page, underlay)| (page, self.plane.place_entity(underlay)))
|
||||||
.collect();
|
.collect();
|
||||||
CmdResult::AttachPdfPages {
|
CmdResult::AttachPdfPages {
|
||||||
|
kind: self.kind,
|
||||||
path: self.stored_path.clone().unwrap_or_else(|| self.path.clone()),
|
path: self.stored_path.clone().unwrap_or_else(|| self.path.clone()),
|
||||||
pages: placed,
|
pages: placed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn frame_at(&self, pt: DVec3) -> Vec<[f64; 3]> {
|
fn frame_at(&self, pt: DVec3) -> Vec<[f64; 3]> {
|
||||||
let (w, h) = (self.page_size.0 * self.scale, self.page_size.1 * self.scale);
|
let at = |x: f64, y: f64| pt + self.plane.vector_to_world(DVec3::new(x * self.scale, y * self.scale, 0.0));
|
||||||
let x = self.plane.vector_to_world(DVec3::new(w, 0.0, 0.0));
|
let [x0, y0, x1, y1] = self.rect;
|
||||||
let y = self.plane.vector_to_world(DVec3::new(0.0, h, 0.0));
|
let corners = [at(x0, y0), at(x1, y0), at(x1, y1), at(x0, y1), at(x0, y0)];
|
||||||
let corners = [pt, pt + x, pt + x + y, pt + y, pt];
|
|
||||||
corners.iter().map(|p| p.to_array()).collect()
|
corners.iter().map(|p| p.to_array()).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,27 +449,43 @@ impl CadCommand for PdfAttachCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"PDFATTACH"
|
match self.kind {
|
||||||
|
UnderlayType::Pdf => "PDFATTACH",
|
||||||
|
UnderlayType::Dwf => "DWFATTACH",
|
||||||
|
UnderlayType::Dgn => "DGNATTACH",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
|
let item = if self.kind == UnderlayType::Dwf { "sheet" } else { "model" };
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::Path => "Path to PDF file to attach:".to_string(),
|
Step::Path => format!(
|
||||||
Step::Page => "Enter page number or [?] <1>:".to_string(),
|
"Path to {} file to attach:",
|
||||||
Step::ListPages => "Enter page(s) to list <*>:".to_string(),
|
match self.kind {
|
||||||
|
UnderlayType::Pdf => "PDF",
|
||||||
|
UnderlayType::Dwf => "DWF",
|
||||||
|
UnderlayType::Dgn => "DGN",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Step::Page if self.kind == UnderlayType::Pdf => "Enter page number or [?] <1>:".to_string(),
|
||||||
|
Step::Page => format!("Enter name of {item} or [?] <{}>:", self.items.first().map_or("", |s| s)),
|
||||||
|
Step::ListPages if self.kind == UnderlayType::Pdf => "Enter page(s) to list <*>:".to_string(),
|
||||||
|
Step::ListPages => format!("Enter {item}(s) to list <*>:"),
|
||||||
|
Step::Conversion => "Specify conversion units [Master/Sub] <Master>:".to_string(),
|
||||||
Step::Insertion => "Specify insertion point:".to_string(),
|
Step::Insertion => "Specify insertion point:".to_string(),
|
||||||
Step::Scale => "Specify scale factor or [Unit] <1>:".to_string(),
|
Step::Scale => format!("Specify scale factor or [Unit] <{}>:", self.default_scale),
|
||||||
Step::Unit => format!(
|
Step::Unit => format!(
|
||||||
"Enter unit [MM/Centimeter/Meter/Kilometer/Inch/Foot/Yard/MILe/Unitless] <{}>:",
|
"Enter unit [MM/Centimeter/Meter/Kilometer/Inch/Foot/Yard/MILe/Unitless] <{}>:",
|
||||||
UNITS[self.unit].0
|
UNITS[self.unit].0
|
||||||
),
|
),
|
||||||
Step::Rotation => "Specify rotation <0>:".to_string(),
|
Step::Rotation => format!("Specify rotation <{}>:", last_rotation()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn options(&self) -> Vec<CmdOption> {
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::Page => vec![CmdOption::new("?", "?")],
|
Step::Page => vec![CmdOption::new("?", "?")],
|
||||||
|
Step::Conversion => vec![CmdOption::new("Master", "M"), CmdOption::new("Sub", "S")],
|
||||||
Step::Scale => vec![CmdOption::new("Unit", "U")],
|
Step::Scale => vec![CmdOption::new("Unit", "U")],
|
||||||
Step::Unit => vec![
|
Step::Unit => vec![
|
||||||
CmdOption::new("MM", "MM"),
|
CmdOption::new("MM", "MM"),
|
||||||
@@ -389,6 +511,9 @@ impl CadCommand for PdfAttachCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
|
if self.step == Step::Conversion {
|
||||||
|
return CmdResult::ReportError("Invalid option keyword.".to_string());
|
||||||
|
}
|
||||||
if self.step != Step::Insertion {
|
if self.step != Step::Insertion {
|
||||||
return CmdResult::NeedPoint;
|
return CmdResult::NeedPoint;
|
||||||
}
|
}
|
||||||
@@ -405,6 +530,7 @@ impl CadCommand for PdfAttachCommand {
|
|||||||
Step::Path => self.accept_path(text),
|
Step::Path => self.accept_path(text),
|
||||||
Step::Page => self.accept_page(text),
|
Step::Page => self.accept_page(text),
|
||||||
Step::ListPages => self.list_pages(text),
|
Step::ListPages => self.list_pages(text),
|
||||||
|
Step::Conversion => self.accept_conversion(text),
|
||||||
Step::Insertion => return None,
|
Step::Insertion => return None,
|
||||||
Step::Scale => self.accept_scale(text),
|
Step::Scale => self.accept_scale(text),
|
||||||
Step::Unit => self.accept_unit(text),
|
Step::Unit => self.accept_unit(text),
|
||||||
@@ -417,6 +543,7 @@ impl CadCommand for PdfAttachCommand {
|
|||||||
Step::Path => CmdResult::Cancel,
|
Step::Path => CmdResult::Cancel,
|
||||||
Step::Page => self.accept_page(""),
|
Step::Page => self.accept_page(""),
|
||||||
Step::ListPages => self.list_pages("*"),
|
Step::ListPages => self.list_pages("*"),
|
||||||
|
Step::Conversion => self.accept_conversion(""),
|
||||||
Step::Insertion => {
|
Step::Insertion => {
|
||||||
CmdResult::ReportError("Point or option keyword required.".to_string())
|
CmdResult::ReportError("Point or option keyword required.".to_string())
|
||||||
}
|
}
|
||||||
@@ -427,7 +554,7 @@ impl CadCommand for PdfAttachCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||||
if self.step != Step::Insertion || self.page_size.0 <= 0.0 {
|
if self.step != Step::Insertion || self.rect[2] <= self.rect[0] {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(WireModel::solid_f64(
|
Some(WireModel::solid_f64(
|
||||||
@@ -442,6 +569,7 @@ impl CadCommand for PdfAttachCommand {
|
|||||||
/// Underlays for `pages` of a file, all at `insertion` with the same scale
|
/// Underlays for `pages` of a file, all at `insertion` with the same scale
|
||||||
/// and rotation (the reference stacks the chosen pages there).
|
/// and rotation (the reference stacks the chosen pages there).
|
||||||
pub fn underlays_for_pages(
|
pub fn underlays_for_pages(
|
||||||
|
kind: UnderlayType,
|
||||||
pages: &[String],
|
pages: &[String],
|
||||||
insertion: DVec3,
|
insertion: DVec3,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
@@ -450,7 +578,7 @@ pub fn underlays_for_pages(
|
|||||||
pages
|
pages
|
||||||
.iter()
|
.iter()
|
||||||
.map(|page| {
|
.map(|page| {
|
||||||
let mut underlay = Underlay::pdf();
|
let mut underlay = Underlay::new(kind);
|
||||||
underlay.insertion_point = Vector3::new(insertion.x, insertion.y, insertion.z);
|
underlay.insertion_point = Vector3::new(insertion.x, insertion.y, insertion.z);
|
||||||
underlay.set_scale(scale);
|
underlay.set_scale(scale);
|
||||||
underlay.rotation = rotation;
|
underlay.rotation = rotation;
|
||||||
@@ -459,19 +587,30 @@ pub fn underlays_for_pages(
|
|||||||
underlay.flags = UnderlayDisplayFlags::ON
|
underlay.flags = UnderlayDisplayFlags::ON
|
||||||
| UnderlayDisplayFlags::CLIPPING
|
| UnderlayDisplayFlags::CLIPPING
|
||||||
| UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND;
|
| UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND;
|
||||||
|
if kind != UnderlayType::Pdf {
|
||||||
|
// The reference creates DWF and DGN underlays at contrast 75,
|
||||||
|
// fade 25.
|
||||||
|
underlay.contrast = 75;
|
||||||
|
underlay.fade = 25;
|
||||||
|
}
|
||||||
(page.clone(), EntityType::Underlay(underlay))
|
(page.clone(), EntityType::Underlay(underlay))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `ACAD_PDFDEFINITIONS` dictionary under the named-objects root,
|
/// The kind's definitions dictionary (`ACAD_PDFDEFINITIONS` …) under the
|
||||||
/// created when the drawing has none.
|
/// named-objects root, created when the drawing has none.
|
||||||
fn ensure_pdf_dictionary(document: &mut CadDocument) -> Option<Handle> {
|
fn ensure_definitions_dictionary(document: &mut CadDocument, kind: UnderlayType) -> Option<Handle> {
|
||||||
|
let name = match kind {
|
||||||
|
UnderlayType::Pdf => "ACAD_PDFDEFINITIONS",
|
||||||
|
UnderlayType::Dwf => "ACAD_DWFDEFINITIONS",
|
||||||
|
UnderlayType::Dgn => "ACAD_DGNDEFINITIONS",
|
||||||
|
};
|
||||||
let root = document.header.named_objects_dict_handle;
|
let root = document.header.named_objects_dict_handle;
|
||||||
let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get(&root) else {
|
let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get(&root) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
if let Some(existing) = root_dictionary.get("ACAD_PDFDEFINITIONS") {
|
if let Some(existing) = root_dictionary.get(name) {
|
||||||
if matches!(document.objects.get(&existing), Some(ObjectType::Dictionary(_))) {
|
if matches!(document.objects.get(&existing), Some(ObjectType::Dictionary(_))) {
|
||||||
return Some(existing);
|
return Some(existing);
|
||||||
}
|
}
|
||||||
@@ -482,7 +621,7 @@ fn ensure_pdf_dictionary(document: &mut CadDocument) -> Option<Handle> {
|
|||||||
dictionary.owner = root;
|
dictionary.owner = root;
|
||||||
document.objects.insert(handle, ObjectType::Dictionary(dictionary));
|
document.objects.insert(handle, ObjectType::Dictionary(dictionary));
|
||||||
if let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get_mut(&root) {
|
if let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get_mut(&root) {
|
||||||
root_dictionary.add_entry("ACAD_PDFDEFINITIONS", handle);
|
root_dictionary.add_entry(name, handle);
|
||||||
}
|
}
|
||||||
Some(handle)
|
Some(handle)
|
||||||
}
|
}
|
||||||
@@ -492,12 +631,13 @@ fn same_path(a: &str, b: &str) -> bool {
|
|||||||
norm(a) == norm(b)
|
norm(a) == norm(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The PDF definition for a file and page: the existing one, or a new one
|
/// The definition for a file and page / sheet / model of the kind: the
|
||||||
/// registered in ACAD_PDFDEFINITIONS as "<file> - <page>".
|
/// existing one, or a new one registered in its dictionary as
|
||||||
pub fn ensure_pdf_definition(document: &mut CadDocument, path: &str, page: &str) -> Handle {
|
/// "<file> - <page>".
|
||||||
|
pub fn ensure_underlay_definition(document: &mut CadDocument, kind: UnderlayType, path: &str, page: &str) -> Handle {
|
||||||
let existing = document.objects.iter().find_map(|(handle, object)| match object {
|
let existing = document.objects.iter().find_map(|(handle, object)| match object {
|
||||||
ObjectType::UnderlayDefinition(def)
|
ObjectType::UnderlayDefinition(def)
|
||||||
if matches!(def.underlay_type, codec::entities::UnderlayType::Pdf)
|
if def.underlay_type == kind
|
||||||
&& same_path(&def.file_path, path)
|
&& same_path(&def.file_path, path)
|
||||||
&& crate::entities::underlay::page_of(def) == page =>
|
&& crate::entities::underlay::page_of(def) == page =>
|
||||||
{
|
{
|
||||||
@@ -509,10 +649,14 @@ pub fn ensure_pdf_definition(document: &mut CadDocument, path: &str, page: &str)
|
|||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
let handle = document.allocate_handle();
|
let handle = document.allocate_handle();
|
||||||
let mut definition = UnderlayDefinition::pdf(path, page);
|
let mut definition = match kind {
|
||||||
|
UnderlayType::Pdf => UnderlayDefinition::pdf(path, page),
|
||||||
|
UnderlayType::Dwf => UnderlayDefinition::dwf(path, page),
|
||||||
|
UnderlayType::Dgn => UnderlayDefinition::dgn(path, page),
|
||||||
|
};
|
||||||
definition.handle = handle;
|
definition.handle = handle;
|
||||||
let base = crate::entities::underlay::definition_display_name(&definition);
|
let base = crate::entities::underlay::definition_display_name(&definition);
|
||||||
if let Some(dictionary) = ensure_pdf_dictionary(document) {
|
if let Some(dictionary) = ensure_definitions_dictionary(document, kind) {
|
||||||
definition.owner_handle = dictionary;
|
definition.owner_handle = dictionary;
|
||||||
if let Some(ObjectType::Dictionary(entries)) = document.objects.get_mut(&dictionary) {
|
if let Some(ObjectType::Dictionary(entries)) = document.objects.get_mut(&dictionary) {
|
||||||
let mut key = base.clone();
|
let mut key = base.clone();
|
||||||
@@ -531,5 +675,5 @@ pub fn ensure_pdf_definition(document: &mut CadDocument, path: &str, page: &str)
|
|||||||
}
|
}
|
||||||
|
|
||||||
inventory::submit!(crate::command::CommandRegistration {
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
names: &["PDFATTACH", "-PDFATTACH"]
|
names: &["PDFATTACH", "-PDFATTACH", "DWFATTACH", "-DWFATTACH", "DGNATTACH", "-DGNATTACH"]
|
||||||
});
|
});
|
||||||
@@ -191,18 +191,23 @@ impl ImageModel {
|
|||||||
if !u.flags.contains(UnderlayDisplayFlags::ON) {
|
if !u.flags.contains(UnderlayDisplayFlags::ON) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if !matches!(def.underlay_type, UnderlayType::Pdf) || def.unloaded {
|
if def.unloaded {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let page = if def.page_name.trim().is_empty() {
|
let page = crate::entities::underlay::page_of(def);
|
||||||
"1"
|
let rect = crate::entities::underlay::definition_rect(def)?;
|
||||||
} else {
|
let (source, raster) = match def.underlay_type {
|
||||||
def.page_name.trim()
|
UnderlayType::Pdf => {
|
||||||
|
// Hidden PDF layers come from the underlay's layer overrides.
|
||||||
|
let source = super::pdf_layers::underlay_source(u, &def.file_path);
|
||||||
|
let raster = pdf_raster::rasterize_page_display(&source, page)?;
|
||||||
|
(source, raster)
|
||||||
|
}
|
||||||
|
kind => (
|
||||||
|
def.file_path.clone(),
|
||||||
|
super::underlay_vector::display_raster(kind, &def.file_path, page)?,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
let (page_w, page_h) = pdf_raster::page_size_inches(&def.file_path, page)?;
|
|
||||||
// Hidden PDF layers come from the underlay's layer overrides.
|
|
||||||
let source = super::pdf_layers::underlay_source(u, &def.file_path);
|
|
||||||
let raster = pdf_raster::rasterize_page_display(&source, page)?;
|
|
||||||
// Dark means an HSL lightness under one half: pure blue counts as
|
// Dark means an HSL lightness under one half: pure blue counts as
|
||||||
// light, (0, 128, 0) as dark.
|
// light, (0, 128, 0) as dark.
|
||||||
let bg_max = background[0].max(background[1]).max(background[2]);
|
let bg_max = background[0].max(background[1]).max(background[2]);
|
||||||
@@ -216,11 +221,17 @@ impl ImageModel {
|
|||||||
contrast: u.contrast.min(100),
|
contrast: u.contrast.min(100),
|
||||||
monochrome: u.flags.contains(UnderlayDisplayFlags::MONOCHROME),
|
monochrome: u.flags.contains(UnderlayDisplayFlags::MONOCHROME),
|
||||||
adjust_for_background: u.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND),
|
adjust_for_background: u.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND),
|
||||||
dark_background: bg_lum < 0.5,
|
// A DGN model is drawn for a black background, so its
|
||||||
|
// colours turn over on a light one instead (white text stays
|
||||||
|
// light on a dark background, as the reference shows it).
|
||||||
|
dark_background: (bg_lum < 0.5) != (def.underlay_type == UnderlayType::Dgn),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Page size in drawing units (1 unit per PDF inch), entity scale applied.
|
// The page rectangle in drawing units (1 unit per PDF inch, or per
|
||||||
|
// DWF/DGN sheet unit), entity scale applied, from its lower-left
|
||||||
|
// corner.
|
||||||
|
let (page_w, page_h) = (rect[2] - rect[0], rect[3] - rect[1]);
|
||||||
let w_du = page_w * u.x_scale;
|
let w_du = page_w * u.x_scale;
|
||||||
let h_du = page_h * u.y_scale;
|
let h_du = page_h * u.y_scale;
|
||||||
if w_du.abs() <= 0.0 || h_du.abs() <= 0.0 {
|
if w_du.abs() <= 0.0 || h_du.abs() <= 0.0 {
|
||||||
@@ -230,9 +241,7 @@ impl ImageModel {
|
|||||||
let (uxv, uyv) = (c * w_du, s * w_du);
|
let (uxv, uyv) = (c * w_du, s * w_du);
|
||||||
let (vxv, vyv) = (-s * h_du, c * h_du);
|
let (vxv, vyv) = (-s * h_du, c * h_du);
|
||||||
|
|
||||||
let oxv = u.insertion_point.x;
|
let [oxv, oyv, ozv] = crate::entities::underlay::local_to_world(u, [rect[0], rect[1]]);
|
||||||
let oyv = u.insertion_point.y;
|
|
||||||
let ozv = u.insertion_point.z;
|
|
||||||
let ox = oxv as f32;
|
let ox = oxv as f32;
|
||||||
let oy = oyv as f32;
|
let oy = oyv as f32;
|
||||||
let oz = ozv as f32;
|
let oz = ozv as f32;
|
||||||
@@ -249,7 +258,7 @@ impl ImageModel {
|
|||||||
];
|
];
|
||||||
let corners_low = [[oxl, oyl, ozl]; 4];
|
let corners_low = [[oxl, oyl, ozl]; 4];
|
||||||
|
|
||||||
let tris_uv = underlay_visible_uv(u, page_w, page_h);
|
let tris_uv = underlay_visible_uv(u, rect);
|
||||||
let verts: Vec<ImageQuadVertex> = tris_uv
|
let verts: Vec<ImageQuadVertex> = tris_uv
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&[fu, fv]| {
|
.map(|&[fu, fv]| {
|
||||||
@@ -281,7 +290,8 @@ impl ImageModel {
|
|||||||
/// Visible region of an underlay page as triangles in page UV (0..1, y up):
|
/// Visible region of an underlay page as triangles in page UV (0..1, y up):
|
||||||
/// the whole page, the clip polygon, or — for an inverted clip — the page
|
/// the whole page, the clip polygon, or — for an inverted clip — the page
|
||||||
/// with the polygon cut out.
|
/// with the polygon cut out.
|
||||||
fn underlay_visible_uv(u: &codec::entities::Underlay, page_w: f64, page_h: f64) -> Vec<[f64; 2]> {
|
fn underlay_visible_uv(u: &codec::entities::Underlay, rect: [f64; 4]) -> Vec<[f64; 2]> {
|
||||||
|
let (page_w, page_h) = (rect[2] - rect[0], rect[3] - rect[1]);
|
||||||
use codec::entities::UnderlayDisplayFlags;
|
use codec::entities::UnderlayDisplayFlags;
|
||||||
let page = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
|
let page = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
|
||||||
let whole = || vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
|
let whole = || vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
|
||||||
@@ -289,7 +299,10 @@ fn underlay_visible_uv(u: &codec::entities::Underlay, page_w: f64, page_h: f64)
|
|||||||
if !u.flags.contains(UnderlayDisplayFlags::CLIPPING) || clip.len() < 3 {
|
if !u.flags.contains(UnderlayDisplayFlags::CLIPPING) || clip.len() < 3 {
|
||||||
return whole();
|
return whole();
|
||||||
}
|
}
|
||||||
let ring: Vec<[f64; 2]> = clip.iter().map(|p| [p[0] / page_w, p[1] / page_h]).collect();
|
let ring: Vec<[f64; 2]> = clip
|
||||||
|
.iter()
|
||||||
|
.map(|p| [(p[0] - rect[0]) / page_w, (p[1] - rect[1]) / page_h])
|
||||||
|
.collect();
|
||||||
let (points, triangles) = if u.clip_inverted {
|
let (points, triangles) = if u.clip_inverted {
|
||||||
kernel::geom2d::triangulate(&page, &[ring])
|
kernel::geom2d::triangulate(&page, &[ring])
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ pub mod ole_pres;
|
|||||||
pub mod pdf_layers;
|
pub mod pdf_layers;
|
||||||
pub mod pdf_raster;
|
pub mod pdf_raster;
|
||||||
pub mod pdf_vector;
|
pub mod pdf_vector;
|
||||||
|
pub mod underlay_vector;
|
||||||
pub mod mesh_model;
|
pub mod mesh_model;
|
||||||
pub mod solid_model;
|
pub mod solid_model;
|
||||||
pub mod solid_history;
|
pub mod solid_history;
|
||||||
|
|||||||
@@ -629,8 +629,7 @@ pub fn underlay_snap_points(
|
|||||||
let Some(def) = crate::entities::underlay::definition(u, document) else {
|
let Some(def) = crate::entities::underlay::definition(u, document) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let source = super::pdf_layers::underlay_source(u, &def.file_path);
|
let Some(vectors) = underlay_vectors(u, def) else {
|
||||||
let Some(vectors) = page_vectors(&source, crate::entities::underlay::page_of(def)) else {
|
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let world = |p: [f64; 2]| {
|
let world = |p: [f64; 2]| {
|
||||||
@@ -665,6 +664,21 @@ pub fn underlay_snap_points(
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The page's vectors in page units: PDF through its layer overrides, DWF
|
||||||
|
/// and DGN from their sheet.
|
||||||
|
fn underlay_vectors(
|
||||||
|
u: &codec::entities::Underlay,
|
||||||
|
def: &codec::entities::UnderlayDefinition,
|
||||||
|
) -> Option<Arc<PageVectors>> {
|
||||||
|
let page = crate::entities::underlay::page_of(def);
|
||||||
|
match def.underlay_type {
|
||||||
|
codec::entities::UnderlayType::Pdf => {
|
||||||
|
page_vectors(&super::pdf_layers::underlay_source(u, &def.file_path), page)
|
||||||
|
}
|
||||||
|
kind => super::underlay_vector::page_vectors(kind, &def.file_path, page),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Most points of the snap-only geometry wire of one underlay.
|
/// Most points of the snap-only geometry wire of one underlay.
|
||||||
// ponytail: flat cap; a spatial index over page segments for huge drawings.
|
// ponytail: flat cap; a spatial index over page segments for huge drawings.
|
||||||
const MAX_GEOMETRY_POINTS: usize = 200_000;
|
const MAX_GEOMETRY_POINTS: usize = 200_000;
|
||||||
@@ -693,8 +707,7 @@ pub fn underlay_snap_geometry(
|
|||||||
if def.unloaded {
|
if def.unloaded {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let source = super::pdf_layers::underlay_source(u, &def.file_path);
|
let Some(vectors) = underlay_vectors(u, def) else {
|
||||||
let Some(vectors) = page_vectors(&source, crate::entities::underlay::page_of(def)) else {
|
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let world = |p: [f64; 2]| crate::entities::underlay::local_to_world(u, p);
|
let world = |p: [f64; 2]| crate::entities::underlay::local_to_world(u, p);
|
||||||
|
|||||||
@@ -0,0 +1,421 @@
|
|||||||
|
//! DGN V7 (ISFF) design files: one model ("Default") of variable-length
|
||||||
|
//! element records. Coordinates are UORs; the design file header (TCB)
|
||||||
|
//! gives the global origin and the master unit the model is read in. Shared
|
||||||
|
//! cells are drawn where their instances place them.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use super::model::{arc_cubics, bspline_points, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text};
|
||||||
|
|
||||||
|
/// A 32-bit integer stored as two little-endian words, high word first.
|
||||||
|
fn int32(b: &[u8], at: usize) -> Option<i32> {
|
||||||
|
let p = b.get(at..at + 4)?;
|
||||||
|
Some(i32::from_le_bytes([p[2], p[3], p[0], p[1]]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn le_i32(b: &[u8], at: usize) -> Option<i32> {
|
||||||
|
Some(i32::from_le_bytes(b.get(at..at + 4)?.try_into().ok()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uint16(b: &[u8], at: usize) -> Option<u16> {
|
||||||
|
Some(u16::from_le_bytes(b.get(at..at + 2)?.try_into().ok()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A VAX D-floating value as an IEEE double.
|
||||||
|
fn vax_double(b: &[u8], at: usize) -> Option<f64> {
|
||||||
|
let s = b.get(at..at + 8)?;
|
||||||
|
let hi = u32::from_le_bytes([s[2], s[3], s[0], s[1]]);
|
||||||
|
let lo = u32::from_le_bytes([s[6], s[7], s[4], s[5]]);
|
||||||
|
let exponent = ((hi >> 23) & 0xff) as i64;
|
||||||
|
if exponent == 0 {
|
||||||
|
return Some(0.0);
|
||||||
|
}
|
||||||
|
let exponent = (exponent - 129 + 1023) as u32;
|
||||||
|
let rnd = lo & 7;
|
||||||
|
let mut lo2 = (lo >> 3) | (hi << 29);
|
||||||
|
if rnd != 0 {
|
||||||
|
lo2 |= 1;
|
||||||
|
}
|
||||||
|
let hi2 = ((hi >> 3) & 0x000f_ffff) | (exponent << 20) | (hi & 0x8000_0000);
|
||||||
|
Some(f64::from_bits(((hi2 as u64) << 32) | lo2 as u64))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The file is a V7 design file: it opens with a design file header.
|
||||||
|
pub fn is_v7(bytes: &[u8]) -> bool {
|
||||||
|
bytes.len() > 1256 && (bytes[1] & 0x7f) == 9 && (bytes[0] & 0x3f) == 8
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Units {
|
||||||
|
uor_per_sub: f64,
|
||||||
|
sub_per_master: f64,
|
||||||
|
origin: [f64; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
fn units(b: &[u8]) -> Option<Units> {
|
||||||
|
let sub_per_master = int32(b, 1112)? as f64;
|
||||||
|
let uor_per_sub = int32(b, 1116)? as f64;
|
||||||
|
let origin = [vax_double(b, 1240)?, vax_double(b, 1248)?];
|
||||||
|
(sub_per_master > 0.0 && uor_per_sub > 0.0).then(|| Units {
|
||||||
|
uor_per_sub,
|
||||||
|
sub_per_master,
|
||||||
|
origin,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The element records, in file order.
|
||||||
|
fn elements(b: &[u8]) -> Vec<&[u8]> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut at = 0usize;
|
||||||
|
while at + 4 <= b.len() && !(b[at] == 0xff && b[at + 1] == 0xff) {
|
||||||
|
let Some(words) = uint16(b, at + 2) else { break };
|
||||||
|
let len = 4 + words as usize * 2;
|
||||||
|
let Some(e) = b.get(at..at + len) else { break };
|
||||||
|
out.push(e);
|
||||||
|
at += len;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The file's colour table (element type 5 on level 1): the colour of
|
||||||
|
/// index i is at 41 + 3i.
|
||||||
|
fn color_table(elements: &[&[u8]]) -> Option<Vec<[u8; 3]>> {
|
||||||
|
elements.iter().find_map(|e| {
|
||||||
|
(e[1] & 0x7f == 5 && e[0] & 0x3f == 1 && e.len() >= 41 + 765).then(|| {
|
||||||
|
(0..256)
|
||||||
|
.map(|i| {
|
||||||
|
let at = 41 + i * 3;
|
||||||
|
e.get(at..at + 3).map(|c| [c[0], c[1], c[2]]).unwrap_or([255, 255, 255])
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A usable default: the first eight DGN colours, then greys.
|
||||||
|
// ponytail: The full DGN default table is not reproduced; the files
|
||||||
|
// the reference writes carry their own colour table.
|
||||||
|
fn default_colors() -> Vec<[u8; 3]> {
|
||||||
|
let base: [[u8; 3]; 8] = [
|
||||||
|
[255, 255, 255],
|
||||||
|
[0, 0, 255],
|
||||||
|
[0, 255, 0],
|
||||||
|
[255, 0, 0],
|
||||||
|
[255, 255, 0],
|
||||||
|
[255, 0, 255],
|
||||||
|
[255, 127, 0],
|
||||||
|
[0, 255, 255],
|
||||||
|
];
|
||||||
|
(0..256).map(|i| if i < 8 { base[i] } else { [i as u8; 3] }).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Model names of a V7 file (it has one).
|
||||||
|
pub fn model_names(bytes: &[u8]) -> Option<Vec<String>> {
|
||||||
|
is_v7(bytes).then(|| vec!["Default".to_string()])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fill colour index of an element's fill linkage (id 0x41), when it has one.
|
||||||
|
fn fill_index(e: &[u8]) -> Option<u8> {
|
||||||
|
// The linkages follow the attribute index's word; the first one starts
|
||||||
|
// at the first word flagged as user data.
|
||||||
|
let base = 32 + uint16(e, 30)? as usize * 2;
|
||||||
|
let mut at = (base..base + 16).step_by(2).find(|&at| {
|
||||||
|
uint16(e, at).is_some_and(|h| h & 0x1000 != 0 && (1..=64).contains(&(h & 0xff)))
|
||||||
|
})?;
|
||||||
|
while at + 4 <= e.len() {
|
||||||
|
let header = uint16(e, at)?;
|
||||||
|
let words = (header & 0x00ff) as usize;
|
||||||
|
if header & 0x1000 == 0 || words == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if uint16(e, at + 2)? == 0x41 {
|
||||||
|
return e.get(at + 8).copied();
|
||||||
|
}
|
||||||
|
at += (words + 1) * 2;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Geometry in raw UORs, mapped to sheet units when placed.
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct Raw {
|
||||||
|
paths: Vec<Path>,
|
||||||
|
texts: Vec<Text>,
|
||||||
|
instances: Vec<Instance>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Instance {
|
||||||
|
name: String,
|
||||||
|
/// x' = m[0]·x + m[1]·y + origin.x, y' = m[2]·x + m[3]·y + origin.y.
|
||||||
|
m: [f64; 4],
|
||||||
|
origin: [f64; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a run of elements into raw geometry: plain elements, complex
|
||||||
|
/// chains and shapes (joined), B-splines, text and shared cell instances.
|
||||||
|
fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
||||||
|
let mut raw = Raw::default();
|
||||||
|
let mut spline: Option<(usize, usize, [u8; 3], f64)> = None;
|
||||||
|
let mut knots: Vec<f64> = Vec::new();
|
||||||
|
// A complex shape or chain collects its components into one path.
|
||||||
|
let mut complex: Option<(usize, PathBuilder, Option<([u8; 3], f64)>, Option<[u8; 3]>, bool)> = None;
|
||||||
|
let push = |raw: &mut Raw, complex: &mut Option<(usize, PathBuilder, Option<([u8; 3], f64)>, Option<[u8; 3]>, bool)>, path: Path| {
|
||||||
|
if let Some((left, pb, _, _, _)) = complex.as_mut() {
|
||||||
|
for sp in &path.subpaths {
|
||||||
|
for seg in &sp.segments {
|
||||||
|
match seg {
|
||||||
|
Segment::Line(a, b) => {
|
||||||
|
if pb.current() != Some(*a) {
|
||||||
|
if pb.current().is_none() { pb.move_to(*a) } else { pb.line_to(*a) }
|
||||||
|
}
|
||||||
|
pb.line_to(*b);
|
||||||
|
}
|
||||||
|
Segment::Cubic(a, c1, c2, b) => {
|
||||||
|
if pb.current() != Some(*a) {
|
||||||
|
if pb.current().is_none() { pb.move_to(*a) } else { pb.line_to(*a) }
|
||||||
|
}
|
||||||
|
pb.cubic_to(*c1, *c2, *b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*left = left.saturating_sub(1);
|
||||||
|
if *left == 0 {
|
||||||
|
let (_, mut pb, stroke, fill, closed) = complex.take().unwrap();
|
||||||
|
if closed {
|
||||||
|
pb.close();
|
||||||
|
}
|
||||||
|
raw.paths.push(Path { subpaths: pb.finish(), stroke, fill });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
raw.paths.push(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for e in run {
|
||||||
|
let kind = e[1] & 0x7f;
|
||||||
|
if e[1] & 0x80 != 0 || e.len() < 36 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let symbology = uint16(e, 34).unwrap_or(0);
|
||||||
|
let color = colors[(symbology >> 8) as usize];
|
||||||
|
let weight = ((symbology & 0xf8) >> 3) as f64;
|
||||||
|
// Weights are screen pixels: a negative width asks for pixels.
|
||||||
|
let stroke = Some((color, -(weight * 2.0 + 1.0)));
|
||||||
|
let fill = fill_index(e).map(|i| colors[i as usize]);
|
||||||
|
let int_pt = |i: usize| -> Option<[f64; 2]> { Some([int32(e, i)? as f64, int32(e, i + 4)? as f64]) };
|
||||||
|
match kind {
|
||||||
|
12 | 14 => {
|
||||||
|
let n = uint16(e, 38).unwrap_or(0) as usize;
|
||||||
|
if n > 0 {
|
||||||
|
complex = Some((n, PathBuilder::default(), stroke, if kind == 14 { fill } else { None }, kind == 14));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
if let (Some(a), Some(b)) = (int_pt(36), int_pt(44)) {
|
||||||
|
push(&mut raw, &mut complex, Path { subpaths: vec![SubPath { segments: vec![Segment::Line(a, b)], closed: false }], stroke, fill: None });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
4 | 6 | 11 => {
|
||||||
|
let n = uint16(e, 36).unwrap_or(0) as usize;
|
||||||
|
let pts: Vec<[f64; 2]> = (0..n).filter_map(|k| int_pt(38 + k * 8)).collect();
|
||||||
|
if pts.len() >= 2 {
|
||||||
|
push(&mut raw, &mut complex, polyline(&pts, kind == 6, stroke, if kind == 6 { fill } else { None }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
15 | 16 => {
|
||||||
|
let arc = if kind == 15 {
|
||||||
|
(|| Some((vax_double(e, 36)?, vax_double(e, 44)?, int32(e, 52)?, vax_double(e, 56)?, vax_double(e, 64)?, 0.0, 360.0)))()
|
||||||
|
} else {
|
||||||
|
(|| {
|
||||||
|
let sw = int32(e, 40)? as u32;
|
||||||
|
let mut sweep = if sw & 0x8000_0000 != 0 { -((sw & 0x7fff_ffff) as f64) } else { sw as f64 } / 360000.0;
|
||||||
|
if sweep == 0.0 {
|
||||||
|
sweep = 360.0;
|
||||||
|
}
|
||||||
|
Some((vax_double(e, 44)?, vax_double(e, 52)?, int32(e, 60)?, vax_double(e, 64)?, vax_double(e, 72)?, int32(e, 36)? as f64 / 360000.0, sweep))
|
||||||
|
})()
|
||||||
|
};
|
||||||
|
let Some((a, b, r, ox, oy, start, sweep)) = arc else { continue };
|
||||||
|
let pieces = arc_cubics([ox, oy], a, b, (r as f64 / 360000.0).to_radians(), start.to_radians(), sweep.to_radians());
|
||||||
|
let closed = sweep.abs() >= 360.0;
|
||||||
|
push(
|
||||||
|
&mut raw,
|
||||||
|
&mut complex,
|
||||||
|
Path {
|
||||||
|
subpaths: vec![SubPath { segments: pieces.into_iter().map(|[a, b, c, d]| Segment::Cubic(a, b, c, d)).collect(), closed }],
|
||||||
|
stroke,
|
||||||
|
fill: if closed { fill } else { None },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
17 => {
|
||||||
|
let (Some(width), Some(height), Some(rotation), Some(origin)) = (int32(e, 38), int32(e, 42), int32(e, 46), int_pt(50)) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let n = e.get(58).copied().unwrap_or(0) as usize;
|
||||||
|
let Some(chars) = e.get(60..60 + n) else { continue };
|
||||||
|
let (height, width) = (height as f64 * 6.0 / 1000.0, width as f64 * 6.0 / 1000.0);
|
||||||
|
raw.texts.push(Text {
|
||||||
|
text: String::from_utf8_lossy(chars).trim_end_matches('\0').to_string(),
|
||||||
|
origin,
|
||||||
|
height,
|
||||||
|
width_factor: if height > 0.0 { width / height } else { 1.0 },
|
||||||
|
rotation: (rotation as f64 / 360000.0).to_radians(),
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
27 => {
|
||||||
|
// B-spline curve header: order, pole count.
|
||||||
|
let order = ((e.get(40).copied().unwrap_or(0) & 0x0f) + 2) as usize;
|
||||||
|
let poles = uint16(e, 42).unwrap_or(0) as usize;
|
||||||
|
spline = Some((order, poles, color, -(weight * 2.0 + 1.0)));
|
||||||
|
knots.clear();
|
||||||
|
}
|
||||||
|
26 => {
|
||||||
|
// Interior knots as fractions of the parameter range.
|
||||||
|
let n = spline.map(|s| s.1.saturating_sub(s.0)).unwrap_or(0);
|
||||||
|
knots = (0..n).filter_map(|k| int32(e, 36 + k * 4)).map(|v| v as u32 as f64 / 2_147_483_648.0).collect();
|
||||||
|
}
|
||||||
|
21 => {
|
||||||
|
let n = uint16(e, 36).unwrap_or(0) as usize;
|
||||||
|
let poles: Vec<[f64; 2]> = (0..n).filter_map(|k| int_pt(38 + k * 8)).collect();
|
||||||
|
if let Some((order, count, color, width)) = spline.take() {
|
||||||
|
if poles.len() == count && count >= order {
|
||||||
|
let interior = if knots.len() == count - order {
|
||||||
|
knots.clone()
|
||||||
|
} else {
|
||||||
|
(1..=count - order).map(|k| k as f64 / (count - order + 1) as f64).collect()
|
||||||
|
};
|
||||||
|
let mut full = vec![0.0; order];
|
||||||
|
full.extend(interior);
|
||||||
|
full.extend(vec![1.0; order]);
|
||||||
|
let pts = bspline_points(order, &poles, &full, 24 * count);
|
||||||
|
push(&mut raw, &mut complex, polyline(&pts, false, Some((color, width)), None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
35 => {
|
||||||
|
// Shared cell instance: the definition's name, a 2×2 matrix
|
||||||
|
// and the origin.
|
||||||
|
let name = e.get(164..180).map(|n| String::from_utf8_lossy(n).trim_end_matches('\0').trim().to_string()).unwrap_or_default();
|
||||||
|
let (Some(m0), Some(m1), Some(m2), Some(m3), Some(ox), Some(oy)) =
|
||||||
|
(vax_double(e, 76), vax_double(e, 84), vax_double(e, 92), vax_double(e, 100), le_i32(e, 148), le_i32(e, 152))
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
raw.instances.push(Instance { name, m: [m0, m1, m2, m3], origin: [ox as f64, oy as f64] });
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raw
|
||||||
|
}
|
||||||
|
|
||||||
|
fn polyline(pts: &[[f64; 2]], closed: bool, stroke: Option<([u8; 3], f64)>, fill: Option<[u8; 3]>) -> Path {
|
||||||
|
let mut pb = PathBuilder::default();
|
||||||
|
pb.move_to(pts[0]);
|
||||||
|
for p in &pts[1..] {
|
||||||
|
pb.line_to(*p);
|
||||||
|
}
|
||||||
|
if closed {
|
||||||
|
pb.close();
|
||||||
|
}
|
||||||
|
Path { subpaths: pb.finish(), stroke, fill }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// x' = a·x + b·y + e, y' = c·x + d·y + f.
|
||||||
|
type Affine = [f64; 6];
|
||||||
|
|
||||||
|
fn apply(m: &Affine, p: [f64; 2]) -> [f64; 2] {
|
||||||
|
[m[0] * p[0] + m[1] * p[1] + m[4], m[2] * p[0] + m[3] * p[1] + m[5]]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compose(outer: &Affine, inner: &Affine) -> Affine {
|
||||||
|
[
|
||||||
|
outer[0] * inner[0] + outer[1] * inner[2],
|
||||||
|
outer[0] * inner[1] + outer[1] * inner[3],
|
||||||
|
outer[2] * inner[0] + outer[3] * inner[2],
|
||||||
|
outer[2] * inner[1] + outer[3] * inner[3],
|
||||||
|
outer[0] * inner[4] + outer[1] * inner[5] + outer[4],
|
||||||
|
outer[2] * inner[4] + outer[3] * inner[5] + outer[5],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Places raw geometry (and the shared cells it instances) through `m`.
|
||||||
|
fn place(raw: &Raw, m: &Affine, defs: &HashMap<String, Raw>, depth: usize, out: &mut Sheet) {
|
||||||
|
let map = |p: [f64; 2]| apply(m, p);
|
||||||
|
for path in &raw.paths {
|
||||||
|
out.paths.push(Path {
|
||||||
|
subpaths: path
|
||||||
|
.subpaths
|
||||||
|
.iter()
|
||||||
|
.map(|sp| SubPath {
|
||||||
|
segments: sp
|
||||||
|
.segments
|
||||||
|
.iter()
|
||||||
|
.map(|s| match s {
|
||||||
|
Segment::Line(a, b) => Segment::Line(map(*a), map(*b)),
|
||||||
|
Segment::Cubic(a, b, c, d) => Segment::Cubic(map(*a), map(*b), map(*c), map(*d)),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
closed: sp.closed,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
stroke: path.stroke,
|
||||||
|
fill: path.fill,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let scale = (m[0] * m[3] - m[1] * m[2]).abs().sqrt();
|
||||||
|
let turn = m[2].atan2(m[0]);
|
||||||
|
for t in &raw.texts {
|
||||||
|
out.texts.push(Text { origin: map(t.origin), height: t.height * scale, rotation: t.rotation + turn, ..t.clone() });
|
||||||
|
}
|
||||||
|
if depth >= 8 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for inst in &raw.instances {
|
||||||
|
let Some(def) = defs.get(&inst.name.to_ascii_uppercase()) else { continue };
|
||||||
|
let local: Affine = [inst.m[0], inst.m[1], inst.m[2], inst.m[3], inst.origin[0], inst.origin[1]];
|
||||||
|
place(def, &compose(m, &local), defs, depth + 1, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The model's geometry, in master units.
|
||||||
|
pub fn model(bytes: &[u8]) -> Option<Sheet> {
|
||||||
|
if !is_v7(bytes) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let u = units(bytes)?;
|
||||||
|
let all = elements(bytes);
|
||||||
|
let colors = color_table(&all).unwrap_or_else(default_colors);
|
||||||
|
// Shared cell definitions (type 34) and their components are drawn only
|
||||||
|
// where instances place them; everything else is the model itself.
|
||||||
|
let mut defs: HashMap<String, Raw> = HashMap::new();
|
||||||
|
let mut top: Vec<&[u8]> = Vec::new();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < all.len() {
|
||||||
|
let e = all[i];
|
||||||
|
if e[1] & 0x7f == 34 && e[0] & 0x80 == 0 {
|
||||||
|
let name = e.get(164..180).map(|n| String::from_utf8_lossy(n).trim_end_matches('\0').trim().to_ascii_uppercase()).unwrap_or_default();
|
||||||
|
let mut j = i + 1;
|
||||||
|
while j < all.len() && all[j][0] & 0x80 != 0 {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
defs.insert(name, read_run(&all[i + 1..j], &colors));
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
top.push(e);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
let raw = read_run(&top, &colors);
|
||||||
|
let per_unit = u.uor_per_sub * u.sub_per_master;
|
||||||
|
let m: Affine = [1.0 / per_unit, 0.0, 0.0, 1.0 / per_unit, -u.origin[0] / per_unit, -u.origin[1] / per_unit];
|
||||||
|
let mut sheet = Sheet { sub_per_master: u.sub_per_master, ..Default::default() };
|
||||||
|
place(&raw, &m, &defs, 0, &mut sheet);
|
||||||
|
sheet.rect = paths_bounds(&sheet.paths).or_else(|| {
|
||||||
|
let t = sheet.texts.first()?;
|
||||||
|
Some([t.origin[0], t.origin[1], t.origin[0] + t.height, t.origin[1] + t.height])
|
||||||
|
})?;
|
||||||
|
Some(sheet)
|
||||||
|
}
|
||||||
@@ -0,0 +1,500 @@
|
|||||||
|
//! DGN V8 design files: an OLE compound file whose models (Dgn-Md/#n)
|
||||||
|
//! keep their graphic elements in a compressed stream (Dgn^G/$1), and whose
|
||||||
|
//! non-model stream (Dgn^Nm/$1) holds the colour table, the levels and the
|
||||||
|
//! shared cell definitions. Element fields are little-endian; coordinates
|
||||||
|
//! are UORs.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use super::model::{arc_cubics, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text};
|
||||||
|
|
||||||
|
// ── Compound file ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
struct Cfb<'a> {
|
||||||
|
b: &'a [u8],
|
||||||
|
sector: usize,
|
||||||
|
fat: Vec<u32>,
|
||||||
|
mini_fat: Vec<u32>,
|
||||||
|
mini: Vec<u8>,
|
||||||
|
cutoff: u32,
|
||||||
|
entries: Vec<Entry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
name: String,
|
||||||
|
kind: u8,
|
||||||
|
left: u32,
|
||||||
|
right: u32,
|
||||||
|
child: u32,
|
||||||
|
start: u32,
|
||||||
|
size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
const END: u32 = 0xFFFF_FFFA;
|
||||||
|
|
||||||
|
fn u32_at(b: &[u8], at: usize) -> Option<u32> {
|
||||||
|
Some(u32::from_le_bytes(b.get(at..at + 4)?.try_into().ok()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Cfb<'a> {
|
||||||
|
fn open(b: &'a [u8]) -> Option<Self> {
|
||||||
|
if !b.starts_with(&[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let sector = 1usize << u16::from_le_bytes([b[30], b[31]]);
|
||||||
|
let mini_sector_shift = u16::from_le_bytes([b[32], b[33]]);
|
||||||
|
if mini_sector_shift != 6 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut difat: Vec<u32> = (0..109).filter_map(|i| u32_at(b, 76 + i * 4)).filter(|&v| v < END).collect();
|
||||||
|
let (mut next, mut count) = (u32_at(b, 68)?, u32_at(b, 72)?);
|
||||||
|
while count > 0 && next < END {
|
||||||
|
let at = (next as usize + 1) * sector;
|
||||||
|
for i in 0..sector / 4 - 1 {
|
||||||
|
if let Some(v) = u32_at(b, at + i * 4).filter(|&v| v < END) {
|
||||||
|
difat.push(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next = u32_at(b, at + sector - 4)?;
|
||||||
|
count -= 1;
|
||||||
|
}
|
||||||
|
let mut fat = Vec::new();
|
||||||
|
for s in difat {
|
||||||
|
let at = (s as usize + 1) * sector;
|
||||||
|
for i in 0..sector / 4 {
|
||||||
|
fat.push(u32_at(b, at + i * 4).unwrap_or(END));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut cfb = Cfb { b, sector, fat, mini_fat: Vec::new(), mini: Vec::new(), cutoff: u32_at(b, 56)?, entries: Vec::new() };
|
||||||
|
let dir = cfb.chain(u32_at(b, 48)?, None);
|
||||||
|
for e in dir.chunks_exact(128) {
|
||||||
|
let len = u16::from_le_bytes([e[64], e[65]]) as usize;
|
||||||
|
let name16: Vec<u16> = e[..len.saturating_sub(2).min(64)].chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect();
|
||||||
|
cfb.entries.push(Entry {
|
||||||
|
name: String::from_utf16_lossy(&name16),
|
||||||
|
kind: e[66],
|
||||||
|
left: u32_at(e, 68)?,
|
||||||
|
right: u32_at(e, 72)?,
|
||||||
|
child: u32_at(e, 76)?,
|
||||||
|
start: u32_at(e, 116)?,
|
||||||
|
size: u32_at(e, 120)?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let root = cfb.entries.first()?;
|
||||||
|
let (root_start, root_size) = (root.start, root.size);
|
||||||
|
cfb.mini = cfb.chain(root_start, Some(root_size as usize));
|
||||||
|
let mini_fat = cfb.chain(u32_at(b, 60)?, None);
|
||||||
|
cfb.mini_fat = mini_fat.chunks_exact(4).map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]])).collect();
|
||||||
|
Some(cfb)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chain(&self, mut s: u32, size: Option<usize>) -> Vec<u8> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut guard = 0;
|
||||||
|
while s < END && guard < 1_000_000 {
|
||||||
|
let at = (s as usize + 1) * self.sector;
|
||||||
|
let Some(block) = self.b.get(at..at + self.sector) else { break };
|
||||||
|
out.extend_from_slice(block);
|
||||||
|
s = self.fat.get(s as usize).copied().unwrap_or(END);
|
||||||
|
guard += 1;
|
||||||
|
}
|
||||||
|
if let Some(n) = size {
|
||||||
|
out.truncate(n);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stream(&self, e: &Entry) -> Vec<u8> {
|
||||||
|
if e.size >= self.cutoff {
|
||||||
|
return self.chain(e.start, Some(e.size as usize));
|
||||||
|
}
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut s = e.start;
|
||||||
|
let mut guard = 0;
|
||||||
|
while s < END && guard < 1_000_000 {
|
||||||
|
let at = s as usize * 64;
|
||||||
|
let Some(block) = self.mini.get(at..at + 64) else { break };
|
||||||
|
out.extend_from_slice(block);
|
||||||
|
s = self.mini_fat.get(s as usize).copied().unwrap_or(END);
|
||||||
|
guard += 1;
|
||||||
|
}
|
||||||
|
out.truncate(e.size as usize);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every stream with its full path ("Dgn-Md/#000000/Dgn^G/$1").
|
||||||
|
fn streams(&self) -> Vec<(String, &Entry)> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
fn walk<'b>(c: &'b Cfb, idx: u32, path: &str, out: &mut Vec<(String, &'b Entry)>, depth: usize) {
|
||||||
|
let Some(e) = c.entries.get(idx as usize) else { return };
|
||||||
|
if depth > 64 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
walk(c, e.left, path, out, depth + 1);
|
||||||
|
let p = if path.is_empty() { e.name.clone() } else { format!("{path}/{}", e.name) };
|
||||||
|
if e.kind == 2 {
|
||||||
|
out.push((p.clone(), e));
|
||||||
|
} else if e.kind == 1 {
|
||||||
|
walk(c, e.child, &p, out, depth + 1);
|
||||||
|
}
|
||||||
|
walk(c, e.right, path, out, depth + 1);
|
||||||
|
}
|
||||||
|
if let Some(root) = self.entries.first() {
|
||||||
|
walk(self, root.child, "", &mut out, 0);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A DGN stream: raw, or zlib data after a 16-byte header.
|
||||||
|
fn inflate(data: &[u8]) -> Vec<u8> {
|
||||||
|
for skip in [16usize, 0] {
|
||||||
|
if let Some(z) = data.get(skip..) {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
if flate2::read::ZlibDecoder::new(z).read_to_end(&mut out).is_ok() && !out.is_empty() {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Elements ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Element records of an element stream (after its 4-byte count).
|
||||||
|
fn elements(d: &[u8]) -> Vec<&[u8]> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut at = 4;
|
||||||
|
while at + 8 <= d.len() {
|
||||||
|
let Some(words) = u32_at(d, at + 4) else { break };
|
||||||
|
let len = 4 + words as usize * 2;
|
||||||
|
if len < 8 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// The stream can end inside its last element (its trailing words are
|
||||||
|
// not stored); what is there is still read.
|
||||||
|
let e = &d[at..(at + len).min(d.len())];
|
||||||
|
out.push(e);
|
||||||
|
at += len;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(e: &[u8]) -> u8 {
|
||||||
|
e[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flags(e: &[u8]) -> u16 {
|
||||||
|
u16::from_le_bytes([e[2], e[3]])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A component of a complex element, cell or shared cell definition.
|
||||||
|
fn is_component(e: &[u8]) -> bool {
|
||||||
|
flags(e) & 0x4000 != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn f64_at(e: &[u8], at: usize) -> Option<f64> {
|
||||||
|
Some(f64::from_le_bytes(e.get(at..at + 8)?.try_into().ok()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// A string stored after the "ff fe 01 00" marker, when the element has one.
|
||||||
|
fn marked_string(e: &[u8], from: usize) -> Option<String> {
|
||||||
|
let i = e.get(from..)?.windows(4).position(|w| w == [0xff, 0xfe, 0x01, 0x00])? + from + 4;
|
||||||
|
let end = e[i..].iter().position(|&c| c == 0 || c < 0x20).map(|n| i + n).unwrap_or(e.len());
|
||||||
|
Some(String::from_utf8_lossy(&e[i..end]).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Styles {
|
||||||
|
colors: Vec<[u8; 3]>,
|
||||||
|
/// Level id → colour index.
|
||||||
|
levels: HashMap<u32, u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Styles {
|
||||||
|
fn color(&self, e: &[u8]) -> [u8; 3] {
|
||||||
|
let explicit = u32_at(e, 44).unwrap_or(u32::MAX);
|
||||||
|
let index = if explicit < 256 {
|
||||||
|
explicit
|
||||||
|
} else {
|
||||||
|
self.levels.get(&u32_at(e, 12).unwrap_or(0)).copied().unwrap_or(0)
|
||||||
|
};
|
||||||
|
self.colors.get(index as usize).copied().unwrap_or([255, 255, 255])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Geometry in raw UORs, placed through a transform.
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct Raw {
|
||||||
|
paths: Vec<Path>,
|
||||||
|
texts: Vec<Text>,
|
||||||
|
instances: Vec<Instance>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Instance {
|
||||||
|
name: String,
|
||||||
|
m: [f64; 4],
|
||||||
|
origin: [f64; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
|
||||||
|
let mut raw = Raw::default();
|
||||||
|
// A complex shape or chain collects its components into one path.
|
||||||
|
let mut complex: Option<(usize, PathBuilder, Option<([u8; 3], f64)>, Option<[u8; 3]>, bool)> = None;
|
||||||
|
fn push(raw: &mut Raw, complex: &mut Option<(usize, PathBuilder, Option<([u8; 3], f64)>, Option<[u8; 3]>, bool)>, path: Path) {
|
||||||
|
if let Some((left, pb, _, _, _)) = complex.as_mut() {
|
||||||
|
for sp in &path.subpaths {
|
||||||
|
for seg in &sp.segments {
|
||||||
|
let (a, b) = match seg {
|
||||||
|
Segment::Line(a, b) | Segment::Cubic(a, _, _, b) => (*a, *b),
|
||||||
|
};
|
||||||
|
if pb.current() != Some(a) {
|
||||||
|
if pb.current().is_none() { pb.move_to(a) } else { pb.line_to(a) }
|
||||||
|
}
|
||||||
|
match seg {
|
||||||
|
Segment::Line(..) => pb.line_to(b),
|
||||||
|
Segment::Cubic(_, c1, c2, _) => pb.cubic_to(*c1, *c2, b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*left = left.saturating_sub(1);
|
||||||
|
if *left == 0 {
|
||||||
|
let (_, mut pb, stroke, fill, closed) = complex.take().unwrap();
|
||||||
|
if closed {
|
||||||
|
pb.close();
|
||||||
|
}
|
||||||
|
raw.paths.push(Path { subpaths: pb.finish(), stroke, fill });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
raw.paths.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for e in run {
|
||||||
|
if e.len() < 104 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let color = styles.color(e);
|
||||||
|
let stroke = Some((color, -1.0));
|
||||||
|
let p2 = |at: usize| -> Option<[f64; 2]> { Some([f64_at(e, at)?, f64_at(e, at + 8)?]) };
|
||||||
|
match kind(e) {
|
||||||
|
12 | 14 => {
|
||||||
|
let n = u32_at(e, 104).unwrap_or(0) as usize;
|
||||||
|
if n > 0 {
|
||||||
|
let fill = (kind(e) == 14 && e.windows(4).any(|w| w == [0x07, 0x10, 0x41, 0x00])).then_some(color);
|
||||||
|
complex = Some((n, PathBuilder::default(), stroke, fill, kind(e) == 14));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
if let (Some(a), Some(b)) = (p2(104), p2(120)) {
|
||||||
|
push(&mut raw, &mut complex, Path { subpaths: vec![SubPath { segments: vec![Segment::Line(a, b)], closed: false }], stroke, fill: None });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
4 | 6 | 11 => {
|
||||||
|
let n = u32_at(e, 104).unwrap_or(0) as usize;
|
||||||
|
let pts: Vec<[f64; 2]> = (0..n).filter_map(|k| p2(112 + k * 16)).collect();
|
||||||
|
if pts.len() >= 2 {
|
||||||
|
let mut pb = PathBuilder::default();
|
||||||
|
pb.move_to(pts[0]);
|
||||||
|
for p in &pts[1..] {
|
||||||
|
pb.line_to(*p);
|
||||||
|
}
|
||||||
|
if kind(e) == 6 {
|
||||||
|
pb.close();
|
||||||
|
}
|
||||||
|
push(&mut raw, &mut complex, Path { subpaths: pb.finish(), stroke, fill: None });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
15 | 16 => {
|
||||||
|
let arc = if kind(e) == 15 {
|
||||||
|
(|| Some((f64_at(e, 104)?, f64_at(e, 112)?, f64_at(e, 120)?, p2(128)?, 0.0, std::f64::consts::TAU)))()
|
||||||
|
} else {
|
||||||
|
(|| Some((f64_at(e, 120)?, f64_at(e, 128)?, f64_at(e, 136)?, p2(144)?, f64_at(e, 104)?, f64_at(e, 112)?)))()
|
||||||
|
};
|
||||||
|
let Some((a, b, rot, c, start, sweep)) = arc else { continue };
|
||||||
|
let pieces = arc_cubics(c, a, b, rot, start, if sweep == 0.0 { std::f64::consts::TAU } else { sweep });
|
||||||
|
let closed = sweep.abs() >= std::f64::consts::TAU - 1e-9 || sweep == 0.0;
|
||||||
|
push(
|
||||||
|
&mut raw,
|
||||||
|
&mut complex,
|
||||||
|
Path { subpaths: vec![SubPath { segments: pieces.into_iter().map(|[a, b, c, d]| Segment::Cubic(a, b, c, d)).collect(), closed }], stroke, fill: None },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
17 => {
|
||||||
|
let (Some(width), Some(height), Some(rotation), Some(origin)) = (f64_at(e, 112), f64_at(e, 120), f64_at(e, 144), p2(152)) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// The text: a byte count (with its 4-byte marker) and the
|
||||||
|
// characters after the marker.
|
||||||
|
let count = u16::from_le_bytes([e[110], e[111]]) as usize;
|
||||||
|
let Some(chars) = e.get(174..170 + count.max(4)) else { continue };
|
||||||
|
let text = String::from_utf8_lossy(chars).trim_end_matches(char::from(0)).to_string();
|
||||||
|
let (height, width) = (height * 6.0 / 1000.0, width * 6.0 / 1000.0);
|
||||||
|
raw.texts.push(Text { text, origin, height, width_factor: if height > 0.0 { width / height } else { 1.0 }, rotation, color });
|
||||||
|
}
|
||||||
|
35 => {
|
||||||
|
let (Some(m0), Some(m1), Some(m3), Some(m4), Some(origin)) = (f64_at(e, 160), f64_at(e, 168), f64_at(e, 184), f64_at(e, 192), p2(232)) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(name) = marked_string(e, 248) else { continue };
|
||||||
|
raw.instances.push(Instance { name: name.to_ascii_uppercase(), m: [m0, m1, m3, m4], origin });
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A complex element cut short keeps the components it has.
|
||||||
|
if let Some((_, mut pb, stroke, fill, closed)) = complex.take() {
|
||||||
|
if closed {
|
||||||
|
pb.close();
|
||||||
|
}
|
||||||
|
raw.paths.push(Path { subpaths: pb.finish(), stroke, fill });
|
||||||
|
}
|
||||||
|
raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type Affine = [f64; 6];
|
||||||
|
|
||||||
|
fn apply(m: &Affine, p: [f64; 2]) -> [f64; 2] {
|
||||||
|
[m[0] * p[0] + m[1] * p[1] + m[4], m[2] * p[0] + m[3] * p[1] + m[5]]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compose(o: &Affine, i: &Affine) -> Affine {
|
||||||
|
[
|
||||||
|
o[0] * i[0] + o[1] * i[2],
|
||||||
|
o[0] * i[1] + o[1] * i[3],
|
||||||
|
o[2] * i[0] + o[3] * i[2],
|
||||||
|
o[2] * i[1] + o[3] * i[3],
|
||||||
|
o[0] * i[4] + o[1] * i[5] + o[4],
|
||||||
|
o[2] * i[4] + o[3] * i[5] + o[5],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn place(raw: &Raw, m: &Affine, defs: &HashMap<String, Raw>, depth: usize, out: &mut Sheet) {
|
||||||
|
let map = |p: [f64; 2]| apply(m, p);
|
||||||
|
for path in &raw.paths {
|
||||||
|
out.paths.push(Path {
|
||||||
|
subpaths: path
|
||||||
|
.subpaths
|
||||||
|
.iter()
|
||||||
|
.map(|sp| SubPath {
|
||||||
|
segments: sp
|
||||||
|
.segments
|
||||||
|
.iter()
|
||||||
|
.map(|s| match s {
|
||||||
|
Segment::Line(a, b) => Segment::Line(map(*a), map(*b)),
|
||||||
|
Segment::Cubic(a, b, c, d) => Segment::Cubic(map(*a), map(*b), map(*c), map(*d)),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
closed: sp.closed,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
stroke: path.stroke,
|
||||||
|
fill: path.fill,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let scale = (m[0] * m[3] - m[1] * m[2]).abs().sqrt();
|
||||||
|
let turn = m[2].atan2(m[0]);
|
||||||
|
for t in &raw.texts {
|
||||||
|
out.texts.push(Text { origin: map(t.origin), height: t.height * scale, rotation: t.rotation + turn, ..t.clone() });
|
||||||
|
}
|
||||||
|
if depth >= 8 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for inst in &raw.instances {
|
||||||
|
let Some(def) = defs.get(&inst.name) else { continue };
|
||||||
|
let local: Affine = [inst.m[0], inst.m[1], inst.m[2], inst.m[3], inst.origin[0], inst.origin[1]];
|
||||||
|
place(def, &compose(m, &local), defs, depth + 1, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Units of a model header: UORs per master and per sub unit.
|
||||||
|
fn units(header: &[u8]) -> (f64, f64) {
|
||||||
|
// Unit definitions are numerator/denominator pairs per metre: master
|
||||||
|
// (4180/4188), sub (4196/4204), storage (4332/4340), and the UORs per
|
||||||
|
// storage unit (4324).
|
||||||
|
let get = |at: usize| f64_at(header, at).filter(|v| v.is_finite() && *v > 0.0);
|
||||||
|
let (mn, md, sn, sd) = (get(4180), get(4188), get(4196), get(4204));
|
||||||
|
let (uor, stn, std) = (get(4324), get(4332), get(4340));
|
||||||
|
let uor_per_metre = match (uor, stn, std) {
|
||||||
|
(Some(u), Some(n), Some(d)) => u * n / d,
|
||||||
|
_ => 1_000_000.0,
|
||||||
|
};
|
||||||
|
let master = match (mn, md) {
|
||||||
|
(Some(n), Some(d)) => uor_per_metre * d / n,
|
||||||
|
_ => uor_per_metre,
|
||||||
|
};
|
||||||
|
let sub = match (sn, sd) {
|
||||||
|
(Some(n), Some(d)) => uor_per_metre * d / n,
|
||||||
|
_ => master / 1000.0,
|
||||||
|
};
|
||||||
|
(master, sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Models of the file: (name, graphic stream, header stream).
|
||||||
|
fn models(cfb: &Cfb) -> Vec<(String, Vec<u8>, Vec<u8>)> {
|
||||||
|
let streams = cfb.streams();
|
||||||
|
let mut dirs: Vec<String> = streams
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(p, _)| p.strip_suffix("/Dgn^G/$1").map(|d| d.to_string()))
|
||||||
|
.collect();
|
||||||
|
dirs.sort();
|
||||||
|
dirs.into_iter()
|
||||||
|
.map(|dir| {
|
||||||
|
let get = |suffix: &str| streams.iter().find(|(p, _)| *p == format!("{dir}/{suffix}")).map(|(_, e)| inflate(&cfb.stream(e))).unwrap_or_default();
|
||||||
|
let graphics = get("Dgn^G/$1");
|
||||||
|
let header = get("Dgn~Mh");
|
||||||
|
let name = marked_string(&header, 0).unwrap_or_else(|| "Default".to_string());
|
||||||
|
(name, graphics, header)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn model_names(bytes: &[u8]) -> Option<Vec<String>> {
|
||||||
|
let cfb = Cfb::open(bytes)?;
|
||||||
|
let names: Vec<String> = models(&cfb).into_iter().map(|m| m.0).collect();
|
||||||
|
(!names.is_empty()).then_some(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn model(bytes: &[u8], name: &str) -> Option<Sheet> {
|
||||||
|
let cfb = Cfb::open(bytes)?;
|
||||||
|
let all = models(&cfb);
|
||||||
|
let (_, graphics, header) = all.iter().find(|m| m.0.eq_ignore_ascii_case(name)).or_else(|| all.first())?;
|
||||||
|
let non_model = cfb.streams().iter().find(|(p, _)| p == "Dgn^Nm/$1").map(|(_, e)| inflate(&cfb.stream(e))).unwrap_or_default();
|
||||||
|
let nm = elements(&non_model);
|
||||||
|
let colors = nm
|
||||||
|
.iter()
|
||||||
|
.find(|e| kind(e) == 5 && e.len() >= 37 + 768)
|
||||||
|
.map(|e| (0..256).map(|i| [e[37 + i * 3], e[38 + i * 3], e[39 + i * 3]]).collect())
|
||||||
|
.unwrap_or_else(|| vec![[255, 255, 255]; 256]);
|
||||||
|
let levels = nm
|
||||||
|
.iter()
|
||||||
|
.filter(|e| kind(e) == 95 && e.len() >= 80)
|
||||||
|
.filter_map(|e| Some((u32_at(e, 32)?, u32_at(e, 72)?)))
|
||||||
|
.collect();
|
||||||
|
let styles = Styles { colors, levels };
|
||||||
|
// Shared cell definitions: a type-34 element and its components.
|
||||||
|
let mut defs = HashMap::new();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < nm.len() {
|
||||||
|
if kind(nm[i]) == 34 && !is_component(nm[i]) {
|
||||||
|
let name = marked_string(nm[i], 248).unwrap_or_default().to_ascii_uppercase();
|
||||||
|
let mut j = i + 1;
|
||||||
|
while j < nm.len() && is_component(nm[j]) {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
defs.insert(name, read_run(&nm[i + 1..j], &styles));
|
||||||
|
i = j;
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let raw = read_run(&elements(graphics), &styles);
|
||||||
|
let (per_unit, sub_uor) = units(header);
|
||||||
|
let m: Affine = [1.0 / per_unit, 0.0, 0.0, 1.0 / per_unit, 0.0, 0.0];
|
||||||
|
let mut sheet = Sheet { sub_per_master: per_unit / sub_uor, ..Default::default() };
|
||||||
|
place(&raw, &m, &defs, 0, &mut sheet);
|
||||||
|
sheet.rect = paths_bounds(&sheet.paths)?;
|
||||||
|
Some(sheet)
|
||||||
|
}
|
||||||
@@ -0,0 +1,782 @@
|
|||||||
|
//! DWF (6.x, a zip after a "(DWF V06.00)" tag, sheets as W2D streams) and
|
||||||
|
//! DWFx (an XPS package, sheets as fixed pages) underlays: sheet names, and
|
||||||
|
//! each sheet's plotted geometry mapped back to the model units it was
|
||||||
|
//! plotted from, which is how an underlay places it.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::{Cursor, Read};
|
||||||
|
|
||||||
|
use super::model::{arc_cubics, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath};
|
||||||
|
|
||||||
|
/// The package's files, with `\` separators turned into `/`.
|
||||||
|
fn unzip(bytes: &[u8]) -> Option<HashMap<String, Vec<u8>>> {
|
||||||
|
// A DWF 6 package is a zip after a short tag; the archive reader finds
|
||||||
|
// the offset itself.
|
||||||
|
let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).ok()?;
|
||||||
|
let mut files = HashMap::new();
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut file = archive.by_index(i).ok()?;
|
||||||
|
let name = file.name().replace('\\', "/").trim_start_matches('/').to_string();
|
||||||
|
let mut data = Vec::new();
|
||||||
|
file.read_to_end(&mut data).ok()?;
|
||||||
|
files.insert(name, data);
|
||||||
|
}
|
||||||
|
Some(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One plotted sheet of the package.
|
||||||
|
struct SheetEntry {
|
||||||
|
name: String,
|
||||||
|
order: i64,
|
||||||
|
/// The sheet's graphics: a W2D stream or an XPS fixed page.
|
||||||
|
graphics: String,
|
||||||
|
/// Paper millimetres per graphics unit and the paper offset (mm).
|
||||||
|
paper: ([f64; 2], [f64; 2]),
|
||||||
|
paper_height_mm: f64,
|
||||||
|
/// DWFx: the W2X resource holding the model units transform.
|
||||||
|
w2x: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attr<'a>(node: roxmltree::Node<'a, '_>, name: &str) -> Option<&'a str> {
|
||||||
|
node.attributes().find(|a| a.name() == name).map(|a| a.value())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn numbers(text: &str) -> Vec<f64> {
|
||||||
|
text.split(|c: char| c == ',' || c.is_whitespace())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.filter_map(|s| s.parse().ok())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every sheet the package plots, in plot order.
|
||||||
|
fn sheets(files: &HashMap<String, Vec<u8>>) -> Vec<SheetEntry> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for (name, data) in files {
|
||||||
|
if !name.ends_with("descriptor.xml") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Ok(text) = std::str::from_utf8(data) else { continue };
|
||||||
|
let Ok(doc) = roxmltree::Document::parse(text.trim_start_matches('\u{feff}')) else { continue };
|
||||||
|
let page = doc.root_element();
|
||||||
|
if page.tag_name().name() != "Page" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let dir = name.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
|
||||||
|
let resolve = |href: &str| -> String {
|
||||||
|
let href = href.replace('\\', "/");
|
||||||
|
let href = href.split('?').next().unwrap_or("").to_string();
|
||||||
|
if let Some(abs) = href.strip_prefix('/') {
|
||||||
|
abs.to_string()
|
||||||
|
} else if files.contains_key(&href) {
|
||||||
|
href
|
||||||
|
} else {
|
||||||
|
// Relative to the descriptor's section folder or its parent.
|
||||||
|
let joined = format!("{dir}/{href}");
|
||||||
|
if files.contains_key(&joined) {
|
||||||
|
joined
|
||||||
|
} else {
|
||||||
|
let parent = dir.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
|
||||||
|
format!("{parent}/{href}").trim_start_matches('/').to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut graphics = None;
|
||||||
|
let mut paper = ([0.0254 * 1000.0 / 1200.0; 2], [0.0; 2]);
|
||||||
|
let mut w2x = None;
|
||||||
|
let mut paper_height_mm = 0.0;
|
||||||
|
for node in page.descendants() {
|
||||||
|
match node.tag_name().name() {
|
||||||
|
"Paper" => {
|
||||||
|
let scale = match attr(node, "units") {
|
||||||
|
Some("in") => 25.4,
|
||||||
|
_ => 1.0,
|
||||||
|
};
|
||||||
|
paper_height_mm = attr(node, "height").and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.0) * scale;
|
||||||
|
}
|
||||||
|
"GraphicResource" if attr(node, "role") == Some("2d streaming graphics") => {
|
||||||
|
graphics = attr(node, "href").map(resolve);
|
||||||
|
let t = attr(node, "transform").map(numbers).unwrap_or_default();
|
||||||
|
if t.len() == 16 {
|
||||||
|
paper = ([t[0], t[5]], [t[12], t[13]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"Resource" if attr(node, "role") == Some("2d graphics extension") => {
|
||||||
|
w2x = attr(node, "href").map(resolve);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(graphics) = graphics else { continue };
|
||||||
|
out.push(SheetEntry {
|
||||||
|
name: attr(page, "name").unwrap_or("").to_string(),
|
||||||
|
order: attr(page, "plotOrder").and_then(|v| v.parse().ok()).unwrap_or(0),
|
||||||
|
graphics,
|
||||||
|
paper,
|
||||||
|
paper_height_mm,
|
||||||
|
w2x,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
out.sort_by(|a, b| a.order.cmp(&b.order).then(a.name.cmp(&b.name)));
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Names of the package's sheets, in plot order.
|
||||||
|
pub fn sheet_names(bytes: &[u8]) -> Option<Vec<String>> {
|
||||||
|
let files = unzip(bytes)?;
|
||||||
|
let names: Vec<String> = sheets(&files).into_iter().map(|s| s.name).collect();
|
||||||
|
(!names.is_empty()).then_some(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Graphics units → model units: model = (graphics − offset) / scale.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct Units {
|
||||||
|
scale: [f64; 2],
|
||||||
|
offset: [f64; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Units {
|
||||||
|
fn identity() -> Self {
|
||||||
|
Self { scale: [1.0, 1.0], offset: [0.0, 0.0] }
|
||||||
|
}
|
||||||
|
fn model(&self, p: [f64; 2]) -> [f64; 2] {
|
||||||
|
[(p[0] - self.offset[0]) / self.scale[0], (p[1] - self.offset[1]) / self.scale[1]]
|
||||||
|
}
|
||||||
|
fn length(&self, v: f64) -> f64 {
|
||||||
|
v / self.scale[0].abs().max(1e-12)
|
||||||
|
}
|
||||||
|
/// A 4×4 matrix written row by row, translation in the last row.
|
||||||
|
fn from_matrix(m: &[f64]) -> Option<Self> {
|
||||||
|
(m.len() == 16 && m[0] != 0.0 && m[5] != 0.0)
|
||||||
|
.then(|| Self { scale: [m[0], m[5]], offset: [m[12], m[13]] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The named sheet's geometry in model units.
|
||||||
|
pub fn sheet(bytes: &[u8], name: &str) -> Option<Sheet> {
|
||||||
|
let files = unzip(bytes)?;
|
||||||
|
let entries = sheets(&files);
|
||||||
|
let entry = entries
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.name.eq_ignore_ascii_case(name))
|
||||||
|
.or_else(|| (name.is_empty()).then(|| entries.first()).flatten())?;
|
||||||
|
let data = files.get(&entry.graphics)?;
|
||||||
|
let (paths, view) = if entry.graphics.to_ascii_lowercase().ends_with(".w2d") {
|
||||||
|
w2d::read(data)?
|
||||||
|
} else {
|
||||||
|
let w2x = entry.w2x.as_ref().and_then(|p| files.get(p));
|
||||||
|
xps::read(data, w2x.map(|v| v.as_slice()), entry)?
|
||||||
|
};
|
||||||
|
let rect = view.or_else(|| paths_bounds(&paths))?;
|
||||||
|
Some(Sheet { rect, paths, texts: Vec::new(), sub_per_master: 1.0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default colour map of a W2D stream: black, a 6×6×6 colour cube from
|
||||||
|
/// index 10 (red 180 steps, green 30, blue 5), greys after it.
|
||||||
|
fn default_color(index: u8) -> [u8; 3] {
|
||||||
|
let i = index as usize;
|
||||||
|
const LEVELS: [u8; 6] = [0, 51, 102, 153, 204, 255];
|
||||||
|
// ponytail: only the cube is confirmed against plotted files; the entries
|
||||||
|
// around it are a grey ramp until a file uses them.
|
||||||
|
match i {
|
||||||
|
10..=225 => {
|
||||||
|
let c = i - 10;
|
||||||
|
[LEVELS[c / 36], LEVELS[(c / 6) % 6], LEVELS[c % 6]]
|
||||||
|
}
|
||||||
|
0 => [0, 0, 0],
|
||||||
|
1..=9 => {
|
||||||
|
let g = (i * 255 / 10) as u8;
|
||||||
|
[g, g, g]
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let g = ((i - 226) * 255 / 29) as u8;
|
||||||
|
[g, g, g]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod w2d {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
struct Reader<'a> {
|
||||||
|
b: &'a [u8],
|
||||||
|
i: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Reader<'a> {
|
||||||
|
fn u8(&mut self) -> Option<u8> {
|
||||||
|
let v = *self.b.get(self.i)?;
|
||||||
|
self.i += 1;
|
||||||
|
Some(v)
|
||||||
|
}
|
||||||
|
fn i16(&mut self) -> Option<i16> {
|
||||||
|
let v = i16::from_le_bytes(self.b.get(self.i..self.i + 2)?.try_into().ok()?);
|
||||||
|
self.i += 2;
|
||||||
|
Some(v)
|
||||||
|
}
|
||||||
|
fn u16(&mut self) -> Option<u16> {
|
||||||
|
let v = u16::from_le_bytes(self.b.get(self.i..self.i + 2)?.try_into().ok()?);
|
||||||
|
self.i += 2;
|
||||||
|
Some(v)
|
||||||
|
}
|
||||||
|
fn i32(&mut self) -> Option<i32> {
|
||||||
|
let v = i32::from_le_bytes(self.b.get(self.i..self.i + 4)?.try_into().ok()?);
|
||||||
|
self.i += 4;
|
||||||
|
Some(v)
|
||||||
|
}
|
||||||
|
fn count(&mut self) -> Option<usize> {
|
||||||
|
match self.u8()? {
|
||||||
|
0 => Some(self.u16()? as usize + 256),
|
||||||
|
n => Some(n as usize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// An extended ASCII opcode "(Name ...)": its name and body.
|
||||||
|
fn extended(&mut self) -> Option<(String, &'a [u8])> {
|
||||||
|
let start = self.i;
|
||||||
|
let mut depth = 0i32;
|
||||||
|
let mut quote = false;
|
||||||
|
while self.i < self.b.len() {
|
||||||
|
let c = self.b[self.i];
|
||||||
|
self.i += 1;
|
||||||
|
match c {
|
||||||
|
b'\'' => quote = !quote,
|
||||||
|
b'(' if !quote => depth += 1,
|
||||||
|
b')' if !quote => {
|
||||||
|
depth -= 1;
|
||||||
|
if depth == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let body = &self.b[start + 1..self.i.saturating_sub(1)];
|
||||||
|
let name_len = body.iter().position(|c| c.is_ascii_whitespace() || *c == b'(').unwrap_or(body.len());
|
||||||
|
Some((String::from_utf8_lossy(&body[..name_len]).to_string(), &body[name_len..]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "((a b c d)(e f g h)(i j k l)(m n o p))" inside a Units opcode.
|
||||||
|
fn units_of(body: &[u8]) -> Option<Units> {
|
||||||
|
let text = String::from_utf8_lossy(body);
|
||||||
|
let text = text.split_once("((").map(|(_, rest)| rest).unwrap_or(&text);
|
||||||
|
let values: Vec<f64> = text
|
||||||
|
.split(|c: char| c == '(' || c == ')' || c.is_whitespace())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.filter_map(|s| s.parse().ok())
|
||||||
|
.collect();
|
||||||
|
Units::from_matrix(&values[..values.len().min(16)])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Integer points of "(View x,y x,y)" or a Contour's points.
|
||||||
|
fn points_of(body: &[u8]) -> Vec<[f64; 2]> {
|
||||||
|
let text = String::from_utf8_lossy(body);
|
||||||
|
text.split_whitespace()
|
||||||
|
.filter_map(|tok| {
|
||||||
|
let (x, y) = tok.split_once(',')?;
|
||||||
|
Some([x.trim_matches(')').parse().ok()?, y.trim_matches(')').parse().ok()?])
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rect_of(points: &[[f64; 2]]) -> Option<[f64; 4]> {
|
||||||
|
let mut r = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
|
||||||
|
for p in points {
|
||||||
|
r = [r[0].min(p[0]), r[1].min(p[1]), r[2].max(p[0]), r[3].max(p[1])];
|
||||||
|
}
|
||||||
|
(r[0] < r[2] && r[1] < r[3]).then_some(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The stream's geometry in model units, and its plotted view.
|
||||||
|
pub fn read(b: &[u8]) -> Option<(Vec<Path>, Option<[f64; 4]>)> {
|
||||||
|
let mut r = Reader { b, i: 0 };
|
||||||
|
let mut units = Units::identity();
|
||||||
|
let mut view: Option<[f64; 4]> = None;
|
||||||
|
let mut color = [0u8, 0, 0];
|
||||||
|
let mut weight = 0.0f64;
|
||||||
|
let mut visible = true;
|
||||||
|
let mut at = [0f64; 2];
|
||||||
|
let mut raw: Vec<(Vec<SubPath>, [u8; 3], f64, bool)> = Vec::new();
|
||||||
|
let rel = |at: &mut [f64; 2], dx: f64, dy: f64| {
|
||||||
|
at[0] += dx;
|
||||||
|
at[1] += dy;
|
||||||
|
*at
|
||||||
|
};
|
||||||
|
while r.i < b.len() {
|
||||||
|
let op = b[r.i];
|
||||||
|
match op {
|
||||||
|
b' ' | b'\t' | b'\r' | b'\n' => r.i += 1,
|
||||||
|
b'(' => {
|
||||||
|
let (name, body) = r.extended()?;
|
||||||
|
match name.as_str() {
|
||||||
|
"Units" => units = units_of(body).unwrap_or(units),
|
||||||
|
"View" => view = rect_of(&points_of(body)).or(view),
|
||||||
|
"Viewport" if view.is_none() => {
|
||||||
|
view = rect_of(&points_of(body));
|
||||||
|
if let Some(u) = units_of(body) {
|
||||||
|
units = u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"Color" => {
|
||||||
|
let v = super::numbers(&String::from_utf8_lossy(body));
|
||||||
|
if v.len() >= 3 {
|
||||||
|
color = [v[0] as u8, v[1] as u8, v[2] as u8];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b'{' => {
|
||||||
|
r.i += 1;
|
||||||
|
let size = r.i32()?;
|
||||||
|
r.i = r.i.checked_add(size.max(0) as usize)?;
|
||||||
|
}
|
||||||
|
b'V' => {
|
||||||
|
visible = true;
|
||||||
|
r.i += 1;
|
||||||
|
}
|
||||||
|
b'v' => {
|
||||||
|
visible = false;
|
||||||
|
r.i += 1;
|
||||||
|
}
|
||||||
|
b'c' => {
|
||||||
|
r.i += 1;
|
||||||
|
color = default_color(r.u8()?);
|
||||||
|
}
|
||||||
|
0x03 => {
|
||||||
|
r.i += 1;
|
||||||
|
let (cr, cg, cb, _ca) = (r.u8()?, r.u8()?, r.u8()?, r.u8()?);
|
||||||
|
color = [cr, cg, cb];
|
||||||
|
}
|
||||||
|
0x17 => {
|
||||||
|
r.i += 1;
|
||||||
|
weight = r.i32()? as f64;
|
||||||
|
}
|
||||||
|
0x0C | b'l' => {
|
||||||
|
r.i += 1;
|
||||||
|
let wide = op == b'l';
|
||||||
|
let mut pb = PathBuilder::default();
|
||||||
|
for k in 0..2 {
|
||||||
|
let (dx, dy) = if wide { (r.i32()? as f64, r.i32()? as f64) } else { (r.i16()? as f64, r.i16()? as f64) };
|
||||||
|
let p = rel(&mut at, dx, dy);
|
||||||
|
if k == 0 { pb.move_to(p) } else { pb.line_to(p) }
|
||||||
|
}
|
||||||
|
raw.push((pb.finish(), color, weight, visible));
|
||||||
|
}
|
||||||
|
0x10 | b'p' => {
|
||||||
|
r.i += 1;
|
||||||
|
let wide = op == b'p';
|
||||||
|
let n = r.count()?;
|
||||||
|
let mut pb = PathBuilder::default();
|
||||||
|
for k in 0..n {
|
||||||
|
let (dx, dy) = if wide { (r.i32()? as f64, r.i32()? as f64) } else { (r.i16()? as f64, r.i16()? as f64) };
|
||||||
|
let p = rel(&mut at, dx, dy);
|
||||||
|
if k == 0 { pb.move_to(p) } else { pb.line_to(p) }
|
||||||
|
}
|
||||||
|
raw.push((pb.finish(), color, weight, visible));
|
||||||
|
}
|
||||||
|
0x14 | b't' => {
|
||||||
|
r.i += 1;
|
||||||
|
let wide = op == b't';
|
||||||
|
let n = r.count()?;
|
||||||
|
let mut pts = Vec::with_capacity(n);
|
||||||
|
for _ in 0..n {
|
||||||
|
let (dx, dy) = if wide { (r.i32()? as f64, r.i32()? as f64) } else { (r.i16()? as f64, r.i16()? as f64) };
|
||||||
|
pts.push(rel(&mut at, dx, dy));
|
||||||
|
}
|
||||||
|
// A triangle strip: each point closes a triangle with the
|
||||||
|
// two before it.
|
||||||
|
let subpaths = pts
|
||||||
|
.windows(3)
|
||||||
|
.map(|t| SubPath {
|
||||||
|
segments: vec![Segment::Line(t[0], t[1]), Segment::Line(t[1], t[2]), Segment::Line(t[2], t[0])],
|
||||||
|
closed: true,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
raw.push((subpaths, color, -1.0, visible));
|
||||||
|
}
|
||||||
|
0x92 => {
|
||||||
|
// Circle or circular arc: relative centre, radius, start
|
||||||
|
// and end in 1/65536 turns (an end of 0 is a full turn).
|
||||||
|
r.i += 1;
|
||||||
|
let c = rel(&mut at, r.i32()? as f64, r.i32()? as f64);
|
||||||
|
let radius = r.i32()? as f64;
|
||||||
|
let (s, e) = (r.u16()? as f64, r.u16()? as f64);
|
||||||
|
let turn = std::f64::consts::TAU / 65536.0;
|
||||||
|
let start = s * turn;
|
||||||
|
let mut sweep = (e - s) * turn;
|
||||||
|
if e == 0.0 && s == 0.0 || sweep == 0.0 {
|
||||||
|
sweep = std::f64::consts::TAU;
|
||||||
|
} else if sweep < 0.0 {
|
||||||
|
sweep += std::f64::consts::TAU;
|
||||||
|
}
|
||||||
|
raw.push((arc_path(c, radius, radius, 0.0, start, sweep), color, weight, visible));
|
||||||
|
}
|
||||||
|
b'e' | b'E' => {
|
||||||
|
// Ellipse: relative centre, major and minor radius, start
|
||||||
|
// and end (1/65536 turns) and tilt.
|
||||||
|
r.i += 1;
|
||||||
|
let c = rel(&mut at, r.i32()? as f64, r.i32()? as f64);
|
||||||
|
let (major, minor) = (r.i32()? as f64, r.i32()? as f64);
|
||||||
|
let (s, e, tilt) = (r.u16()? as f64, r.u16()? as f64, r.u16()? as f64);
|
||||||
|
let turn = std::f64::consts::TAU / 65536.0;
|
||||||
|
let mut sweep = (e - s) * turn;
|
||||||
|
if sweep <= 0.0 {
|
||||||
|
sweep += std::f64::consts::TAU;
|
||||||
|
}
|
||||||
|
let fill = op == b'E';
|
||||||
|
raw.push((arc_path(c, major, minor, tilt * turn, s * turn, sweep), color, if fill { -1.0 } else { weight }, visible));
|
||||||
|
}
|
||||||
|
b'x' => {
|
||||||
|
// Text: relative position and a string (drawn as the
|
||||||
|
// outlines that follow it, so only the position counts).
|
||||||
|
r.i += 1;
|
||||||
|
rel(&mut at, r.i32()? as f64, r.i32()? as f64);
|
||||||
|
skip_string(&mut r)?;
|
||||||
|
}
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let paths = raw
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_, _, _, visible)| *visible)
|
||||||
|
.map(|(subpaths, color, weight, _)| {
|
||||||
|
let subpaths = subpaths.into_iter().map(|sp| map_subpath(sp, |p| units.model(p))).collect();
|
||||||
|
if weight < 0.0 {
|
||||||
|
Path { subpaths, stroke: None, fill: Some(color) }
|
||||||
|
} else {
|
||||||
|
Path { subpaths, stroke: Some((color, units.length(weight))), fill: None }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let view = view.map(|v| {
|
||||||
|
let a = units.model([v[0], v[1]]);
|
||||||
|
let b = units.model([v[2], v[3]]);
|
||||||
|
[a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])]
|
||||||
|
});
|
||||||
|
Some((paths, view))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A W2D string: quoted ASCII, or a count followed by UTF-16 units.
|
||||||
|
fn skip_string(r: &mut Reader) -> Option<()> {
|
||||||
|
if r.b.get(r.i) == Some(&b'\'') {
|
||||||
|
r.i += 1;
|
||||||
|
while *r.b.get(r.i)? != b'\'' {
|
||||||
|
r.i += 1;
|
||||||
|
}
|
||||||
|
r.i += 1;
|
||||||
|
} else {
|
||||||
|
let n = r.count()?;
|
||||||
|
r.i += n * 2;
|
||||||
|
}
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arc_path(c: [f64; 2], rx: f64, ry: f64, rot: f64, start: f64, sweep: f64) -> Vec<SubPath> {
|
||||||
|
let pieces = arc_cubics(c, rx, ry, rot, start, sweep);
|
||||||
|
let closed = (sweep.abs() - std::f64::consts::TAU).abs() < 1e-9;
|
||||||
|
vec![SubPath { segments: pieces.into_iter().map(|[a, b, cc, d]| Segment::Cubic(a, b, cc, d)).collect(), closed }]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_subpath(sp: SubPath, f: impl Fn([f64; 2]) -> [f64; 2]) -> SubPath {
|
||||||
|
SubPath {
|
||||||
|
segments: sp
|
||||||
|
.segments
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| match s {
|
||||||
|
Segment::Line(a, b) => Segment::Line(f(a), f(b)),
|
||||||
|
Segment::Cubic(a, b, c, d) => Segment::Cubic(f(a), f(b), f(c), f(d)),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
closed: sp.closed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod xps {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Affine a, b, c, d, e, f: x' = a·x + c·y + e, y' = b·x + d·y + f.
|
||||||
|
type Affine = [f64; 6];
|
||||||
|
|
||||||
|
fn mul(outer: Affine, inner: Affine) -> Affine {
|
||||||
|
let [a, b, c, d, e, f] = outer;
|
||||||
|
let [a2, b2, c2, d2, e2, f2] = inner;
|
||||||
|
[
|
||||||
|
a * a2 + c * b2,
|
||||||
|
b * a2 + d * b2,
|
||||||
|
a * c2 + c * d2,
|
||||||
|
b * c2 + d * d2,
|
||||||
|
a * e2 + c * f2 + e,
|
||||||
|
b * e2 + d * f2 + f,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(m: Affine, p: [f64; 2]) -> [f64; 2] {
|
||||||
|
[m[0] * p[0] + m[2] * p[1] + m[4], m[1] * p[0] + m[3] * p[1] + m[5]]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_affine(text: &str) -> Option<Affine> {
|
||||||
|
let v = numbers(text);
|
||||||
|
(v.len() == 6).then(|| [v[0], v[1], v[2], v[3], v[4], v[5]])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_color(text: &str) -> Option<[u8; 3]> {
|
||||||
|
let hex = text.trim().strip_prefix('#')?;
|
||||||
|
let hex = if hex.len() == 8 { &hex[2..] } else { hex };
|
||||||
|
(hex.len() == 6).then(|| {
|
||||||
|
let v = u32::from_str_radix(hex, 16).unwrap_or(0);
|
||||||
|
[(v >> 16) as u8, (v >> 8) as u8, v as u8]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The fixed page's paths in model units, and the plotted view.
|
||||||
|
pub fn read(page: &[u8], w2x: Option<&[u8]>, entry: &SheetEntry) -> Option<(Vec<Path>, Option<[f64; 4]>)> {
|
||||||
|
let mut units = Units::identity();
|
||||||
|
let mut view_log = None;
|
||||||
|
if let Some(w2x) = w2x.and_then(|b| std::str::from_utf8(b).ok()) {
|
||||||
|
if let Ok(doc) = roxmltree::Document::parse(w2x.trim_start_matches('\u{feff}')) {
|
||||||
|
for node in doc.descendants() {
|
||||||
|
match node.tag_name().name() {
|
||||||
|
"Units" => {
|
||||||
|
if let Some(u) = attr(node, "Transform").map(numbers).and_then(|m| Units::from_matrix(&m)) {
|
||||||
|
units = u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"View" => {
|
||||||
|
let v = attr(node, "Area").map(numbers).unwrap_or_default();
|
||||||
|
if v.len() == 4 {
|
||||||
|
view_log = Some([v[0], v[1], v[2], v[3]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let text = std::str::from_utf8(page).ok()?;
|
||||||
|
let doc = roxmltree::Document::parse(text.trim_start_matches('\u{feff}')).ok()?;
|
||||||
|
// Page units (1/96 inch, y down) → graphics units: paper millimetres
|
||||||
|
// with y up, less the paper offset, over the graphics scale.
|
||||||
|
let (scale, offset) = entry.paper;
|
||||||
|
let px_mm = 25.4 / 96.0;
|
||||||
|
let to_logical = |p: [f64; 2]| {
|
||||||
|
let x = p[0] * px_mm;
|
||||||
|
let y = entry.paper_height_mm - p[1] * px_mm;
|
||||||
|
[(x - offset[0]) / scale[0], (y - offset[1]) / scale[1]]
|
||||||
|
};
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
walk(doc.root_element(), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0], &mut |node, m| {
|
||||||
|
let Some(data) = attr(node, "Data") else { return };
|
||||||
|
let m = match attr(node, "RenderTransform").and_then(parse_affine) {
|
||||||
|
Some(local) => mul(m, local),
|
||||||
|
None => m,
|
||||||
|
};
|
||||||
|
let map = |p: [f64; 2]| units.model(to_logical(apply(m, p)));
|
||||||
|
let subpaths: Vec<SubPath> = parse_data(data).into_iter().map(|sp| map_subpath(sp, map)).collect();
|
||||||
|
if subpaths.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let stroke_scale = (m[0] * m[3] - m[1] * m[2]).abs().sqrt() * px_mm / scale[0] / units.scale[0].abs();
|
||||||
|
let stroke = attr(node, "Stroke").and_then(parse_color).map(|c| {
|
||||||
|
let t = attr(node, "StrokeThickness").and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.0);
|
||||||
|
(c, t * stroke_scale)
|
||||||
|
});
|
||||||
|
let fill = attr(node, "Fill").and_then(parse_color);
|
||||||
|
if stroke.is_some() || fill.is_some() {
|
||||||
|
paths.push(Path { subpaths, stroke, fill });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let view = view_log.map(|v: [f64; 4]| {
|
||||||
|
let a = units.model([v[0], v[1]]);
|
||||||
|
let b = units.model([v[2], v[3]]);
|
||||||
|
[a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])]
|
||||||
|
});
|
||||||
|
Some((paths, view))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk(node: roxmltree::Node, m: Affine, f: &mut impl FnMut(roxmltree::Node, Affine)) {
|
||||||
|
for child in node.children().filter(|n| n.is_element()) {
|
||||||
|
match child.tag_name().name() {
|
||||||
|
"Canvas" => {
|
||||||
|
let m2 = match attr(child, "RenderTransform").and_then(parse_affine) {
|
||||||
|
Some(local) => mul(m, local),
|
||||||
|
None => m,
|
||||||
|
};
|
||||||
|
walk(child, m2, f);
|
||||||
|
}
|
||||||
|
"Path" => f(child, m),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The path mini-language: M L H V C Q S A Z and their relative forms.
|
||||||
|
fn parse_data(data: &str) -> Vec<SubPath> {
|
||||||
|
let mut toks: Vec<Tok> = Vec::new();
|
||||||
|
let bytes = data.as_bytes();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < bytes.len() {
|
||||||
|
let c = bytes[i] as char;
|
||||||
|
if c.is_ascii_alphabetic() && c != 'e' && c != 'E' {
|
||||||
|
if c == 'F' {
|
||||||
|
// Fill rule prefix "F0"/"F1".
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
toks.push(Tok::Cmd(c));
|
||||||
|
i += 1;
|
||||||
|
} else if c == '-' || c == '+' || c == '.' || c.is_ascii_digit() {
|
||||||
|
let start = i;
|
||||||
|
i += 1;
|
||||||
|
while i < bytes.len() {
|
||||||
|
let d = bytes[i] as char;
|
||||||
|
let prev = bytes[i - 1] as char;
|
||||||
|
if d.is_ascii_digit() || d == '.' || d == 'e' || d == 'E' || ((d == '-' || d == '+') && (prev == 'e' || prev == 'E')) {
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(v) = data[start..i].parse() {
|
||||||
|
toks.push(Tok::Num(v));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut pb = PathBuilder::default();
|
||||||
|
let mut cmd = 'M';
|
||||||
|
let mut k = 0;
|
||||||
|
let mut last_ctrl: Option<[f64; 2]> = None;
|
||||||
|
let num = |toks: &Vec<Tok>, k: &mut usize| -> Option<f64> {
|
||||||
|
match toks.get(*k) {
|
||||||
|
Some(Tok::Num(v)) => {
|
||||||
|
*k += 1;
|
||||||
|
Some(*v)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
while k < toks.len() {
|
||||||
|
if let Tok::Cmd(c) = toks[k] {
|
||||||
|
cmd = c;
|
||||||
|
k += 1;
|
||||||
|
if c == 'Z' || c == 'z' {
|
||||||
|
pb.close();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cur = pb.current().unwrap_or([0.0, 0.0]);
|
||||||
|
let relative = cmd.is_ascii_lowercase();
|
||||||
|
let pt = |x: f64, y: f64| if relative { [cur[0] + x, cur[1] + y] } else { [x, y] };
|
||||||
|
match cmd.to_ascii_uppercase() {
|
||||||
|
'M' => {
|
||||||
|
let (Some(x), Some(y)) = (num(&toks, &mut k), num(&toks, &mut k)) else { break };
|
||||||
|
pb.move_to(pt(x, y));
|
||||||
|
cmd = if relative { 'l' } else { 'L' };
|
||||||
|
}
|
||||||
|
'L' => {
|
||||||
|
let (Some(x), Some(y)) = (num(&toks, &mut k), num(&toks, &mut k)) else { break };
|
||||||
|
pb.line_to(pt(x, y));
|
||||||
|
}
|
||||||
|
'H' => {
|
||||||
|
let Some(x) = num(&toks, &mut k) else { break };
|
||||||
|
pb.line_to([if relative { cur[0] + x } else { x }, cur[1]]);
|
||||||
|
}
|
||||||
|
'V' => {
|
||||||
|
let Some(y) = num(&toks, &mut k) else { break };
|
||||||
|
pb.line_to([cur[0], if relative { cur[1] + y } else { y }]);
|
||||||
|
}
|
||||||
|
'C' => {
|
||||||
|
let v: Vec<f64> = (0..6).filter_map(|_| num(&toks, &mut k)).collect();
|
||||||
|
if v.len() < 6 { break }
|
||||||
|
let (c1, c2, p) = (pt(v[0], v[1]), pt(v[2], v[3]), pt(v[4], v[5]));
|
||||||
|
pb.cubic_to(c1, c2, p);
|
||||||
|
last_ctrl = Some(c2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
'S' => {
|
||||||
|
let v: Vec<f64> = (0..4).filter_map(|_| num(&toks, &mut k)).collect();
|
||||||
|
if v.len() < 4 { break }
|
||||||
|
let c1 = last_ctrl.map(|c| [2.0 * cur[0] - c[0], 2.0 * cur[1] - c[1]]).unwrap_or(cur);
|
||||||
|
let (c2, p) = (pt(v[0], v[1]), pt(v[2], v[3]));
|
||||||
|
pb.cubic_to(c1, c2, p);
|
||||||
|
last_ctrl = Some(c2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
'Q' => {
|
||||||
|
let v: Vec<f64> = (0..4).filter_map(|_| num(&toks, &mut k)).collect();
|
||||||
|
if v.len() < 4 { break }
|
||||||
|
let (q, p) = (pt(v[0], v[1]), pt(v[2], v[3]));
|
||||||
|
let c1 = [cur[0] + 2.0 / 3.0 * (q[0] - cur[0]), cur[1] + 2.0 / 3.0 * (q[1] - cur[1])];
|
||||||
|
let c2 = [p[0] + 2.0 / 3.0 * (q[0] - p[0]), p[1] + 2.0 / 3.0 * (q[1] - p[1])];
|
||||||
|
pb.cubic_to(c1, c2, p);
|
||||||
|
}
|
||||||
|
'A' => {
|
||||||
|
let v: Vec<f64> = (0..7).filter_map(|_| num(&toks, &mut k)).collect();
|
||||||
|
if v.len() < 7 { break }
|
||||||
|
let end = pt(v[5], v[6]);
|
||||||
|
for [_, c1, c2, p] in svg_arc(cur, v[0], v[1], v[2].to_radians(), v[3] != 0.0, v[4] != 0.0, end) {
|
||||||
|
pb.cubic_to(c1, c2, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => k += 1,
|
||||||
|
}
|
||||||
|
last_ctrl = None;
|
||||||
|
}
|
||||||
|
pb.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Tok {
|
||||||
|
Cmd(char),
|
||||||
|
Num(f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An SVG/XPS endpoint arc as cubic pieces.
|
||||||
|
fn svg_arc(p0: [f64; 2], rx: f64, ry: f64, phi: f64, large: bool, sweep: bool, p1: [f64; 2]) -> Vec<[[f64; 2]; 4]> {
|
||||||
|
let (mut rx, mut ry) = (rx.abs(), ry.abs());
|
||||||
|
if rx == 0.0 || ry == 0.0 || p0 == p1 {
|
||||||
|
return vec![[p0, p0, p1, p1]];
|
||||||
|
}
|
||||||
|
let (c, s) = (phi.cos(), phi.sin());
|
||||||
|
let dx = (p0[0] - p1[0]) / 2.0;
|
||||||
|
let dy = (p0[1] - p1[1]) / 2.0;
|
||||||
|
let x1 = c * dx + s * dy;
|
||||||
|
let y1 = -s * dx + c * dy;
|
||||||
|
let lambda = (x1 * x1) / (rx * rx) + (y1 * y1) / (ry * ry);
|
||||||
|
if lambda > 1.0 {
|
||||||
|
rx *= lambda.sqrt();
|
||||||
|
ry *= lambda.sqrt();
|
||||||
|
}
|
||||||
|
let num = (rx * rx * ry * ry - rx * rx * y1 * y1 - ry * ry * x1 * x1).max(0.0);
|
||||||
|
let den = rx * rx * y1 * y1 + ry * ry * x1 * x1;
|
||||||
|
let mut coef = if den > 0.0 { (num / den).sqrt() } else { 0.0 };
|
||||||
|
if large == sweep {
|
||||||
|
coef = -coef;
|
||||||
|
}
|
||||||
|
let cx1 = coef * rx * y1 / ry;
|
||||||
|
let cy1 = -coef * ry * x1 / rx;
|
||||||
|
let cx = c * cx1 - s * cy1 + (p0[0] + p1[0]) / 2.0;
|
||||||
|
let cy = s * cx1 + c * cy1 + (p0[1] + p1[1]) / 2.0;
|
||||||
|
let ang = |ux: f64, uy: f64, vx: f64, vy: f64| {
|
||||||
|
let a = (ux * vy - uy * vx).atan2(ux * vx + uy * vy);
|
||||||
|
a
|
||||||
|
};
|
||||||
|
let t1 = ang(1.0, 0.0, (x1 - cx1) / rx, (y1 - cy1) / ry);
|
||||||
|
let mut dt = ang((x1 - cx1) / rx, (y1 - cy1) / ry, (-x1 - cx1) / rx, (-y1 - cy1) / ry);
|
||||||
|
if !sweep && dt > 0.0 {
|
||||||
|
dt -= std::f64::consts::TAU;
|
||||||
|
} else if sweep && dt < 0.0 {
|
||||||
|
dt += std::f64::consts::TAU;
|
||||||
|
}
|
||||||
|
let mut pieces = arc_cubics([cx, cy], rx, ry, phi, t1, dt);
|
||||||
|
// Pin the ends so the path joins exactly.
|
||||||
|
if let Some(first) = pieces.first_mut() {
|
||||||
|
first[0] = p0;
|
||||||
|
}
|
||||||
|
if let Some(last) = pieces.last_mut() {
|
||||||
|
last[3] = p1;
|
||||||
|
}
|
||||||
|
pieces
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
//! DWF and DGN underlay content: sheet (DWF) or model (DGN) names, each
|
||||||
|
//! sheet's geometry in its own units, a display raster of it and the
|
||||||
|
//! vectors object snaps use — the counterpart of `pdf_raster` and
|
||||||
|
//! `pdf_vector` for the other underlay kinds.
|
||||||
|
//!
|
||||||
|
//! A DWF sheet is in the model units it was plotted from, with the plot's
|
||||||
|
//! model origin at the underlay's insertion point; a DGN model is in its
|
||||||
|
//! master units with the global origin there.
|
||||||
|
|
||||||
|
mod dgn7;
|
||||||
|
mod dgn8;
|
||||||
|
mod dwf;
|
||||||
|
mod model;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
|
||||||
|
use codec::entities::UnderlayType;
|
||||||
|
|
||||||
|
pub use model::{Path, Segment, Sheet, SubPath};
|
||||||
|
|
||||||
|
use super::pdf_raster::{source_bytes, PdfPage};
|
||||||
|
|
||||||
|
/// Sheet names (DWF) or model names (DGN) of a file.
|
||||||
|
pub fn item_names(kind: UnderlayType, path: &str) -> Option<Vec<String>> {
|
||||||
|
let bytes = source_bytes(path)?;
|
||||||
|
match kind {
|
||||||
|
UnderlayType::Dwf => dwf::sheet_names(&bytes),
|
||||||
|
UnderlayType::Dgn if dgn7::is_v7(&bytes) => dgn7::model_names(&bytes),
|
||||||
|
UnderlayType::Dgn => dgn8::model_names(&bytes),
|
||||||
|
UnderlayType::Pdf => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Key = (String, String);
|
||||||
|
|
||||||
|
fn sheet_cache() -> &'static Mutex<HashMap<Key, Option<Arc<Sheet>>>> {
|
||||||
|
static CACHE: OnceLock<Mutex<HashMap<Key, Option<Arc<Sheet>>>>> = OnceLock::new();
|
||||||
|
CACHE.get_or_init(Default::default)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raster_cache() -> &'static Mutex<HashMap<Key, Option<Arc<PdfPage>>>> {
|
||||||
|
static CACHE: OnceLock<Mutex<HashMap<Key, Option<Arc<PdfPage>>>>> = OnceLock::new();
|
||||||
|
CACHE.get_or_init(Default::default)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forget what was read from `path` (its bytes changed).
|
||||||
|
pub fn forget(path: &str) {
|
||||||
|
sheet_cache().lock().unwrap_or_else(|e| e.into_inner()).retain(|k, _| k.0 != path);
|
||||||
|
raster_cache().lock().unwrap_or_else(|e| e.into_inner()).retain(|k, _| k.0 != path);
|
||||||
|
vector_cache().lock().unwrap_or_else(|e| e.into_inner()).retain(|k, _| k.0 != path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The named sheet or model (the first when the name is empty or unknown),
|
||||||
|
/// memoised. Its text is drawn as strokes with the drawing's text engine.
|
||||||
|
pub fn sheet(kind: UnderlayType, path: &str, item: &str) -> Option<Arc<Sheet>> {
|
||||||
|
let key = (path.to_string(), item.to_string());
|
||||||
|
if let Some(hit) = sheet_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
|
||||||
|
return hit.clone();
|
||||||
|
}
|
||||||
|
let read = || -> Option<Sheet> {
|
||||||
|
let bytes = source_bytes(path)?;
|
||||||
|
let mut sheet = match kind {
|
||||||
|
UnderlayType::Dwf => dwf::sheet(&bytes, item)?,
|
||||||
|
UnderlayType::Dgn if dgn7::is_v7(&bytes) => dgn7::model(&bytes)?,
|
||||||
|
UnderlayType::Dgn => dgn8::model(&bytes, item)?,
|
||||||
|
UnderlayType::Pdf => return None,
|
||||||
|
};
|
||||||
|
outline_texts(&mut sheet);
|
||||||
|
Some(sheet)
|
||||||
|
};
|
||||||
|
let value = read().map(Arc::new);
|
||||||
|
sheet_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Text as strokes (and fills for outline fonts), in the text's colour.
|
||||||
|
fn outline_texts(sheet: &mut Sheet) {
|
||||||
|
for t in std::mem::take(&mut sheet.texts) {
|
||||||
|
let (strokes, fills) = crate::scene::text::lff::tessellate_text_ex(
|
||||||
|
[t.origin[0] as f32, t.origin[1] as f32],
|
||||||
|
t.height as f32,
|
||||||
|
t.rotation as f32,
|
||||||
|
t.width_factor as f32,
|
||||||
|
0.0,
|
||||||
|
"txt",
|
||||||
|
&t.text,
|
||||||
|
);
|
||||||
|
let at = |p: [f32; 2]| [p[0] as f64, p[1] as f64];
|
||||||
|
let subpaths: Vec<SubPath> = strokes
|
||||||
|
.iter()
|
||||||
|
.filter(|s| s.len() >= 2)
|
||||||
|
.map(|s| SubPath { segments: s.windows(2).map(|w| Segment::Line(at(w[0]), at(w[1]))).collect(), closed: false })
|
||||||
|
.collect();
|
||||||
|
if !subpaths.is_empty() {
|
||||||
|
sheet.paths.push(Path { subpaths, stroke: Some((t.color, -1.0)), fill: None });
|
||||||
|
}
|
||||||
|
let triangles: Vec<SubPath> = fills
|
||||||
|
.chunks_exact(3)
|
||||||
|
.map(|t| SubPath {
|
||||||
|
segments: vec![Segment::Line(at(t[0]), at(t[1])), Segment::Line(at(t[1]), at(t[2])), Segment::Line(at(t[2]), at(t[0]))],
|
||||||
|
closed: true,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if !triangles.is_empty() {
|
||||||
|
sheet.paths.push(Path { subpaths: triangles, stroke: None, fill: Some(t.color) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Longest side of a sheet's display raster, pixels.
|
||||||
|
const RASTER_SIDE: f64 = 3072.0;
|
||||||
|
|
||||||
|
/// The sheet drawn on a transparent background, straight alpha, rows from
|
||||||
|
/// the top, memoised.
|
||||||
|
pub fn display_raster(kind: UnderlayType, path: &str, item: &str) -> Option<Arc<PdfPage>> {
|
||||||
|
let key = (path.to_string(), item.to_string());
|
||||||
|
if let Some(hit) = raster_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
|
||||||
|
return hit.clone();
|
||||||
|
}
|
||||||
|
let value = sheet(kind, path, item).and_then(|s| rasterize(&s)).map(Arc::new);
|
||||||
|
raster_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rasterize(sheet: &Sheet) -> Option<PdfPage> {
|
||||||
|
let [x0, y0, x1, y1] = sheet.rect;
|
||||||
|
let (w, h) = (x1 - x0, y1 - y0);
|
||||||
|
if !(w > 0.0 && h > 0.0) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let scale = RASTER_SIDE / w.max(h);
|
||||||
|
let (pw, ph) = ((w * scale).ceil().max(1.0) as u32, (h * scale).ceil().max(1.0) as u32);
|
||||||
|
let mut pixmap = tiny_skia::Pixmap::new(pw, ph)?;
|
||||||
|
let to_px = |p: [f64; 2]| (((p[0] - x0) * scale) as f32, ((y1 - p[1]) * scale) as f32);
|
||||||
|
for path in &sheet.paths {
|
||||||
|
let mut pb = tiny_skia::PathBuilder::new();
|
||||||
|
for sp in &path.subpaths {
|
||||||
|
let Some(first) = sp.segments.first() else { continue };
|
||||||
|
let s = to_px(first.start());
|
||||||
|
pb.move_to(s.0, s.1);
|
||||||
|
for seg in &sp.segments {
|
||||||
|
match seg {
|
||||||
|
Segment::Line(_, b) => {
|
||||||
|
let b = to_px(*b);
|
||||||
|
pb.line_to(b.0, b.1);
|
||||||
|
}
|
||||||
|
Segment::Cubic(_, c1, c2, b) => {
|
||||||
|
let (c1, c2, b) = (to_px(*c1), to_px(*c2), to_px(*b));
|
||||||
|
pb.cubic_to(c1.0, c1.1, c2.0, c2.1, b.0, b.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sp.closed {
|
||||||
|
pb.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(p) = pb.finish() else { continue };
|
||||||
|
if let Some(c) = path.fill {
|
||||||
|
let mut paint = tiny_skia::Paint::default();
|
||||||
|
paint.set_color_rgba8(c[0], c[1], c[2], 255);
|
||||||
|
// Fills that share an edge (triangle strips) would show a seam.
|
||||||
|
paint.anti_alias = false;
|
||||||
|
pixmap.fill_path(&p, &paint, tiny_skia::FillRule::Winding, tiny_skia::Transform::identity(), None);
|
||||||
|
}
|
||||||
|
if let Some((c, width)) = path.stroke {
|
||||||
|
let mut paint = tiny_skia::Paint::default();
|
||||||
|
paint.set_color_rgba8(c[0], c[1], c[2], 255);
|
||||||
|
paint.anti_alias = true;
|
||||||
|
let stroke = tiny_skia::Stroke {
|
||||||
|
// A negative width is in pixels (DGN line weights).
|
||||||
|
width: if width < 0.0 { (-width) as f32 } else { ((width * scale) as f32).max(1.0) },
|
||||||
|
line_cap: tiny_skia::LineCap::Round,
|
||||||
|
line_join: tiny_skia::LineJoin::Round,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
pixmap.stroke_path(&p, &paint, &stroke, tiny_skia::Transform::identity(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut pixels = pixmap.take();
|
||||||
|
for px in pixels.chunks_exact_mut(4) {
|
||||||
|
let a = px[3] as u32;
|
||||||
|
if a > 0 && a < 255 {
|
||||||
|
for c in &mut px[..3] {
|
||||||
|
*c = ((*c as u32 * 255 + a / 2) / a).min(255) as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(PdfPage { pixels: Arc::new(pixels), width: pw, height: ph, dpi: 0.0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vector_cache() -> &'static Mutex<HashMap<Key, Option<Arc<super::pdf_vector::PageVectors>>>> {
|
||||||
|
static CACHE: OnceLock<Mutex<HashMap<Key, Option<Arc<super::pdf_vector::PageVectors>>>>> = OnceLock::new();
|
||||||
|
CACHE.get_or_init(Default::default)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sheet's geometry as page vectors for object snaps, in sheet units,
|
||||||
|
/// memoised.
|
||||||
|
pub fn page_vectors(kind: UnderlayType, path: &str, item: &str) -> Option<Arc<super::pdf_vector::PageVectors>> {
|
||||||
|
let key = (path.to_string(), item.to_string());
|
||||||
|
if let Some(hit) = vector_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
|
||||||
|
return hit.clone();
|
||||||
|
}
|
||||||
|
let value = sheet(kind, path, item).map(|s| Arc::new(to_page_vectors(&s)));
|
||||||
|
vector_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_page_vectors(sheet: &Sheet) -> super::pdf_vector::PageVectors {
|
||||||
|
use super::pdf_vector as pv;
|
||||||
|
let paths = sheet
|
||||||
|
.paths
|
||||||
|
.iter()
|
||||||
|
.map(|p| pv::PdfPath {
|
||||||
|
subpaths: p
|
||||||
|
.subpaths
|
||||||
|
.iter()
|
||||||
|
.map(|sp| pv::SubPath {
|
||||||
|
segments: sp
|
||||||
|
.segments
|
||||||
|
.iter()
|
||||||
|
.map(|s| match s {
|
||||||
|
Segment::Line(a, b) => pv::Segment::Line(*a, *b),
|
||||||
|
Segment::Cubic(a, b, c, d) => pv::Segment::Cubic(*a, *b, *c, *d),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
closed: sp.closed,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
stroke: p.stroke.map(|(c, w)| (c, w.max(0.0))),
|
||||||
|
fill: p.fill,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
pv::PageVectors { paths, ..Default::default() }
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
//! Vector content of a DWF sheet or DGN model, in the underlay's own units
|
||||||
|
//! (the sheet's model units), and what an underlay needs from it.
|
||||||
|
|
||||||
|
/// One segment of a path.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum Segment {
|
||||||
|
Line([f64; 2], [f64; 2]),
|
||||||
|
Cubic([f64; 2], [f64; 2], [f64; 2], [f64; 2]),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Segment {
|
||||||
|
pub fn start(&self) -> [f64; 2] {
|
||||||
|
match self {
|
||||||
|
Segment::Line(a, _) | Segment::Cubic(a, ..) => *a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct SubPath {
|
||||||
|
pub segments: Vec<Segment>,
|
||||||
|
pub closed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Path {
|
||||||
|
pub subpaths: Vec<SubPath>,
|
||||||
|
/// Stroke colour and width (sheet units; 0 draws one pixel wide).
|
||||||
|
pub stroke: Option<([u8; 3], f64)>,
|
||||||
|
pub fill: Option<[u8; 3]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A sheet (DWF) or model (DGN): its extent and what it draws.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct Sheet {
|
||||||
|
/// min x, min y, max x, max y in sheet units.
|
||||||
|
pub rect: [f64; 4],
|
||||||
|
pub paths: Vec<Path>,
|
||||||
|
pub texts: Vec<Text>,
|
||||||
|
/// DGN: sub units per master unit (the scale a Sub conversion offers).
|
||||||
|
pub sub_per_master: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds paths from pen moves.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct PathBuilder {
|
||||||
|
pub subpaths: Vec<SubPath>,
|
||||||
|
current: Vec<Segment>,
|
||||||
|
start: Option<[f64; 2]>,
|
||||||
|
at: Option<[f64; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathBuilder {
|
||||||
|
pub fn move_to(&mut self, p: [f64; 2]) {
|
||||||
|
self.flush(false);
|
||||||
|
self.start = Some(p);
|
||||||
|
self.at = Some(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn line_to(&mut self, p: [f64; 2]) {
|
||||||
|
let Some(a) = self.at else {
|
||||||
|
self.move_to(p);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.current.push(Segment::Line(a, p));
|
||||||
|
self.at = Some(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cubic_to(&mut self, c1: [f64; 2], c2: [f64; 2], p: [f64; 2]) {
|
||||||
|
let Some(a) = self.at else {
|
||||||
|
self.move_to(p);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.current.push(Segment::Cubic(a, c1, c2, p));
|
||||||
|
self.at = Some(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(&mut self) {
|
||||||
|
if let (Some(s), Some(a)) = (self.start, self.at) {
|
||||||
|
if s != a {
|
||||||
|
self.current.push(Segment::Line(a, s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.flush(true);
|
||||||
|
self.at = self.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current(&self) -> Option<[f64; 2]> {
|
||||||
|
self.at
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self, closed: bool) {
|
||||||
|
if !self.current.is_empty() {
|
||||||
|
self.subpaths.push(SubPath { segments: std::mem::take(&mut self.current), closed });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(mut self) -> Vec<SubPath> {
|
||||||
|
self.flush(false);
|
||||||
|
self.subpaths
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cubic pieces of an elliptical arc: centre, radii, axis rotation and the
|
||||||
|
/// start and sweep angles (radians, counter-clockwise positive).
|
||||||
|
pub fn arc_cubics(
|
||||||
|
center: [f64; 2],
|
||||||
|
rx: f64,
|
||||||
|
ry: f64,
|
||||||
|
rotation: f64,
|
||||||
|
start: f64,
|
||||||
|
sweep: f64,
|
||||||
|
) -> Vec<[[f64; 2]; 4]> {
|
||||||
|
let n = ((sweep.abs() / (std::f64::consts::FRAC_PI_2)).ceil() as usize).max(1);
|
||||||
|
let step = sweep / n as f64;
|
||||||
|
let k = 4.0 / 3.0 * (step / 4.0).tan();
|
||||||
|
let (cr, sr) = (rotation.cos(), rotation.sin());
|
||||||
|
let at = |x: f64, y: f64| [center[0] + x * cr - y * sr, center[1] + x * sr + y * cr];
|
||||||
|
let mut out = Vec::with_capacity(n);
|
||||||
|
for i in 0..n {
|
||||||
|
let a0 = start + step * i as f64;
|
||||||
|
let a1 = a0 + step;
|
||||||
|
let (c0, s0) = (a0.cos(), a0.sin());
|
||||||
|
let (c1, s1) = (a1.cos(), a1.sin());
|
||||||
|
let p0 = at(rx * c0, ry * s0);
|
||||||
|
let p3 = at(rx * c1, ry * s1);
|
||||||
|
let p1 = at(rx * (c0 - k * s0), ry * (s0 + k * c0));
|
||||||
|
let p2 = at(rx * (c1 + k * s1), ry * (s1 - k * c1));
|
||||||
|
out.push([p0, p1, p2, p3]);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bounds of the paths, when there are any.
|
||||||
|
pub fn paths_bounds(paths: &[Path]) -> Option<[f64; 4]> {
|
||||||
|
let mut b = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
|
||||||
|
let mut add = |p: [f64; 2]| {
|
||||||
|
b = [b[0].min(p[0]), b[1].min(p[1]), b[2].max(p[0]), b[3].max(p[1])];
|
||||||
|
};
|
||||||
|
for path in paths {
|
||||||
|
for sp in &path.subpaths {
|
||||||
|
for seg in &sp.segments {
|
||||||
|
match seg {
|
||||||
|
Segment::Line(a, c) => {
|
||||||
|
add(*a);
|
||||||
|
add(*c);
|
||||||
|
}
|
||||||
|
Segment::Cubic(a, c1, c2, c) => {
|
||||||
|
add(*a);
|
||||||
|
add(*c1);
|
||||||
|
add(*c2);
|
||||||
|
add(*c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(b[0] <= b[2]).then_some(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A line of text the host draws with its own font.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Text {
|
||||||
|
pub text: String,
|
||||||
|
/// Baseline start, sheet units.
|
||||||
|
pub origin: [f64; 2],
|
||||||
|
pub height: f64,
|
||||||
|
/// Width of a character relative to its height.
|
||||||
|
pub width_factor: f64,
|
||||||
|
pub rotation: f64,
|
||||||
|
pub color: [u8; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Points of a B-spline curve: its order, control points and knot vector
|
||||||
|
/// (clamped: `order` equal knots at each end).
|
||||||
|
pub fn bspline_points(order: usize, poles: &[[f64; 2]], knots: &[f64], samples: usize) -> Vec<[f64; 2]> {
|
||||||
|
let n = poles.len();
|
||||||
|
if order < 2 || n < order || knots.len() != n + order {
|
||||||
|
return poles.to_vec();
|
||||||
|
}
|
||||||
|
let (t0, t1) = (knots[order - 1], knots[n]);
|
||||||
|
let mut out = Vec::with_capacity(samples + 1);
|
||||||
|
for s in 0..=samples {
|
||||||
|
let t = t0 + (t1 - t0) * s as f64 / samples as f64;
|
||||||
|
// de Boor
|
||||||
|
let mut k = order - 1;
|
||||||
|
while k < n - 1 && t >= knots[k + 1] {
|
||||||
|
k += 1;
|
||||||
|
}
|
||||||
|
let mut d: Vec<[f64; 2]> = (0..order).map(|j| poles[j + k + 1 - order]).collect();
|
||||||
|
for r in 1..order {
|
||||||
|
for j in (r..order).rev() {
|
||||||
|
let i = j + k + 1 - order;
|
||||||
|
let den = knots[i + order - r] - knots[i];
|
||||||
|
let a = if den.abs() < 1e-15 { 0.0 } else { (t - knots[i]) / den };
|
||||||
|
d[j] = [d[j - 1][0] * (1.0 - a) + d[j][0] * a, d[j - 1][1] * (1.0 - a) + d[j][1] * a];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push(d[order - 1]);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
@@ -220,6 +220,7 @@ impl ImageGpu {
|
|||||||
"image.texture:{}:{}:{}",
|
"image.texture:{}:{}:{}",
|
||||||
model.file_path, x.content_start, y.content_start
|
model.file_path, x.content_start, y.content_start
|
||||||
);
|
);
|
||||||
|
let levels = mip_chain(&model.pixels, model.width, x.data_start, y.data_start, width, height);
|
||||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
label: Some(&tex_label),
|
label: Some(&tex_label),
|
||||||
size: wgpu::Extent3d {
|
size: wgpu::Extent3d {
|
||||||
@@ -227,7 +228,7 @@ impl ImageGpu {
|
|||||||
height,
|
height,
|
||||||
depth_or_array_layers: 1,
|
depth_or_array_layers: 1,
|
||||||
},
|
},
|
||||||
mip_level_count: 1,
|
mip_level_count: levels.len() as u32,
|
||||||
sample_count: 1,
|
sample_count: 1,
|
||||||
dimension: wgpu::TextureDimension::D2,
|
dimension: wgpu::TextureDimension::D2,
|
||||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||||
@@ -235,22 +236,27 @@ impl ImageGpu {
|
|||||||
| wgpu::TextureUsages::COPY_DST,
|
| wgpu::TextureUsages::COPY_DST,
|
||||||
view_formats: &[],
|
view_formats: &[],
|
||||||
});
|
});
|
||||||
queue.write_texture(
|
for (level, (w, h, data)) in levels.iter().enumerate() {
|
||||||
texture.as_image_copy(),
|
queue.write_texture(
|
||||||
&model.pixels[((y.data_start as usize * model.width as usize
|
wgpu::TexelCopyTextureInfo {
|
||||||
+ x.data_start as usize)
|
texture: &texture,
|
||||||
* 4)..],
|
mip_level: level as u32,
|
||||||
wgpu::TexelCopyBufferLayout {
|
origin: wgpu::Origin3d::ZERO,
|
||||||
offset: 0,
|
aspect: wgpu::TextureAspect::All,
|
||||||
bytes_per_row: Some(4 * model.width),
|
},
|
||||||
rows_per_image: Some(model.height),
|
data,
|
||||||
},
|
wgpu::TexelCopyBufferLayout {
|
||||||
wgpu::Extent3d {
|
offset: 0,
|
||||||
width,
|
bytes_per_row: Some(4 * w),
|
||||||
height,
|
rows_per_image: Some(*h),
|
||||||
depth_or_array_layers: 1,
|
},
|
||||||
},
|
wgpu::Extent3d {
|
||||||
);
|
width: *w,
|
||||||
|
height: *h,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
let tex_view =
|
let tex_view =
|
||||||
texture.create_view(&wgpu::TextureViewDescriptor::default());
|
texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
@@ -302,6 +308,52 @@ struct TileRange {
|
|||||||
data_end: u32,
|
data_end: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A tile's pixels and its smaller levels down to 1×1, so a far view
|
||||||
|
/// samples a level near its own size. A level keeps the most opaque of each
|
||||||
|
/// 2×2 block unless all four are equally opaque (then their average): thin
|
||||||
|
/// lines on a transparent page (PDF, DWF, DGN underlays) stay visible when
|
||||||
|
/// zoomed out instead of falling between samples, and opaque images are
|
||||||
|
/// averaged as usual.
|
||||||
|
fn mip_chain(pixels: &[u8], stride: u32, x0: u32, y0: u32, w: u32, h: u32) -> Vec<(u32, u32, Vec<u8>)> {
|
||||||
|
let (stride, x0, y0) = (stride as usize, x0 as usize, y0 as usize);
|
||||||
|
let mut base = Vec::with_capacity(w as usize * h as usize * 4);
|
||||||
|
for row in 0..h as usize {
|
||||||
|
let start = ((y0 + row) * stride + x0) * 4;
|
||||||
|
base.extend_from_slice(&pixels[start..start + w as usize * 4]);
|
||||||
|
}
|
||||||
|
let mut levels = vec![(w, h, base)];
|
||||||
|
loop {
|
||||||
|
let (pw, ph, prev) = levels.last().expect("base level");
|
||||||
|
if *pw == 1 && *ph == 1 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let (pw, ph) = (*pw as usize, *ph as usize);
|
||||||
|
let (nw, nh) = ((pw / 2).max(1), (ph / 2).max(1));
|
||||||
|
let mut next = Vec::with_capacity(nw * nh * 4);
|
||||||
|
for y in 0..nh {
|
||||||
|
for x in 0..nw {
|
||||||
|
let block = [(0, 0), (1, 0), (0, 1), (1, 1)].map(|(dx, dy)| {
|
||||||
|
let at = (((2 * y + dy).min(ph - 1)) * pw + (2 * x + dx).min(pw - 1)) * 4;
|
||||||
|
[prev[at], prev[at + 1], prev[at + 2], prev[at + 3]]
|
||||||
|
});
|
||||||
|
if block.iter().all(|p| p[3] == block[0][3]) {
|
||||||
|
let mut sum = [0u32; 4];
|
||||||
|
for p in &block {
|
||||||
|
for c in 0..4 {
|
||||||
|
sum[c] += p[c] as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next.extend(sum.map(|v| ((v + 2) / 4) as u8));
|
||||||
|
} else {
|
||||||
|
next.extend(*block.iter().max_by_key(|p| p[3]).expect("four pixels"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
levels.push((nw as u32, nh as u32, next));
|
||||||
|
}
|
||||||
|
levels
|
||||||
|
}
|
||||||
|
|
||||||
fn tile_ranges(size: u32, limit: u32) -> Vec<TileRange> {
|
fn tile_ranges(size: u32, limit: u32) -> Vec<TileRange> {
|
||||||
if size <= limit {
|
if size <= limit {
|
||||||
return vec![TileRange {
|
return vec![TileRange {
|
||||||
|
|||||||
@@ -1087,7 +1087,6 @@ impl XrefManagerPanel {
|
|||||||
/// directly — mirrors the three [`collect_entries`] scans so nested entries
|
/// directly — mirrors the three [`collect_entries`] scans so nested entries
|
||||||
/// (foreign handles) classify correctly even on handle collisions.
|
/// (foreign handles) classify correctly even on handle collisions.
|
||||||
fn direct_identities(doc: &CadDocument) -> HashSet<(u64, String)> {
|
fn direct_identities(doc: &CadDocument) -> HashSet<(u64, String)> {
|
||||||
use codec::entities::UnderlayType;
|
|
||||||
use codec::objects::ObjectType;
|
use codec::objects::ObjectType;
|
||||||
|
|
||||||
let mut ids: HashSet<(u64, String)> = HashSet::new();
|
let mut ids: HashSet<(u64, String)> = HashSet::new();
|
||||||
@@ -1102,9 +1101,7 @@ fn direct_identities(doc: &CadDocument) -> HashSet<(u64, String)> {
|
|||||||
ids.insert((handle.value(), def.file_name.clone()));
|
ids.insert((handle.value(), def.file_name.clone()));
|
||||||
}
|
}
|
||||||
ObjectType::UnderlayDefinition(def) => {
|
ObjectType::UnderlayDefinition(def) => {
|
||||||
if def.underlay_type == UnderlayType::Pdf {
|
ids.insert((handle.value(), def.file_path.clone()));
|
||||||
ids.insert((handle.value(), def.file_path.clone()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -1185,7 +1182,17 @@ fn type_text(entry: &ReferenceEntry) -> std::borrow::Cow<'static, str> {
|
|||||||
// Raster images display their file format (spec: type column shows
|
// Raster images display their file format (spec: type column shows
|
||||||
// the image format); unknown extensions fall back to Image.
|
// the image format); unknown extensions fall back to Image.
|
||||||
RefKind::Image => image_format(&entry.saved_path),
|
RefKind::Image => image_format(&entry.saved_path),
|
||||||
RefKind::Pdf => crate::t!("PDF"),
|
RefKind::Underlay => {
|
||||||
|
let ext = std::path::Path::new(&entry.saved_path.replace('\\', "/"))
|
||||||
|
.extension()
|
||||||
|
.map(|e| e.to_string_lossy().to_ascii_uppercase())
|
||||||
|
.unwrap_or_default();
|
||||||
|
match ext.as_str() {
|
||||||
|
"DWF" | "DWFX" => crate::t!("DWF"),
|
||||||
|
"DGN" => crate::t!("DGN"),
|
||||||
|
_ => crate::t!("PDF"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2118,7 +2125,7 @@ fn reference_preview(entry: &ReferenceEntry) -> Option<image::RgbaImage> {
|
|||||||
let img = image::open(found).ok()?;
|
let img = image::open(found).ok()?;
|
||||||
Some(img.thumbnail(PREVIEW_MAX, PREVIEW_MAX).to_rgba8())
|
Some(img.thumbnail(PREVIEW_MAX, PREVIEW_MAX).to_rgba8())
|
||||||
}
|
}
|
||||||
RefKind::Pdf => None,
|
RefKind::Underlay => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user