forked from 1b-agent/opencadstudio-upstream
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 <[email protected]>
This commit is contained in:
1 parent
0c50013ca7
commit
3f6bdf8c59
57 files changed
+5049
-365
No files matched your search
Generated
+1
-1
@@ -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",
|
||||
|
||||
+2
-2
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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ığı
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+19
-17
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<acadrust::Handle> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.iter()
|
||||
.map(|(h, _)| *h)
|
||||
.collect();
|
||||
let mut targets: Vec<LayerTarget> = 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<String> = 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<LayerTarget>) {
|
||||
use crate::scene::model::pdf_layers::{hidden_layers, LAYER_OVERRIDE_APP};
|
||||
let changed: Vec<LayerTarget> = 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<Message> {
|
||||
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::<usize>().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::<f64>().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<Message> {
|
||||
use crate::command::CadCommand;
|
||||
let Some(state) = self.pdf_attach.as_ref() else {
|
||||
return Task::none();
|
||||
};
|
||||
let number = |s: &str| s.trim().parse::<f64>().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()
|
||||
}
|
||||
+162
-17
@@ -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<Vec<String>> = 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
|
||||
}
|
||||
@@ -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<Message> {
|
||||
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::<u8>().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<Task<Message>> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -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::<i16>().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 {
|
||||
|
||||
@@ -493,6 +493,13 @@ pub(super) struct OpenCADStudio {
|
||||
drawing_units: Option<crate::ui::window::drawing_units::State>,
|
||||
/// Working copy of the Block Definition dialog; `None` while it is closed.
|
||||
block_definition: Option<crate::ui::window::block_definition::BlockDefinitionState>,
|
||||
/// PDF dialogs' working copies; `None` while closed.
|
||||
pdf_attach: Option<crate::ui::window::pdf_dialogs::PdfAttachState>,
|
||||
underlay_layers: Option<crate::ui::window::pdf_dialogs::UnderlayLayersState>,
|
||||
pdf_import_settings: Option<crate::modules::insert::pdf_import::PdfImportSettings>,
|
||||
pdf_import_file: Option<crate::ui::window::pdf_dialogs::PdfImportFileState>,
|
||||
/// The dialog Options was opened from; it comes back when Options closes.
|
||||
options_parent: Option<ModalKind>,
|
||||
/// Working copy of the structured feature-control-frame editor.
|
||||
geometric_tolerance: Option<crate::ui::window::geometric_tolerance::State>,
|
||||
/// 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<Vec<u8>>), String>),
|
||||
/// Result of the PDFATTACH file picker.
|
||||
PdfAttachPickResult(Result<(std::path::PathBuf, std::sync::Arc<Vec<u8>>), 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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.",
|
||||
|
||||
+24
-16
@@ -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()
|
||||
|
||||
@@ -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| {
|
||||
|
||||
+11
-6
@@ -1556,6 +1556,17 @@ pub enum CmdResult {
|
||||
BatchCopy(Vec<Handle>, Vec<EntityTransform>),
|
||||
/// Erase `handle` and replace with new entities; command stays active.
|
||||
ReplaceEntity(Handle, Vec<EntityType>),
|
||||
/// 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<acadrust::Handle> {
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
+30
-2
@@ -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<PathBuf> {
|
||||
@@ -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<String, Strin
|
||||
}
|
||||
Ok(name)
|
||||
}
|
||||
RefTarget::Image { name, .. } | RefTarget::Pdf { name, .. } => 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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<String>,
|
||||
/// Pages after the first, attached beside it (dialog selection).
|
||||
extra_pages: Vec<String>,
|
||||
/// Scale and rotation fixed in the dialog: their prompts are skipped.
|
||||
preset_scale: Option<f64>,
|
||||
preset_rotation: Option<f64>,
|
||||
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<f64>,
|
||||
rotation_deg: Option<f64>,
|
||||
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<String> = 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<Handle> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
+625
-189
@@ -6,26 +6,132 @@
|
||||
// Keep, Detach or Unload PDF underlay? [Keep/Detach/Unload] <Unload>:
|
||||
// Binding PDF file <path>, page <n> ...
|
||||
//
|
||||
// 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 <font>"
|
||||
// text style.
|
||||
// Content of a PDF layer goes to PDF_<layer>; 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 <font>" 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_<name>"); 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<Option<PdfImportSettings>> = 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<EntityType>,
|
||||
pub layers: Vec<(String, Color)>,
|
||||
pub text_styles: Vec<(String, String)>,
|
||||
pub images: Vec<ImageImport>,
|
||||
/// 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::<f64>() / n,
|
||||
starts.iter().map(|p| p[1]).sum::<f64>() / 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::<f64>() / 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<u8>,
|
||||
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<Stroke>) -> Vec<Stroke> {
|
||||
let mut out: Vec<Stroke> = Vec::new();
|
||||
let mut open: Vec<Stroke> = 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<Stroke>) -> Vec<(Stroke, Option<f64>)> {
|
||||
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<f64>)> = Vec::new();
|
||||
let mut run: Vec<Stroke> = Vec::new();
|
||||
let flush = |run: &mut Vec<Stroke>, out: &mut Vec<(Stroke, Option<f64>)>| {
|
||||
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<String>, 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<Color> = None;
|
||||
let mut text_color: Option<Color> = 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<Color>,
|
||||
) {
|
||||
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<Vector2> = 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<Stroke> = Vec::new();
|
||||
let mut curves: Vec<EntityType> = 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<Vector2> = 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<Vector3> = 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<f64>)> = 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<Vector3> = 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<EntityType> {
|
||||
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<Vector2> = 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"]
|
||||
});
|
||||
@@ -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
|
||||
|
||||
+9
-3
@@ -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,
|
||||
},
|
||||
|
||||
+14
-5
@@ -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<Handle>,
|
||||
/// 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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<u16> = 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<PdfLayer> {
|
||||
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::<Dict<'_>>(xref.root_id()) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(props) = catalog.get::<Dict<'_>>(b"OCProperties") else {
|
||||
return Vec::new();
|
||||
};
|
||||
let config = props.get::<Dict<'_>>(b"D").unwrap_or_default();
|
||||
let ids = |key: &[u8]| -> Vec<(i32, i32)> {
|
||||
config
|
||||
.get::<Array<'_>>(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::<Name<'_>>(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::<Array<'_>>(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::<Dict<'_>>(ObjectIdentifier::from(r))?;
|
||||
let name = dict
|
||||
.get::<hayro::hayro_syntax::object::String<'_>>(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<Vec<u8>> {
|
||||
let pdf = Pdf::new(Arc::new(bytes.to_vec())).ok()?;
|
||||
let xref = pdf.xref();
|
||||
let root = xref.root_id();
|
||||
let catalog = xref.get::<Dict<'_>>(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::<Vec<_>>()
|
||||
.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<String> {
|
||||
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<String> {
|
||||
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)
|
||||
}
|
||||
@@ -48,6 +48,15 @@ pub fn register_source(path: &str, bytes: Arc<Vec<u8>>) {
|
||||
.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<Vec<u8>>) {
|
||||
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<Arc<PdfPage>> {
|
||||
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<usize> {
|
||||
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;
|
||||
|
||||
@@ -37,13 +37,13 @@ impl Segment {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SubPath {
|
||||
pub segments: Vec<Segment>,
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PdfPath {
|
||||
pub subpaths: Vec<SubPath>,
|
||||
/// 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<u8>,
|
||||
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<PdfPath>,
|
||||
pub texts: Vec<PdfText>,
|
||||
/// Filled only when images are asked for (`page_content`).
|
||||
pub images: Vec<PdfImage>,
|
||||
}
|
||||
|
||||
type Key = (String, String);
|
||||
@@ -101,7 +117,66 @@ pub fn page_vectors(path: &str, page: &str) -> Option<Arc<PageVectors>> {
|
||||
built
|
||||
}
|
||||
|
||||
/// A page's paths, text and raster images (not cached; import only).
|
||||
pub fn page_content(path: &str, page: &str) -> Option<PageVectors> {
|
||||
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<Vec<(Option<String>, PageVectors)>> {
|
||||
let layers = super::pdf_layers::layers(path);
|
||||
if layers.is_empty() {
|
||||
return Some(vec![(None, page_content(path, page)?)]);
|
||||
}
|
||||
let names: Vec<String> = 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<String, usize> = 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<String> = 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<PageVectors> {
|
||||
read_page_with(path, page, false)
|
||||
}
|
||||
|
||||
fn read_page_with(path: &str, page: &str, images: bool) -> Option<PageVectors> {
|
||||
let bytes = super::pdf_raster::source_bytes(path)?;
|
||||
let pdf = Pdf::new(bytes).ok()?;
|
||||
let page_no = page.trim().parse::<usize>().ok()?;
|
||||
@@ -117,7 +192,10 @@ fn read_page(path: &str, page: &str) -> Option<PageVectors> {
|
||||
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<PageVectors> {
|
||||
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<PendingText>,
|
||||
current: Option<PendingText>,
|
||||
font_order: Vec<u128>,
|
||||
want_images: bool,
|
||||
images: Vec<PdfImage>,
|
||||
}
|
||||
|
||||
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<PdfImage> = 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::<f64>() / n,
|
||||
starts.iter().map(|p| p[1]).sum::<f64>() / 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::<f64>() / 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
|
||||
}
|
||||
+106
-70
@@ -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<AtomicBool>,
|
||||
/// The selected PDF underlay's values while the contextual tab exists.
|
||||
underlay_ctx: Option<UnderlayContext>,
|
||||
/// 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<Element<'a, Message>>,
|
||||
) -> 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<Panel<'_>> = 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<Panel<'_>> = 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<Element<'a, Message>>,
|
||||
active_tool: &Option<String>,
|
||||
open_dd: &Option<String>,
|
||||
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<Element<Message>> = Vec::new();
|
||||
let mut items_row: Vec<Element<Message>> = lead;
|
||||
let mut small_buf: Vec<Element<Message>> = 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()
|
||||
}
|
||||
@@ -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<Vec<RibbonGroup>> = 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<UnderlayContext>) {
|
||||
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<Panel<'a>> {
|
||||
let Some(ctx) = self.underlay_ctx.as_ref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
groups()
|
||||
.iter()
|
||||
.map(|g| {
|
||||
let lead = |_: bool| -> Vec<Element<'a, Message>> {
|
||||
if g.title == ADJUST {
|
||||
vec![adjust_rows(ctx)]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
self.panel(g, ts, style_ctx, &lead)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -158,7 +158,7 @@ impl BlockDefinitionState {
|
||||
}
|
||||
}
|
||||
|
||||
fn group<'a>(
|
||||
pub(crate) fn group<'a>(
|
||||
title: impl Into<String>,
|
||||
body: impl Into<Element<'a, Message>>,
|
||||
height: impl Into<Length>,
|
||||
@@ -188,7 +188,7 @@ fn group<'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
fn labeled_checkbox<'a>(
|
||||
pub(crate) fn labeled_checkbox<'a>(
|
||||
label: impl Into<String>,
|
||||
is_checked: bool,
|
||||
on_toggle: impl Fn(bool) -> Message + 'a,
|
||||
|
||||
@@ -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;
|
||||
@@ -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<image::Handle>,
|
||||
}
|
||||
|
||||
/// 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<PageThumb> {
|
||||
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<usize>, 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::<Vec<_>>())
|
||||
.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<Option<AttachMemory>> = 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<String>,
|
||||
pub pages: Vec<PageThumb>,
|
||||
pub selected: Vec<usize>,
|
||||
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<String> {
|
||||
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<String>,
|
||||
pub hidden: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct UnderlayLayersState {
|
||||
pub targets: Vec<LayerTarget>,
|
||||
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<String> = 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::<Vec<_>>()))
|
||||
.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<PageThumb>,
|
||||
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()
|
||||
}
|
||||
Reference in new issue
Block a user