From 3f6bdf8c593048b0beb87a5591166738463d398f Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Thu, 24 Sep 2026 18:31:40 +0300 Subject: [PATCH] PDF underlays: attach dialog with pages, underlay tab, layers, import settings and file import, unload on the definition Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- Cargo.toml | 4 +- locales/ar-SA/opencadstudio.ftl | 69 +++ locales/bg-BG/opencadstudio.ftl | 69 +++ locales/cs-CZ/opencadstudio.ftl | 69 +++ locales/de-DE/opencadstudio.ftl | 69 +++ locales/el-GR/opencadstudio.ftl | 69 +++ locales/en-US/opencadstudio.ftl | 69 +++ locales/es-ES/opencadstudio.ftl | 69 +++ locales/fi-FI/opencadstudio.ftl | 69 +++ locales/fr-FR/opencadstudio.ftl | 69 +++ locales/hi-IN/opencadstudio.ftl | 69 +++ locales/hu-HU/opencadstudio.ftl | 69 +++ locales/it-IT/opencadstudio.ftl | 69 +++ locales/ja-JP/opencadstudio.ftl | 69 +++ locales/ko-KR/opencadstudio.ftl | 69 +++ locales/nl-NL/opencadstudio.ftl | 69 +++ locales/pl-PL/opencadstudio.ftl | 69 +++ locales/pt-BR/opencadstudio.ftl | 69 +++ locales/ru-RU/opencadstudio.ftl | 69 +++ locales/tr-TR/opencadstudio.ftl | 69 +++ locales/zh-CN/opencadstudio.ftl | 69 +++ locales/zh-TW/opencadstudio.ftl | 69 +++ src/app/command_driver.rs | 36 +- src/app/commands/display.rs | 3 +- src/app/commands/mod.rs | 8 + src/app/commands/pdf_dialogs.rs | 507 ++++++++++++++++++ src/app/commands/pdf_import.rs | 179 ++++++- src/app/commands/pdf_underlay.rs | 166 ++++++ src/app/commands/styleprops.rs | 32 ++ src/app/mod.rs | 21 + src/app/properties.rs | 1 + src/app/update/dialog.rs | 16 + src/app/update/mod.rs | 40 +- src/app/view/modal.rs | 29 ++ src/command.rs | 17 +- src/entities/underlay.rs | 10 +- src/io/mod.rs | 1 + src/io/xref.rs | 32 +- src/locale_catalog.rs | 53 ++ src/modules/insert/pdf_attach.rs | 126 ++++- src/modules/insert/pdf_clip.rs | 12 + src/modules/insert/pdf_import.rs | 814 ++++++++++++++++++++++------- src/scene/convert/tess.rs | 14 + src/scene/entity.rs | 12 +- src/scene/mod.rs | 19 +- src/scene/model/image_model.rs | 8 +- src/scene/model/mod.rs | 1 + src/scene/model/pdf_layers.rs | 255 +++++++++ src/scene/model/pdf_raster.rs | 67 ++- src/scene/model/pdf_vector.rs | 240 ++++++++- src/ui/ribbon/mod.rs | 176 ++++--- src/ui/ribbon/underlay_tab.rs | 215 ++++++++ src/ui/ribbon/widgets.rs | 6 + src/ui/window/block_definition.rs | 4 +- src/ui/window/mod.rs | 1 + src/ui/window/pdf_dialogs.rs | 838 ++++++++++++++++++++++++++++++ 57 files changed, 5049 insertions(+), 365 deletions(-) create mode 100644 src/app/commands/pdf_dialogs.rs create mode 100644 src/app/commands/pdf_underlay.rs create mode 100644 src/scene/model/pdf_layers.rs create mode 100644 src/ui/ribbon/underlay_tab.rs create mode 100644 src/ui/window/pdf_dialogs.rs diff --git a/Cargo.lock b/Cargo.lock index 0c77e6230..90edec928 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.5.5" -source = "git+https://github.com/ramox81/cadcodec.git?rev=55a8987#55a8987aca3432a4b0a3ffde708d32e4ffaca644" +source = "git+https://github.com/ramox81/cadcodec.git?rev=d290dbe#d290dbe1712666d83dfd0fc63341ee3246f8de76" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 0885e24da..3a6a349ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ rfd = "0.17" clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" log = "0.4" -acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "55a8987", features = ["serde"] } +acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "d290dbe", features = ["serde"] } cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6eae7c9", features = ["acis", "offset"] } cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6eae7c9" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } @@ -82,7 +82,7 @@ block2 = "0.5" objc2-foundation = { version = "0.2", features = ["NSArray", "NSURL", "NSString", "NSObject", "NSNotification", "NSThread", "NSEnumerator"] } [patch."https://github.com/HakanSeven12/cadcodec.git"] -acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "55a8987" } +acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "d290dbe" } [patch.crates-io] iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 9e79d6bc7..0d26ba692 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -3510,6 +3510,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE حدد نقطة الإدراج: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = سحب @@ -4259,6 +4327,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index e45f2a189..c4daec8a7 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -3497,6 +3497,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Посочи точка на вмъкване: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = Облаци @@ -4246,6 +4314,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 90e7c9b26..8dc539abf 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -3495,6 +3495,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Zadejte bod vložení: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = Bod @@ -4244,6 +4312,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index f34281cfe..56baae68f 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -3498,6 +3498,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Einführstelle angeben: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = Punkt- @@ -4245,6 +4313,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 29774b130..f7b0790ee 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -3498,6 +3498,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Ορίστε σημείο εισαγωγής: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .revolve-creation-mode-solid-surface = REVOLVE Τρόπος δημιουργίας [Solid/Surface] .revolve-enter-angle-expression = REVOLVE Δώστε έκφραση γωνίας: @@ -4247,6 +4315,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 8b9e26020..cacdb234d 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -3499,6 +3499,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Specify insertion point: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = Point @@ -4248,6 +4316,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS specify point in positive XY plane: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 9add0ea44..9a95ea165 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -3498,6 +3498,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Especifique el punto de inserción: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = Nubes de @@ -4247,6 +4315,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index f4a0de853..e1ce84999 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -3497,6 +3497,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Määritä lisäyskohta: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = Point @@ -4246,6 +4314,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 63a20a483..273300703 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -3498,6 +3498,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Spécifier le point d'insertion: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = Nuages de @@ -4247,6 +4315,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index c03bd8804..03f6c0667 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -3486,6 +3486,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE निर्दिष्ट सम्मिलन बिंदु: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = बिंदु @@ -4235,6 +4303,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index c0abf50e4..b02abc786 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -3497,6 +3497,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Beszúrási pont megadása: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = Pont @@ -4246,6 +4314,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 1a82f50e5..dd9793daf 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -3497,6 +3497,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Specificare il punto di inserimento: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = Punto @@ -4246,6 +4314,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index db905df75..af527e0fa 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -3488,6 +3488,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE 挿入点を指定: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = 点群 .attsync-block-name-to-sync = ATTSYNC 同期するブロック名: @@ -4235,6 +4303,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 9ce446ba3..41723ab54 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -3497,6 +3497,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE 삽입점 지정: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = 포인트 @@ -4246,6 +4314,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index fc171b009..7c83d70a9 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -3496,6 +3496,74 @@ annotate = .dimradius-select-arc-circle-or-polyline-arc = DIMRADIUS Selecteer een boog, cirkel of polylijnboog: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = Punten- @@ -4245,6 +4313,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index af6a58c4f..0ebc0ea01 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -3497,6 +3497,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Określ punkt wstawienia: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = Punkt @@ -4246,6 +4314,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 283b3405b..8246414d7 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -3498,6 +3498,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Especifique o ponto de inserção: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = Nuvens de @@ -4247,6 +4315,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index ac2e744aa..bb9da517f 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -3499,6 +3499,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Укажите точку вставки: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = Облака @@ -4248,6 +4316,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index abb234aca..ecee8ce9b 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -3472,6 +3472,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE Ekleme noktasını belirtin: insert = + .pdfui-specify-insertion-point = Ekleme noktasını belirtin: + .pdfui-no-pdf-underlays = PDF altlığı bulunamadı. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Bir PDF dosyasının geometrisini, dolu alanlarını, raster görüntülerini ve metnini çizim nesneleri olarak içe aktarır. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Bir PDF altlığının katmanlarını açar veya kapatır. + .pdfui-attaches-a-pdf-file-as-an-underlay = Bir PDF dosyasını altlık olarak ekler. + .pdfui-rotation = Döndürme: + .pdfui-scale = Ölçek: + .pdfui-specify-insertion-point-on-screen = Ekleme noktasını ekranda belirt + .pdfui-page-size-size = Sayfa boyutu: __ocs_fmt_0__ + .pdfui-total-count = Toplam: __ocs_fmt_0__ + .pdfui-page = Sayfa: + .pdfui-page-to-import = İçe aktarılacak sayfa + .pdfui-infer-linetypes-from-collinear-dashes = Aynı doğrultudaki kesiklerden çizgi tipi çıkar + .pdfui-apply-lineweight-properties = Çizgi kalınlığı özelliklerini uygula + .pdfui-convert-solid-fills-to-hatches = Dolu alanları taramaya dönüştür + .pdfui-join-line-and-arc-segments = Çizgi ve yay parçalarını birleştir + .pdfui-import-as-block = Blok olarak içe aktar + .pdfui-import-options = İçe aktarma seçenekleri + .pdfui-create-object-layers = Nesne katmanları oluştur + .pdfui-use-pdf-layers = PDF katmanlarını kullan + .pdfui-raster-images = Raster görüntüler + .pdfui-truetype-text = TrueType metin + .pdfui-solid-fills = Dolu alanlar + .pdfui-vector-geometry = Vektör geometri + .pdfui-pdf-data-to-import = İçe aktarılacak PDF verisi + .pdfui-select-an-underlay-to-view-its-layers = Katmanlarını görmek için bir altlık seçin. + .pdfui-this-file-does-not-contain-any-layers = Bu dosya herhangi bir katman içermiyor. + .pdfui-search-for-layer = Katman ara + .pdfui-reference-name = Referans adı: + .pdfui-show-details = Ayrıntıları Göster + .pdfui-hide-details = Ayrıntıları Gizle + .pdfui-page-size = Sayfa boyutu: + .pdfui-saved-path = Kayıtlı yol: + .pdfui-found-in = Bulunduğu yer: + .pdfui-angle = Açı: + .pdfui-insertion-point = Ekleme noktası + .pdfui-select-one-or-more-pages-from-the-pdf-file = PDF dosyasından bir veya daha fazla sayfa seçin: + .pdfui-browse = Gözat... + .pdfui-import-pdf = PDF İçe Aktar + .pdfui-pdf-import-settings = PDF İçe Aktarma Ayarları + .pdfui-underlay-layers = Altlık Katmanları + .pdfui-attach-pdf-underlay = PDF Altlığı Ekle + .pdfui-pdf-import = PDF İçe Aktar + .pdfui-pdf-layers = PDF Katmanları + .pdfui-import-as-objects = + Nesne Olarak + İçe Aktar + .pdfui-edit-layers = + Katmanları + Düzenle + .pdfui-external-references = + Harici + Referanslar + .pdfui-enable-snap = + Yakalamayı + Etkinleştir + .pdfui-show-underlay = + Altlığı + Göster + .pdfui-remove-clipping = + Kırpmayı + Kaldır + .pdfui-create-clipping-boundary = + Kırpma Sınırı + Oluştur + .pdfui-display-in-monochrome = + Tek renkli + göster .select-pdf-file = PDF Dosyası Seç .point-clouds = Nokta @@ -4187,6 +4255,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Uygulandı .pdf-underlay = PDF Altlığı .dwf-underlay = DWF Altlığı .dgn-underlay = DGN Altlığı diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 6b06b914c..ee1af45e1 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -3485,6 +3485,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE 指定插入点: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = 点云 .attsync-block-name-to-sync = ATTSYNC 要同步的块名 : @@ -4230,6 +4298,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 52d6fa83b..cde0c6275 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -3497,6 +3497,74 @@ annotate = .tolerance-specify-insertion-point = TOLERANCE 指定插入點: insert = + .pdfui-specify-insertion-point = Specify insertion point: + .pdfui-no-pdf-underlays = No PDF underlays found. + .pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f = Imports the geometry, fills, raster images and text of a PDF file as drawing objects. + .pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off = Turns the layers of a PDF underlay on or off. + .pdfui-attaches-a-pdf-file-as-an-underlay = Attaches a PDF file as an underlay. + .pdfui-rotation = Rotation: + .pdfui-scale = Scale: + .pdfui-specify-insertion-point-on-screen = Specify insertion point on-screen + .pdfui-page-size-size = Page size: {size} + .pdfui-total-count = Total: {count} + .pdfui-page = Page: + .pdfui-page-to-import = Page to import + .pdfui-infer-linetypes-from-collinear-dashes = Infer linetypes from collinear dashes + .pdfui-apply-lineweight-properties = Apply lineweight properties + .pdfui-convert-solid-fills-to-hatches = Convert solid fills to hatches + .pdfui-join-line-and-arc-segments = Join line and arc segments + .pdfui-import-as-block = Import as block + .pdfui-import-options = Import options + .pdfui-create-object-layers = Create object layers + .pdfui-use-pdf-layers = Use PDF layers + .pdfui-raster-images = Raster images + .pdfui-truetype-text = TrueType text + .pdfui-solid-fills = Solid fills + .pdfui-vector-geometry = Vector geometry + .pdfui-pdf-data-to-import = PDF data to import + .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-search-for-layer = Search for layer + .pdfui-reference-name = Reference name: + .pdfui-show-details = Show Details + .pdfui-hide-details = Hide Details + .pdfui-page-size = Page size: + .pdfui-saved-path = Saved path: + .pdfui-found-in = Found in: + .pdfui-angle = Angle: + .pdfui-insertion-point = Insertion point + .pdfui-select-one-or-more-pages-from-the-pdf-file = Select one or more pages from the PDF file: + .pdfui-browse = Browse... + .pdfui-import-pdf = Import PDF + .pdfui-pdf-import-settings = PDF Import Settings + .pdfui-underlay-layers = Underlay Layers + .pdfui-attach-pdf-underlay = Attach PDF Underlay + .pdfui-pdf-import = PDF Import + .pdfui-pdf-layers = PDF Layers + .pdfui-import-as-objects = + Import As + Objects + .pdfui-edit-layers = + Edit + Layers + .pdfui-external-references = + External + References + .pdfui-enable-snap = + Enable + Snap + .pdfui-show-underlay = + Show + Underlay + .pdfui-remove-clipping = + Remove + Clipping + .pdfui-create-clipping-boundary = + Create Clipping + Boundary + .pdfui-display-in-monochrome = + Display in + Monochrome .select-pdf-file = Select PDF File .point-clouds = 點雲 .attsync-block-name-to-sync = ATTSYNC 要同步的圖塊名 : @@ -4242,6 +4310,7 @@ view = .ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }: properties = + .applied = Applied .pdf-underlay = PDF Underlay .dwf-underlay = DWF Underlay .dgn-underlay = DGN Underlay diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index a47c435d9..8f0a41b7c 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -3190,24 +3190,8 @@ impl OpenCADStudio { // (before `entity` is moved into commit_entity). self.update_cont_anchor(&entity); let label = self.history_label_from_active_cmd(i, "ENTITY"); - // A PDF attach creates its definition (and the definitions - // dictionary) with the underlay, in the same undo step. - let pdf_source = self.tabs[i] - .active_cmd - .as_ref() - .and_then(|cmd| cmd.pdf_attach_source()); - let delta_safe = pdf_source.is_none() && self.delta_add_safe(i, &entity); + let delta_safe = self.delta_add_safe(i, &entity); let pending = self.begin_undo(i, label, 1, delta_safe); - if let Some((path, page)) = pdf_source { - let definition = crate::modules::insert::pdf_attach::ensure_pdf_definition( - &mut self.tabs[i].scene.document, - &path, - &page, - ); - if let acadrust::EntityType::Underlay(underlay) = &mut entity { - underlay.definition_handle = definition; - } - } let is_associative_dimension = matches!( entity, acadrust::EntityType::Dimension( @@ -3947,6 +3931,24 @@ impl OpenCADStudio { } self.refresh_properties(); } + CmdResult::OpenPdfImportSettings => self.open_pdf_import_settings(), + CmdResult::PdfImportFile(import) => { + self.tabs[i].scene.clear_preview_wire(); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.run_pdf_import( + i, + crate::app::commands::pdf_import::PdfImportSource::File(import), + ); + } + CmdResult::AttachPdfPages { path, pages } => { + let label = self.history_label_from_active_cmd(i, "PDFATTACH"); + self.tabs[i].scene.clear_preview_wire(); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.attach_pdf_pages(i, label, &path, pages); + self.restore_pre_cmd_tangent(); + } CmdResult::PdfImport(request) => { self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index 57e25b121..6f9bf63ee 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -1861,7 +1861,8 @@ impl OpenCADStudio { } } - "POINTCLOUDATTACH" | "RECAP" | "SYNCPVIEWPORTS" | "UNDERLAYLAYERS" => { + "UNDERLAYLAYERS" | "ULAYERS" => self.open_underlay_layers_dialog(i), + "POINTCLOUDATTACH" | "RECAP" | "SYNCPVIEWPORTS" => { self.command_line .push_info(crate::tf!("{cmd}: not yet implemented.").as_ref()); } diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index f5f10a396..a7ee27ed0 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -13,6 +13,8 @@ mod inquiry; mod layerprops; mod layers; pub(crate) mod pdf_import; +mod pdf_underlay; +mod pdf_dialogs; mod plotvars; mod styleprops; mod view; @@ -250,6 +252,9 @@ impl OpenCADStudio { if let Some(t) = self.dispatch_blocks(cmd, i) { return Some(t); } + if let Some(t) = self.dispatch_pdf_underlay(cmd, i) { + return Some(t); + } if let Some(t) = self.dispatch_draw(cmd, i) { return Some(t); } @@ -609,6 +614,9 @@ inventory::submit!(crate::command::CommandRegistration { "PDFFRAME", "PDFOSNAP", "UOSNAP", + "PDFIMPORTMODE", + "PDFIMPORTFILTER", + "PDFIMPORTLAYERS", "POINTCLOUDCLIPFRAME", "XCLIPFRAME", "WIPEOUTFRAME", diff --git a/src/app/commands/pdf_dialogs.rs b/src/app/commands/pdf_dialogs.rs new file mode 100644 index 000000000..2fb522cf8 --- /dev/null +++ b/src/app/commands/pdf_dialogs.rs @@ -0,0 +1,507 @@ +//! The PDF dialogs' state changes: Attach PDF Underlay, Underlay Layers, +//! PDF Import Settings and Import PDF. + +use super::*; +use crate::io::xref_model::Pathtype; +use crate::modules::insert::pdf_import::{self, PdfFileImport}; +use crate::ui::window::pdf_dialogs::{ + click_page, page_thumbs, LayerTarget, PdfAttachState, PdfDialogMsg, PdfImportFileState, + RotationChoice, UnderlayLayersState, +}; + +/// "7.8740 × 3.9369" (inches) for a page. +fn page_size_text(path: &str, page: &str) -> String { + crate::scene::model::pdf_raster::page_size_inches(path, page) + .map(|(w, h)| format!("{w:.4} × {h:.4}")) + .unwrap_or_default() +} + +fn file_stem(path: &str) -> String { + std::path::Path::new(&path.replace('\\', "/")) + .file_stem() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_default() +} + +impl OpenCADStudio { + /// The path an attached file is stored with: full, relative to the saved + /// drawing (the full path until it is saved), or the file name alone. + fn pdf_stored_path(&self, path: &str, path_type: Pathtype) -> String { + match path_type { + Pathtype::Full => path.to_string(), + Pathtype::Relative => self.tabs[self.active_tab] + .current_path + .as_deref() + .and_then(|host| { + crate::io::xref_model::to_pathtype_result(path, host, Pathtype::Relative).ok() + }) + .unwrap_or_else(|| path.to_string()), + Pathtype::None => std::path::Path::new(&path.replace('\\', "/")) + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| path.to_string()), + } + } + + /// PDFATTACH after the file is chosen. + pub(in crate::app) fn open_pdf_attach_dialog(&mut self, path: &str) { + let i = self.active_tab; + let mut existing: Vec<(String, String)> = Vec::new(); + for object in self.tabs[i].scene.document.objects.values() { + if let acadrust::objects::ObjectType::UnderlayDefinition(def) = object { + let name = file_stem(&def.file_path); + if !existing.iter().any(|(n, _)| *n == name) { + existing.push((name, def.file_path.clone())); + } + } + } + let mut state = PdfAttachState::new(existing); + self.load_pdf_attach_file(&mut state, path); + self.pdf_attach = Some(state); + self.active_modal = Some(crate::app::ModalKind::PdfAttach); + } + + fn load_pdf_attach_file(&self, state: &mut PdfAttachState, path: &str) { + state.path = path.to_string(); + state.name = file_stem(path); + state.pages = page_thumbs(path); + state.selected = vec![0]; + state.anchor = 0; + state.found_in = crate::entities::underlay::display_path(path); + state.page_size = page_size_text(path, "1"); + state.saved_path = + crate::entities::underlay::display_path(&self.pdf_stored_path(path, state.path_type.0)); + } + + /// ULAYERS: every PDF underlay of the drawing, the selected one first + /// shown. + pub(in crate::app) fn open_underlay_layers_dialog(&mut self, i: usize) { + let document = &self.tabs[i].scene.document; + let selected: Vec = self.tabs[i] + .scene + .selected_entities() + .iter() + .map(|(h, _)| *h) + .collect(); + let mut targets: Vec = Vec::new(); + for entity in document.entities() { + let acadrust::EntityType::Underlay(u) = entity else { + continue; + }; + if u.underlay_type != acadrust::entities::UnderlayType::Pdf { + continue; + } + let Some(def) = crate::entities::underlay::definition(u, document) else { + continue; + }; + let mut name = crate::entities::underlay::definition_display_name(def); + let same = targets.iter().filter(|t| t.name.starts_with(&name)).count(); + if same > 0 { + name = format!("{name} ({})", same + 1); + } + let mut layers: Vec = crate::scene::model::pdf_layers::layers(&def.file_path) + .into_iter() + .map(|l| l.name) + .collect(); + layers.sort_by_key(|n| n.to_lowercase()); + layers.dedup(); + targets.push(LayerTarget { + name, + handle: u.common.handle, + layers, + hidden: crate::scene::model::pdf_layers::hidden_layers(u), + }); + } + if targets.is_empty() { + self.command_line.push_error("No PDF underlays found."); + return; + } + let current = targets + .iter() + .position(|t| selected.contains(&t.handle)) + .unwrap_or(0); + self.underlay_layers = Some(UnderlayLayersState { + targets, + current, + search: String::new(), + }); + self.active_modal = Some(crate::app::ModalKind::UnderlayLayers); + } + + /// PDFIMPORT's Settings option: the dialog over the running command. + pub(in crate::app) fn open_pdf_import_settings(&mut self) { + self.pdf_import_settings = Some(pdf_import::import_settings()); + self.active_modal = Some(crate::app::ModalKind::PdfImportSettings); + } + + /// PDFIMPORT's File option after the file is chosen. + pub(in crate::app) fn open_pdf_import_file(&mut self, path: &str) { + self.pdf_import_file = Some(PdfImportFileState { + path: path.to_string(), + pages: page_thumbs(path), + selected: 0, + page_text: "1".into(), + insert_on_screen: false, + scale: "1".into(), + rotation: RotationChoice(0), + page_size: page_size_mm(path, "1"), + settings: pdf_import::import_settings(), + }); + self.active_modal = Some(crate::app::ModalKind::PdfImportFile); + } + + /// Writes the layers each underlay turns off, as one undo step. + fn apply_underlay_layers(&mut self, i: usize, targets: Vec) { + use crate::scene::model::pdf_layers::{hidden_layers, LAYER_OVERRIDE_APP}; + let changed: Vec = targets + .into_iter() + .filter(|t| match self.tabs[i].scene.document.get_entity(t.handle) { + Some(acadrust::EntityType::Underlay(u)) => hidden_layers(u) != t.hidden, + _ => false, + }) + .collect(); + if changed.is_empty() { + return; + } + self.push_undo_snapshot(i, "ULAYERS"); + self.tabs[i].scene.ensure_app_id(LAYER_OVERRIDE_APP); + let app_handle = self.tabs[i] + .scene + .document + .app_ids + .get(LAYER_OVERRIDE_APP) + .map(|a| a.handle.value()); + for target in &changed { + if let Some(acadrust::EntityType::Underlay(u)) = + self.tabs[i].scene.document.get_entity_mut(target.handle) + { + let data = &mut u.common.extended_data; + if let Some(app) = app_handle { + data.raw_dwg_eed.retain(|(a, _)| *a != app); + } + data.remove_record(LAYER_OVERRIDE_APP); + if !target.hidden.is_empty() { + let mut record = acadrust::xdata::ExtendedDataRecord::new(LAYER_OVERRIDE_APP); + for name in &target.hidden { + record.add_value(acadrust::xdata::XDataValue::String(name.clone())); + } + data.add_record(record); + } + } + self.tabs[i].scene.reseed_derived_caches(target.handle); + } + let changes: Vec<_> = changed + .iter() + .map(|t| (t.handle, crate::scene::ChangeKind::Modified)) + .collect(); + self.tabs[i].scene.bump_entities(&changes); + self.tabs[i].dirty = true; + self.refresh_properties(); + } + + pub(in crate::app) fn update_pdf_dialog(&mut self, message: PdfDialogMsg) -> Task { + let i = self.active_tab; + match message { + PdfDialogMsg::Help(which) => { + let text = match which { + "attach" => crate::t!("Attaches a PDF file as an underlay."), + "layers" => crate::t!("Turns the layers of a PDF underlay on or off."), + _ => crate::t!("Imports the geometry, fills, raster images and text of a PDF file as drawing objects."), + }; + self.command_line.push_info(text.as_ref()); + } + PdfDialogMsg::Options => { + self.options_parent = self.active_modal; + self.options_open(); + } + + // ── Attach ──────────────────────────────────────────────────── + PdfDialogMsg::AttachBrowse => { + if let Some(state) = &self.pdf_attach { + state.remember(); + } + return Task::done(Message::PdfAttachPick); + } + PdfDialogMsg::AttachName(name) => { + let Some(mut state) = self.pdf_attach.take() else { + return Task::none(); + }; + if let Some((_, path)) = state.existing.iter().find(|(n, _)| *n == name).cloned() { + self.load_pdf_attach_file(&mut state, &path); + } + self.pdf_attach = Some(state); + } + PdfDialogMsg::AttachPage(page) => { + let (ctrl, shift) = (self.ctrl_down, self.shift_down); + if let Some(state) = self.pdf_attach.as_mut() { + click_page(&mut state.selected, &mut state.anchor, page, ctrl, shift); + let first = state.page_labels().into_iter().next().unwrap_or_default(); + state.page_size = page_size_text(&state.path, &first); + } + } + PdfDialogMsg::AttachPathType(choice) => { + let Some(mut state) = self.pdf_attach.take() else { + return Task::none(); + }; + state.path_type = choice; + state.saved_path = crate::entities::underlay::display_path( + &self.pdf_stored_path(&state.path, choice.0), + ); + self.pdf_attach = Some(state); + } + PdfDialogMsg::AttachInsertOnScreen(on) => { + if let Some(s) = self.pdf_attach.as_mut() { + s.insert_on_screen = on; + } + } + PdfDialogMsg::AttachInsert(axis, value) => { + if let Some(s) = self.pdf_attach.as_mut() { + s.insert[axis.min(2)] = value; + } + } + PdfDialogMsg::AttachScaleOnScreen(on) => { + if let Some(s) = self.pdf_attach.as_mut() { + s.scale_on_screen = on; + } + } + PdfDialogMsg::AttachScale(value) => { + if let Some(s) = self.pdf_attach.as_mut() { + s.scale = value; + } + } + PdfDialogMsg::AttachRotationOnScreen(on) => { + if let Some(s) = self.pdf_attach.as_mut() { + s.rotation_on_screen = on; + } + } + PdfDialogMsg::AttachRotation(value) => { + if let Some(s) = self.pdf_attach.as_mut() { + s.rotation = value; + } + } + PdfDialogMsg::AttachDetails(on) => { + if let Some(s) = self.pdf_attach.as_mut() { + s.details = on; + } + } + PdfDialogMsg::AttachOk => return self.pdf_attach_ok(i), + + // ── Underlay Layers ─────────────────────────────────────────── + PdfDialogMsg::LayersUnderlay(name) => { + if let Some(s) = self.underlay_layers.as_mut() { + if let Some(at) = s.targets.iter().position(|t| t.name == name) { + s.current = at; + } + } + } + PdfDialogMsg::LayersSearch(text) => { + if let Some(s) = self.underlay_layers.as_mut() { + s.search = text; + } + } + PdfDialogMsg::LayersToggle(layer) => { + if let Some(s) = self.underlay_layers.as_mut() { + if let Some(t) = s.targets.get_mut(s.current) { + match t.hidden.iter().position(|h| *h == layer) { + Some(at) => { + t.hidden.remove(at); + } + None => t.hidden.push(layer), + } + } + } + } + PdfDialogMsg::LayersOk => { + if let Some(state) = self.underlay_layers.take() { + self.apply_underlay_layers(i, state.targets); + } + self.close_active_modal(); + } + + // ── Import settings ─────────────────────────────────────────── + PdfDialogMsg::Vector(_) + | PdfDialogMsg::Fills(_) + | PdfDialogMsg::Text(_) + | PdfDialogMsg::Raster(_) + | PdfDialogMsg::Layers(_) + | PdfDialogMsg::AsBlock(_) + | PdfDialogMsg::Join(_) + | PdfDialogMsg::Hatches(_) + | PdfDialogMsg::Lineweights(_) + | PdfDialogMsg::Linetypes(_) => { + let settings = if self.active_modal == Some(crate::app::ModalKind::PdfImportFile) { + self.pdf_import_file.as_mut().map(|s| &mut s.settings) + } else { + self.pdf_import_settings.as_mut() + }; + if let Some(s) = settings { + match message { + PdfDialogMsg::Vector(v) => s.vector = v, + PdfDialogMsg::Fills(v) => s.fills = v, + PdfDialogMsg::Text(v) => s.text = v, + PdfDialogMsg::Raster(v) => s.raster = v, + PdfDialogMsg::Layers(v) => s.layers = v, + PdfDialogMsg::AsBlock(v) => s.as_block = v, + PdfDialogMsg::Join(v) => s.join = v, + PdfDialogMsg::Hatches(v) => s.hatches = v, + PdfDialogMsg::Lineweights(v) => s.lineweights = v, + PdfDialogMsg::Linetypes(v) => s.linetypes = v, + _ => {} + } + } + } + PdfDialogMsg::SettingsOk => { + if let Some(s) = self.pdf_import_settings.take() { + pdf_import::set_import_settings(s); + } + self.close_active_modal(); + } + + // ── Import PDF ──────────────────────────────────────────────── + PdfDialogMsg::ImportBrowse => return Task::done(Message::PdfImportPick), + PdfDialogMsg::ImportPage(page) => { + if let Some(s) = self.pdf_import_file.as_mut() { + s.selected = page; + s.page_text = (page + 1).to_string(); + s.page_size = page_size_mm(&s.path, &s.page_text); + } + } + PdfDialogMsg::ImportPageText(text) => { + if let Some(s) = self.pdf_import_file.as_mut() { + if let Some(n) = text.trim().parse::().ok().filter(|n| (1..=s.pages.len()).contains(n)) { + s.selected = n - 1; + s.page_size = page_size_mm(&s.path, &n.to_string()); + } + s.page_text = text; + } + } + PdfDialogMsg::ImportInsertOnScreen(on) => { + if let Some(s) = self.pdf_import_file.as_mut() { + s.insert_on_screen = on; + } + } + PdfDialogMsg::ImportScale(value) => { + if let Some(s) = self.pdf_import_file.as_mut() { + s.scale = value; + } + } + PdfDialogMsg::ImportRotation(r) => { + if let Some(s) = self.pdf_import_file.as_mut() { + s.rotation = r; + } + } + PdfDialogMsg::ImportOk => { + let Some(state) = self.pdf_import_file.as_ref() else { + return Task::none(); + }; + let Some(scale) = state.scale.trim().parse::().ok().filter(|v| *v > 0.0) else { + self.command_line.push_error("Value must be positive and nonzero."); + return Task::none(); + }; + let state = self.pdf_import_file.take().expect("checked above"); + pdf_import::set_import_settings(state.settings); + self.close_active_modal(); + let import = PdfFileImport { + path: state.path.clone(), + page: (state.selected + 1).to_string(), + scale, + rotation: (state.rotation.0 as f64).to_radians(), + insertion: glam::DVec3::ZERO, + }; + if state.insert_on_screen { + use crate::command::CadCommand; + let command = pdf_import::PdfImportPointCommand::new(import); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + return self.focus_cmd_input(); + } + self.run_pdf_import(i, crate::app::commands::pdf_import::PdfImportSource::File(import)); + } + } + Task::none() + } + + fn pdf_attach_ok(&mut self, i: usize) -> Task { + use crate::command::CadCommand; + let Some(state) = self.pdf_attach.as_ref() else { + return Task::none(); + }; + let number = |s: &str| s.trim().parse::().ok(); + let insertion = if state.insert_on_screen { + None + } else { + match (number(&state.insert[0]), number(&state.insert[1]), number(&state.insert[2])) { + (Some(x), Some(y), Some(z)) => Some(glam::DVec3::new(x, y, z)), + _ => { + self.command_line.push_error("Requires numeric value."); + return Task::none(); + } + } + }; + let scale = if state.scale_on_screen { + None + } else { + match number(&state.scale).filter(|v| *v > 0.0) { + Some(v) => Some(v), + None => { + self.command_line.push_error("Value must be positive and nonzero."); + return Task::none(); + } + } + }; + let rotation = if state.rotation_on_screen { + None + } else { + match number(&state.rotation) { + Some(v) => Some(v), + None => { + self.command_line.push_error("Requires numeric value."); + return Task::none(); + } + } + }; + let state = self.pdf_attach.take().expect("checked above"); + state.remember(); + let stored = self.pdf_stored_path(&state.path, state.path_type.0); + if stored != state.path { + if let Some(bytes) = crate::scene::model::pdf_raster::source_bytes(&state.path) { + crate::scene::model::pdf_raster::register_source(&stored, bytes); + } + } + self.close_active_modal(); + let insunits = self.tabs[i].scene.document.header.insertion_units; + let mut command = crate::modules::insert::pdf_attach::PdfAttachCommand::from_dialog( + &state.path, + &stored, + &state.page_labels(), + scale, + rotation, + insunits, + ); + match insertion { + Some(point) => { + let result = command.on_point(point); + self.tabs[i].active_cmd = Some(Box::new(command)); + if matches!(result, crate::command::CmdResult::NeedPoint) { + self.command_line.push_info( + &self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()).unwrap_or_default(), + ); + return self.focus_cmd_input(); + } + self.apply_cmd_result(result) + } + None => { + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + self.focus_cmd_input() + } + } + } +} + +/// "200 × 100 mm" for a page. +fn page_size_mm(path: &str, page: &str) -> String { + crate::scene::model::pdf_raster::page_size_inches(path, page) + .map(|(w, h)| format!("{:.0} × {:.0} mm", w * 25.4, h * 25.4)) + .unwrap_or_default() +} diff --git a/src/app/commands/pdf_import.rs b/src/app/commands/pdf_import.rs index 9195650e2..5b677fe27 100644 --- a/src/app/commands/pdf_import.rs +++ b/src/app/commands/pdf_import.rs @@ -9,7 +9,7 @@ use crate::modules::insert::pdf_import::{self, ImportArea, PdfImportRequest, Und /// origin at full size. pub(crate) enum PdfImportSource { Underlay(PdfImportRequest), - File(String), + File(pdf_import::PdfFileImport), } impl OpenCADStudio { @@ -26,11 +26,7 @@ impl OpenCADStudio { let Some(def) = crate::entities::underlay::definition(underlay, document) else { return; }; - if self.tabs[i] - .scene - .unloaded_underlay_definitions - .contains(&underlay.definition_handle) - { + if def.unloaded { self.command_line .push_error("Cannot bind a PDF underlay that is unloaded."); return; @@ -44,10 +40,10 @@ impl OpenCADStudio { Some(request.underlay), ) } - PdfImportSource::File(path) => ( - acadrust::entities::Underlay::pdf(), - path.clone(), - "1".to_string(), + PdfImportSource::File(import) => ( + import.placement(), + import.path.clone(), + import.page.clone(), ImportArea::All, None, None, @@ -62,14 +58,80 @@ impl OpenCADStudio { .command_line .push_output(&format!("Importing page {page} of PDF file: {shown}...")), } - let Some(vectors) = crate::scene::model::pdf_vector::page_vectors(&path, &page) else { + // The underlay's own layer overrides leave those layers out. + let hidden = crate::scene::model::pdf_layers::hidden_layers(&underlay); + let Some(content) = + crate::scene::model::pdf_vector::page_content_by_layer(&path, &page, &hidden) + else { self.command_line.push_error(&format!("{shown} not found.")); return; }; - let result = pdf_import::convert(&vectors, &underlay, &area); + let settings = pdf_import::import_settings(); + let current_layer = { + let header = &self.tabs[i].scene.document.header; + if header.current_layer_name.is_empty() { + self.tabs[i].active_layer.clone() + } else { + header.current_layer_name.clone() + } + }; + let naming = pdf_import::LayerNaming { + settings, + prefix: import_prefix(&path), + current: current_layer.clone(), + }; + let mut result = pdf_import::convert(&content, &underlay, &area, &naming); + let images = std::mem::take(&mut result.images); + let image_dir = self.tabs[i] + .current_path + .as_deref() + .and_then(|p| p.parent()) + .map(|p| p.to_path_buf()) + .or_else(|| std::path::Path::new(&path).parent().map(|p| p.to_path_buf())) + .map(|dir| dir.join("PDF Images")); self.push_undo_snapshot(i, "PDFIMPORT"); let scene = &mut self.tabs[i].scene; + if result.dashed && !scene.document.line_types.contains(pdf_import::DASH_LINETYPE) { + let mut lt = acadrust::tables::LineType::new(pdf_import::DASH_LINETYPE); + lt.handle = scene.document.allocate_handle(); + lt.add_element(acadrust::tables::LineTypeElement::dash(0.8)); + lt.add_element(acadrust::tables::LineTypeElement::space(0.2)); + lt.pattern_length = 1.0; + let _ = scene.document.line_types.add(lt); + } + // Raster images: each written as a PNG under "PDF Images" next to the + // drawing (or the PDF while the drawing is unsaved), then referenced. + let stem = std::path::Path::new(&path.replace('\\', "/")) + .file_stem() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| "PDF".to_string()); + for (n, image) in images.iter().enumerate() { + let Some(dir) = &image_dir else { break }; + if std::fs::create_dir_all(dir).is_err() { + break; + } + let file = dir.join(format!("{stem}{:08x}.png", image_file_id(&path, &page, n))); + if image::save_buffer(&file, &image.rgba, image.width, image.height, image::ColorType::Rgba8) + .is_err() + { + continue; + } + let mut raster = acadrust::entities::RasterImage::with_size( + file.to_string_lossy().as_ref(), + image.insertion, + image.width as f64, + image.height as f64, + 1.0, + 1.0, + ); + raster.u_vector = image.u; + raster.v_vector = image.v; + raster.flags = acadrust::entities::ImageDisplayFlags::SHOW_IMAGE + | acadrust::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY; + raster.common.layer = image.layer.clone(); + result.entities.push(acadrust::EntityType::RasterImage(raster)); + } for (name, color) in &result.layers { if !scene.document.layers.contains(name) { let mut layer = acadrust::tables::Layer::new(name.as_str()); @@ -86,16 +148,40 @@ impl OpenCADStudio { let _ = scene.document.text_styles.add(style); } } - for entity in result.entities { - scene.add_entity(entity); + if settings.as_block && !result.entities.is_empty() { + // One block named after the file, inserted at the origin on the + // current layer. + let mut name = stem.clone(); + let mut n = 1; + while scene.document.block_records.get(&name).is_some() { + n += 1; + name = format!("{stem}({n})"); + } + if scene + .define_block_from_owned_entities(result.entities, &name, glam::DVec3::ZERO) + .is_ok() + { + let mut insert = acadrust::entities::Insert::new( + name, + acadrust::types::Vector3::new(0.0, 0.0, 0.0), + ); + insert.common.layer = current_layer; + scene.add_entity(acadrust::EntityType::Insert(insert)); + } + } else { + for entity in result.entities { + scene.add_entity(entity); + } } if let (Some(mode), Some(handle)) = (mode, handle) { match mode { UnderlayMode::Keep => {} UnderlayMode::Unload => { - scene - .unloaded_underlay_definitions - .insert(underlay.definition_handle); + if let Some(ObjectType::UnderlayDefinition(definition)) = + scene.document.objects.get_mut(&underlay.definition_handle) + { + definition.unloaded = true; + } scene.reseed_underlays(); } UnderlayMode::Detach => { @@ -122,3 +208,62 @@ impl OpenCADStudio { self.refresh_properties(); } } + +impl OpenCADStudio { + /// PDFATTACH's end: every page's definition (created or reused, with the + /// definitions dictionary) and underlay in one undo step. + pub(crate) fn attach_pdf_pages( + &mut self, + i: usize, + label: String, + path: &str, + pages: Vec<(String, acadrust::EntityType)>, + ) { + let pending = self.begin_undo(i, label, pages.len(), false); + + for (page, mut entity) in pages { + let definition = crate::modules::insert::pdf_attach::ensure_pdf_definition( + &mut self.tabs[i].scene.document, + path, + &page, + ); + if let acadrust::EntityType::Underlay(underlay) = &mut entity { + underlay.definition_handle = definition; + } + self.commit_entity_handle(entity); + } + self.tabs[i].dirty = true; + if let Some(pd) = pending { + self.commit_undo_delta(i, pd); + } + } +} + +/// The layer prefix of an imported file: "PDF_" for the first file of the +/// session, "PDF2_", "PDF3_" … for each other file. +// ponytail: remembered for the session only; a reopened drawing starts from +// "PDF_" again. +fn import_prefix(path: &str) -> String { + static FILES: std::sync::Mutex> = std::sync::Mutex::new(Vec::new()); + let mut files = FILES.lock().unwrap_or_else(|e| e.into_inner()); + let index = match files.iter().position(|f| f.eq_ignore_ascii_case(path)) { + Some(at) => at, + None => { + files.push(path.to_string()); + files.len() - 1 + } + }; + if index == 0 { + "PDF_".to_string() + } else { + format!("PDF{}_", index + 1) + } +} + +/// A file name suffix for an imported image, different per import. +fn image_file_id(path: &str, page: &str, n: usize) -> u32 { + use std::hash::{Hash, Hasher}; + let mut h = std::collections::hash_map::DefaultHasher::new(); + (path, page, n, std::time::SystemTime::now()).hash(&mut h); + h.finish() as u32 +} diff --git a/src/app/commands/pdf_underlay.rs b/src/app/commands/pdf_underlay.rs new file mode 100644 index 000000000..fec953ea5 --- /dev/null +++ b/src/app/commands/pdf_underlay.rs @@ -0,0 +1,166 @@ +//! The contextual PDF Underlay ribbon tab: its values come from the selected +//! underlays, its buttons edit them. + +use super::*; +use acadrust::entities::{Underlay, UnderlayDisplayFlags, UnderlayType}; +use crate::ui::ribbon::{UnderlayContext, UnderlayTabMsg}; + +impl OpenCADStudio { + /// The selected PDF underlays, or nothing when anything else is selected. + fn selected_pdf_underlays(&self, i: usize) -> Vec<(acadrust::Handle, Underlay)> { + let mut out = Vec::new(); + for (handle, entity) in self.tabs[i].scene.selected_entities() { + match entity { + acadrust::EntityType::Underlay(u) if u.underlay_type == UnderlayType::Pdf => { + out.push((handle, u.clone())); + } + _ => return Vec::new(), + } + } + out + } + + /// Show the tab while only PDF underlays are selected. + pub(in crate::app) fn sync_underlay_tab(&mut self) { + let i = self.active_tab; + let context = if self.tabs[i].is_start { + None + } else { + self.selected_pdf_underlays(i).first().map(|(_, u)| { + UnderlayContext::new( + u.contrast, + u.fade, + u.flags.contains(UnderlayDisplayFlags::MONOCHROME), + u.flags.contains(UnderlayDisplayFlags::ON), + crate::scene::model::pdf_vector::pdf_osnap(), + ) + }) + }; + self.ribbon.set_underlay_context(context); + } + + /// One undo step that edits every selected PDF underlay. + fn edit_selected_underlays(&mut self, i: usize, label: &str, edit: impl Fn(&mut Underlay)) { + let handles: Vec<_> = self + .selected_pdf_underlays(i) + .into_iter() + .map(|(h, _)| h) + .filter(|h| !self.tabs[i].scene.is_layer_locked(*h)) + .collect(); + if handles.is_empty() { + return; + } + self.push_undo_snapshot(i, label); + for handle in &handles { + if let Some(acadrust::EntityType::Underlay(u)) = + self.tabs[i].scene.document.get_entity_mut(*handle) + { + edit(u); + } + self.tabs[i].scene.reseed_derived_caches(*handle); + } + let changes: Vec<_> = handles + .iter() + .map(|h| (*h, crate::scene::ChangeKind::Modified)) + .collect(); + self.tabs[i].scene.bump_entities(&changes); + self.tabs[i].dirty = true; + self.refresh_properties(); + } + + pub(in crate::app) fn update_underlay_tab(&mut self, message: UnderlayTabMsg) -> Task { + let i = self.active_tab; + let Some(ctx) = self.ribbon.underlay_context_mut() else { + return Task::none(); + }; + match message { + UnderlayTabMsg::Contrast(v) => { + ctx.contrast = v; + ctx.contrast_text = v.to_string(); + } + UnderlayTabMsg::Fade(v) => { + ctx.fade = v; + ctx.fade_text = v.to_string(); + } + UnderlayTabMsg::ContrastText(s) => ctx.contrast_text = s, + UnderlayTabMsg::FadeText(s) => ctx.fade_text = s, + UnderlayTabMsg::Commit => { + // A typed value outside 0–100 or not a number keeps the value. + let parse = |s: &str, old: u8| { + s.trim().parse::().ok().filter(|v| *v <= 100).unwrap_or(old) + }; + let (contrast, fade) = ( + parse(&ctx.contrast_text, ctx.contrast), + parse(&ctx.fade_text, ctx.fade), + ); + ctx.contrast_text = contrast.to_string(); + ctx.fade_text = fade.to_string(); + self.edit_selected_underlays(i, "PDFADJUST", |u| { + u.set_contrast(contrast); + u.set_fade(fade); + }); + } + } + Task::none() + } + + /// The tab's buttons (host-only command names). + pub(super) fn dispatch_pdf_underlay(&mut self, cmd: &str, i: usize) -> Option> { + match cmd { + "_PDFULMONO" => { + let on = !self + .selected_pdf_underlays(i) + .first() + .is_some_and(|(_, u)| u.flags.contains(UnderlayDisplayFlags::MONOCHROME)); + self.edit_selected_underlays(i, "PDFADJUST", |u| u.set_monochrome(on)); + } + "_PDFULSHOW" => { + let on = !self + .selected_pdf_underlays(i) + .first() + .is_some_and(|(_, u)| u.flags.contains(UnderlayDisplayFlags::ON)); + self.edit_selected_underlays(i, "PDFUNDERLAY", |u| u.set_on(on)); + } + "_PDFULSNAP" => { + let on = !crate::scene::model::pdf_vector::pdf_osnap(); + crate::scene::model::pdf_vector::set_pdf_osnap(on); + self.tabs[i].scene.reseed_underlays(); + self.sync_underlay_tab(); + } + "_PDFULUNCLIP" => { + self.edit_selected_underlays(i, "PDFCLIP", |u| { + u.clip_boundary_vertices.clear(); + u.clip_inverted = false; + u.flags -= UnderlayDisplayFlags::CLIPPING; + }); + } + "_PDFULCLIP" => { + use crate::command::CadCommand; + let Some((handle, underlay)) = self.selected_pdf_underlays(i).into_iter().next() + else { + return Some(Task::none()); + }; + let (command, first) = + crate::modules::insert::pdf_clip::PdfClipCommand::new_boundary(handle, underlay); + if let crate::command::CmdResult::ReportMeasurement(text) = first { + for line in text.lines() { + self.command_line.push_output(line); + } + } + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + "_PDFULIMPORT" => { + use crate::command::CadCommand; + let Some((handle, _)) = self.selected_pdf_underlays(i).into_iter().next() else { + return Some(Task::none()); + }; + let command = crate::modules::insert::pdf_import::PdfImportCommand::for_underlay(handle); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + _ => return None, + } + Some(self.finish_dispatch(cmd)) + } +} diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index ba100ee1b..c19c8dee6 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -1017,6 +1017,9 @@ impl OpenCADStudio { | "PDFFRAME" | "PDFOSNAP" | "UOSNAP" + | "PDFIMPORTMODE" + | "PDFIMPORTFILTER" + | "PDFIMPORTLAYERS" | "POINTCLOUDCLIPFRAME" | "XCLIPFRAME" | "WIPEOUTFRAME" @@ -1110,6 +1113,35 @@ impl OpenCADStudio { } // Snapping to the geometry inside underlays (one switch for // PDF underlays and underlays in general). + // The PDF Import Settings' options as bits. + if matches!(name.as_str(), "PDFIMPORTMODE" | "PDFIMPORTFILTER" | "PDFIMPORTLAYERS") { + use crate::modules::insert::pdf_import::{import_settings, set_import_settings, ImportLayers}; + let mut settings = import_settings(); + let (current, max) = match name.as_str() { + "PDFIMPORTMODE" => (settings.mode(), 31), + "PDFIMPORTFILTER" => (settings.filter(), 15), + _ => (settings.layers_value(), 2), + }; + if let Some(value) = &value { + match value.parse::().ok().filter(|v| (0..=max).contains(v)) { + Some(v) => { + match name.as_str() { + "PDFIMPORTMODE" => settings.set_mode(v), + "PDFIMPORTFILTER" => settings.set_filter(v), + _ => { + settings.layers = [ImportLayers::Pdf, ImportLayers::Object, ImportLayers::Current][v as usize] + } + } + set_import_settings(settings); + } + None => self.command_line.push_error(&format!("Requires an integer between 0 and {max}.")), + } + } else { + self.command_line.push_output(&format!("Enter new value for {name} <{current}>:")); + self.pending_setvar = Some(name.clone()); + } + return Some(self.finish_dispatch(cmd)); + } if matches!(name.as_str(), "PDFOSNAP" | "UOSNAP") { let current = i16::from(crate::scene::model::pdf_vector::pdf_osnap()); if let Some(value) = &value { diff --git a/src/app/mod.rs b/src/app/mod.rs index 7e4a873df..fb29bb761 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -493,6 +493,13 @@ pub(super) struct OpenCADStudio { drawing_units: Option, /// Working copy of the Block Definition dialog; `None` while it is closed. block_definition: Option, + /// PDF dialogs' working copies; `None` while closed. + pdf_attach: Option, + underlay_layers: Option, + pdf_import_settings: Option, + pdf_import_file: Option, + /// The dialog Options was opened from; it comes back when Options closes. + options_parent: Option, /// Working copy of the structured feature-control-frame editor. geometric_tolerance: Option, /// PICKDRAG (#226): false (default) = press-drag lassoes; true = @@ -1871,6 +1878,10 @@ pub enum ModalKind { LayerTranslator, DrawingUnits, BlockDefinition, + PdfAttach, + UnderlayLayers, + PdfImportSettings, + PdfImportFile, GeometricTolerance, DraftingSettings, AutoConstrainSettings, @@ -3773,6 +3784,11 @@ pub enum Message { PdfAttachPick, /// PDFIMPORT File: pick the PDF to import. PdfImportPick, + /// The contextual PDF Underlay tab. + RibbonSelectUnderlayTab, + /// An edit in one of the PDF dialogs. + PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg), + UnderlayTab(crate::ui::ribbon::UnderlayTabMsg), PdfImportPickResult(Result<(std::path::PathBuf, std::sync::Arc>), String>), /// Result of the PDFATTACH file picker. PdfAttachPickResult(Result<(std::path::PathBuf, std::sync::Arc>), String>), @@ -3944,6 +3960,11 @@ impl OpenCADStudio { layer_translator: None, drawing_units: None, block_definition: None, + pdf_attach: None, + underlay_layers: None, + pdf_import_settings: None, + pdf_import_file: None, + options_parent: None, geometric_tolerance: None, pick_drag_rect: false, perf_hud: false, diff --git a/src/app/properties.rs b/src/app/properties.rs index ac503be92..bfef367ec 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -2610,6 +2610,7 @@ filter={:.1} local={:.1} aggregate={:.1} entities={}", let m_panel = t_all.map(|t| t.elapsed().as_secs_f64() * 1000.0); let t_ribbon = crate::perf::enabled().then(iced::time::Instant::now); self.sync_ribbon_from_selection(); + self.sync_underlay_tab(); if let Some(t) = t_all { let ribbon_ms = t_ribbon.map_or(0.0, |r| r.elapsed().as_secs_f64() * 1000.0); let total_ms = t.elapsed().as_secs_f64() * 1000.0; diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index 7c20cb776..57a7e732b 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -990,6 +990,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven Err(msg) => self.command_line.push_error(msg.as_str()), } } + self.tabs[i].scene.reseed_underlays(); } XrefPaletteOp::Reload => { let base_dir: std::path::PathBuf = host @@ -1016,6 +1017,21 @@ 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.", name ).as_ref()); + } else if *kind == crate::io::xref_model::RefKind::Pdf { + // A PDF reloads by clearing its definition's unloaded state. + for (key, row_name, row_kind, _) in &picked { + if row_kind != kind || row_name != name { + continue; + } + let handle = acadrust::types::Handle::new(*key); + if let Some(acadrust::objects::ObjectType::UnderlayDefinition(def)) = + self.tabs[i].scene.document.objects.get_mut(&handle) + { + def.unloaded = false; + } + self.tabs[i].xref_unloaded.remove(key); + } + self.tabs[i].scene.reseed_underlays(); } else if *kind != crate::io::xref_model::RefKind::DwgXref { self.command_line.push_error(crate::tf!( "{}: reload applies to drawing references only.", diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index c4f290490..6835fe512 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -128,12 +128,19 @@ impl OpenCADStudio { /// Close the active in-canvas modal (Plan B), mirroring what closing the /// old OS window did: a style editor discards its staged (un-applied) /// changes, and the ribbon tool that launched the dialog is de-highlighted. - fn close_active_modal(&mut self) { + pub(in crate::app) fn close_active_modal(&mut self) { self.mark_startup_modal_shown(); use super::ModalKind::*; // Plot Style opened from PLOT behaves as a child modal. // Closing it restores the parent Plot dialog instead of returning // to the drawing. + // Options opened from a PDF dialog returns to it. + if self.active_modal == Some(Options) { + if let Some(parent) = self.options_parent.take() { + self.active_modal = Some(parent); + return; + } + } if self.active_modal == Some(Plotstyle) { if let Some((plot_offset, plot_resize)) = self.plotstyle_parent_plot_geometry.take() { self.active_modal = Some(Plot); @@ -208,6 +215,10 @@ impl OpenCADStudio { } Some(GeometricTolerance) => self.geometric_tolerance = None, Some(BlockDefinition) => self.block_definition = None, + Some(PdfAttach) => self.pdf_attach = None, + Some(UnderlayLayers) => self.underlay_layers = None, + Some(PdfImportSettings) => self.pdf_import_settings = None, + Some(PdfImportFile) => self.pdf_import_file = None, Some(Hyperlink) => { self.hyperlink_editor_handles.clear(); self.hyperlink_editor_url.clear(); @@ -1254,20 +1265,13 @@ impl OpenCADStudio { ), Message::PdfAttachPickResult(Ok((path, bytes))) => { - use crate::command::CadCommand; - use crate::modules::insert::pdf_attach::PdfAttachCommand; - let i = self.active_tab; let path_str = path.to_string_lossy().into_owned(); + let _ = i; crate::scene::model::pdf_raster::register_source(&path_str, bytes); - // The definition is created with the underlay when it is - // placed; the command asks for the page first. - let insunits = self.tabs[i].scene.document.header.insertion_units; - let cmd = PdfAttachCommand::with_file(&path_str, insunits); - - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - + // Pages, placement and path type are chosen in the Attach + // dialog; the definition is created with the underlay. + self.open_pdf_attach_dialog(&path_str); Task::none() } @@ -1292,11 +1296,9 @@ impl OpenCADStudio { Message::PdfImportPickResult(Ok((path, bytes))) => { let i = self.active_tab; let path_str = path.to_string_lossy().into_owned(); + let _ = i; crate::scene::model::pdf_raster::register_source(&path_str, bytes); - self.run_pdf_import( - i, - crate::app::commands::pdf_import::PdfImportSource::File(path_str), - ); + self.open_pdf_import_file(&path_str); Task::none() } Message::PdfImportPickResult(Err(_)) => Task::none(), @@ -1711,6 +1713,12 @@ impl OpenCADStudio { Task::none() } + Message::RibbonSelectUnderlayTab => { + self.ribbon.select_underlay_tab(); + Task::none() + } + Message::UnderlayTab(message) => self.update_underlay_tab(message), + Message::PdfDialog(message) => self.update_pdf_dialog(message), Message::RibbonSelectTab(idx) => { self.ribbon.select(idx); Task::none() diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index b7a7c092e..a0560e2c5 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -24,6 +24,10 @@ impl OpenCADStudio { Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(), Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(), Some(K::BlockDefinition) => crate::t!("Block Definition").into_owned(), + Some(K::PdfAttach) => crate::t!("Attach PDF Underlay").into_owned(), + Some(K::UnderlayLayers) => crate::t!("Underlay Layers").into_owned(), + Some(K::PdfImportSettings) => crate::t!("PDF Import Settings").into_owned(), + Some(K::PdfImportFile) => crate::t!("Import PDF").into_owned(), Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(), Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(), Some(K::AutoConstrainSettings) => crate::t!("Constraint Settings").into_owned(), @@ -506,6 +510,31 @@ impl OpenCADStudio { crate::ui::window::block_definition::view_window(state, flow) }) } + super::super::ModalKind::PdfAttach => { + let state = self.pdf_attach.as_ref()?; + let height = if state.details { 520 } else { 440 }; + sized_flow(ex, 820, height, |flow| { + crate::ui::window::pdf_dialogs::view_attach(state, flow) + }) + } + super::super::ModalKind::UnderlayLayers => { + let state = self.underlay_layers.as_ref()?; + sized_flow(ex, 500, 500, |flow| { + crate::ui::window::pdf_dialogs::view_layers(state, flow) + }) + } + super::super::ModalKind::PdfImportSettings => { + let settings = self.pdf_import_settings.as_ref()?; + sized_flow(ex, 600, 360, |flow| { + crate::ui::window::pdf_dialogs::view_import_settings(settings, flow) + }) + } + super::super::ModalKind::PdfImportFile => { + let state = self.pdf_import_file.as_ref()?; + sized_flow(ex, 900, 560, |flow| { + crate::ui::window::pdf_dialogs::view_import_file(state, flow) + }) + } super::super::ModalKind::GeometricTolerance => { let state = self.geometric_tolerance.as_ref()?; sized_flow(ex, 670, 530, |flow| { diff --git a/src/command.rs b/src/command.rs index 8730f689b..74e927d71 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1556,6 +1556,17 @@ pub enum CmdResult { BatchCopy(Vec, Vec), /// Erase `handle` and replace with new entities; command stays active. ReplaceEntity(Handle, Vec), + /// Attach PDF pages: each page's underlay with its file (as stored) and + /// page; the host creates or reuses the definitions and commits them all + /// in one undo step, then ends the command. + /// Open the PDF Import Settings dialog; the command keeps its prompt. + OpenPdfImportSettings, + /// Import a page of a file (PDFIMPORT File); ends the command. + PdfImportFile(crate::modules::insert::pdf_import::PdfFileImport), + AttachPdfPages { + path: String, + pages: Vec<(String, EntityType)>, + }, /// Import the vector content of a PDF underlay (PDFIMPORT); ends the command. PdfImport(crate::modules::insert::pdf_import::PdfImportRequest), /// Update one entity in place, preserve its handle, and end the command. @@ -2846,12 +2857,6 @@ pub trait CadCommand: Send { None } - /// If this command attaches a PDF page: its file and page. The host - /// creates (or reuses) the definition with the committed underlay. - fn pdf_attach_source(&self) -> Option<(String, String)> { - None - } - /// The block reference an ATTEDIT pick has resolved to, awaiting the /// attribute editor dialog; else None. fn attedit_pending_handle(&self) -> Option { diff --git a/src/entities/underlay.rs b/src/entities/underlay.rs index 142533c88..b8a5bdcde 100644 --- a/src/entities/underlay.rs +++ b/src/entities/underlay.rs @@ -420,7 +420,15 @@ impl PropertyEditable for Underlay { ro(t!("Saved Path").as_ref(), "ul_path", String::new()), yes_no(t!("Show underlay").as_ref(), "ul_on", show), yes_no(t!("Show clipped").as_ref(), "ul_clip", clipping), - ro(t!("Layer display overrides").as_ref(), "ul_layers", t!("None").into_owned()), + ro( + t!("Layer display overrides").as_ref(), + "ul_layers", + if crate::scene::model::pdf_layers::hidden_layers(self).is_empty() { + t!("None").into_owned() + } else { + t!("Applied").into_owned() + }, + ), ], }, ] diff --git a/src/io/mod.rs b/src/io/mod.rs index 646c0d536..fd284c277 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -372,6 +372,7 @@ async fn open_path_with_phase_attempt( }); callback }; + crate::io::xref::register_underlay_sources(&doc, base_dir); crate::io::xref::resolve_xrefs_with_progress( &mut doc, base_dir, diff --git a/src/io/xref.rs b/src/io/xref.rs index 948965cb6..2c2e55551 100644 --- a/src/io/xref.rs +++ b/src/io/xref.rs @@ -305,6 +305,25 @@ fn resolve_xrefs_with_filter( (result, dropped) } +/// Underlay files named by a relative (or moved) path: read them from where +/// the drawing is and register the bytes under the stored path, which is +/// what the underlay's definition names. +#[cfg(not(target_arch = "wasm32"))] +pub fn register_underlay_sources(doc: &CadDocument, base_dir: &Path) { + for object in doc.objects.values() { + let acadrust::objects::ObjectType::UnderlayDefinition(def) = object else { + continue; + }; + let stored = def.file_path.as_str(); + if stored.is_empty() || Path::new(stored).is_absolute() && Path::new(stored).exists() { + continue; + } + if let Some(bytes) = resolve_path(stored, base_dir).and_then(|p| std::fs::read(p).ok()) { + crate::scene::model::pdf_raster::register_source(stored, std::sync::Arc::new(bytes)); + } + } +} + /// Try to build an absolute path from a raw xref path string. /// Handles absolute paths, relative paths, and Windows-style separators. fn resolve_path(raw: &str, base_dir: &Path) -> Option { @@ -1089,7 +1108,7 @@ pub fn collect_entries_with_prev( }; let mut entry = ReferenceEntry::new(key, name, RefKind::Pdf); entry.saved_path = def.file_path.clone(); - if unloaded.contains(&UnloadKey::Direct(key)) { + if def.unloaded || unloaded.contains(&UnloadKey::Direct(key)) { entry.status = RefStatus::Unloaded; resolve_only(&mut entry, &def.file_path, base_dir); } else { @@ -1282,7 +1301,16 @@ pub fn unload_reference(doc: &mut CadDocument, key: u64) -> Result Ok(name), + RefTarget::Image { name, .. } => Ok(name), + // A PDF keeps its unloaded state on the definition (saved with it). + RefTarget::Pdf { handle, name } => { + if let Some(acadrust::objects::ObjectType::UnderlayDefinition(def)) = + doc.objects.get_mut(&handle) + { + def.unloaded = true; + } + Ok(name) + } } } diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 6334d967b..375b36099 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -2910,6 +2910,9 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Unable to save: file is in use." => Some(("command-line", "unable-to-save-file-is-in-use")), "Unable to verify the server certificate" => Some(("plugins", "unable-to-verify-the-server-certificate")), "Unavailable" => Some(("plugins", "unavailable")), + "Applied" => Some(("properties", "applied")), + "No PDF underlays found." => Some(("insert", "pdfui-no-pdf-underlays")), + "Specify insertion point:" => Some(("insert", "pdfui-specify-insertion-point")), "Underlay" => Some(("properties", "underlay")), "PDF Underlay" => Some(("properties", "pdf-underlay")), "DWF Underlay" => Some(("properties", "dwf-underlay")), @@ -2917,6 +2920,56 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Adjust colors for background" => Some(("properties", "adjust-colors-for-background")), "Page number" => Some(("properties", "page-number")), "Layer display overrides" => Some(("properties", "layer-display-overrides")), + "Display in\nMonochrome" => Some(("insert", "pdfui-display-in-monochrome")), + "Create Clipping\nBoundary" => Some(("insert", "pdfui-create-clipping-boundary")), + "Remove\nClipping" => Some(("insert", "pdfui-remove-clipping")), + "Show\nUnderlay" => Some(("insert", "pdfui-show-underlay")), + "Enable\nSnap" => Some(("insert", "pdfui-enable-snap")), + "External\nReferences" => Some(("insert", "pdfui-external-references")), + "Edit\nLayers" => Some(("insert", "pdfui-edit-layers")), + "Import As\nObjects" => Some(("insert", "pdfui-import-as-objects")), + "PDF Layers" => Some(("insert", "pdfui-pdf-layers")), + "PDF Import" => Some(("insert", "pdfui-pdf-import")), + "Attach PDF Underlay" => Some(("insert", "pdfui-attach-pdf-underlay")), + "Underlay Layers" => Some(("insert", "pdfui-underlay-layers")), + "PDF Import Settings" => Some(("insert", "pdfui-pdf-import-settings")), + "Import PDF" => Some(("insert", "pdfui-import-pdf")), + "Browse..." => Some(("insert", "pdfui-browse")), + "Select one or more pages from the PDF file:" => Some(("insert", "pdfui-select-one-or-more-pages-from-the-pdf-file")), + "Insertion point" => Some(("insert", "pdfui-insertion-point")), + "Angle:" => Some(("insert", "pdfui-angle")), + "Found in:" => Some(("insert", "pdfui-found-in")), + "Saved path:" => Some(("insert", "pdfui-saved-path")), + "Page size:" => Some(("insert", "pdfui-page-size")), + "Hide Details" => Some(("insert", "pdfui-hide-details")), + "Show Details" => Some(("insert", "pdfui-show-details")), + "Reference name:" => Some(("insert", "pdfui-reference-name")), + "Search for layer" => Some(("insert", "pdfui-search-for-layer")), + "This file does not contain any layers." => Some(("insert", "pdfui-this-file-does-not-contain-any-layers")), + "Select an underlay to view its layers." => Some(("insert", "pdfui-select-an-underlay-to-view-its-layers")), + "PDF data to import" => Some(("insert", "pdfui-pdf-data-to-import")), + "Vector geometry" => Some(("insert", "pdfui-vector-geometry")), + "Solid fills" => Some(("insert", "pdfui-solid-fills")), + "TrueType text" => Some(("insert", "pdfui-truetype-text")), + "Raster images" => Some(("insert", "pdfui-raster-images")), + "Use PDF layers" => Some(("insert", "pdfui-use-pdf-layers")), + "Create object layers" => Some(("insert", "pdfui-create-object-layers")), + "Import options" => Some(("insert", "pdfui-import-options")), + "Import as block" => Some(("insert", "pdfui-import-as-block")), + "Join line and arc segments" => Some(("insert", "pdfui-join-line-and-arc-segments")), + "Convert solid fills to hatches" => Some(("insert", "pdfui-convert-solid-fills-to-hatches")), + "Apply lineweight properties" => Some(("insert", "pdfui-apply-lineweight-properties")), + "Infer linetypes from collinear dashes" => Some(("insert", "pdfui-infer-linetypes-from-collinear-dashes")), + "Page to import" => Some(("insert", "pdfui-page-to-import")), + "Page:" => Some(("insert", "pdfui-page")), + "Total: {count}" => Some(("insert", "pdfui-total-count")), + "Page size: {size}" => Some(("insert", "pdfui-page-size-size")), + "Specify insertion point on-screen" => Some(("insert", "pdfui-specify-insertion-point-on-screen")), + "Scale:" => Some(("insert", "pdfui-scale")), + "Rotation:" => Some(("insert", "pdfui-rotation")), + "Attaches a PDF file as an underlay." => Some(("insert", "pdfui-attaches-a-pdf-file-as-an-underlay")), + "Turns the layers of a PDF underlay on or off." => Some(("insert", "pdfui-turns-the-layers-of-a-pdf-underlay-on-or-off")), + "Imports the geometry, fills, raster images and text of a PDF file as drawing objects." => Some(("insert", "pdfui-imports-the-geometry-fills-raster-images-and-text-of-a-pdf-f")), "Select PDF File" => Some(("insert", "select-pdf-file")), "Underlay\nLayers" => Some(("insert", "underlay-layers")), "Underlay Adjust" => Some(("properties", "underlay-adjust")), diff --git a/src/modules/insert/pdf_attach.rs b/src/modules/insert/pdf_attach.rs index 3e873e53f..8a2d7c02b 100644 --- a/src/modules/insert/pdf_attach.rs +++ b/src/modules/insert/pdf_attach.rs @@ -101,7 +101,15 @@ enum Step { pub struct PdfAttachCommand { step: Step, + /// The file read for the page (absolute or registered source). path: String, + /// The path the definition stores (full, relative or file name only). + stored_path: Option, + /// Pages after the first, attached beside it (dialog selection). + extra_pages: Vec, + /// Scale and rotation fixed in the dialog: their prompts are skipped. + preset_scale: Option, + preset_rotation: Option, page: String, page_count: usize, page_size: (f64, f64), @@ -122,6 +130,10 @@ impl PdfAttachCommand { Self { step: Step::Path, path: String::new(), + stored_path: None, + extra_pages: Vec::new(), + preset_scale: None, + preset_rotation: None, page: "1".to_string(), page_count: 0, page_size: (0.0, 0.0), @@ -143,6 +155,46 @@ impl PdfAttachCommand { command } + /// The dialog's attach with the insertion point asked on screen: pages + /// chosen, the path to store, and any scale / rotation already given. + pub fn from_dialog( + read_path: &str, + stored_path: &str, + pages: &[String], + scale: Option, + rotation_deg: Option, + insunits: i16, + ) -> Self { + let mut command = Self::with_file(read_path, insunits); + command.stored_path = Some(stored_path.to_string()); + if let Some((first, rest)) = pages.split_first() { + command.page = first.clone(); + command.extra_pages = rest.to_vec(); + } + command.page_size = + crate::scene::model::pdf_raster::page_size_inches(read_path, &command.page) + .unwrap_or((0.0, 0.0)); + command.preset_scale = scale; + if let Some(s) = scale { + command.scale = s; + } + command.preset_rotation = rotation_deg; + command.step = Step::Insertion; + command + } + + /// After the insertion point or the scale: the next prompt, or the + /// attach itself when the dialog fixed what is left. + fn after_scale(&mut self) -> CmdResult { + match self.preset_rotation { + Some(deg) => self.commit(deg.to_radians()), + None => { + self.step = Step::Rotation; + CmdResult::NeedPoint + } + } + } + fn base_image_size(&self) -> String { format!( "Base image size: Width: {:.4}, Height: {:.4}, {}", @@ -207,8 +259,7 @@ impl PdfAttachCommand { fn accept_scale(&mut self, text: &str) -> CmdResult { let text = text.trim(); if text.is_empty() { - self.step = Step::Rotation; - return CmdResult::NeedPoint; + return self.after_scale(); } if text.eq_ignore_ascii_case("U") || text.eq_ignore_ascii_case("UNIT") { self.step = Step::Unit; @@ -217,8 +268,7 @@ impl PdfAttachCommand { match crate::entities::common::parse_f64(text) { Some(v) if v > 0.0 => { self.scale = v; - self.step = Step::Rotation; - CmdResult::NeedPoint + self.after_scale() } Some(_) => CmdResult::ReportError("Value must be positive and nonzero.".to_string()), None => CmdResult::ReportError("Requires numeric value or option keyword.".to_string()), @@ -260,17 +310,23 @@ impl PdfAttachCommand { } fn commit(&mut self, rotation: f64) -> CmdResult { - let point = self.plane.to_local(self.insertion); - let mut underlay = Underlay::pdf(); - underlay.insertion_point = Vector3::new(point.x, point.y, point.z); - underlay.set_scale(self.scale); - underlay.rotation = rotation; - // On, clipped by its boundary and colour-adjusted for the background, - // as the reference creates an underlay. - underlay.flags = UnderlayDisplayFlags::ON - | UnderlayDisplayFlags::CLIPPING - | UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND; - CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Underlay(underlay))) + let pages: Vec = std::iter::once(self.page.clone()) + .chain(self.extra_pages.iter().cloned()) + .collect(); + let placed = underlays_for_pages( + &self.path, + &pages, + self.plane.to_local(self.insertion), + self.scale, + rotation, + ) + .into_iter() + .map(|(page, underlay)| (page, self.plane.place_entity(underlay))) + .collect(); + CmdResult::AttachPdfPages { + path: self.stored_path.clone().unwrap_or_else(|| self.path.clone()), + pages: placed, + } } fn frame_at(&self, pt: DVec3) -> Vec<[f32; 3]> { @@ -336,15 +392,14 @@ impl CadCommand for PdfAttachCommand { } } - fn pdf_attach_source(&self) -> Option<(String, String)> { - (!self.path.is_empty()).then(|| (self.path.clone(), self.page.clone())) - } - fn on_point(&mut self, pt: DVec3) -> CmdResult { if self.step != Step::Insertion { return CmdResult::NeedPoint; } self.insertion = pt; + if self.preset_scale.is_some() { + return self.after_scale(); + } self.step = Step::Scale; CmdResult::ReportMeasurement(self.base_image_size()) } @@ -388,6 +443,39 @@ impl CadCommand for PdfAttachCommand { } } +/// Underlays for `pages` of a file: the first at `insertion`, each next one +/// beside the previous along the rotated X axis (page width times scale). +// ponytail: side-by-side placement of the pages after the first is not +// measured against the reference. +pub fn underlays_for_pages( + read_path: &str, + pages: &[String], + insertion: DVec3, + scale: f64, + rotation: f64, +) -> Vec<(String, EntityType)> { + let mut origin = insertion; + let (c, s) = (rotation.cos(), rotation.sin()); + pages + .iter() + .map(|page| { + let mut underlay = Underlay::pdf(); + underlay.insertion_point = Vector3::new(origin.x, origin.y, origin.z); + underlay.set_scale(scale); + underlay.rotation = rotation; + // On, clipped by its boundary and colour-adjusted for the + // background, as the reference creates an underlay. + underlay.flags = UnderlayDisplayFlags::ON + | UnderlayDisplayFlags::CLIPPING + | UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND; + let width = crate::scene::model::pdf_raster::page_size_inches(read_path, page) + .map_or(0.0, |(w, _)| w * scale); + origin += DVec3::new(c * width, s * width, 0.0); + (page.clone(), EntityType::Underlay(underlay)) + }) + .collect() +} + /// The `ACAD_PDFDEFINITIONS` dictionary under the named-objects root, /// created when the drawing has none. fn ensure_pdf_dictionary(document: &mut CadDocument) -> Option { diff --git a/src/modules/insert/pdf_clip.rs b/src/modules/insert/pdf_clip.rs index a689c3263..1d21ed186 100644 --- a/src/modules/insert/pdf_clip.rs +++ b/src/modules/insert/pdf_clip.rs @@ -58,6 +58,18 @@ impl PdfClipCommand { } } + /// A new boundary for an underlay already chosen (the underlay tab's + /// Create Clipping Boundary): the New boundary branch at once. Returns the + /// command and the result of its first step. + pub fn new_boundary(handle: Handle, underlay: Underlay) -> (Self, CmdResult) { + let mut command = Self::new(false); + command.handle = handle; + command.underlay = Some(underlay); + command.step = Step::Option; + let first = command.option("N"); + (command, first) + } + fn finish(&self, underlay: Underlay) -> CmdResult { CmdResult::UpdateEntityAndFinish { handle: self.handle, diff --git a/src/modules/insert/pdf_import.rs b/src/modules/insert/pdf_import.rs index f87bb7440..4d7e3c0d2 100644 --- a/src/modules/insert/pdf_import.rs +++ b/src/modules/insert/pdf_import.rs @@ -6,26 +6,132 @@ // Keep, Detach or Unload PDF underlay? [Keep/Detach/Unload] : // Binding PDF file , page ... // -// Geometry goes to PDF_Geometry and text to PDF_Text (the PDF has no layers -// of its own to use): chains of straight segments become polylines, a closed -// four-arc Bézier loop that is a circle becomes a CIRCLE, other curves become -// splines, filled areas solid hatches and text runs MTEXT in a "PDF " -// text style. +// Content of a PDF layer goes to PDF_; the rest to PDF_Geometry, +// PDF_Solid Fills, PDF_Text and PDF_Images (or those alone, or the current +// layer, per the settings). Straight strokes that meet become polylines, a +// closed four-arc Bézier loop that is a circle a CIRCLE, other curves +// splines, filled triangles and quadrilaterals SOLIDs at 50% transparency +// (other fills solid hatches), text runs MTEXT in a "PDF " style and +// raster images PNG files referenced by IMAGE objects. use acadrust::entities::{ AttachmentPoint, BoundaryEdge, BoundaryPath, Circle, Hatch, LwPolyline, MText, PolylineEdge, - Spline, Underlay, UnderlayType, + Solid, Spline, Underlay, UnderlayType, }; use acadrust::types::{Color, Handle, LineWeight, Vector2, Vector3}; use acadrust::EntityType; use glam::DVec3; use crate::command::{CadCommand, CmdOption, CmdResult, InputKind}; -use crate::scene::model::pdf_vector::{PageVectors, PdfPath, Segment, SubPath}; +use crate::scene::model::pdf_vector::{ + bezier, circle_of as as_circle, PageVectors, PdfPath, Segment, SubPath, +}; use crate::scene::model::wire_model::WireModel; -pub const GEOMETRY_LAYER: &str = "PDF_Geometry"; -pub const TEXT_LAYER: &str = "PDF_Text"; +/// Which layers imported objects go to. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ImportLayers { + /// The PDF's own layers ("PDF_"); content outside them to + /// PDF_Geometry / PDF_Text. + Pdf, + /// One layer per kind of object. + Object, + /// The current layer. + Current, +} + +/// The PDF Import Settings, kept for the session. The options below the +/// data section are PDFIMPORTMODE's bits. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PdfImportSettings { + pub vector: bool, + pub fills: bool, + pub text: bool, + pub raster: bool, + pub layers: ImportLayers, + pub as_block: bool, + pub join: bool, + pub hatches: bool, + pub lineweights: bool, + pub linetypes: bool, +} + +impl Default for PdfImportSettings { + fn default() -> Self { + Self { + vector: true, + fills: true, + text: true, + raster: false, + layers: ImportLayers::Pdf, + as_block: false, + join: true, + hatches: false, + lineweights: true, + linetypes: false, + } + } +} + +impl PdfImportSettings { + /// PDFIMPORTMODE: 1 import as block, 2 apply lineweights, 4 join + /// segments, 8 fills as hatches, 16 infer linetypes (default 6). + pub fn mode(&self) -> i16 { + i16::from(self.as_block) + | i16::from(self.lineweights) << 1 + | i16::from(self.join) << 2 + | i16::from(self.hatches) << 3 + | i16::from(self.linetypes) << 4 + } + + /// PDFIMPORTFILTER: the kinds left out — 1 vector geometry, 2 text, + /// 4 solid fills, 8 raster images (default 8). + pub fn filter(&self) -> i16 { + i16::from(!self.vector) + | i16::from(!self.text) << 1 + | i16::from(!self.fills) << 2 + | i16::from(!self.raster) << 3 + } + + pub fn set_filter(&mut self, filter: i16) { + self.vector = filter & 1 == 0; + self.text = filter & 2 == 0; + self.fills = filter & 4 == 0; + self.raster = filter & 8 == 0; + } + + /// PDFIMPORTLAYERS: 0 PDF layers, 1 object layers, 2 current layer. + pub fn layers_value(&self) -> i16 { + match self.layers { + ImportLayers::Pdf => 0, + ImportLayers::Object => 1, + ImportLayers::Current => 2, + } + } + + pub fn set_mode(&mut self, mode: i16) { + self.as_block = mode & 1 != 0; + self.lineweights = mode & 2 != 0; + self.join = mode & 4 != 0; + self.hatches = mode & 8 != 0; + self.linetypes = mode & 16 != 0; + } +} + +static SETTINGS: std::sync::Mutex> = std::sync::Mutex::new(None); + +pub fn import_settings() -> PdfImportSettings { + SETTINGS + .lock() + .map(|s| s.unwrap_or_default()) + .unwrap_or_default() +} + +pub fn set_import_settings(settings: PdfImportSettings) { + if let Ok(mut s) = SETTINGS.lock() { + *s = Some(settings); + } +} #[derive(Clone, Debug, PartialEq)] pub enum ImportArea { @@ -47,6 +153,55 @@ pub struct PdfImportRequest { pub mode: UnderlayMode, } +/// A page of a file imported without an underlay (the File option): placed +/// at `insertion` with a scale and rotation. +#[derive(Clone, Debug)] +pub struct PdfFileImport { + pub path: String, + pub page: String, + pub scale: f64, + pub rotation: f64, + pub insertion: DVec3, +} + +impl PdfFileImport { + /// The underlay the page would be as an attachment, for placing it. + pub fn placement(&self) -> Underlay { + let mut u = Underlay::pdf(); + u.insertion_point = Vector3::new(self.insertion.x, self.insertion.y, self.insertion.z); + u.set_scale(self.scale); + u.rotation = self.rotation; + u + } +} + +/// Import PDF with the insertion point asked on screen. +pub struct PdfImportPointCommand { + import: PdfFileImport, +} + +impl PdfImportPointCommand { + pub fn new(import: PdfFileImport) -> Self { + Self { import } + } +} + +impl CadCommand for PdfImportPointCommand { + fn name(&self) -> &'static str { + "PDFIMPORT" + } + fn prompt(&self) -> String { + "Specify insertion point:".to_string() + } + fn on_point(&mut self, pt: DVec3) -> CmdResult { + self.import.insertion = pt; + CmdResult::PdfImportFile(self.import.clone()) + } + fn on_enter(&mut self) -> CmdResult { + CmdResult::Cancel + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Step { Select, @@ -65,6 +220,16 @@ pub struct PdfImportCommand { } impl PdfImportCommand { + /// Import from an underlay already chosen (the underlay tab's button): + /// starts at the area prompt. + pub fn for_underlay(handle: Handle) -> Self { + Self { + step: Step::Area, + handle, + ..Self::new() + } + } + pub fn new() -> Self { Self { step: Step::Select, @@ -87,9 +252,7 @@ impl PdfImportCommand { self.step = Step::Polygon; CmdResult::NeedPoint } - "S" | "SETTINGS" => { - CmdResult::ReportError("PDF import settings are not available.".to_string()) - } + "S" | "SETTINGS" => CmdResult::OpenPdfImportSettings, _ => CmdResult::ReportError("Invalid option keyword.".to_string()), } } @@ -267,6 +430,9 @@ pub struct ImportResult { pub entities: Vec, pub layers: Vec<(String, Color)>, pub text_styles: Vec<(String, String)>, + pub images: Vec, + /// Some line uses the dash linetype. + pub dashed: bool, } /// A PDF colour as an index colour when it is one of the basic seven, @@ -318,51 +484,6 @@ fn font_file_for(font: &str) -> String { .to_string() } -fn bezier(seg: &Segment, t: f64) -> [f64; 2] { - match seg { - Segment::Line(a, b) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t], - Segment::Cubic(p0, p1, p2, p3) => { - let u = 1.0 - t; - let (a, b, c, d) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t); - [ - a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0], - a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1], - ] - } - } -} - -/// Centre and radius when a closed loop of cubics traces a circle. -fn as_circle(sp: &SubPath) -> Option<([f64; 2], f64)> { - if !sp.closed || sp.segments.len() < 4 { - return None; - } - let cubics: Vec<&Segment> = sp - .segments - .iter() - .filter(|s| matches!(s, Segment::Cubic(..))) - .collect(); - if cubics.len() != sp.segments.len() && sp.segments.len() - cubics.len() > 1 { - return None; - } - let starts: Vec<[f64; 2]> = cubics.iter().map(|s| s.start()).collect(); - let n = starts.len() as f64; - let c = [ - starts.iter().map(|p| p[0]).sum::() / n, - starts.iter().map(|p| p[1]).sum::() / n, - ]; - let dist = |p: [f64; 2]| ((p[0] - c[0]).powi(2) + (p[1] - c[1]).powi(2)).sqrt(); - let r = starts.iter().map(|p| dist(*p)).sum::() / n; - if r <= 0.0 { - return None; - } - let tol = r * 2e-3; - let on_circle = cubics.iter().all(|s| { - [0.0, 0.25, 0.5, 0.75].iter().all(|t| (dist(bezier(s, *t)) - r).abs() <= tol) - }); - on_circle.then_some((c, r)) -} - fn inside(poly: &[[f64; 2]], p: [f64; 2]) -> bool { let mut odd = false; let mut j = poly.len() - 1; @@ -388,14 +509,202 @@ fn path_in_area(path: &PdfPath, area: Option<&[[f64; 2]]>) -> bool { }) } -/// The objects an import of `page` creates for an underlay, with the -/// area in world coordinates. -pub fn convert(page: &PageVectors, underlay: &Underlay, area: &ImportArea) -> ImportResult { +/// Where imported objects of each kind go. +pub struct LayerNaming { + pub settings: PdfImportSettings, + /// "PDF_", or "PDF2_", "PDF3_" … for the next files imported. + pub prefix: String, + /// The current layer, for [`ImportLayers::Current`]. + pub current: String, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Kind { + Geometry, + Fill, + Text, + Image, +} + +impl LayerNaming { + fn layer(&self, pdf_layer: Option<&str>, kind: Kind) -> String { + let object = |kind| match kind { + Kind::Geometry => "Geometry", + Kind::Fill => "Solid Fills", + Kind::Text => "Text", + Kind::Image => "Images", + }; + match (self.settings.layers, pdf_layer) { + (ImportLayers::Current, _) => self.current.clone(), + (ImportLayers::Pdf, Some(name)) => format!("{}{name}", self.prefix), + _ => format!("{}{}", self.prefix, object(kind)), + } + } +} + +/// An image the import places: its pixels go to a PNG file named by the +/// caller, then a raster image at `insertion` spanning `u` × `v` per pixel. +pub struct ImageImport { + pub rgba: Vec, + pub width: u32, + pub height: u32, + pub insertion: Vector3, + pub u: Vector3, + pub v: Vector3, + pub layer: String, +} + +/// The linetype collinear dashes become ("Infer linetypes"): dash 0.8, +/// space 0.2, scaled per object to the dash length. +pub const DASH_LINETYPE: &str = "PDF_IMPORT"; + +/// A stroke before it becomes an object: straight chains can still be joined +/// or read as dashes. +struct Stroke { + layer: String, + color: Color, + weight: LineWeight, + points: Vec<[f64; 2]>, + closed: bool, +} + +fn same_point(a: [f64; 2], b: [f64; 2]) -> bool { + (a[0] - b[0]).abs() < 1e-7 && (a[1] - b[1]).abs() < 1e-7 +} + +/// Open straight strokes of one layer, colour and weight that meet end to +/// end become one polyline (closed when the chain returns to its start). +fn join_strokes(strokes: Vec) -> Vec { + let mut out: Vec = Vec::new(); + let mut open: Vec = Vec::new(); + for s in strokes { + if s.closed { + out.push(s); + } else { + open.push(s); + } + } + while let Some(mut chain) = open.pop() { + loop { + let end = *chain.points.last().expect("strokes have points"); + let start = chain.points[0]; + let fits = |o: &Stroke| { + o.layer == chain.layer && o.color == chain.color && o.weight == chain.weight + }; + if let Some(i) = open.iter().position(|o| { + fits(o) && (same_point(o.points[0], end) || same_point(*o.points.last().unwrap(), end)) + }) { + let mut next = open.swap_remove(i); + if !same_point(next.points[0], end) { + next.points.reverse(); + } + chain.points.extend(next.points.into_iter().skip(1)); + } else if let Some(i) = open.iter().position(|o| { + fits(o) + && (same_point(*o.points.last().unwrap(), start) || same_point(o.points[0], start)) + }) { + let mut prev = open.swap_remove(i); + if !same_point(*prev.points.last().unwrap(), start) { + prev.points.reverse(); + } + prev.points.pop(); + prev.points.extend(chain.points); + chain.points = prev.points; + } else { + break; + } + } + if chain.points.len() > 3 && same_point(chain.points[0], *chain.points.last().unwrap()) { + chain.points.pop(); + chain.closed = true; + } + out.push(chain); + } + out +} + +/// Runs of three or more equal collinear dashes with equal gaps become one +/// line from the first dash's start to the last's end, with the dash length +/// (the linetype scale). +// ponytail: dashes are matched along one direction at a time in drawing +// order; patterns of mixed dash lengths stay separate lines. +fn infer_dashes(strokes: Vec) -> Vec<(Stroke, Option)> { + let is_dash = |s: &Stroke| !s.closed && s.points.len() == 2; + let len = |s: &Stroke| { + let (a, b) = (s.points[0], s.points[1]); + (b[0] - a[0]).hypot(b[1] - a[1]) + }; + let mut out: Vec<(Stroke, Option)> = Vec::new(); + let mut run: Vec = Vec::new(); + let flush = |run: &mut Vec, out: &mut Vec<(Stroke, Option)>| { + if run.len() >= 3 { + let dash = len(&run[0]); + let mut line = run.remove(0); + let last = run.pop().expect("three or more"); + line.points[1] = last.points[1]; + run.clear(); + out.push((line, Some(dash))); + } else { + out.extend(run.drain(..).map(|s| (s, None))); + } + }; + for s in strokes { + if !is_dash(&s) { + flush(&mut run, &mut out); + out.push((s, None)); + continue; + } + let continues = run.last().is_some_and(|prev| { + let first = &run[0]; + let (a, b) = (prev.points[0], prev.points[1]); + let dir = [(b[0] - a[0]) / len(prev), (b[1] - a[1]) / len(prev)]; + let (c, d) = (s.points[0], s.points[1]); + let gap = [c[0] - b[0], c[1] - b[1]]; + let along = gap[0] * dir[0] + gap[1] * dir[1]; + let across = (gap[0] * dir[1] - gap[1] * dir[0]).abs(); + let seg = [(d[0] - c[0]) / len(&s), (d[1] - c[1]) / len(&s)]; + let parallel = (seg[0] * dir[1] - seg[1] * dir[0]).abs() < 1e-6 + && seg[0] * dir[0] + seg[1] * dir[1] > 0.0; + let first_gap = run.get(1).map(|second| { + let (e, f) = (first.points[1], second.points[0]); + (f[0] - e[0]).hypot(f[1] - e[1]) + }); + s.layer == prev.layer + && s.color == prev.color + && s.weight == prev.weight + && parallel + && across < 1e-6 + && along > 1e-9 + && (len(&s) - len(first)).abs() < 1e-6 + && first_gap.is_none_or(|g| (g - along).abs() < 1e-6) + }); + if !continues { + flush(&mut run, &mut out); + } + run.push(s); + } + flush(&mut run, &mut out); + out +} + +/// The objects an import creates from a page's content split by PDF layer +/// (see `page_content_by_layer`), placed as `underlay` places the page. +pub fn convert( + content: &[(Option, PageVectors)], + underlay: &Underlay, + area: &ImportArea, + naming: &LayerNaming, +) -> ImportResult { + let settings = &naming.settings; let world = |p: [f64; 2]| crate::entities::underlay::local_to_world(underlay, p); let world2 = |p: [f64; 2]| { let w = world(p); Vector2::new(w[0], w[1]) }; + let world3 = |p: [f64; 2]| { + let w = world(p); + Vector3::new(w[0], w[1], w[2]) + }; let z = underlay.insertion_point.z; let scale = underlay.x_scale.abs(); // The area is tested in page space, where the page content lives. @@ -416,152 +725,279 @@ pub fn convert(page: &PageVectors, underlay: &Underlay, area: &ImportArea) -> Im .collect(), ), }; + let in_area = |p: [f64; 2]| area_local.as_deref().is_none_or(|a| inside(a, p)); let mut result = ImportResult::default(); - let mut geometry_color: Option = None; - let mut text_color: Option = None; - // The layer takes the colour of its first object; objects of that - // colour then follow the layer. - fn push( - result: &mut ImportResult, - mut entity: EntityType, - color: Color, - layer: &str, - layer_color: &mut Option, - ) { - let layer_color = *layer_color.get_or_insert(color); - let common = entity.common_mut(); - common.layer = layer.to_string(); - common.color = if color == layer_color { Color::ByLayer } else { color }; - result.entities.push(entity); - } - - for path in &page.paths { - if !path_in_area(path, area_local.as_deref()) { - continue; - } - let (color, weight) = match (path.stroke, path.fill) { - (Some((rgb, width)), _) => (color_of(rgb), lineweight_of(width)), - (None, Some(rgb)) => (color_of(rgb), LineWeight::ByLayer), - (None, None) => continue, + let mut layer_colors: Vec<(String, Color)> = Vec::new(); + // A layer takes the colour of its first object; objects of that colour + // then follow the layer. + let mut color_on = |layer: &str, color: Color| -> Color { + let layer_color = match layer_colors.iter().find(|(n, _)| n == layer) { + Some((_, c)) => *c, + None => { + layer_colors.push((layer.to_string(), color)); + color + } }; - if path.stroke.is_none() { - // A filled area: one solid hatch over its loops. - let mut hatch = Hatch::solid(); - for sp in &path.subpaths { - let mut ring: Vec = Vec::new(); - for seg in &sp.segments { - let steps = if matches!(seg, Segment::Cubic(..)) { 8 } else { 1 }; - for k in 0..steps { - ring.push(world2(bezier(seg, k as f64 / steps as f64))); - } - } - if ring.len() < 3 { + if color == layer_color { + Color::ByLayer + } else { + color + } + }; + let fill_transparency = acadrust::types::Transparency::from_alpha_value(0x0200_007F); + let mut strokes: Vec = Vec::new(); + let mut curves: Vec = Vec::new(); + + for (pdf_layer, page) in content { + let pdf_layer = pdf_layer.as_deref(); + if settings.vector { + for path in &page.paths { + if !path_in_area(path, area_local.as_deref()) { continue; } - let mut boundary = BoundaryPath::external(); - boundary.flags.set_polyline(true); - boundary.edges.push(BoundaryEdge::Polyline(PolylineEdge::new(ring, true))); - hatch.add_path(boundary); - } - if hatch.paths.is_empty() { - continue; - } - hatch.elevation = z; - push(&mut result, EntityType::Hatch(hatch), color, GEOMETRY_LAYER, &mut geometry_color); - continue; - } - for sp in &path.subpaths { - let mut entity = if let Some((c, r)) = as_circle(sp) { - let w = world(c); - EntityType::Circle(Circle::from_center_radius( - Vector3::new(w[0], w[1], z), - r * scale, - )) - } else if sp.segments.iter().all(|s| matches!(s, Segment::Line(..))) { - let mut points: Vec = vec![world2(sp.segments[0].start())]; - points.extend(sp.segments.iter().map(|s| world2(s.end()))); - if sp.closed && points.len() > 2 { - points.pop(); - } - let mut pl = LwPolyline::from_points(points); - pl.is_closed = sp.closed; - pl.elevation = z; - EntityType::LwPolyline(pl) - } else { - // Mixed or curved: one cubic B-spline through the Bézier - // control points (lines as degree-elevated cubics). - let mut spline = Spline::new(); - spline.degree = 3; - let mut controls: Vec = Vec::new(); - for (i, seg) in sp.segments.iter().enumerate() { - let (p0, p1, p2, p3) = match seg { - Segment::Cubic(p0, p1, p2, p3) => (*p0, *p1, *p2, *p3), - Segment::Line(a, b) => ( - *a, - [a[0] + (b[0] - a[0]) / 3.0, a[1] + (b[1] - a[1]) / 3.0], - [a[0] + (b[0] - a[0]) * 2.0 / 3.0, a[1] + (b[1] - a[1]) * 2.0 / 3.0], - *b, - ), - }; - let pts = if i == 0 { vec![p0, p1, p2, p3] } else { vec![p1, p2, p3] }; - for p in pts { - let w = world(p); - controls.push(Vector3::new(w[0], w[1], z)); + match (path.stroke, path.fill) { + (Some((rgb, width)), _) => { + let layer = naming.layer(pdf_layer, Kind::Geometry); + let color = color_on(&layer, color_of(rgb)); + let weight = if settings.lineweights { + lineweight_of(width) + } else { + LineWeight::ByLayer + }; + for sp in &path.subpaths { + if let Some((c, r)) = as_circle(sp) { + let w = world(c); + let mut circle = EntityType::Circle(Circle::from_center_radius( + Vector3::new(w[0], w[1], z), + r * scale, + )); + let common = circle.common_mut(); + (common.layer, common.color, common.line_weight) = + (layer.clone(), color, weight); + curves.push(circle); + } else if sp.segments.iter().all(|s| matches!(s, Segment::Line(..))) { + let mut points = vec![sp.segments[0].start()]; + points.extend(sp.segments.iter().map(|s| s.end())); + if sp.closed && points.len() > 2 { + points.pop(); + } + strokes.push(Stroke { + layer: layer.clone(), + color, + weight, + points, + closed: sp.closed, + }); + } else { + let mut spline = EntityType::Spline(spline_of(sp, &world3)); + let common = spline.common_mut(); + (common.layer, common.color, common.line_weight) = + (layer.clone(), color, weight); + curves.push(spline); + } + } } + (None, Some(rgb)) if settings.fills => { + let layer = naming.layer(pdf_layer, Kind::Fill); + let color = color_on(&layer, color_of(rgb)); + for mut entity in fill_entities(path, settings.hatches, &world2, &world3, z) { + let common = entity.common_mut(); + (common.layer, common.color, common.transparency) = + (layer.clone(), color, fill_transparency); + result.entities.push(entity); + } + } + _ => {} } - let spans = sp.segments.len(); - let mut knots = vec![0.0; 4]; - for k in 1..spans { - knots.extend([k as f64; 3]); - } - knots.extend([spans as f64; 4]); - spline.control_points = controls; - spline.weights = Vec::new(); - spline.knots = knots; - spline.flags.closed = sp.closed; - EntityType::Spline(spline) - }; - entity.common_mut().line_weight = weight; - push(&mut result, entity, color, GEOMETRY_LAYER, &mut geometry_color); - } - } - - for text in &page.texts { - let cap = if text.cap_height > 0.0 { text.cap_height } else { 0.0 }; - let probe = [text.origin[0] + text.width / 2.0, text.origin[1] + cap / 2.0]; - if let Some(area) = &area_local { - if !inside(area, probe) && !inside(area, text.origin) { - continue; } } - let style = format!("PDF {}", text.font); - if !result.text_styles.iter().any(|(name, _)| *name == style) { - result.text_styles.push((style.clone(), font_file_for(&text.font))); + + if settings.text { + for text in &page.texts { + let cap = text.cap_height.max(0.0); + let probe = [text.origin[0] + text.width / 2.0, text.origin[1] + cap / 2.0]; + if !in_area(probe) && !in_area(text.origin) { + continue; + } + let style = format!("PDF {}", text.font); + if !result.text_styles.iter().any(|(name, _)| *name == style) { + result.text_styles.push((style.clone(), font_file_for(&text.font))); + } + let (dx, dy) = (-text.rotation.sin(), text.rotation.cos()); + let mid = [text.origin[0] + dx * cap / 2.0, text.origin[1] + dy * cap / 2.0]; + let mut mtext = MText::new(); + mtext.value = text.text.trim_end().to_string(); + mtext.insertion_point = world3(mid); + mtext.height = cap * scale; + mtext.rectangle_width = 0.0; + mtext.rotation = text.rotation + underlay.rotation; + mtext.style = style; + mtext.attachment_point = AttachmentPoint::MiddleLeft; + let layer = naming.layer(pdf_layer, Kind::Text); + mtext.common.color = color_on(&layer, color_of(text.color)); + mtext.common.layer = layer; + result.entities.push(EntityType::MText(mtext)); + } + } + + if settings.raster { + let (c, s) = (underlay.rotation.cos(), underlay.rotation.sin()); + let turn = |d: [f64; 2]| { + Vector3::new( + (d[0] * c - d[1] * s) * underlay.x_scale, + (d[0] * s + d[1] * c) * underlay.y_scale, + 0.0, + ) + }; + for image in &page.images { + let mid = [ + image.origin[0] + (image.u[0] * image.width as f64 + image.v[0] * image.height as f64) / 2.0, + image.origin[1] + (image.u[1] * image.width as f64 + image.v[1] * image.height as f64) / 2.0, + ]; + if !in_area(mid) { + continue; + } + result.images.push(ImageImport { + rgba: image.rgba.clone(), + width: image.width, + height: image.height, + insertion: world3(image.origin), + u: turn(image.u), + v: turn(image.v), + layer: naming.layer(pdf_layer, Kind::Image), + }); + } } - let (dx, dy) = (-text.rotation.sin(), text.rotation.cos()); - let mid = [text.origin[0] + dx * cap / 2.0, text.origin[1] + dy * cap / 2.0]; - let w = world(mid); - let mut mtext = MText::new(); - mtext.value = text.text.trim_end().to_string(); - mtext.insertion_point = Vector3::new(w[0], w[1], z); - mtext.height = cap * scale; - mtext.rectangle_width = 0.0; - mtext.rotation = text.rotation + underlay.rotation; - mtext.style = style; - mtext.attachment_point = AttachmentPoint::MiddleLeft; - push(&mut result, EntityType::MText(mtext), color_of(text.color), TEXT_LAYER, &mut text_color); } - if let Some(color) = geometry_color { - result.layers.push((GEOMETRY_LAYER.to_string(), color)); + // The joined strokes, then (when asked) dashes read as one dashed line. + let strokes = join_strokes(strokes); + let strokes: Vec<(Stroke, Option)> = if settings.linetypes { + infer_dashes(strokes) + } else { + strokes.into_iter().map(|s| (s, None)).collect() + }; + for (stroke, dash) in strokes { + let mut pl = LwPolyline::from_points(stroke.points.iter().map(|p| world2(*p)).collect()); + pl.is_closed = stroke.closed; + pl.elevation = z; + (pl.common.layer, pl.common.color, pl.common.line_weight) = + (stroke.layer, stroke.color, stroke.weight); + if let Some(dash) = dash { + pl.common.linetype = DASH_LINETYPE.to_string(); + pl.common.linetype_scale = dash * scale; + result.dashed = true; + } + result.entities.push(EntityType::LwPolyline(pl)); } - if let Some(color) = text_color { - result.layers.push((TEXT_LAYER.to_string(), color)); + result.entities.extend(curves); + result.layers = layer_colors; + for image in &result.images { + if !result.layers.iter().any(|(n, _)| *n == image.layer) { + result.layers.push((image.layer.clone(), Color::Index(7))); + } } result } +/// One cubic B-spline through a subpath's Bézier control points (lines as +/// degree-elevated cubics). +fn spline_of(sp: &SubPath, world3: &dyn Fn([f64; 2]) -> Vector3) -> Spline { + let mut spline = Spline::new(); + spline.degree = 3; + let mut controls: Vec = Vec::new(); + for (i, seg) in sp.segments.iter().enumerate() { + let (p0, p1, p2, p3) = match seg { + Segment::Cubic(p0, p1, p2, p3) => (*p0, *p1, *p2, *p3), + Segment::Line(a, b) => ( + *a, + [a[0] + (b[0] - a[0]) / 3.0, a[1] + (b[1] - a[1]) / 3.0], + [a[0] + (b[0] - a[0]) * 2.0 / 3.0, a[1] + (b[1] - a[1]) * 2.0 / 3.0], + *b, + ), + }; + let pts = if i == 0 { vec![p0, p1, p2, p3] } else { vec![p1, p2, p3] }; + controls.extend(pts.into_iter().map(world3)); + } + let spans = sp.segments.len(); + let mut knots = vec![0.0; 4]; + for k in 1..spans { + knots.extend([k as f64; 3]); + } + knots.extend([spans as f64; 4]); + spline.control_points = controls; + spline.weights = Vec::new(); + spline.knots = knots; + spline.flags.closed = sp.closed; + spline +} + +/// A filled area: a triangle or four-cornered straight loop is a SOLID; any +/// other loop, and every loop but a triangle when fills become hatches, one +/// solid HATCH. +fn fill_entities( + path: &PdfPath, + hatches: bool, + world2: &dyn Fn([f64; 2]) -> Vector2, + world3: &dyn Fn([f64; 2]) -> Vector3, + z: f64, +) -> Vec { + let straight = |sp: &SubPath| sp.segments.iter().all(|s| matches!(s, Segment::Line(..))); + let corners = |sp: &SubPath| { + let mut pts: Vec<[f64; 2]> = sp.segments.iter().map(|s| s.start()).collect(); + pts.dedup_by(|a, b| same_point(*a, *b)); + if pts.len() > 1 && same_point(pts[0], *pts.last().unwrap()) { + pts.pop(); + } + pts + }; + if path.subpaths.len() == 1 && straight(&path.subpaths[0]) { + let pts = corners(&path.subpaths[0]); + match pts.len() { + 3 => { + return vec![EntityType::Solid(Solid::triangle( + world3(pts[0]), + world3(pts[1]), + world3(pts[2]), + ))] + } + 4 if !hatches => { + // SOLID corners run 1-2-4-3 (the second edge crosses). + return vec![EntityType::Solid(Solid::new( + world3(pts[0]), + world3(pts[1]), + world3(pts[3]), + world3(pts[2]), + ))]; + } + _ => {} + } + } + let mut hatch = Hatch::solid(); + for sp in &path.subpaths { + let mut ring: Vec = Vec::new(); + for seg in &sp.segments { + let steps = if matches!(seg, Segment::Cubic(..)) { 8 } else { 1 }; + for k in 0..steps { + ring.push(world2(bezier(seg, k as f64 / steps as f64))); + } + } + if ring.len() < 3 { + continue; + } + let mut boundary = BoundaryPath::external(); + boundary.flags.set_polyline(true); + boundary.edges.push(BoundaryEdge::Polyline(PolylineEdge::new(ring, true))); + hatch.add_path(boundary); + } + if hatch.paths.is_empty() { + return Vec::new(); + } + hatch.elevation = z; + vec![EntityType::Hatch(hatch)] +} + inventory::submit!(crate::command::CommandRegistration { names: &["PDFIMPORT"] }); diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index e3d13fd81..81128aa0d 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -1285,6 +1285,20 @@ fn tessellate_entity_inner( b.set_fixed_screen_width(2.0); } } + // PDF underlay geometry: a hidden, unplotted wire that object snaps + // (nearest, intersection, perpendicular) find, beside the page frame. + if let EntityType::Underlay(underlay) = e { + let geometry = crate::scene::model::pdf_vector::underlay_snap_geometry(underlay, document); + if !geometry.is_empty() { + let (points, points_low) = convert::tessellate::points_to_ds(geometry); + let mut wire = WireModel::solid(h.value().to_string(), points, entity_color, sel); + wire.points_low = points_low; + wire.display_visible = false; + wire.plot_visible = false; + set_wire_aabb(&mut wire, aabb); + bases.push(wire); + } + } // A hidden mask frame remains selectable and appears while selected, but // contributes no visible line work during normal display. The interior diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 2cf5ac77a..6343a6ae7 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -152,7 +152,7 @@ impl Scene { let _ = self.document.layers.add(layer); } - pub(super) fn ensure_app_id(&mut self, name: &str) { + pub(crate) fn ensure_app_id(&mut self, name: &str) { if name.trim().is_empty() || self.document.app_ids.contains(name) { return; } @@ -2406,10 +2406,16 @@ impl Scene { match entity { EntityType::RasterImage(img) => ImageModel::from_raster_image(img), EntityType::Ole2Frame(ole) => ImageModel::from_ole2frame(ole), - EntityType::Underlay(u) if self.unloaded_underlay_definitions.contains(&u.definition_handle) => None, EntityType::Underlay(u) => match self.document.objects.get(&u.definition_handle) { Some(acadrust::objects::ObjectType::UnderlayDefinition(def)) => { - ImageModel::from_underlay(u, def, self.bg_color) + // A paper-space underlay adjusts to the sheet, not the canvas. + let owner = u.common.owner_handle; + let background = if owner.is_null() || owner == self.model_space_block_handle() { + self.bg_color + } else { + self.paper_bg_color + }; + ImageModel::from_underlay(u, def, background) } _ => None, }, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 3280285ec..b18d0f998 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1268,7 +1268,20 @@ fn build_derived_caches_impl( EntityType::Ole2Frame(ole) => ImageModel::from_ole2frame(ole).map(|m| (handle, m)), EntityType::Underlay(u) => match doc.objects.get(&u.definition_handle) { Some(acadrust::objects::ObjectType::UnderlayDefinition(def)) => { - ImageModel::from_underlay(u, def, LOAD_BG).map(|m| (handle, m)) + { + // Paper-space underlays adjust to the white sheet. + let model = doc.objects.values().find_map(|object| match object { + acadrust::objects::ObjectType::Layout(l) if l.name == "Model" => Some(l.block_record), + _ => None, + }); + let owner = u.common.owner_handle; + let background = if owner.is_null() || model.is_none_or(|m| m == owner) { + LOAD_BG + } else { + [1.0, 1.0, 1.0, 1.0] + }; + ImageModel::from_underlay(u, def, background).map(|m| (handle, m)) + } } _ => None, }, @@ -2250,9 +2263,6 @@ pub struct Scene { pub bg_color: [f32; 4], /// Custom paper-space background fill color for Wipeout entities. pub paper_bg_color: [f32; 4], - /// PDF definitions unloaded in this session (PDFIMPORT Unload): their - /// underlays show no page content. - pub unloaded_underlay_definitions: std::collections::HashSet, /// Dense model-space cluster half-span used for viewport recovery. pub local_extent_max: f32, /// Dense model-space cluster median used for viewport recovery. @@ -2662,7 +2672,6 @@ impl Scene { active_viewport: None, bg_color: [33.0 / 255.0, 40.0 / 255.0, 48.0 / 255.0, 1.0], paper_bg_color: [1.0, 1.0, 1.0, 1.0], - unloaded_underlay_definitions: std::collections::HashSet::new(), local_extent_max: 1e9, local_center: [0.0, 0.0], annotation_scale: 1.0, diff --git a/src/scene/model/image_model.rs b/src/scene/model/image_model.rs index c7b4a91a7..11e409428 100644 --- a/src/scene/model/image_model.rs +++ b/src/scene/model/image_model.rs @@ -205,7 +205,7 @@ impl ImageModel { if !u.flags.contains(UnderlayDisplayFlags::ON) { return None; } - if !matches!(def.underlay_type, UnderlayType::Pdf) { + if !matches!(def.underlay_type, UnderlayType::Pdf) || def.unloaded { return None; } let page = if def.page_name.trim().is_empty() { @@ -214,10 +214,12 @@ impl ImageModel { def.page_name.trim() }; let (page_w, page_h) = pdf_raster::page_size_inches(&def.file_path, page)?; - let raster = pdf_raster::rasterize_page_display(&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)?; let bg_lum = 0.299 * background[0] + 0.587 * background[1] + 0.114 * background[2]; let pixels = pdf_raster::adjusted_pixels( - &def.file_path, + &source, page, &raster, PageAdjust { diff --git a/src/scene/model/mod.rs b/src/scene/model/mod.rs index 44cb1b5e4..f6d6563bb 100644 --- a/src/scene/model/mod.rs +++ b/src/scene/model/mod.rs @@ -6,6 +6,7 @@ pub mod image_model; pub mod instance_model; pub mod material_model; pub mod ole_pres; +pub mod pdf_layers; pub mod pdf_raster; pub mod pdf_vector; pub mod mesh_model; diff --git a/src/scene/model/pdf_layers.rs b/src/scene/model/pdf_layers.rs new file mode 100644 index 000000000..270d2d975 --- /dev/null +++ b/src/scene/model/pdf_layers.rs @@ -0,0 +1,255 @@ +//! PDF layers (optional-content groups): what a file declares, and a source +//! whose layer visibility is overridden for an underlay. +//! +//! The override is an incremental update that replaces the catalog's default +//! optional-content configuration, so the rasteriser and the vector reader +//! both skip the hidden layers without any change to how they read a page. + +use std::sync::Arc; + +use hayro::hayro_syntax::object::{Array, Dict, Name, ObjectIdentifier}; +use hayro::hayro_syntax::Pdf; + +use super::pdf_raster::{register_derived_source, source_bytes}; + +/// An optional-content group (layer) of a PDF. +#[derive(Clone, Debug, PartialEq)] +pub struct PdfLayer { + pub name: String, + /// Shown by the file's default configuration. + pub on_by_default: bool, + object: (i32, i32), +} + +fn text_string(bytes: &[u8]) -> String { + if bytes.starts_with(&[0xFE, 0xFF]) { + let units: Vec = bytes[2..] + .chunks_exact(2) + .map(|c| u16::from_be_bytes([c[0], c[1]])) + .collect(); + return String::from_utf16_lossy(&units); + } + bytes.iter().map(|&b| b as char).collect() +} + +/// The layers a PDF declares, in file order. +pub fn layers(path: &str) -> Vec { + let Some(bytes) = source_bytes(path) else { + return Vec::new(); + }; + let Ok(pdf) = Pdf::new(bytes) else { + return Vec::new(); + }; + let xref = pdf.xref(); + let Some(catalog) = xref.get::>(xref.root_id()) else { + return Vec::new(); + }; + let Some(props) = catalog.get::>(b"OCProperties") else { + return Vec::new(); + }; + let config = props.get::>(b"D").unwrap_or_default(); + let ids = |key: &[u8]| -> Vec<(i32, i32)> { + config + .get::>(key) + .map(|a| { + a.raw_iter() + .filter_map(|item| item.as_obj_ref()) + .map(|r| (r.obj_number, r.gen_number)) + .collect() + }) + .unwrap_or_default() + }; + let base_off = config + .get::>(b"BaseState") + .is_some_and(|n| n.as_ref() == b"OFF"); + let (on, off) = (ids(b"ON"), ids(b"OFF")); + let Some(ocgs) = props.get::>(b"OCGs") else { + return Vec::new(); + }; + ocgs.raw_iter() + .filter_map(|item| item.as_obj_ref()) + .filter_map(|r| { + let id = (r.obj_number, r.gen_number); + let dict = xref.get::>(ObjectIdentifier::from(r))?; + let name = dict + .get::>(b"Name") + .map(|s| text_string(s.as_bytes())) + .unwrap_or_default(); + let on_by_default = if base_off { + on.contains(&id) + } else { + !off.contains(&id) + }; + Some(PdfLayer { + name, + on_by_default, + object: id, + }) + }) + .collect() +} + +/// A source whose `hidden` layers are off and every other layer on: the +/// original path when that is the file's own state, else a derived source +/// name registered with the updated bytes. Falls back to the original path +/// when the file cannot be updated (encrypted, unreadable catalog). +pub fn source_with_layers(path: &str, hidden: &[String]) -> String { + let all = layers(path); + if all.is_empty() { + return path.to_string(); + } + let off: Vec<&PdfLayer> = all + .iter() + .filter(|l| hidden.iter().any(|h| *h == l.name)) + .collect(); + if off.is_empty() && all.iter().all(|l| l.on_by_default) { + return path.to_string(); + } + let mut names: Vec<&str> = off.iter().map(|l| l.name.as_str()).collect(); + names.sort_unstable(); + let key = format!("{path}#layers-off={}", names.join("|")); + if source_bytes(&key).is_some() { + return key; + } + let Some(updated) = source_bytes(path).and_then(|bytes| with_layer_config(&bytes, &all, &off)) + else { + return path.to_string(); + }; + register_derived_source(&key, Arc::new(updated)); + key +} + +/// The bytes with an incremental update whose catalog lists `off` layers +/// as off in the default configuration and all others on. +fn with_layer_config(bytes: &[u8], all: &[PdfLayer], off: &[&PdfLayer]) -> Option> { + let pdf = Pdf::new(Arc::new(bytes.to_vec())).ok()?; + let xref = pdf.xref(); + let root = xref.root_id(); + let catalog = xref.get::>(root)?; + let raw = std::str::from_utf8(catalog.data()).ok()?.trim().to_string(); + let tail = String::from_utf8_lossy(&bytes[bytes.len().saturating_sub(4096)..]).into_owned(); + if tail.contains("/Encrypt") { + return None; + } + let prev: usize = tail + .rfind("startxref") + .and_then(|i| tail[i + 9..].split_whitespace().next()?.parse().ok())?; + let size: usize = { + let region = String::from_utf8_lossy(&bytes[prev.min(bytes.len())..]).into_owned(); + let region = if region.contains("/Size") { region } else { tail.clone() }; + let i = region.find("/Size")?; + region[i + 5..] + .trim_start() + .split(|c: char| !c.is_ascii_digit()) + .next()? + .parse() + .ok()? + }; + let body = without_key(&raw, "/OCProperties")?; + let refs = |layers: Vec<&PdfLayer>| { + layers + .iter() + .map(|l| format!("{} {} R", l.object.0, l.object.1)) + .collect::>() + .join(" ") + }; + let props = format!( + "/OCProperties << /OCGs [{}] /D << /BaseState /ON /OFF [{}] >> >>", + refs(all.iter().collect()), + refs(off.to_vec()), + ); + let open = body.trim_end().strip_suffix(">>")?.trim_end(); + let catalog_text = format!("{open} {props} >>"); + let mut out = bytes.to_vec(); + if !out.ends_with(b"\n") { + out.push(b'\n'); + } + let object_offset = out.len(); + out.extend_from_slice( + format!( + "{} {} obj\n{catalog_text}\nendobj\n", + root.obj_number, root.gen_number + ) + .as_bytes(), + ); + let xref_offset = out.len(); + out.extend_from_slice( + format!( + "xref\n0 1\n0000000000 65535 f \n{} 1\n{:010} {:05} n \ntrailer\n<< /Size {size} /Root {} {} R /Prev {prev} >>\nstartxref\n{xref_offset}\n%%EOF\n", + root.obj_number, object_offset, root.gen_number, root.obj_number, root.gen_number, + ) + .as_bytes(), + ); + Some(out) +} + +/// A dictionary's text without one key and its value (a dictionary, an +/// array, a reference or a single token). +fn without_key(dict: &str, key: &str) -> Option { + let Some(start) = dict.find(key) else { + return Some(dict.to_string()); + }; + let rest = &dict[start + key.len()..]; + let value = rest.trim_start(); + let lead = rest.len() - value.len(); + let length = if value.starts_with("<<") || value.starts_with('[') { + let (open, close) = if value.starts_with('[') { ("[", "]") } else { ("<<", ">>") }; + let mut depth = 0i32; + let mut i = 0; + loop { + if i >= value.len() { + return None; + } + if value[i..].starts_with(open) { + depth += 1; + i += open.len(); + } else if value[i..].starts_with(close) { + depth -= 1; + i += close.len(); + if depth == 0 { + break i; + } + } else { + i += value[i..].chars().next()?.len_utf8(); + } + } + } else { + let parts: Vec<&str> = value.split_whitespace().take(3).collect(); + if parts.len() == 3 && parts[2].starts_with('R') { + value.find('R')? + 1 + } else { + value + .find(|c: char| c.is_whitespace() || c == '/' || c == '>') + .unwrap_or(value.len()) + } + }; + Some(format!("{}{}", &dict[..start], &rest[lead + length..])) +} + +/// Layer names an underlay has switched off: the strings of its +/// `AdeskUnderlayLayerOverrideData` extended data, as the reference stores +/// them. +pub const LAYER_OVERRIDE_APP: &str = "AdeskUnderlayLayerOverrideData"; + +pub fn hidden_layers(u: &acadrust::entities::Underlay) -> Vec { + u.common + .extended_data + .records() + .iter() + .filter(|r| r.application_name.eq_ignore_ascii_case(LAYER_OVERRIDE_APP)) + .flat_map(|r| r.values.iter()) + .filter_map(|v| match v { + acadrust::xdata::XDataValue::String(s) => Some(s.clone()), + _ => None, + }) + .collect() +} + +/// The source an underlay's page is read from, with its layer overrides. +pub fn underlay_source(u: &acadrust::entities::Underlay, file_path: &str) -> String { + let hidden = hidden_layers(u); + if hidden.is_empty() { + return file_path.to_string(); + } + source_with_layers(file_path, &hidden) +} diff --git a/src/scene/model/pdf_raster.rs b/src/scene/model/pdf_raster.rs index 59947b1a8..b156b5b24 100644 --- a/src/scene/model/pdf_raster.rs +++ b/src/scene/model/pdf_raster.rs @@ -48,6 +48,15 @@ pub fn register_source(path: &str, bytes: Arc>) { .retain(|key, _| key.0 != path); } +/// Register bytes under a derived source name (a layer override of a +/// file), leaving the file's own caches alone. +pub(crate) fn register_derived_source(key: &str, bytes: Arc>) { + source_cache() + .lock() + .unwrap_or_else(|error| error.into_inner()) + .insert(key.to_string(), bytes); +} + /// Rasterise a 1-based PDF page, memoised by source path and page name. pub fn rasterize_page(path: &str, page: &str) -> Option> { rasterize_page_at_dpi(path, page, RASTER_DPI) @@ -134,6 +143,46 @@ fn unpremultiply(pixels: &mut [u8]) { } } +fn rgb_to_hsl([r, g, b]: [f32; 3]) -> (f32, f32, f32) { + let max = r.max(g).max(b); + let min = r.min(g).min(b); + let l = (max + min) / 2.0; + let d = max - min; + if d <= f32::EPSILON { + return (0.0, 0.0, l); + } + let s = if l > 0.5 { d / (2.0 - max - min) } else { d / (max + min) }; + let h = if max == r { + ((g - b) / d).rem_euclid(6.0) + } else if max == g { + (b - r) / d + 2.0 + } else { + (r - g) / d + 4.0 + } / 6.0; + (h, s, l) +} + +fn hsl_to_rgb(h: f32, s: f32, l: f32) -> [f32; 3] { + if s <= f32::EPSILON { + return [l; 3]; + } + let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s }; + let p = 2.0 * l - q; + let channel = |t: f32| { + let t = t.rem_euclid(1.0); + if t < 1.0 / 6.0 { + p + (q - p) * 6.0 * t + } else if t < 0.5 { + q + } else if t < 2.0 / 3.0 { + p + (q - p) * (2.0 / 3.0 - t) * 6.0 + } else { + p + } + }; + [channel(h + 1.0 / 3.0), channel(h), channel(h - 1.0 / 3.0)] +} + /// Number of pages in a PDF source. pub fn page_count(path: &str) -> Option { let bytes = source_bytes(path)?; @@ -155,7 +204,8 @@ pub fn page_size_inches(path: &str, page: &str) -> Option<(f64, f64)> { /// Display adjustments of an underlay page, applied to its display raster. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct PageAdjust { - /// 0..=100; 100 keeps the colours, lower values pull them to mid grey. + /// 0..=100; 100 keeps the colours, lower values pull their lightness + /// towards one third. pub contrast: u8, pub monochrome: bool, /// Lighten dark, unsaturated content on a dark background (black lines @@ -198,20 +248,27 @@ pub fn adjusted_pixels(path: &str, page: &str, raster: &PdfPage, adjust: PageAdj rgb = [l; 3]; } if adjust.adjust_for_background { + // Content that would vanish into the background — near-black on a + // dark background, near-white on a light one, without much + // colour — turns to its opposite; coloured and mid-tone content + // stays as drawn. let max = rgb[0].max(rgb[1]).max(rgb[2]); let min = rgb[0].min(rgb[1]).min(rgb[2]); let saturation = if max > 0.0 { (max - min) / max } else { 0.0 }; let l = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]; - // ponytail: greyscale inversion of low-saturation content only; - // hue-preserving remap if coloured content must adapt too. - let clash = if adjust.dark_background { l < 0.5 } else { l > 0.5 }; + // ponytail: fixed 0.2 / 0.8 thresholds measured on black, white + // and 30 % grey; a contrast-to-background test if mid tones clash. + let clash = if adjust.dark_background { l < 0.2 } else { l > 0.8 }; if saturation < 0.25 && clash { rgb = rgb.map(|c| 1.0 - c); } } if adjust.contrast < 100 { + // Lightness is pulled towards one third, hue and saturation kept: + // contrast 0 leaves every colour at a third of full lightness. + let (h, s, l) = rgb_to_hsl(rgb); let k = adjust.contrast as f32 / 100.0; - rgb = rgb.map(|c| 0.5 + (c - 0.5) * k); + rgb = hsl_to_rgb(h, s, 1.0 / 3.0 + (l - 1.0 / 3.0) * k); } for (dst, c) in px[..3].iter_mut().zip(rgb) { *dst = (c.clamp(0.0, 1.0) * 255.0).round() as u8; diff --git a/src/scene/model/pdf_vector.rs b/src/scene/model/pdf_vector.rs index 2a1ec56c8..76d0676d6 100644 --- a/src/scene/model/pdf_vector.rs +++ b/src/scene/model/pdf_vector.rs @@ -37,13 +37,13 @@ impl Segment { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct SubPath { pub segments: Vec, pub closed: bool, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct PdfPath { pub subpaths: Vec, /// Stroke colour (RGB) and width in points, for stroked paths. @@ -53,7 +53,7 @@ pub struct PdfPath { } /// A run of glyphs on one baseline in one font and size. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct PdfText { pub text: String, /// Base font name without a subset prefix ("Helvetica"). @@ -69,10 +69,26 @@ pub struct PdfText { pub color: [u8; 3], } +/// A raster image drawn on a page: its pixels and where they land. +#[derive(Clone, Debug, PartialEq)] +pub struct PdfImage { + /// Straight RGBA, rows from the top. + pub rgba: Vec, + pub width: u32, + pub height: u32, + /// Lower-left corner, page inches. + pub origin: [f64; 2], + /// One pixel along the image's rows and up its columns, inches. + pub u: [f64; 2], + pub v: [f64; 2], +} + #[derive(Default, Debug)] pub struct PageVectors { pub paths: Vec, pub texts: Vec, + /// Filled only when images are asked for (`page_content`). + pub images: Vec, } type Key = (String, String); @@ -101,7 +117,66 @@ pub fn page_vectors(path: &str, page: &str) -> Option> { built } +/// A page's paths, text and raster images (not cached; import only). +pub fn page_content(path: &str, page: &str) -> Option { + read_page_with(path, page, true) +} + +/// A page's content split by PDF layer: `None` for what no layer holds, +/// then each named layer that is not in `hidden`. Each layer's share is +/// what the page shows with that layer alone on, less what it shows with +/// every layer off. +// ponytail: content in several layers at once (membership dictionaries) is +// credited to the first layer that shows it. +pub fn page_content_by_layer( + path: &str, + page: &str, + hidden: &[String], +) -> Option, PageVectors)>> { + let layers = super::pdf_layers::layers(path); + if layers.is_empty() { + return Some(vec![(None, page_content(path, page)?)]); + } + let names: Vec = layers.iter().map(|l| l.name.clone()).collect(); + let base = page_content(&super::pdf_layers::source_with_layers(path, &names), page)?; + let key = |s: &dyn std::fmt::Debug| format!("{s:?}"); + let mut seen: HashMap = HashMap::new(); + for item in base.paths.iter().map(|p| key(p)).chain(base.texts.iter().map(|t| key(t))) { + *seen.entry(item).or_default() += 1; + } + let mut out = Vec::new(); + for layer in &layers { + if hidden.contains(&layer.name) { + continue; + } + let others: Vec = names.iter().filter(|n| **n != layer.name).cloned().collect(); + let Some(mut only) = + page_content(&super::pdf_layers::source_with_layers(path, &others), page) + else { + continue; + }; + let mut left = seen.clone(); + let mut fresh = |item: String| match left.get_mut(&item) { + Some(n) if *n > 0 => { + *n -= 1; + false + } + _ => true, + }; + only.paths.retain(|p| fresh(key(p))); + only.texts.retain(|t| fresh(key(t))); + only.images.retain(|i| !base.images.contains(i)); + out.push((Some(layer.name.clone()), only)); + } + out.insert(0, (None, base)); + Some(out) +} + fn read_page(path: &str, page: &str) -> Option { + read_page_with(path, page, false) +} + +fn read_page_with(path: &str, page: &str, images: bool) -> Option { let bytes = super::pdf_raster::source_bytes(path)?; let pdf = Pdf::new(bytes).ok()?; let page_no = page.trim().parse::().ok()?; @@ -117,7 +192,10 @@ fn read_page(path: &str, page: &str) -> Option { page.xref(), InterpreterSettings::default(), ); - let mut device = Collector::default(); + let mut device = Collector { + want_images: images, + ..Collector::default() + }; interpret_page(page, &mut context, &mut device); device.finish_text(); @@ -150,6 +228,7 @@ fn read_page(path: &str, page: &str) -> Option { Some(PageVectors { paths: device.paths, texts: texts.into_iter().map(|t| t.text).collect(), + images: device.images, }) } @@ -169,6 +248,8 @@ struct Collector { texts: Vec, current: Option, font_order: Vec, + want_images: bool, + images: Vec, } impl Collector { @@ -355,11 +436,104 @@ impl<'a> Device<'a> for Collector { .sqrt(); } } - fn draw_image(&mut self, _: Image<'a, '_>, _: Affine) {} + fn draw_image(&mut self, image: Image<'a, '_>, transform: Affine) { + let Image::Raster(raster) = image else { + return; + }; + if !self.want_images { + return; + } + let mut decoded: Option = None; + raster.with_rgba( + |data, alpha| { + use hayro::hayro_interpret::ImageData; + let (w, h) = (data.width(), data.height()); + let n = (w * h) as usize; + let mut rgba = vec![255u8; n * 4]; + match &data { + ImageData::Rgb(rgb) => { + for i in 0..n { + rgba[i * 4..i * 4 + 3].copy_from_slice(&rgb.data[i * 3..i * 3 + 3]); + } + } + ImageData::Luma(luma) => { + for i in 0..n { + rgba[i * 4..i * 4 + 3].fill(luma.data[i]); + } + } + } + if let Some(a) = alpha.filter(|a| a.width == w && a.height == h) { + for i in 0..n { + rgba[i * 4 + 3] = a.data[i]; + } + } + // The transform maps pixel space (y down) onto the page. + let at = |x: f64, y: f64| pt(transform * Point::new(x, y)); + let (wf, hf) = (w as f64, h as f64); + let (o, x1, y1) = (at(0.0, hf), at(wf, hf), at(0.0, 0.0)); + decoded = Some(PdfImage { + rgba, + width: w, + height: h, + origin: o, + u: [(x1[0] - o[0]) / w as f64, (x1[1] - o[1]) / w as f64], + v: [(y1[0] - o[0]) / h as f64, (y1[1] - o[1]) / h as f64], + }); + }, + None, + ); + self.images.extend(decoded); + } fn pop_clip_path(&mut self) {} fn pop_transparency_group(&mut self) {} } +/// A point of a segment at parameter `t` (0..1). +pub fn bezier(seg: &Segment, t: f64) -> [f64; 2] { + match seg { + Segment::Line(a, b) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t], + Segment::Cubic(p0, p1, p2, p3) => { + let u = 1.0 - t; + let (a, b, c, d) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t); + [ + a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0], + a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1], + ] + } + } +} + +/// Centre and radius when a closed loop of cubics traces a circle. +pub fn circle_of(sp: &SubPath) -> Option<([f64; 2], f64)> { + if !sp.closed || sp.segments.len() < 4 { + return None; + } + let cubics: Vec<&Segment> = sp + .segments + .iter() + .filter(|s| matches!(s, Segment::Cubic(..))) + .collect(); + if cubics.len() != sp.segments.len() && sp.segments.len() - cubics.len() > 1 { + return None; + } + let starts: Vec<[f64; 2]> = cubics.iter().map(|s| s.start()).collect(); + let n = starts.len() as f64; + let c = [ + starts.iter().map(|p| p[0]).sum::() / n, + starts.iter().map(|p| p[1]).sum::() / n, + ]; + let dist = |p: [f64; 2]| ((p[0] - c[0]).powi(2) + (p[1] - c[1]).powi(2)).sqrt(); + let r = starts.iter().map(|p| dist(*p)).sum::() / n; + if r <= 0.0 { + return None; + } + let tol = r * 2e-3; + let on_circle = cubics.iter().all(|s| { + [0.0, 0.25, 0.5, 0.75].iter().all(|t| (dist(bezier(s, *t)) - r).abs() <= tol) + }); + on_circle.then_some((c, r)) +} + // ── Snapping ──────────────────────────────────────────────────────────────── static PDF_OSNAP: AtomicBool = AtomicBool::new(true); @@ -391,7 +565,8 @@ pub fn underlay_snap_points( let Some(def) = crate::entities::underlay::definition(u, document) else { return Vec::new(); }; - let Some(vectors) = page_vectors(&def.file_path, crate::entities::underlay::page_of(def)) else { + let source = super::pdf_layers::underlay_source(u, &def.file_path); + let Some(vectors) = page_vectors(&source, crate::entities::underlay::page_of(def)) else { return Vec::new(); }; let world = |p: [f64; 2]| { @@ -401,6 +576,13 @@ pub fn underlay_snap_points( let mut out = Vec::new(); 'paths: for path in &vectors.paths { for sp in &path.subpaths { + // A PDF circle offers its centre and quadrants, as a circle does. + if let Some((c, r)) = circle_of(sp) { + out.push((world(c), SnapHint::Center)); + for (dx, dy) in [(r, 0.0), (0.0, r), (-r, 0.0), (0.0, -r)] { + out.push((world([c[0] + dx, c[1] + dy]), SnapHint::Quadrant)); + } + } for seg in &sp.segments { if out.len() >= MAX_SNAP_POINTS { break 'paths; @@ -418,3 +600,49 @@ pub fn underlay_snap_points( } out } + +/// Most points of the snap-only geometry wire of one underlay. +// ponytail: flat cap; a spatial index over page segments for huge drawings. +const MAX_GEOMETRY_POINTS: usize = 200_000; + +/// The underlay's PDF geometry as world polylines (NaN-separated, curves +/// flattened) for nearest, intersection and perpendicular snaps. Empty when +/// PDFOSNAP is off or the page is not shown. +pub fn underlay_snap_geometry( + u: &acadrust::entities::Underlay, + document: &acadrust::CadDocument, +) -> Vec<[f64; 3]> { + if !pdf_osnap() || !u.flags.contains(acadrust::entities::UnderlayDisplayFlags::ON) { + return Vec::new(); + } + let Some(def) = crate::entities::underlay::definition(u, document) else { + return Vec::new(); + }; + if def.unloaded { + return Vec::new(); + } + let source = super::pdf_layers::underlay_source(u, &def.file_path); + let Some(vectors) = page_vectors(&source, crate::entities::underlay::page_of(def)) else { + return Vec::new(); + }; + let world = |p: [f64; 2]| crate::entities::underlay::local_to_world(u, p); + let mut out: Vec<[f64; 3]> = Vec::new(); + for path in &vectors.paths { + for sp in &path.subpaths { + if out.len() >= MAX_GEOMETRY_POINTS { + return out; + } + if !out.is_empty() { + out.push([f64::NAN; 3]); + } + out.push(world(sp.segments[0].start())); + for seg in &sp.segments { + let steps = if matches!(seg, Segment::Cubic(..)) { 12 } else { 1 }; + for k in 1..=steps { + out.push(world(bezier(seg, k as f64 / steps as f64))); + } + } + } + } + out +} diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index c1c2852f3..5effc43c4 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -24,6 +24,8 @@ mod widgets; mod draw_panel; mod modify_panel; mod color_dropdown; +mod underlay_tab; +pub use underlay_tab::{UnderlayContext, UnderlayTabMsg}; use widgets::{StyleContext, *}; pub(crate) use widgets::{REDO_HISTORY_ID, UNDO_HISTORY_ID}; mod collapse; @@ -102,6 +104,10 @@ pub struct Ribbon { /// Set by `CollapsePanels` when the tool row is in its tight state; the mode /// selector hides itself then to give the cramped tab row its space back. collapse_tight: Arc, + /// The selected PDF underlay's values while the contextual tab exists. + underlay_ctx: Option, + /// The contextual underlay tab is the one shown. + underlay_tab_active: bool, } /// Per-layer display data shown in the ribbon layer dropdown. @@ -195,6 +201,8 @@ impl Ribbon { tool_bar_h: Arc::new(AtomicU32::new(TOOL_BAR_H.to_bits())), collapse_mode: CollapseMode::default(), collapse_tight: Arc::new(AtomicBool::new(false)), + underlay_ctx: None, + underlay_tab_active: false, } } @@ -272,6 +280,7 @@ impl Ribbon { pub fn select(&mut self, index: usize) { if index < self.modules.len() { self.active = index; + self.underlay_tab_active = false; } } @@ -335,6 +344,58 @@ impl Ribbon { show_block_palette, show_file_tabs: self.show_file_tabs, show_layout_tabs: self.show_layout_tabs, + underlay_mono: self.underlay_ctx.as_ref().is_some_and(|c| c.monochrome), + underlay_shown: self.underlay_ctx.as_ref().is_some_and(|c| c.shown), + underlay_snap: self.underlay_ctx.as_ref().is_some_and(|c| c.snap), + } + } + + /// One panel in its four densities; `lead` adds controls before the + /// group's own tools. + fn panel<'a>( + &'a self, + g: &'a RibbonGroup, + ts: widgets::ToggleState, + style_ctx: &StyleContext<'_>, + lead: &dyn Fn(bool) -> Vec>, + ) -> Panel<'a> { + let group = |compact: bool| { + render_group( + compact, + g, + lead(compact), + &self.active_tool, + &self.open_dropdown, + &self.last_cmd, + ts, + &self.layer_infos, + &self.active_layer, + self.active_color, + &self.active_linetype, + self.active_lineweight, + style_ctx, + ) + }; + let button = |tight: bool| { + collapse_button( + g, + self.last_panel_tool.get(g.title).copied(), + &self.active_tool, + &self.open_dropdown, + &self.last_cmd, + ts, + &self.layer_infos, + &self.active_layer, + self.active_color, + &self.active_linetype, + self.active_lineweight, + style_ctx, + tight, + ) + }; + Panel { + id: g.title.to_string(), + elements: [group(false), group(true), button(false), button(true)], } } @@ -453,7 +514,7 @@ impl Ribbon { return acc; } - let is_active = i == self.active; + let is_active = i == self.active && !self.underlay_tab_shown(); let is_contextual = module.id() == "layout"; let btn = container( button(text(crate::i18n::ribbon_module_title(module.id(), module.title())).size(12)) @@ -523,6 +584,11 @@ impl Ribbon { }, ); + let mut tab_items = tab_items; + if self.underlay_ctx.is_some() { + tab_items.push(underlay_tab_button(self.underlay_tab_shown())); + } + // Tabs may squeeze their gaps to fit before wrapping: from the normal 6px // down to -12px on a narrow (e.g. phone) tab row, tucking neighbours into // each other's 14px side padding without overlapping the labels. When the @@ -602,74 +668,15 @@ impl Ribbon { // fit they degrade from the right — a panel's large buttons first // shrink to compact icon columns, then it collapses to a ▾ flyout // button. See `CollapsePanels`. - let panels: Vec> = groups - .iter() - .map(|g| { - let ts = self.toggle_state(show_block_palette); - Panel { - id: g.title.to_string(), - elements: [render_group( - false, - g, - &self.active_tool, - &self.open_dropdown, - &self.last_cmd, - ts, - &self.layer_infos, - &self.active_layer, - self.active_color, - &self.active_linetype, - self.active_lineweight, - &style_ctx, - ), - render_group( - true, - g, - &self.active_tool, - &self.open_dropdown, - &self.last_cmd, - ts, - &self.layer_infos, - &self.active_layer, - self.active_color, - &self.active_linetype, - self.active_lineweight, - &style_ctx, - ), - collapse_button( - g, - self.last_panel_tool.get(g.title).copied(), - &self.active_tool, - &self.open_dropdown, - &self.last_cmd, - ts, - &self.layer_infos, - &self.active_layer, - self.active_color, - &self.active_linetype, - self.active_lineweight, - &style_ctx, - false, - ), - collapse_button( - g, - self.last_panel_tool.get(g.title).copied(), - &self.active_tool, - &self.open_dropdown, - &self.last_cmd, - ts, - &self.layer_infos, - &self.active_layer, - self.active_color, - &self.active_linetype, - self.active_lineweight, - &style_ctx, - true, - ), - ], - } - }) - .collect(); + let ts = self.toggle_state(show_block_palette); + let panels: Vec> = if self.underlay_tab_shown() { + self.underlay_panels(ts, &style_ctx) + } else { + groups + .iter() + .map(|g| self.panel(g, ts, &style_ctx, &|_| Vec::new())) + .collect() + }; CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H) .report_height(self.tool_bar_h.clone()) .report_tight(self.collapse_tight.clone()) @@ -1321,6 +1328,7 @@ impl Ribbon { fn render_group<'a>( compact: bool, group: &RibbonGroup, + lead: Vec>, active_tool: &Option, open_dd: &Option, last_cmd: &HashMap<&'static str, &'static str>, @@ -1332,7 +1340,7 @@ fn render_group<'a>( active_lineweight: LineWeight, style_ctx: &StyleContext<'_>, ) -> Element<'a, Message> { - let mut items_row: Vec> = Vec::new(); + let mut items_row: Vec> = lead; let mut small_buf: Vec> = Vec::new(); let ctx = widgets::RenderCtx { @@ -1614,3 +1622,31 @@ mod tests { assert_eq!(ribbon.open_dropdown, None); } } + +/// The contextual underlay tab's button: framed in the accent colour. +fn underlay_tab_button<'a>(active: bool) -> Element<'a, Message> { + let btn = button(text(t!("PDF Underlay")).size(12)) + .on_press(Message::RibbonSelectUnderlayTab) + .style(move |theme: &Theme, status| { + let palette = theme.palette(); + let pair = match (active, status) { + (true, _) => palette.background.weakest, + (false, button::Status::Hovered) => palette.background.weak, + _ => palette.background.base, + }; + button::Style { + background: (active || matches!(status, button::Status::Hovered)) + .then_some(Background::Color(pair.color)), + text_color: if active { pair.text } else { palette.primary.base.color }, + border: Border { + color: palette.primary.base.color, + width: if active { 2.0 } else { 1.0 }, + radius: 0.0.into(), + }, + shadow: iced::Shadow::default(), + snap: false, + } + }) + .padding([5, 14]); + container(btn).into() +} diff --git a/src/ui/ribbon/underlay_tab.rs b/src/ui/ribbon/underlay_tab.rs new file mode 100644 index 000000000..31a478b5f --- /dev/null +++ b/src/ui/ribbon/underlay_tab.rs @@ -0,0 +1,215 @@ +//! The contextual "PDF Underlay" tab: shown and brought forward while only +//! PDF underlays are selected, gone when the selection changes. + +use iced::widget::{column, row, slider, text, text_input}; +use iced::{Element, Length}; + +use super::*; +use crate::modules::{ModuleEvent, ToolDef}; + +/// What the tab shows for the selected underlay (the first one). +#[derive(Clone, Debug, PartialEq)] +pub struct UnderlayContext { + pub contrast: u8, + pub fade: u8, + pub monochrome: bool, + pub shown: bool, + pub snap: bool, + pub contrast_text: String, + pub fade_text: String, +} + +impl UnderlayContext { + pub fn new(contrast: u8, fade: u8, monochrome: bool, shown: bool, snap: bool) -> Self { + Self { + contrast, + fade, + monochrome, + shown, + snap, + contrast_text: contrast.to_string(), + fade_text: fade.to_string(), + } + } +} + +/// The tab's edits; the host writes them to the selected underlays. +#[derive(Clone, Debug)] +pub enum UnderlayTabMsg { + Contrast(u8), + Fade(u8), + ContrastText(String), + FadeText(String), + /// Slider released or value submitted: write contrast and fade. + Commit, +} + +pub(super) const ADJUST: &str = "Adjust"; + +fn tool(id: &'static str, label: &'static str, icon: &'static [u8]) -> ToolDef { + ToolDef { + id, + label, + icon: IconKind::Svg(icon), + event: ModuleEvent::Command(id.to_string()), + } +} + +fn groups() -> &'static [RibbonGroup] { + static GROUPS: std::sync::OnceLock> = std::sync::OnceLock::new(); + const FRAMES: &[u8] = include_bytes!("../../../assets/icons/underlay_frames.svg"); + const CLIP: &[u8] = include_bytes!("../../../assets/icons/xclip.svg"); + GROUPS.get_or_init(|| { + vec![ + RibbonGroup { + title: ADJUST, + tools: vec![RibbonItem::LargeTool(tool( + "_PDFULMONO", + "Display in\nMonochrome", + FRAMES, + ))], + }, + RibbonGroup { + title: "Clipping", + tools: vec![ + RibbonItem::LargeTool(tool("_PDFULCLIP", "Create Clipping\nBoundary", CLIP)), + RibbonItem::LargeTool(tool("_PDFULUNCLIP", "Remove\nClipping", CLIP)), + ], + }, + RibbonGroup { + title: "Options", + tools: vec![ + RibbonItem::LargeTool(tool("_PDFULSHOW", "Show\nUnderlay", FRAMES)), + RibbonItem::LargeTool(tool( + "_PDFULSNAP", + "Enable\nSnap", + include_bytes!("../../../assets/icons/snap_underlays.svg"), + )), + RibbonItem::LargeTool(tool( + "EXTERNALREFERENCES", + "External\nReferences", + crate::ui::icons::FOLDER_OPEN, + )), + ], + }, + RibbonGroup { + title: "PDF Layers", + tools: vec![RibbonItem::LargeTool(tool( + "ULAYERS", + "Edit\nLayers", + include_bytes!("../../../assets/icons/underlay_layers.svg"), + ))], + }, + RibbonGroup { + title: "PDF Import", + tools: vec![RibbonItem::LargeTool(tool( + "_PDFULIMPORT", + "Import As\nObjects", + include_bytes!("../../../assets/icons/cui_import.svg"), + ))], + }, + ] + }) +} + +/// Contrast / Fade: a label, a 0–100 slider and the value box. +fn adjust_rows<'a>(ctx: &'a UnderlayContext) -> Element<'a, Message> { + let line = |label: &'static str, + value: u8, + buffer: &'a str, + on_slide: fn(u8) -> UnderlayTabMsg, + on_text: fn(String) -> UnderlayTabMsg| { + row![ + text(t!(label)).size(11).width(Length::Fixed(64.0)), + slider(0..=100u8, value, move |v| Message::UnderlayTab(on_slide(v))) + .on_release(Message::UnderlayTab(UnderlayTabMsg::Commit)) + .width(Length::Fixed(110.0)), + text_input("", buffer) + .on_input(move |s| Message::UnderlayTab(on_text(s))) + .on_submit(Message::UnderlayTab(UnderlayTabMsg::Commit)) + .size(11) + .padding([2, 4]) + .width(Length::Fixed(44.0)), + ] + .spacing(6) + .align_y(iced::Center) + }; + column![ + line( + "Contrast", + ctx.contrast, + &ctx.contrast_text, + UnderlayTabMsg::Contrast, + UnderlayTabMsg::ContrastText, + ), + line( + "Fade", + ctx.fade, + &ctx.fade_text, + UnderlayTabMsg::Fade, + UnderlayTabMsg::FadeText, + ), + ] + .spacing(8) + .padding([6, 4]) + .into() +} + +impl Ribbon { + /// Show the tab for the selected underlay, or drop it (`None`). A tab + /// that appears comes to the front; one that goes returns the tab that + /// was active before it. + pub fn set_underlay_context(&mut self, context: Option) { + match (&self.underlay_ctx, &context) { + (None, Some(_)) => self.underlay_tab_active = true, + (Some(_), None) => self.underlay_tab_active = false, + _ => {} + } + // A value being typed survives a refresh that leaves the value as is. + let mut context = context; + if let (Some(old), Some(new)) = (&self.underlay_ctx, &mut context) { + if (old.contrast, old.fade) == (new.contrast, new.fade) { + new.contrast_text = old.contrast_text.clone(); + new.fade_text = old.fade_text.clone(); + } + } + self.underlay_ctx = context; + } + + pub fn underlay_context_mut(&mut self) -> Option<&mut UnderlayContext> { + self.underlay_ctx.as_mut() + } + + pub fn select_underlay_tab(&mut self) { + if self.underlay_ctx.is_some() { + self.underlay_tab_active = true; + } + } + + pub(super) fn underlay_tab_shown(&self) -> bool { + self.underlay_tab_active && self.underlay_ctx.is_some() + } + + pub(super) fn underlay_panels<'a>( + &'a self, + ts: widgets::ToggleState, + style_ctx: &StyleContext<'_>, + ) -> Vec> { + let Some(ctx) = self.underlay_ctx.as_ref() else { + return Vec::new(); + }; + groups() + .iter() + .map(|g| { + let lead = |_: bool| -> Vec> { + if g.title == ADJUST { + vec![adjust_rows(ctx)] + } else { + Vec::new() + } + }; + self.panel(g, ts, style_ctx, &lead) + }) + .collect() + } +} diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index 72b765fa2..17d317706 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -40,6 +40,9 @@ pub(super) struct ToggleState { pub show_block_palette: bool, pub show_file_tabs: bool, pub show_layout_tabs: bool, + pub underlay_mono: bool, + pub underlay_shown: bool, + pub underlay_snap: bool, } // ── Layout constants (single source of truth: ROW_H from ui::mod) ───────── @@ -375,6 +378,9 @@ pub(super) fn is_active_tool( "UCSICON" => state.show_ucs_icon, "PROPERTIES" => state.show_properties, "BLOCKPALETTE" => state.show_block_palette, + "_PDFULMONO" => state.underlay_mono, + "_PDFULSHOW" => state.underlay_shown, + "_PDFULSNAP" => state.underlay_snap, "FILETAB" => state.show_file_tabs, "LAYOUTTAB" => state.show_layout_tabs, id => active_tool.as_deref() == Some(id), diff --git a/src/ui/window/block_definition.rs b/src/ui/window/block_definition.rs index 126f44321..f1c296f11 100644 --- a/src/ui/window/block_definition.rs +++ b/src/ui/window/block_definition.rs @@ -158,7 +158,7 @@ impl BlockDefinitionState { } } -fn group<'a>( +pub(crate) fn group<'a>( title: impl Into, body: impl Into>, height: impl Into, @@ -188,7 +188,7 @@ fn group<'a>( .into() } -fn labeled_checkbox<'a>( +pub(crate) fn labeled_checkbox<'a>( label: impl Into, is_checked: bool, on_toggle: impl Fn(bool) -> Message + 'a, diff --git a/src/ui/window/mod.rs b/src/ui/window/mod.rs index 71af8baf6..77d4608ef 100644 --- a/src/ui/window/mod.rs +++ b/src/ui/window/mod.rs @@ -24,5 +24,6 @@ pub mod annotation_data; pub mod alias_editor; pub mod find_replace; pub mod named_parameters; +pub mod pdf_dialogs; pub mod xref_help; pub mod xref_manager; diff --git a/src/ui/window/pdf_dialogs.rs b/src/ui/window/pdf_dialogs.rs new file mode 100644 index 000000000..b1738ec07 --- /dev/null +++ b/src/ui/window/pdf_dialogs.rs @@ -0,0 +1,838 @@ +//! PDF dialogs: Attach PDF Underlay (pages, path type, insertion, scale, +//! rotation), Underlay Layers, PDF Import Settings and Import PDF. + +use std::fmt; + +use iced::widget::{ + button, column, combo_box, container, image, pick_list, radio, row, scrollable, text, + text_input, Space, +}; +use iced::{Background, Border, Element, Fill, Length, Theme}; + +use crate::app::Message; +use crate::io::xref_model::Pathtype; +use crate::modules::insert::pdf_import::{ImportLayers, PdfImportSettings}; +use crate::t; +use crate::ui::style::common::muted_style; +use crate::ui::style::form::{button_style, field_style}; +use crate::ui::window::block_definition::{group, labeled_checkbox}; + +/// One edit in any of the PDF dialogs. +#[derive(Debug, Clone)] +pub enum PdfDialogMsg { + // Attach + AttachName(String), + AttachBrowse, + AttachPage(usize), + AttachPathType(PathTypeChoice), + AttachInsertOnScreen(bool), + AttachInsert(usize, String), + AttachScaleOnScreen(bool), + AttachScale(String), + AttachRotationOnScreen(bool), + AttachRotation(String), + AttachDetails(bool), + AttachOk, + // Underlay Layers + LayersUnderlay(String), + LayersSearch(String), + LayersToggle(String), + LayersOk, + // Import settings (the settings dialog and the Import PDF dialog) + Vector(bool), + Fills(bool), + Text(bool), + Raster(bool), + Layers(ImportLayers), + AsBlock(bool), + Join(bool), + Hatches(bool), + Lineweights(bool), + Linetypes(bool), + SettingsOk, + // Import PDF + ImportBrowse, + ImportPage(usize), + ImportPageText(String), + ImportInsertOnScreen(bool), + ImportScale(String), + ImportRotation(RotationChoice), + ImportOk, + Options, + Help(&'static str), +} + +fn msg(m: PdfDialogMsg) -> Message { + Message::PdfDialog(m) +} + +/// Path type as the Attach dialog lists it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PathTypeChoice(pub Pathtype); + +impl PathTypeChoice { + pub const ALL: [PathTypeChoice; 3] = [ + PathTypeChoice(Pathtype::Full), + PathTypeChoice(Pathtype::Relative), + PathTypeChoice(Pathtype::None), + ]; +} + +impl fmt::Display for PathTypeChoice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let label = match self.0 { + Pathtype::Full => t!("Full path"), + Pathtype::Relative => t!("Relative path"), + Pathtype::None => t!("No path"), + }; + f.write_str(label.as_ref()) + } +} + +/// Rotation of an imported page: the four right angles. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RotationChoice(pub u16); + +impl RotationChoice { + pub const ALL: [RotationChoice; 4] = [ + RotationChoice(0), + RotationChoice(90), + RotationChoice(180), + RotationChoice(270), + ]; +} + +impl fmt::Display for RotationChoice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// A page of the chosen file and its thumbnail. +#[derive(Clone)] +pub struct PageThumb { + pub label: String, + pub image: Option, +} + +/// Thumbnails of every page, about 100 px wide. +// ponytail: all pages are rasterised when the dialog opens; lazy per-row +// thumbnails if files with hundreds of pages matter. +pub fn page_thumbs(path: &str) -> Vec { + use crate::scene::model::pdf_raster::{page_count, page_size_inches, rasterize_page_at_dpi}; + let count = page_count(path).unwrap_or(0); + (1..=count) + .map(|n| { + let label = n.to_string(); + let image = page_size_inches(path, &label).and_then(|(w, h)| { + let dpi = (100.0 / w.max(h).max(0.1)) as f32; + let page = rasterize_page_at_dpi(path, &label, dpi)?; + Some(image::Handle::from_rgba(page.width, page.height, page.pixels.to_vec())) + }); + PageThumb { label, image } + }) + .collect() +} + +/// Ctrl adds or removes a page, Shift takes the range from the last click, +/// a plain click selects that page alone. +pub fn click_page(selected: &mut Vec, anchor: &mut usize, page: usize, ctrl: bool, shift: bool) { + if shift { + let (a, b) = if *anchor <= page { (*anchor, page) } else { (page, *anchor) }; + *selected = (a..=b).collect(); + } else if ctrl { + match selected.iter().position(|p| *p == page) { + Some(at) if selected.len() > 1 => { + selected.remove(at); + } + Some(_) => {} + None => { + selected.push(page); + selected.sort_unstable(); + } + } + *anchor = page; + } else { + *selected = vec![page]; + *anchor = page; + } +} + +fn frame_style(theme: &Theme) -> container::Style { + container::Style { + background: Some(Background::Color(theme.palette().background.base.color)), + border: Border { + width: 1.0, + radius: 2.0.into(), + color: theme.palette().background.strong.color, + }, + ..Default::default() + } +} + +fn page_grid<'a>( + pages: &'a [PageThumb], + selected: &[usize], + on_click: fn(usize) -> PdfDialogMsg, + height: f32, +) -> Element<'a, Message> { + let tiles = pages.iter().enumerate().map(|(index, page)| { + let chosen = selected.contains(&index); + let picture: Element<'a, Message> = match &page.image { + Some(handle) => image(handle.clone()).width(Fill).height(Fill).into(), + None => Space::new().width(Fill).height(Fill).into(), + }; + let tile = column![ + container(picture) + .width(Length::Fixed(100.0)) + .height(Length::Fixed(60.0)) + .padding(2) + .style(move |theme: &Theme| container::Style { + background: Some(Background::Color(iced::Color::WHITE)), + border: Border { + width: if chosen { 2.0 } else { 1.0 }, + radius: 1.0.into(), + color: if chosen { + theme.palette().primary.base.color + } else { + theme.palette().background.strong.color + }, + }, + ..Default::default() + }), + container(text(page.label.clone()).size(11)) + .width(Length::Fixed(100.0)) + .align_x(iced::Center) + .style(move |theme: &Theme| container::Style { + background: chosen + .then(|| Background::Color(theme.palette().primary.weak.color)), + ..Default::default() + }), + ] + .spacing(1); + button(tile) + .on_press(msg(on_click(index))) + .padding(2) + .style(|_: &Theme, _| button::Style::default()) + .into() + }); + container(scrollable( + iced::widget::Row::with_children(tiles.collect::>()) + .spacing(8) + .wrap() + .vertical_spacing(8), + )) + .padding(6) + .width(Fill) + .height(Length::Fixed(height)) + .style(frame_style) + .into() +} + +fn value_field<'a>( + label: String, + value: &'a str, + enabled: bool, + label_width: f32, + on_input: impl Fn(String) -> Message + 'a, +) -> Element<'a, Message> { + let mut field = text_input("", value) + .size(11) + .padding([3, 6]) + .width(Fill) + .style(field_style); + if enabled { + field = field.on_input(on_input); + } + row![ + text(label).size(11).style(muted_style).width(Length::Fixed(label_width)), + field + ] + .spacing(6) + .align_y(iced::Center) + .into() +} + +fn read_only<'a>(label: String, value: &'a str, label_width: f32) -> Element<'a, Message> { + value_field(label, value, false, label_width, |_| Message::Noop) +} + +fn footer<'a>(ok: PdfDialogMsg, help: &'static str) -> iced::widget::Row<'a, Message> { + row![ + Space::new().width(Fill), + button(text(t!("OK")).size(11)) + .on_press(msg(ok)) + .style(button_style(true)) + .padding([4, 16]), + button(text(t!("Cancel")).size(11)) + .on_press(Message::CloseModal) + .style(button_style(false)) + .padding([4, 12]), + button(text(t!("Help")).size(11)) + .on_press(msg(PdfDialogMsg::Help(help))) + .style(button_style(false)) + .padding([4, 12]), + ] + .spacing(6) + .align_y(iced::Center) +} + +// ── Attach PDF Underlay ──────────────────────────────────────────────────── + +/// Choices the Attach dialog keeps for its next opening. +#[derive(Clone)] +pub struct AttachMemory { + pub path_type: PathTypeChoice, + pub insert_on_screen: bool, + pub scale_on_screen: bool, + pub scale: String, + pub rotation_on_screen: bool, + pub rotation: String, + pub details: bool, +} + +impl Default for AttachMemory { + fn default() -> Self { + Self { + path_type: PathTypeChoice(Pathtype::Relative), + insert_on_screen: true, + scale_on_screen: false, + scale: "1.0000".into(), + rotation_on_screen: false, + rotation: "0".into(), + details: false, + } + } +} + +pub static ATTACH_MEMORY: std::sync::Mutex> = std::sync::Mutex::new(None); + +pub struct PdfAttachState { + /// The file read (absolute). + pub path: String, + pub name: String, + /// PDF files already attached: name and read path. + pub existing: Vec<(String, String)>, + pub name_combo: combo_box::State, + pub pages: Vec, + pub selected: Vec, + pub anchor: usize, + pub path_type: PathTypeChoice, + pub insert_on_screen: bool, + pub insert: [String; 3], + pub scale_on_screen: bool, + pub scale: String, + pub rotation_on_screen: bool, + pub rotation: String, + pub found_in: String, + pub saved_path: String, + pub page_size: String, + pub details: bool, +} + +impl PdfAttachState { + pub fn new(existing: Vec<(String, String)>) -> Self { + let memory = ATTACH_MEMORY + .lock() + .ok() + .and_then(|m| m.clone()) + .unwrap_or_default(); + let names = existing.iter().map(|(name, _)| name.clone()).collect(); + Self { + path: String::new(), + name: String::new(), + existing, + name_combo: combo_box::State::new(names), + pages: Vec::new(), + selected: vec![0], + anchor: 0, + path_type: memory.path_type, + insert_on_screen: memory.insert_on_screen, + insert: ["0.0000".into(), "0.0000".into(), "0.0000".into()], + scale_on_screen: memory.scale_on_screen, + scale: memory.scale, + rotation_on_screen: memory.rotation_on_screen, + rotation: memory.rotation, + found_in: String::new(), + saved_path: String::new(), + page_size: String::new(), + details: memory.details, + } + } + + pub fn remember(&self) { + if let Ok(mut m) = ATTACH_MEMORY.lock() { + *m = Some(AttachMemory { + path_type: self.path_type, + insert_on_screen: self.insert_on_screen, + scale_on_screen: self.scale_on_screen, + scale: self.scale.clone(), + rotation_on_screen: self.rotation_on_screen, + rotation: self.rotation.clone(), + details: self.details, + }); + } + } + + pub fn page_labels(&self) -> Vec { + self.selected + .iter() + .filter_map(|i| self.pages.get(*i).map(|p| p.label.clone())) + .collect() + } +} + +pub fn view_attach<'a>( + state: &'a PdfAttachState, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let name_combo = combo_box( + &state.name_combo, + "", + (!state.name.is_empty()).then_some(&state.name), + |chosen: String| msg(PdfDialogMsg::AttachName(chosen)), + ) + .size(11) + .padding([3, 6]) + .width(Fill); + let name_section = column![ + text(t!("Name:")).size(11).style(muted_style), + row![ + name_combo, + button(text(t!("Browse...")).size(11)) + .on_press(msg(PdfDialogMsg::AttachBrowse)) + .style(button_style(false)) + .padding([4, 12]) + .width(Length::Fixed(96.0)), + ] + .spacing(8) + .align_y(iced::Center), + ] + .spacing(3); + + let pages = group( + t!("Select one or more pages from the PDF file:").into_owned(), + page_grid(&state.pages, &state.selected, PdfDialogMsg::AttachPage, 250.0), + Length::Shrink, + ); + + let path_type = group( + t!("Path type").into_owned(), + pick_list(Some(state.path_type), PathTypeChoice::ALL, |c| c.to_string()) + .on_select(|v| msg(PdfDialogMsg::AttachPathType(v))) + .text_size(11) + .padding([3, 6]) + .width(Fill), + Length::Shrink, + ); + let insert_enabled = !state.insert_on_screen; + let insertion = group( + t!("Insertion point").into_owned(), + column![ + labeled_checkbox(t!("Specify On-screen"), state.insert_on_screen, |v| msg( + PdfDialogMsg::AttachInsertOnScreen(v) + )), + value_field("X:".into(), &state.insert[0], insert_enabled, 16.0, |v| msg( + PdfDialogMsg::AttachInsert(0, v) + )), + value_field("Y:".into(), &state.insert[1], insert_enabled, 16.0, |v| msg( + PdfDialogMsg::AttachInsert(1, v) + )), + value_field("Z:".into(), &state.insert[2], insert_enabled, 16.0, |v| msg( + PdfDialogMsg::AttachInsert(2, v) + )), + ] + .spacing(5), + Length::Shrink, + ); + let middle = column![path_type, insertion].spacing(6).width(Length::FillPortion(1)); + + let mut scale_field = text_input("", &state.scale) + .size(11) + .padding([3, 6]) + .width(Fill) + .style(field_style); + if !state.scale_on_screen { + scale_field = scale_field.on_input(|v| msg(PdfDialogMsg::AttachScale(v))); + } + let scale = group( + t!("Scale").into_owned(), + column![ + labeled_checkbox(t!("Specify On-screen"), state.scale_on_screen, |v| msg( + PdfDialogMsg::AttachScaleOnScreen(v) + )), + scale_field, + ] + .spacing(5), + Length::Shrink, + ); + let rotation = group( + t!("Rotation").into_owned(), + column![ + labeled_checkbox(t!("Specify On-screen"), state.rotation_on_screen, |v| msg( + PdfDialogMsg::AttachRotationOnScreen(v) + )), + value_field( + t!("Angle:").into_owned(), + &state.rotation, + !state.rotation_on_screen, + 44.0, + |v| msg(PdfDialogMsg::AttachRotation(v)) + ), + ] + .spacing(5), + Length::Shrink, + ); + let right = column![scale, rotation].spacing(6).width(Length::FillPortion(1)); + + let body = row![ + container(pages).width(Length::FillPortion(1)), + middle, + right + ] + .spacing(6) + .width(Fill); + + let details: Element<'a, Message> = if state.details { + group( + t!("Location").into_owned(), + column![ + read_only(t!("Found in:").into_owned(), &state.found_in, 92.0), + read_only(t!("Saved path:").into_owned(), &state.saved_path, 92.0), + read_only(t!("Page size:").into_owned(), &state.page_size, 92.0), + ] + .spacing(5), + Length::Shrink, + ) + } else { + Space::new().height(0).into() + }; + + let details_label = if state.details { + t!("Hide Details") + } else { + t!("Show Details") + }; + let footer = row![button(text(details_label).size(11)) + .on_press(msg(PdfDialogMsg::AttachDetails(!state.details))) + .style(button_style(false)) + .padding([4, 12])] + .push(footer(PdfDialogMsg::AttachOk, "attach")) + .align_y(iced::Center); + + column![name_section, body, details, footer] + .spacing(6) + .padding([8, 10]) + .width(sizing.width) + .into() +} + +// ── Underlay Layers ──────────────────────────────────────────────────────── + +/// One PDF underlay the dialog can show: its name, its file's layers, and +/// which of them it turns off. +#[derive(Clone)] +pub struct LayerTarget { + pub name: String, + pub handle: acadrust::Handle, + pub layers: Vec, + pub hidden: Vec, +} + +pub struct UnderlayLayersState { + pub targets: Vec, + pub current: usize, + pub search: String, +} + +pub fn view_layers<'a>( + state: &'a UnderlayLayersState, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let names: Vec = state.targets.iter().map(|t| t.name.clone()).collect(); + let target = state.targets.get(state.current); + let reference = row![ + text(t!("Reference name:")).size(11).width(Length::Fixed(110.0)), + pick_list(target.map(|t| t.name.clone()), names, |n: &String| n.clone()) + .on_select(|n| msg(PdfDialogMsg::LayersUnderlay(n))) + .text_size(11) + .padding([3, 6]) + .width(Fill), + ] + .spacing(6) + .align_y(iced::Center); + let search = text_input(t!("Search for layer").as_ref(), &state.search) + .on_input(|s| msg(PdfDialogMsg::LayersSearch(s))) + .size(11) + .padding([3, 6]) + .width(Length::Fixed(220.0)) + .style(field_style); + + let header = row![ + text(t!("On")).size(11).style(muted_style).width(Length::Fixed(44.0)), + text(t!("Name")).size(11).style(muted_style), + ] + .padding([4, 8]); + let body: Element<'a, Message> = match target { + Some(t) if !t.layers.is_empty() => { + let needle = state.search.to_lowercase(); + let rows = t + .layers + .iter() + .filter(|name| needle.is_empty() || name.to_lowercase().contains(&needle)) + .map(|name| { + let on = !t.hidden.contains(name); + let bulb = button(text("●").size(12).style(move |theme: &Theme| text::Style { + color: Some(if on { + theme.palette().warning.base.color + } else { + theme.palette().background.strong.color + }), + })) + .on_press(msg(PdfDialogMsg::LayersToggle(name.clone()))) + .padding([0, 4]) + .style(|_: &Theme, _| button::Style::default()); + row![ + container(bulb).width(Length::Fixed(44.0)), + text(name.clone()).size(11) + ] + .padding([2, 8]) + .align_y(iced::Center) + .into() + }); + scrollable(iced::widget::Column::with_children(rows.collect::>())) + .height(Fill) + .into() + } + _ => container(text(t!("This file does not contain any layers.")).size(11)) + .width(Fill) + .height(Fill) + .align_x(iced::Center) + .align_y(iced::Center) + .into(), + }; + let list = container(column![header, body]) + .width(Fill) + .height(Length::Fixed(300.0)) + .style(frame_style); + + column![ + text(t!("Select an underlay to view its layers.")).size(11), + reference, + search, + list, + footer(PdfDialogMsg::LayersOk, "layers"), + ] + .spacing(8) + .padding([8, 10]) + .width(sizing.width) + .into() +} + +// ── PDF Import Settings ──────────────────────────────────────────────────── + +fn settings_groups<'a>(s: &'a PdfImportSettings) -> (Element<'a, Message>, Element<'a, Message>, Element<'a, Message>) { + let fills: Element<'a, Message> = if s.vector { + labeled_checkbox(t!("Solid fills"), s.fills, |v| msg(PdfDialogMsg::Fills(v))) + } else { + labeled_checkbox(t!("Solid fills"), s.fills, move |_| Message::Noop) + }; + let data = group( + t!("PDF data to import").into_owned(), + column![ + labeled_checkbox(t!("Vector geometry"), s.vector, |v| msg(PdfDialogMsg::Vector(v))), + container(fills).padding(iced::Padding { left: 20.0, ..Default::default() }), + labeled_checkbox(t!("TrueType text"), s.text, |v| msg(PdfDialogMsg::Text(v))), + labeled_checkbox(t!("Raster images"), s.raster, |v| msg(PdfDialogMsg::Raster(v))), + ] + .spacing(6), + Length::Fill, + ); + let layer_radio = |label: std::borrow::Cow<'static, str>, value: ImportLayers| { + radio(label, value, Some(s.layers), |v| msg(PdfDialogMsg::Layers(v))) + .size(14) + .text_size(11) + }; + let layers = group( + t!("Layers").into_owned(), + column![ + layer_radio(t!("Use PDF layers"), ImportLayers::Pdf), + layer_radio(t!("Create object layers"), ImportLayers::Object), + layer_radio(t!("Current layer"), ImportLayers::Current), + ] + .spacing(6), + Length::Fill, + ); + let options = group( + t!("Import options").into_owned(), + column![ + labeled_checkbox(t!("Import as block"), s.as_block, |v| msg(PdfDialogMsg::AsBlock(v))), + labeled_checkbox(t!("Join line and arc segments"), s.join, |v| msg( + PdfDialogMsg::Join(v) + )), + labeled_checkbox(t!("Convert solid fills to hatches"), s.hatches, |v| msg( + PdfDialogMsg::Hatches(v) + )), + labeled_checkbox(t!("Apply lineweight properties"), s.lineweights, |v| msg( + PdfDialogMsg::Lineweights(v) + )), + labeled_checkbox(t!("Infer linetypes from collinear dashes"), s.linetypes, |v| msg( + PdfDialogMsg::Linetypes(v) + )), + ] + .spacing(6), + Length::Shrink, + ); + (data, layers, options) +} + +pub fn view_import_settings<'a>( + settings: &'a PdfImportSettings, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let (data, layers, options) = settings_groups(settings); + let side_button = |label: std::borrow::Cow<'static, str>, on: Message, accent: bool| { + button(text(label).size(11).width(Fill).align_x(iced::Center)) + .on_press(on) + .style(button_style(accent)) + .padding([4, 12]) + .width(Length::Fixed(110.0)) + }; + let left = column![ + row![data, layers].spacing(8).height(Length::Fixed(140.0)), + options + ] + .spacing(8) + .width(Fill); + let right = column![ + side_button(t!("OK"), msg(PdfDialogMsg::SettingsOk), true), + side_button(t!("Cancel"), Message::CloseModal, false), + side_button(t!("Options..."), msg(PdfDialogMsg::Options), false), + side_button(t!("Help"), msg(PdfDialogMsg::Help("settings")), false), + ] + .spacing(8); + row![left, right] + .spacing(10) + .padding([8, 10]) + .width(sizing.width) + .into() +} + +// ── Import PDF (File) ────────────────────────────────────────────────────── + +pub struct PdfImportFileState { + pub path: String, + pub pages: Vec, + pub selected: usize, + pub page_text: String, + pub insert_on_screen: bool, + pub scale: String, + pub rotation: RotationChoice, + pub page_size: String, + pub settings: PdfImportSettings, +} + +pub fn view_import_file<'a>( + state: &'a PdfImportFileState, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let file_name = std::path::Path::new(&state.path) + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_default(); + let file_row = row![ + text(t!("File name:")).size(11).width(Length::Fixed(70.0)), + text_input("", &file_name) + .size(11) + .padding([3, 6]) + .width(Fill) + .style(field_style), + button(text(t!("Browse...")).size(11)) + .on_press(msg(PdfDialogMsg::ImportBrowse)) + .style(button_style(false)) + .padding([4, 12]) + .width(Length::Fixed(96.0)), + ] + .spacing(8) + .align_y(iced::Center); + + let selected = [state.selected]; + let page_group = group( + t!("Page to import").into_owned(), + column![ + row![ + text(t!("Page:")).size(11), + text_input("", &state.page_text) + .on_input(|s| msg(PdfDialogMsg::ImportPageText(s))) + .size(11) + .padding([3, 6]) + .width(Length::Fixed(56.0)) + .style(field_style), + text(crate::tf!("Total: {count}", count = state.pages.len())) + .size(11) + .style(muted_style), + ] + .spacing(8) + .align_y(iced::Center), + page_grid(&state.pages, &selected, PdfDialogMsg::ImportPage, 300.0), + row![ + text(crate::tf!("Page size: {size}", size = state.page_size.clone())) + .size(11) + .style(muted_style), + ], + ] + .spacing(6), + Length::Shrink, + ); + + let location = group( + t!("Location").into_owned(), + column![ + labeled_checkbox(t!("Specify insertion point on-screen"), state.insert_on_screen, |v| { + msg(PdfDialogMsg::ImportInsertOnScreen(v)) + }), + row![ + text(t!("Scale:")).size(11).style(muted_style), + text_input("", &state.scale) + .on_input(|s| msg(PdfDialogMsg::ImportScale(s))) + .size(11) + .padding([3, 6]) + .width(Length::Fixed(80.0)) + .style(field_style), + Space::new().width(Length::Fixed(20.0)), + text(t!("Rotation:")).size(11).style(muted_style), + pick_list(Some(state.rotation), RotationChoice::ALL, |c| c.to_string()) + .on_select(|v| msg(PdfDialogMsg::ImportRotation(v))) + .text_size(11) + .padding([3, 6]) + .width(Length::Fixed(76.0)), + ] + .spacing(6) + .align_y(iced::Center), + ] + .spacing(6), + Length::Shrink, + ); + let (data, layers, options) = settings_groups(&state.settings); + let right = column![ + location, + row![data, layers].spacing(8).height(Length::Fixed(140.0)), + options + ] + .spacing(8) + .width(Length::FillPortion(1)); + let body = row![container(page_group).width(Length::FillPortion(1)), right].spacing(8); + + let footer = row![button(text(t!("Options...")).size(11)) + .on_press(msg(PdfDialogMsg::Options)) + .style(button_style(false)) + .padding([4, 12])] + .push(footer(PdfDialogMsg::ImportOk, "import")) + .align_y(iced::Center); + + column![file_row, body, footer] + .spacing(8) + .padding([8, 10]) + .width(sizing.width) + .into() +}