forked from 1b-agent/opencadstudio-upstream
Xref attach: Attach dialog, reference prompts, and xref drawings other applications can open
Attach External Reference dialog after XATTACH / ATTACH; ATTACH routes drawings, PDFs and images; placement prompts and messages follow the reference (presets, Corner/XYZ, nonzero check, existing definition reuse, missing file, self-reference); name keeps the file's case; overlay and path type at attach; one undo step for definition and INSERT; -XREF option prompt and `?` table; XREF opens the palette; ER alias; xref Properties rows; dependent symbols flagged and remapped and resolved content left out of the saved file; dialog_ok automation action; localized labels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f2d534bf71
commit
6bd55bc32e
44 files changed
+2514
-359
No files matched your search
@@ -43,6 +43,7 @@ SS, *STRETCH
|
||||
S, *STRETCH
|
||||
O, *OFFSET
|
||||
TR, *TRIM
|
||||
ER, *EXTERNALREFERENCES
|
||||
EX, *EXTEND
|
||||
F, *FILLET
|
||||
CHA, *CHAMFER
|
||||
|
||||
@@ -3469,6 +3469,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE حدد نقطة الإدراج:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds =
|
||||
سحب
|
||||
النقاط
|
||||
|
||||
@@ -3456,6 +3456,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE Посочи точка на вмъкване:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds =
|
||||
Облаци
|
||||
от точки
|
||||
|
||||
@@ -3454,6 +3454,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE Zadejte bod vložení:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds =
|
||||
Bod
|
||||
Mraky
|
||||
|
||||
@@ -3457,6 +3457,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE Einführstelle angeben:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds =
|
||||
Punkt-
|
||||
wolken
|
||||
|
||||
@@ -3457,6 +3457,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE Ορίστε σημείο εισαγωγής:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.revolve-creation-mode-solid-surface = REVOLVE Τρόπος δημιουργίας [Solid/Surface]
|
||||
.revolve-enter-angle-expression = REVOLVE Δώστε έκφραση γωνίας:
|
||||
.revolve-select-line-ray-or-construction-line-for-axis = REVOLVE Επιλέξτε γραμμή, ημιευθεία ή βοηθητική ευθεία ως άξονα:
|
||||
|
||||
@@ -3458,6 +3458,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE Specify insertion point:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds =
|
||||
Point
|
||||
Clouds
|
||||
|
||||
@@ -3457,6 +3457,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE Especifique el punto de inserción:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds =
|
||||
Nubes de
|
||||
puntos
|
||||
|
||||
@@ -3456,6 +3456,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE Määritä lisäyskohta:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds =
|
||||
Point
|
||||
Pilviä
|
||||
|
||||
@@ -3457,6 +3457,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE Spécifier le point d'insertion:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds =
|
||||
Nuages de
|
||||
points
|
||||
|
||||
@@ -3445,6 +3445,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE निर्दिष्ट सम्मिलन बिंदु:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds =
|
||||
बिंदु
|
||||
बादल
|
||||
|
||||
@@ -3456,6 +3456,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE Beszúrási pont megadása:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds =
|
||||
Pont
|
||||
Felhők
|
||||
|
||||
@@ -3456,6 +3456,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE Specificare il punto di inserimento:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds =
|
||||
Punto
|
||||
Nuvole
|
||||
|
||||
@@ -3447,6 +3447,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE 挿入点を指定:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds = 点群
|
||||
.attsync-block-name-to-sync = ATTSYNC 同期するブロック名:
|
||||
.attsync-no-block-named-arg = ATTSYNC: "__ocs_fmt_0__" というブロックはありません。
|
||||
|
||||
@@ -3456,6 +3456,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE 삽입점 지정:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds =
|
||||
포인트
|
||||
구름
|
||||
|
||||
@@ -3455,6 +3455,28 @@ annotate =
|
||||
.dimradius-select-arc-circle-or-polyline-arc = DIMRADIUS Selecteer een boog, cirkel of polylijnboog:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds =
|
||||
Punten-
|
||||
wolken
|
||||
|
||||
@@ -3456,6 +3456,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE Określ punkt wstawienia:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds =
|
||||
Punkt
|
||||
Chmury
|
||||
|
||||
@@ -3457,6 +3457,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE Especifique o ponto de inserção:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds =
|
||||
Nuvens de
|
||||
pontos
|
||||
|
||||
@@ -3458,6 +3458,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE Укажите точку вставки:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds =
|
||||
Облака
|
||||
точек
|
||||
|
||||
@@ -2004,7 +2004,7 @@ common =
|
||||
.fit-data-does-not-define-a-valid-control-curve = Fit data does not define a valid control curve.
|
||||
.fit-points-do-not-define-a-valid-spline = Fit points do not define a valid spline.
|
||||
.formula = Formula
|
||||
.full-path = Full path
|
||||
.full-path = Tam yol
|
||||
.fully-constrained-no-remaining-degrees-of-freedom = Fully constrained — no remaining degrees of freedom
|
||||
.grid-on-f7 = Grid On (F7)
|
||||
.grid-x-spacing = Grid X spacing:
|
||||
@@ -2059,7 +2059,7 @@ common =
|
||||
.named-parameters = Named Parameters
|
||||
.named-range = Named range
|
||||
.nearest-to-face = Nearest to face
|
||||
.no-path = No path
|
||||
.no-path = Yol yok
|
||||
.no-preview-available = No preview available.
|
||||
.no-surfaces-selected = No surfaces selected.
|
||||
.objectscale-the-selected-objects-already-have-the-current-scale-or-do-not-support-annotation-scales = OBJECTSCALE: the selected objects already have the current scale, or do not support annotation scales.
|
||||
@@ -2099,7 +2099,7 @@ common =
|
||||
.recent-colors = Recent Colors
|
||||
.reference-changes-are-not-available-on-web-the-reference-list-is-read-only = Reference changes are not available on web — the reference list is read-only.
|
||||
.refine-data = Refine data
|
||||
.relative-path = Relative path
|
||||
.relative-path = Göreli yol
|
||||
.remaining-degrees-of-freedom-in-the-current-parametric-constraint-scope = Remaining degrees of freedom in the current parametric constraint scope
|
||||
.requires-an-integer-between-0-and-99 = Requires an integer between 0 and 99.
|
||||
.requires-numeric-distance-or-two-points = Requires numeric distance or two points.
|
||||
@@ -3431,6 +3431,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE Ekleme noktasını belirtin:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Harici Referans Ekle
|
||||
.xref-attach-browse = Gözat...
|
||||
.xref-attach-reference-type = Referans Türü
|
||||
.xref-attach-specify-on-screen = Ekranda belirt
|
||||
.xref-attach-uniform-scale = Eşit ölçek
|
||||
.xref-attach-insertion-point = Ekleme noktası
|
||||
.xref-attach-angle = Açı:
|
||||
.xref-attach-block-unit = Blok birimi
|
||||
.xref-attach-unit = Birim:
|
||||
.xref-attach-factor = Çarpan:
|
||||
.xref-attach-found-in = Bulunduğu yer:
|
||||
.xref-attach-saved-path = Kayıtlı yol:
|
||||
.xref-attach-show-details = Ayrıntıları Göster
|
||||
.xref-attach-hide-details = Ayrıntıları Gizle
|
||||
.xref-attach-select-file = Referans Dosyası Seç
|
||||
.xref-attach-all-files = Tüm Referans Dosyaları
|
||||
.xref-attach-help = Diğer çizimler, raster görüntüler ve altlıklar gibi harici dosyalara referans ekler.
|
||||
.xref-attach-invalid-input = Geçersiz giriş.
|
||||
.xref-insertion-point-x = Ekleme noktası X
|
||||
.xref-insertion-point-y = Ekleme noktası Y
|
||||
.xref-insertion-point-z = Ekleme noktası Z
|
||||
.xref-layer-property-overrides = Katman özelliği geçersiz kılmaları
|
||||
.point-clouds =
|
||||
Nokta
|
||||
Bulutları
|
||||
|
||||
@@ -3444,6 +3444,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE 指定插入点:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds = 点云
|
||||
.attsync-block-name-to-sync = ATTSYNC 要同步的块名 :
|
||||
.attsync-no-block-named-arg = ATTSYNC:没有块名“__ocs_fmt_0__”。
|
||||
|
||||
@@ -3456,6 +3456,28 @@ annotate =
|
||||
.tolerance-specify-insertion-point = TOLERANCE 指定插入點:
|
||||
|
||||
insert =
|
||||
.xref-attach-dialog-title = Attach External Reference
|
||||
.xref-attach-browse = Browse...
|
||||
.xref-attach-reference-type = Reference Type
|
||||
.xref-attach-specify-on-screen = Specify On-screen
|
||||
.xref-attach-uniform-scale = Uniform Scale
|
||||
.xref-attach-insertion-point = Insertion point
|
||||
.xref-attach-angle = Angle:
|
||||
.xref-attach-block-unit = Block Unit
|
||||
.xref-attach-unit = Unit:
|
||||
.xref-attach-factor = Factor:
|
||||
.xref-attach-found-in = Found in:
|
||||
.xref-attach-saved-path = Saved path:
|
||||
.xref-attach-show-details = Show Details
|
||||
.xref-attach-hide-details = Hide Details
|
||||
.xref-attach-select-file = Select Reference File
|
||||
.xref-attach-all-files = All Reference Files
|
||||
.xref-attach-help = Inserts references to external files such as other drawings, raster images, and underlays.
|
||||
.xref-attach-invalid-input = Invalid input.
|
||||
.xref-insertion-point-x = Insertion point X
|
||||
.xref-insertion-point-y = Insertion point Y
|
||||
.xref-insertion-point-z = Insertion point Z
|
||||
.xref-layer-property-overrides = Layer property overrides
|
||||
.point-clouds = 點雲
|
||||
.attsync-block-name-to-sync = ATTSYNC 要同步的圖塊名 :
|
||||
.attsync-no-block-named-arg = ATTSYNC:沒有圖塊名“__ocs_fmt_0__”。
|
||||
|
||||
+3
-2
@@ -74,8 +74,8 @@ const WEB_ALIAS_KEY: &str = "opencadstudio.aliases";
|
||||
/// defaults add aliases or change a default target, so existing profiles can be
|
||||
/// migrated forward (see `introduced_at` / `migrate_aliases`). Version 1 shipped
|
||||
/// the pre-REDRAW table; version 2 introduced the REDRAW-family aliases; version 3
|
||||
/// introduced HB (HATCHTOBACK).
|
||||
const DEFAULT_ALIASES_VERSION: u32 = 3;
|
||||
/// introduced HB (HATCHTOBACK); version 4 introduced ER (EXTERNALREFERENCES).
|
||||
const DEFAULT_ALIASES_VERSION: u32 = 4;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
const WEB_ALIAS_VERSION_KEY: &str = "opencadstudio.aliases.version";
|
||||
@@ -93,6 +93,7 @@ fn introduced_at(version: u32) -> &'static [(&'static str, &'static str)] {
|
||||
("REA", "REGENALL"),
|
||||
],
|
||||
3 => &[("HB", "HATCHTOBACK")],
|
||||
4 => &[("ER", "EXTERNALREFERENCES")],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
+12
-55
@@ -3127,60 +3127,17 @@ impl OpenCADStudio {
|
||||
self.sync_dyn_fields();
|
||||
self.refresh_area_preview(i);
|
||||
}
|
||||
CmdResult::CommitAndExit(mut entity) => {
|
||||
// For XATTACH: ensure the xref block definition exists before
|
||||
// committing the INSERT entity that references it.
|
||||
// Extract path early to avoid borrow conflicts.
|
||||
let xattach_path: Option<String> = {
|
||||
let tab = &self.tabs[i];
|
||||
if let Some(cmd) = tab.active_cmd.as_ref() {
|
||||
if cmd.name() == "XATTACH" {
|
||||
cmd.xattach_path()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(path) = xattach_path {
|
||||
// XREF-Task6: the host drawing path lives in the tab
|
||||
// (`current_path`), not in `Scene`, so the self-attach
|
||||
// guard runs here via the pure `is_self_attach` helper.
|
||||
let host_file = self.tabs[i].current_path.clone();
|
||||
let host_base = host_file
|
||||
.as_deref()
|
||||
.and_then(|p| p.parent())
|
||||
.map(|p| p.to_path_buf());
|
||||
if let Some(host) = host_file.as_deref() {
|
||||
if crate::modules::insert::xattach::is_self_attach(
|
||||
host,
|
||||
&path,
|
||||
host_base.as_deref(),
|
||||
) {
|
||||
self.command_line.push_error(crate::t!("XATTACH: cannot attach the host drawing into itself.").as_ref());
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
let prepared_name = crate::modules::insert::xattach::prepare_xref_block(
|
||||
&mut self.tabs[i].scene,
|
||||
&path,
|
||||
host_base.as_deref(),
|
||||
);
|
||||
// `prepare_xref_block` may suffix a colliding stem. The
|
||||
// command was created before that collision was known, so
|
||||
// retarget its pending INSERT to the actual new block.
|
||||
if let acadrust::EntityType::Insert(insert) = &mut entity {
|
||||
insert.block_name = prepared_name;
|
||||
}
|
||||
// Resolving the xref merged its layer / linetype tables
|
||||
// into the document — mirror them into the Layers panel
|
||||
// and ribbon dropdowns now, not on the next reopen (#407).
|
||||
self.refresh_layer_panel();
|
||||
CmdResult::CommitAndExit(entity) => {
|
||||
// XATTACH: the definition and its INSERT are created together
|
||||
// in one undo step.
|
||||
let xattach_request = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.filter(|cmd| cmd.name() == "XATTACH")
|
||||
.and_then(|cmd| cmd.xattach_request());
|
||||
if let Some(request) = xattach_request {
|
||||
self.commit_xref_attach(i, request, entity);
|
||||
return Task::none();
|
||||
}
|
||||
let insert_block_name = match &entity {
|
||||
acadrust::EntityType::Insert(ins) => Some(ins.block_name.clone()),
|
||||
@@ -9274,7 +9231,7 @@ impl OpenCADStudio {
|
||||
out
|
||||
}
|
||||
|
||||
fn restore_pre_cmd_tangent(&mut self) {
|
||||
pub(super) fn restore_pre_cmd_tangent(&mut self) {
|
||||
if let Some(was_on) = self.pre_cmd_tangent.take() {
|
||||
if !was_on {
|
||||
self.snapper.enabled.remove(&crate::snap::SnapType::Tangent);
|
||||
|
||||
@@ -663,10 +663,6 @@ impl OpenCADStudio {
|
||||
"PDFATTACH" => {
|
||||
return Some(Task::done(Message::PdfAttachPick));
|
||||
}
|
||||
"XATTACH" => {
|
||||
// Launch the file picker; XAttachPickResult will start the command.
|
||||
return Some(Task::done(Message::XAttachPick));
|
||||
}
|
||||
cmd if cmd == "WBLOCK" || cmd == "WB" || cmd.starts_with("WBLOCK ") => {
|
||||
let arg = cmd.splitn(2, ' ').nth(1).unwrap_or("").trim();
|
||||
if arg.is_empty() {
|
||||
@@ -1366,11 +1362,16 @@ impl OpenCADStudio {
|
||||
if after.is_empty() {
|
||||
return Some(Task::done(Message::XAttachPick));
|
||||
}
|
||||
let cmd = crate::modules::insert::xattach::XAttachCommand::with_path(
|
||||
after.to_string(),
|
||||
self.start_xref_attach(
|
||||
i,
|
||||
crate::modules::insert::xattach::XrefAttachRequest {
|
||||
path: after.trim_matches('"').to_string(),
|
||||
overlay: false,
|
||||
path_type: crate::io::xref_model::Pathtype::Full,
|
||||
},
|
||||
crate::modules::insert::xattach::XrefPlacement::on_screen(),
|
||||
"-XREF",
|
||||
);
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
} else {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"XREF: unknown option '{}'. Options: ? Reload Unload Detach Path Pathtype Bind Overlay Attach",
|
||||
|
||||
@@ -5,6 +5,7 @@ use iced::Task;
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod blocks;
|
||||
mod xref_attach;
|
||||
mod dim;
|
||||
pub(crate) mod display;
|
||||
mod draw;
|
||||
@@ -246,6 +247,9 @@ impl OpenCADStudio {
|
||||
if let Some(t) = self.dispatch_layers(cmd, i) {
|
||||
return Some(t);
|
||||
}
|
||||
if let Some(t) = self.dispatch_xref_attach(cmd, i) {
|
||||
return Some(t);
|
||||
}
|
||||
if let Some(t) = self.dispatch_blocks(cmd, i) {
|
||||
return Some(t);
|
||||
}
|
||||
@@ -652,6 +656,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"3DORBIT",
|
||||
"3O",
|
||||
"ABOUT",
|
||||
"ATTACH",
|
||||
"ATTDISP",
|
||||
"ATTEXT",
|
||||
"BACKGROUND",
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
//! Attaching drawing references: ATTACH / XATTACH (file picker, then the
|
||||
//! Attach External Reference dialog), the -XREF command line, the `?` list,
|
||||
//! and the commit that creates the definition and its INSERT as one undo step.
|
||||
|
||||
use super::*;
|
||||
use crate::io::xref_model::Pathtype;
|
||||
use crate::modules::insert::xattach::{
|
||||
existing_reference, path_to_block_name, prepare_xref_definition, stored_path,
|
||||
XAttachCommand, XrefAttachRequest, XrefPlacement,
|
||||
};
|
||||
use crate::modules::insert::xref_cmd::display_path;
|
||||
use crate::ui::window::xref_attach::{XrefAttachMsg, XrefAttachState};
|
||||
|
||||
impl OpenCADStudio {
|
||||
/// Commands owned here; `None` defers to the other families.
|
||||
pub(super) fn dispatch_xref_attach(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||
let upper = cmd.to_ascii_uppercase();
|
||||
let (verb, rest) = match cmd.split_once(char::is_whitespace) {
|
||||
Some((verb, rest)) => (verb.to_ascii_uppercase(), rest.trim()),
|
||||
None => (upper.clone(), ""),
|
||||
};
|
||||
match (verb.as_str(), rest.is_empty()) {
|
||||
("ATTACH", true) => Some(Task::done(Message::AttachPick)),
|
||||
("XATTACH", true) => Some(Task::done(Message::XAttachPick)),
|
||||
// A file named on the command line skips the picker.
|
||||
("ATTACH", false) => Some(Task::done(Message::AttachPickResult(Ok(
|
||||
std::path::PathBuf::from(rest.trim_matches('"')),
|
||||
)))),
|
||||
("XATTACH", false) => {
|
||||
self.open_xref_attach_dialog(std::path::PathBuf::from(rest.trim_matches('"')));
|
||||
Some(Task::none())
|
||||
}
|
||||
// XREF opens the External References palette; -XREF asks on the
|
||||
// command line. Both keep their argument forms for scripts.
|
||||
("XREF", true) | ("XREFSHOW", true) => {
|
||||
if !self.show_external_references {
|
||||
return self.dispatch_blocks("EXTERNALREFERENCES", i);
|
||||
}
|
||||
self.refresh_xref_manager();
|
||||
Some(self.finish_dispatch(cmd))
|
||||
}
|
||||
("-XREF", true) => {
|
||||
let saved: Vec<(String, String)> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.iter()
|
||||
.filter(|br| br.flags.is_xref || br.flags.is_xref_overlay)
|
||||
.map(|br| (br.name.clone(), br.xref_path.clone()))
|
||||
.collect();
|
||||
let command = crate::modules::insert::xref_cmd::XrefCommand::new(saved);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
Some(Task::none())
|
||||
}
|
||||
("XREFLIST", _) => {
|
||||
self.list_xrefs(i, if rest.is_empty() { "*" } else { rest });
|
||||
Some(self.finish_dispatch(cmd))
|
||||
}
|
||||
("XREFATTACHFILE", false) | ("XREFOVERLAYFILE", false) => {
|
||||
let request = XrefAttachRequest {
|
||||
path: rest.trim_matches('"').to_string(),
|
||||
overlay: verb == "XREFOVERLAYFILE",
|
||||
// The reference default (REFPATHTYPE 1): relative once the
|
||||
// drawing is saved, the full path before that.
|
||||
path_type: Pathtype::Relative,
|
||||
};
|
||||
self.start_xref_attach(i, request, XrefPlacement::on_screen(), "-XREF");
|
||||
Some(Task::none())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `-XREF ?` — the reference table as the reference application prints it.
|
||||
fn list_xrefs(&mut self, i: usize, pattern: &str) {
|
||||
let rows: Vec<(String, &'static str, String)> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.iter()
|
||||
.filter(|br| br.flags.is_xref || br.flags.is_xref_overlay)
|
||||
.filter(|br| {
|
||||
pattern == "*" || crate::io::xref_model::wildcard_match(&br.name, pattern)
|
||||
})
|
||||
.map(|br| {
|
||||
let kind = if br.flags.is_xref_overlay { "Overlay" } else { "Attach" };
|
||||
(br.name.clone(), kind, display_path(&br.xref_path))
|
||||
})
|
||||
.collect();
|
||||
let out = &mut self.command_line;
|
||||
out.push_output("");
|
||||
out.push_output(&format!(
|
||||
"{:<34}{:<13}{}",
|
||||
"Xref name",
|
||||
"Xref Type",
|
||||
"Path"
|
||||
));
|
||||
out.push_output(&format!("{:<34}{:<13}{}", "----------------------", "---------", "----------"));
|
||||
out.push_output("");
|
||||
for (name, kind, path) in &rows {
|
||||
out.push_output(&format!("{:<34}{:<13}{}", format!("\"{name}\""), *kind, path));
|
||||
}
|
||||
out.push_output("");
|
||||
out.push_output(&format!("Total Xref(s): {}", rows.len()));
|
||||
}
|
||||
|
||||
/// Checks the file, reports it the way the reference does, then asks
|
||||
/// for whatever is left on-screen (or commits straight away).
|
||||
pub(in crate::app) fn start_xref_attach(
|
||||
&mut self,
|
||||
i: usize,
|
||||
request: XrefAttachRequest,
|
||||
placement: XrefPlacement,
|
||||
label: &'static str,
|
||||
) {
|
||||
let name = path_to_block_name(&request.path);
|
||||
let full = crate::modules::insert::xattach::resolve_xref_store_path(&request.path, None);
|
||||
let host = self.tabs[i].current_path.clone();
|
||||
if let Some(host) = host.as_deref() {
|
||||
if crate::modules::insert::xattach::is_self_attach(host, &full, host.parent()) {
|
||||
self.command_line.push_error(
|
||||
"Error: Possible circular reference to current drawing. *Invalid*",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Some(existing) = existing_reference(&self.tabs[i].scene, &name) {
|
||||
self.command_line
|
||||
.push_output(&format!("Xref \"{}\" has already been defined.", existing));
|
||||
self.command_line
|
||||
.push_output("Using existing definition.");
|
||||
} else {
|
||||
let verb = if request.overlay {
|
||||
format!("Overlay Xref \"{}\": {}", name, display_path(&full))
|
||||
} else {
|
||||
format!("Attach Xref \"{}\": {}", name, display_path(&full))
|
||||
};
|
||||
self.command_line.push_output(&verb);
|
||||
if !std::path::Path::new(&full).is_file() {
|
||||
let file = std::path::Path::new(&full.replace('\\', "/"))
|
||||
.file_name()
|
||||
.map(|f| f.to_string_lossy().into_owned())
|
||||
.unwrap_or(full.clone());
|
||||
self.command_line
|
||||
.push_output(&format!("\"{}\" cannot be found.", file));
|
||||
self.command_line.push_error("*Invalid*");
|
||||
return;
|
||||
}
|
||||
self.command_line
|
||||
.push_output(&format!("\"{}\" loaded.", name));
|
||||
}
|
||||
let command = XAttachCommand::new(request.clone(), placement, label);
|
||||
if let Some(entity) = command.immediate() {
|
||||
self.commit_xref_attach(i, request, entity);
|
||||
return;
|
||||
}
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
|
||||
/// Creates (or reuses) the definition and commits its INSERT — one undo
|
||||
/// step for both, so undo leaves no definition behind.
|
||||
pub(in crate::app) fn commit_xref_attach(
|
||||
&mut self,
|
||||
i: usize,
|
||||
request: XrefAttachRequest,
|
||||
mut entity: acadrust::EntityType,
|
||||
) {
|
||||
let pending = self.begin_undo(i, "XATTACH", 1, false);
|
||||
let host = self.tabs[i].current_path.clone();
|
||||
let name = prepare_xref_definition(&mut self.tabs[i].scene, &request, host.as_deref());
|
||||
if let acadrust::EntityType::Insert(insert) = &mut entity {
|
||||
insert.block_name = name;
|
||||
}
|
||||
// Resolving merged the reference's layers and linetypes — mirror
|
||||
// them into the Layers panel and ribbon dropdowns now (#407).
|
||||
self.refresh_layer_panel();
|
||||
let _ = self.commit_entity_handle(entity);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
self.refresh_xref_manager();
|
||||
}
|
||||
|
||||
/// Opens the Attach External Reference dialog for `path`.
|
||||
pub(in crate::app) fn open_xref_attach_dialog(&mut self, path: std::path::PathBuf) {
|
||||
let i = self.active_tab;
|
||||
let existing: Vec<(String, String)> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.iter()
|
||||
.filter(|br| br.flags.is_xref || br.flags.is_xref_overlay)
|
||||
.map(|br| (br.name.clone(), br.xref_path.clone()))
|
||||
.collect();
|
||||
let mut state = XrefAttachState::new(existing);
|
||||
self.load_xref_attach_file(&mut state, &path.to_string_lossy());
|
||||
self.xref_attach = Some(state);
|
||||
self.active_modal = Some(crate::app::ModalKind::XrefAttach);
|
||||
}
|
||||
|
||||
/// Fills the dialog from the chosen file: name, preview, block unit and
|
||||
/// the paths shown under Details.
|
||||
fn load_xref_attach_file(&self, state: &mut XrefAttachState, path: &str) {
|
||||
let i = self.active_tab;
|
||||
state.path = path.to_string();
|
||||
state.name = path_to_block_name(path);
|
||||
state.found_in = display_path(path);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
state.preview = crate::io::thumbnail::read_handle(std::path::Path::new(path));
|
||||
let host_units = self.tabs[i].scene.document.header.insertion_units;
|
||||
let source_units = crate::io::load_file(std::path::Path::new(path))
|
||||
.map(|doc| doc.header.insertion_units)
|
||||
.unwrap_or(0);
|
||||
state.unit_label = crate::t!(crate::app::properties::insunits_name(source_units))
|
||||
.into_owned();
|
||||
let factor =
|
||||
crate::app::properties::insert_unit_scale(host_units, source_units).unwrap_or(1.0);
|
||||
state.unit_factor = crate::app::properties::format_unit_factor(factor);
|
||||
}
|
||||
self.refresh_xref_attach_saved_path(state);
|
||||
}
|
||||
|
||||
fn refresh_xref_attach_saved_path(&self, state: &mut XrefAttachState) {
|
||||
let host = self.tabs[self.active_tab].current_path.clone();
|
||||
let request = XrefAttachRequest {
|
||||
path: state.path.clone(),
|
||||
overlay: state.overlay,
|
||||
path_type: state.path_type.0,
|
||||
};
|
||||
state.saved_path = display_path(&stored_path(&request, host.as_deref()));
|
||||
}
|
||||
|
||||
/// Messages of the ATTACH picker and the dialog.
|
||||
pub(in crate::app) fn update_xref_attach(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::AttachPick => Task::perform(
|
||||
async {
|
||||
let handle = crate::sys::file_dialog()
|
||||
.set_title(crate::t!("Select Reference File").as_ref())
|
||||
.add_filter(
|
||||
crate::t!("All Reference Files").as_ref(),
|
||||
&[
|
||||
"dwg", "dxf", "pdf", "png", "jpg", "jpeg", "bmp", "tif", "tiff",
|
||||
],
|
||||
)
|
||||
.add_filter(crate::t!("Drawing").as_ref(), &["dwg", "dxf"])
|
||||
.add_filter(crate::t!("PDF Files").as_ref(), &["pdf"])
|
||||
.add_filter(
|
||||
crate::t!("Images").as_ref(),
|
||||
&["png", "jpg", "jpeg", "bmp", "tif", "tiff"],
|
||||
)
|
||||
.pick_file()
|
||||
.await;
|
||||
match handle {
|
||||
Some(h) => Ok(crate::sys::handle_path(&h)),
|
||||
None => Err("Cancelled".to_string()),
|
||||
}
|
||||
},
|
||||
Message::AttachPickResult,
|
||||
),
|
||||
Message::AttachPickResult(Ok(path)) => {
|
||||
let ext = path
|
||||
.extension()
|
||||
.map(|e| e.to_string_lossy().to_ascii_lowercase())
|
||||
.unwrap_or_default();
|
||||
match ext.as_str() {
|
||||
"pdf" => match std::fs::read(&path) {
|
||||
Ok(bytes) => self.update(Message::PdfAttachPickResult(Ok((
|
||||
path,
|
||||
std::sync::Arc::new(bytes),
|
||||
)))),
|
||||
Err(e) => self.update(Message::PdfAttachPickResult(Err(e.to_string()))),
|
||||
},
|
||||
"png" | "jpg" | "jpeg" | "bmp" | "tif" | "tiff" => {
|
||||
let result = image::image_dimensions(&path)
|
||||
.map(|(w, h)| (path.clone(), w, h))
|
||||
.map_err(|e| e.to_string());
|
||||
self.update(Message::ImagePickResult(result))
|
||||
}
|
||||
_ => {
|
||||
self.open_xref_attach_dialog(path);
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::AttachPickResult(Err(e)) => {
|
||||
if e != "Cancelled" {
|
||||
self.command_line
|
||||
.push_error(format!("ATTACH: {e}").as_ref());
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::XrefAttachBrowseResult(Ok(path)) => {
|
||||
if let Some(mut state) = self.xref_attach.take() {
|
||||
self.load_xref_attach_file(&mut state, &path.to_string_lossy());
|
||||
self.xref_attach = Some(state);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::XrefAttachBrowseResult(Err(_)) => Task::none(),
|
||||
Message::XrefAttach(edit) => self.edit_xref_attach(edit),
|
||||
_ => Task::none(),
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_xref_attach(&mut self, edit: XrefAttachMsg) -> Task<Message> {
|
||||
let Some(mut state) = self.xref_attach.take() else {
|
||||
return Task::none();
|
||||
};
|
||||
let mut task = Task::none();
|
||||
match edit {
|
||||
XrefAttachMsg::Name(name) => {
|
||||
if let Some((_, saved)) = state
|
||||
.existing
|
||||
.iter()
|
||||
.find(|(n, _)| n.eq_ignore_ascii_case(&name))
|
||||
.cloned()
|
||||
{
|
||||
let host = self.tabs[self.active_tab].current_path.clone();
|
||||
let base = host.as_deref().and_then(|h| h.parent());
|
||||
let full = crate::modules::insert::xattach::resolve_xref_store_path(&saved, base);
|
||||
self.load_xref_attach_file(&mut state, &full);
|
||||
}
|
||||
}
|
||||
XrefAttachMsg::Browse => {
|
||||
task = Task::perform(
|
||||
async {
|
||||
let handle = crate::sys::file_dialog()
|
||||
.set_title(crate::t!("Select Reference File").as_ref())
|
||||
.add_filter(crate::t!("Drawing").as_ref(), &["dwg", "dxf"])
|
||||
.pick_file()
|
||||
.await;
|
||||
match handle {
|
||||
Some(h) => Ok(crate::sys::handle_path(&h)),
|
||||
None => Err("Cancelled".to_string()),
|
||||
}
|
||||
},
|
||||
Message::XrefAttachBrowseResult,
|
||||
);
|
||||
}
|
||||
XrefAttachMsg::Overlay(overlay) => state.overlay = overlay,
|
||||
XrefAttachMsg::ScaleOnScreen(on) => state.scale_on_screen = on,
|
||||
XrefAttachMsg::Scale(axis, value) => {
|
||||
if axis == 0 && state.uniform {
|
||||
state.scale = [value.clone(), value.clone(), value];
|
||||
} else if let Some(slot) = state.scale.get_mut(axis) {
|
||||
*slot = value;
|
||||
}
|
||||
}
|
||||
XrefAttachMsg::Uniform(on) => {
|
||||
state.uniform = on;
|
||||
if on {
|
||||
let x = state.scale[0].clone();
|
||||
state.scale = [x.clone(), x.clone(), x];
|
||||
}
|
||||
}
|
||||
XrefAttachMsg::InsertOnScreen(on) => state.insert_on_screen = on,
|
||||
XrefAttachMsg::Insert(axis, value) => {
|
||||
if let Some(slot) = state.insert.get_mut(axis) {
|
||||
*slot = value;
|
||||
}
|
||||
}
|
||||
XrefAttachMsg::PathType(choice) => {
|
||||
state.path_type = choice;
|
||||
self.refresh_xref_attach_saved_path(&mut state);
|
||||
}
|
||||
XrefAttachMsg::RotationOnScreen(on) => state.rotation_on_screen = on,
|
||||
XrefAttachMsg::Rotation(value) => state.rotation = value,
|
||||
XrefAttachMsg::Details(on) => state.details = on,
|
||||
XrefAttachMsg::Help => {
|
||||
self.command_line.push_info(
|
||||
crate::t!("Inserts references to external files such as other drawings, raster images, and underlays.")
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
XrefAttachMsg::Apply => {
|
||||
let placement = XrefPlacement {
|
||||
insert: (!state.insert_on_screen)
|
||||
.then(|| state.insert_point())
|
||||
.flatten(),
|
||||
scale: (!state.scale_on_screen).then(|| state.scale_values()).flatten(),
|
||||
rotation: (!state.rotation_on_screen)
|
||||
.then(|| crate::entities::common::parse_typed_angle(&state.rotation))
|
||||
.flatten()
|
||||
.map(f64::to_degrees),
|
||||
};
|
||||
let invalid = (!state.insert_on_screen && placement.insert.is_none())
|
||||
|| (!state.scale_on_screen
|
||||
&& placement
|
||||
.scale
|
||||
.is_none_or(|s| s.iter().any(|v| *v == 0.0)))
|
||||
|| (!state.rotation_on_screen && placement.rotation.is_none());
|
||||
if invalid || state.path.is_empty() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("Invalid input.").as_ref());
|
||||
self.xref_attach = Some(state);
|
||||
return Task::none();
|
||||
}
|
||||
let request = XrefAttachRequest {
|
||||
path: state.path.clone(),
|
||||
overlay: state.overlay,
|
||||
path_type: state.path_type.0,
|
||||
};
|
||||
self.active_modal = None;
|
||||
let i = self.active_tab;
|
||||
self.start_xref_attach(i, request, placement, "XATTACH");
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
self.xref_attach = Some(state);
|
||||
task
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ fn color_value(color: acadrust::types::Color) -> Value {
|
||||
}
|
||||
pub(super) const NAMES: &[&str] = &[
|
||||
"close_modal",
|
||||
"dialog_ok",
|
||||
"close_document",
|
||||
"toggle_properties",
|
||||
"toggle_layers",
|
||||
@@ -255,6 +256,14 @@ impl OpenCADStudio {
|
||||
let name = string(req, "name")?;
|
||||
let msg = match name {
|
||||
"close_modal" => Message::CloseModal,
|
||||
// The open dialog's OK button.
|
||||
"dialog_ok" => match self.active_modal {
|
||||
Some(crate::app::ModalKind::XrefAttach) => Message::XrefAttach(
|
||||
crate::ui::window::xref_attach::XrefAttachMsg::Apply,
|
||||
),
|
||||
Some(crate::app::ModalKind::BlockDefinition) => Message::BlockDefApply,
|
||||
_ => return Err(failure("no_dialog", "No dialog with an OK button is open")),
|
||||
},
|
||||
"close_document" => Message::TabClose(self.active_tab),
|
||||
"toggle_properties" => Message::ToggleProperties,
|
||||
"toggle_layers" => Message::ToggleLayers,
|
||||
|
||||
@@ -493,6 +493,8 @@ 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>,
|
||||
/// Working copy of the Attach External Reference dialog; None while closed.
|
||||
xref_attach: Option<crate::ui::window::xref_attach::XrefAttachState>,
|
||||
/// 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 +1873,7 @@ pub enum ModalKind {
|
||||
LayerTranslator,
|
||||
DrawingUnits,
|
||||
BlockDefinition,
|
||||
XrefAttach,
|
||||
GeometricTolerance,
|
||||
DraftingSettings,
|
||||
AutoConstrainSettings,
|
||||
@@ -3779,6 +3782,14 @@ pub enum Message {
|
||||
XAttachPick,
|
||||
/// Result of the XATTACH file picker.
|
||||
XAttachPickResult(Result<std::path::PathBuf, String>),
|
||||
/// ATTACH: pick a drawing, image or PDF to reference.
|
||||
AttachPick,
|
||||
/// Result of the ATTACH file picker.
|
||||
AttachPickResult(Result<std::path::PathBuf, String>),
|
||||
/// An edit in the Attach External Reference dialog.
|
||||
XrefAttach(crate::ui::window::xref_attach::XrefAttachMsg),
|
||||
/// Result of the dialog's Browse picker.
|
||||
XrefAttachBrowseResult(Result<std::path::PathBuf, String>),
|
||||
// ── WBLOCK ────────────────────────────────────────────────────────────
|
||||
/// Trigger the WBLOCK save dialog for `block_name` (or `*` = selection).
|
||||
WblockSave(String),
|
||||
@@ -3941,6 +3952,7 @@ impl OpenCADStudio {
|
||||
layer_translator: None,
|
||||
drawing_units: None,
|
||||
block_definition: None,
|
||||
xref_attach: None,
|
||||
geometric_tolerance: None,
|
||||
pick_drag_rect: false,
|
||||
perf_hud: false,
|
||||
|
||||
+85
-3
@@ -1478,6 +1478,25 @@ impl OpenCADStudio {
|
||||
},
|
||||
);
|
||||
}
|
||||
if let Some(br) = doc
|
||||
.block_records
|
||||
.get(&ins.block_name)
|
||||
.filter(|br| br.flags.is_xref || br.flags.is_xref_overlay)
|
||||
{
|
||||
let base_dir = self.tabs[i]
|
||||
.current_path
|
||||
.as_deref()
|
||||
.and_then(|p| p.parent())
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."));
|
||||
let overrides = crate::io::xref::layer_overrides(
|
||||
doc,
|
||||
&br.name,
|
||||
&br.xref_path,
|
||||
&base_dir,
|
||||
);
|
||||
xref_rows(&mut sections, ins, &br.xref_path, factor, overrides);
|
||||
}
|
||||
}
|
||||
// Underlay: name + path from the referenced definition.
|
||||
acadrust::EntityType::Underlay(ul) => {
|
||||
@@ -3702,6 +3721,69 @@ fn merge_prop_value(
|
||||
/// Set the first property row matching `field` (across all sections) to a
|
||||
/// read-only `value`. No-op when the field is absent. Used to fill the
|
||||
/// doc-dependent placeholder rows the entity builders leave empty.
|
||||
/// An external reference's rows as the reference application lists them:
|
||||
/// Insertion point X/Y/Z, Scale X/Y/Z without the unit conversion the INSERT
|
||||
/// carries (no uniform-scale or Annotative rows), Saved Path after the name
|
||||
/// and Layer property overrides last.
|
||||
fn xref_rows(
|
||||
sections: &mut [crate::scene::model::object::PropSection],
|
||||
ins: &acadrust::entities::Insert,
|
||||
saved_path: &str,
|
||||
factor: f64,
|
||||
overrides: bool,
|
||||
) {
|
||||
use crate::entities::common::{edit_prop, ro_prop};
|
||||
let factor = if factor.abs() > 0.0 { factor } else { 1.0 };
|
||||
for section in sections.iter_mut() {
|
||||
for prop in section.props.iter_mut() {
|
||||
let label = match prop.field {
|
||||
"ins_x" => "Insertion point X",
|
||||
"ins_y" => "Insertion point Y",
|
||||
"ins_z" => "Insertion point Z",
|
||||
_ => continue,
|
||||
};
|
||||
prop.label = t!(label).into_owned();
|
||||
}
|
||||
let scale_at = section.props.iter().position(|p| {
|
||||
matches!(p.field, "x_scale" | "y_scale" | "z_scale" | "u_scale" | "ins_uniform")
|
||||
});
|
||||
section.props.retain(|p| {
|
||||
!matches!(
|
||||
p.field,
|
||||
"x_scale" | "y_scale" | "z_scale" | "u_scale" | "ins_uniform" | "annotative"
|
||||
)
|
||||
});
|
||||
if let Some(at) = scale_at {
|
||||
let at = at.min(section.props.len());
|
||||
section.props.splice(
|
||||
at..at,
|
||||
[
|
||||
edit_prop(t!("Scale X").as_ref(), "xref_x_scale", ins.x_scale() / factor),
|
||||
edit_prop(t!("Scale Y").as_ref(), "xref_y_scale", ins.y_scale() / factor),
|
||||
edit_prop(t!("Scale Z").as_ref(), "xref_z_scale", ins.z_scale() / factor),
|
||||
],
|
||||
);
|
||||
}
|
||||
if let Some(at) = section.props.iter().position(|p| p.field == "block") {
|
||||
section.props.insert(
|
||||
at + 1,
|
||||
ro_prop(
|
||||
t!("Saved Path").as_ref(),
|
||||
"xref_saved_path",
|
||||
crate::modules::insert::xref_cmd::display_path(saved_path),
|
||||
),
|
||||
);
|
||||
}
|
||||
if section.props.iter().any(|p| p.field == "unit_factor") {
|
||||
section.props.push(ro_prop(
|
||||
t!("Layer property overrides").as_ref(),
|
||||
"xref_layer_overrides",
|
||||
if overrides { t!("Yes") } else { t!("No") }.into_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_row(sections: &mut [crate::scene::model::object::PropSection], field: &str, value: String) {
|
||||
for section in sections.iter_mut() {
|
||||
if let Some(row) = section.props.iter_mut().find(|p| p.field == field) {
|
||||
@@ -4092,7 +4174,7 @@ fn dim_lineweight_label(dimlwd: i16) -> String {
|
||||
}
|
||||
|
||||
/// Human-readable INSUNITS name (DXF group 70 unit codes).
|
||||
fn insunits_name(code: i16) -> &'static str {
|
||||
pub(in crate::app) fn insunits_name(code: i16) -> &'static str {
|
||||
match code {
|
||||
1 => "Inches",
|
||||
2 => "Feet",
|
||||
@@ -4124,7 +4206,7 @@ fn insunits_name(code: i16) -> &'static str {
|
||||
}
|
||||
|
||||
/// Unit-conversion scale for a new INSERT.
|
||||
fn insert_unit_scale(host_units: i16, src_units: i16) -> Option<f64> {
|
||||
pub(in crate::app) fn insert_unit_scale(host_units: i16, src_units: i16) -> Option<f64> {
|
||||
let host_mm = insunits_to_mm(host_units)?;
|
||||
let src_mm = insunits_to_mm(src_units)?;
|
||||
let ratio = src_mm / host_mm;
|
||||
@@ -4134,7 +4216,7 @@ fn insert_unit_scale(host_units: i16, src_units: i16) -> Option<f64> {
|
||||
Some(ratio)
|
||||
}
|
||||
|
||||
fn format_unit_factor(factor: f64) -> String {
|
||||
pub(in crate::app) fn format_unit_factor(factor: f64) -> String {
|
||||
let magnitude = factor.abs();
|
||||
if magnitude > 0.0 && !(1.0e-4..1.0e7).contains(&magnitude) {
|
||||
format!("{factor:.4e}")
|
||||
|
||||
@@ -3471,6 +3471,46 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(axis) = match field {
|
||||
"xref_x_scale" => Some(0),
|
||||
"xref_y_scale" => Some(1),
|
||||
"xref_z_scale" => Some(2),
|
||||
_ => None,
|
||||
} {
|
||||
// An xref's Scale rows show the scale without the
|
||||
// unit conversion its INSERT carries; write it back
|
||||
// with that conversion.
|
||||
let value = val
|
||||
.trim()
|
||||
.parse::<f64>()
|
||||
.ok()
|
||||
.filter(|v| v.is_finite() && *v != 0.0);
|
||||
if let Some(value) = value {
|
||||
let host = self.tabs[i].scene.document.header.insertion_units;
|
||||
for &handle in &handles {
|
||||
let factor = match self.tabs[i].scene.document.get_entity(handle) {
|
||||
Some(acadrust::EntityType::Insert(ins)) => self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get(&ins.block_name)
|
||||
.and_then(|br| {
|
||||
crate::app::properties::insert_unit_scale(host, br.units)
|
||||
})
|
||||
.unwrap_or(1.0),
|
||||
_ => continue,
|
||||
};
|
||||
if let Some(acadrust::EntityType::Insert(ins)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
match axis {
|
||||
0 => ins.set_x_scale(value * factor),
|
||||
1 => ins.set_y_scale(value * factor),
|
||||
_ => ins.set_z_scale(value * factor),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if field == "block" {
|
||||
// Name row on a block reference: an existing name
|
||||
// re-points the selected inserts; a new one renames
|
||||
|
||||
@@ -2105,7 +2105,8 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
let mut recent_task = Task::none();
|
||||
let saved = match crate::io::save_to_bytes(&self.tabs[i].scene.document, &ext, version) {
|
||||
let document = self.tabs[i].scene.document_for_save();
|
||||
let saved = match crate::io::save_to_bytes(&document, &ext, version) {
|
||||
Ok(bytes) => {
|
||||
crate::sys::download_bytes(&filename, &bytes);
|
||||
let cache_name = std::path::Path::new(&filename)
|
||||
|
||||
@@ -208,6 +208,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
Some(GeometricTolerance) => self.geometric_tolerance = None,
|
||||
Some(BlockDefinition) => self.block_definition = None,
|
||||
Some(XrefAttach) => self.xref_attach = None,
|
||||
Some(Hyperlink) => {
|
||||
self.hyperlink_editor_handles.clear();
|
||||
self.hyperlink_editor_url.clear();
|
||||
@@ -1278,15 +1279,13 @@ impl OpenCADStudio {
|
||||
),
|
||||
|
||||
Message::XAttachPickResult(Ok(path)) => {
|
||||
use crate::command::CadCommand;
|
||||
use crate::modules::insert::xattach::XAttachCommand;
|
||||
let path_str = path.to_string_lossy().into_owned();
|
||||
let cmd = XAttachCommand::with_path(path_str);
|
||||
let i = self.active_tab;
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
self.open_xref_attach_dialog(path);
|
||||
Task::none()
|
||||
}
|
||||
message @ (Message::AttachPick
|
||||
| Message::AttachPickResult(_)
|
||||
| Message::XrefAttach(_)
|
||||
| Message::XrefAttachBrowseResult(_)) => self.update_xref_attach(message),
|
||||
|
||||
Message::XAttachPickResult(Err(e)) => {
|
||||
if e != "Cancelled" {
|
||||
|
||||
@@ -24,6 +24,7 @@ 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::XrefAttach) => crate::t!("Attach External Reference").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 +507,13 @@ impl OpenCADStudio {
|
||||
crate::ui::window::block_definition::view_window(state, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::XrefAttach => {
|
||||
let state = self.xref_attach.as_ref()?;
|
||||
let height = if state.details { 520 } else { 450 };
|
||||
sized_flow(ex, 740, height, |flow| {
|
||||
crate::ui::window::xref_attach::view_window(state, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::GeometricTolerance => {
|
||||
let state = self.geometric_tolerance.as_ref()?;
|
||||
sized_flow(ex, 670, 530, |flow| {
|
||||
|
||||
+3
-3
@@ -2838,9 +2838,9 @@ pub trait CadCommand: Send {
|
||||
false
|
||||
}
|
||||
|
||||
/// If this command is XATTACH, returns the file path to attach.
|
||||
/// Default: None.
|
||||
fn xattach_path(&self) -> Option<String> {
|
||||
/// If this command is XATTACH, what it attaches and how the reference is
|
||||
/// stored. Default: None.
|
||||
fn xattach_request(&self) -> Option<crate::modules::insert::xattach::XrefAttachRequest> {
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
+245
-26
@@ -305,6 +305,82 @@ fn resolve_xrefs_with_filter(
|
||||
(result, dropped)
|
||||
}
|
||||
|
||||
/// Layer properties an override can change, as compared for the
|
||||
/// "Layer property overrides" row.
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct LayerLook {
|
||||
color: acadrust::types::Color,
|
||||
line_type: String,
|
||||
line_weight: acadrust::types::LineWeight,
|
||||
off: bool,
|
||||
frozen: bool,
|
||||
locked: bool,
|
||||
plottable: bool,
|
||||
}
|
||||
|
||||
impl LayerLook {
|
||||
fn of(layer: &acadrust::tables::Layer) -> Self {
|
||||
Self {
|
||||
color: layer.color.clone(),
|
||||
line_type: layer.line_type.to_uppercase(),
|
||||
line_weight: layer.line_weight,
|
||||
off: layer.flags.off,
|
||||
frozen: layer.flags.frozen,
|
||||
locked: layer.flags.locked,
|
||||
plottable: layer.is_plottable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The source file's layers by upper-case name, read once per file version.
|
||||
fn source_layers(path: &Path) -> Option<HashMap<String, LayerLook>> {
|
||||
type Cache = std::sync::Mutex<HashMap<PathBuf, (std::time::SystemTime, HashMap<String, LayerLook>)>>;
|
||||
static CACHE: std::sync::OnceLock<Cache> = std::sync::OnceLock::new();
|
||||
let modified = std::fs::metadata(path).and_then(|m| m.modified()).ok()?;
|
||||
let cache = CACHE.get_or_init(Default::default);
|
||||
if let Some((stamp, layers)) = cache.lock().ok()?.get(path) {
|
||||
if *stamp == modified {
|
||||
return Some(layers.clone());
|
||||
}
|
||||
}
|
||||
let doc = super::load_file(path).ok()?;
|
||||
let layers: HashMap<String, LayerLook> = doc
|
||||
.layers
|
||||
.iter()
|
||||
.map(|layer| (layer.name.to_uppercase(), LayerLook::of(layer)))
|
||||
.collect();
|
||||
cache
|
||||
.lock()
|
||||
.ok()?
|
||||
.insert(path.to_path_buf(), (modified, layers.clone()));
|
||||
Some(layers)
|
||||
}
|
||||
|
||||
/// Whether any of reference `name`'s layers was changed in the host — the
|
||||
/// Properties "Layer property overrides" row. Compares the host's dependent
|
||||
/// layers with the source file's own.
|
||||
pub fn layer_overrides(doc: &CadDocument, name: &str, raw_path: &str, base_dir: &Path) -> bool {
|
||||
let Some(source) = resolve_path(raw_path, base_dir).and_then(|p| source_layers(&p)) else {
|
||||
return false;
|
||||
};
|
||||
let prefix = format!("{}|", name.to_uppercase());
|
||||
doc.layers.iter().any(|layer| {
|
||||
let upper = layer.name.to_uppercase();
|
||||
let Some(own) = upper.strip_prefix(&prefix) else {
|
||||
return false;
|
||||
};
|
||||
let Some(original) = source.get(own) else {
|
||||
return false;
|
||||
};
|
||||
let mut here = LayerLook::of(layer);
|
||||
// Dependent linetypes carry the reference prefix.
|
||||
if let Some(stripped) = here.line_type.strip_prefix(&prefix) {
|
||||
here.line_type = stripped.to_string();
|
||||
}
|
||||
here != *original
|
||||
})
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
@@ -342,18 +418,44 @@ fn resolve_path(raw: &str, base_dir: &Path) -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Make sure `doc` has BLOCK + ENDBLK entities for `block_name`.
|
||||
/// These are required so renderers can find the block content.
|
||||
fn ensure_block_entities(doc: &mut CadDocument, block_name: &str) {
|
||||
let has_block = doc
|
||||
.entities()
|
||||
.any(|e| matches!(e, EntityType::Block(b) if b.name == block_name));
|
||||
if has_block {
|
||||
/// Give a block record its BLOCK / ENDBLK markers, linked through the
|
||||
/// record's marker handles. The DWG writer emits the markers the record
|
||||
/// points at; a record left pointing at a null handle is written with
|
||||
/// handle 0, which makes the whole drawing unreadable elsewhere.
|
||||
pub(crate) fn ensure_block_entities(doc: &mut CadDocument, block_name: &str) {
|
||||
let Some((owner, begin, end, path)) = doc.block_records.get(block_name).map(|br| {
|
||||
(
|
||||
br.handle,
|
||||
br.block_entity_handle,
|
||||
br.block_end_handle,
|
||||
br.xref_path.clone(),
|
||||
)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
if begin.is_null() || !matches!(doc.get_entity(begin), Some(EntityType::Block(_))) {
|
||||
let handle = doc.allocate_handle();
|
||||
let mut block = Block::new(block_name, Vector3::zero());
|
||||
if !path.is_empty() {
|
||||
block = block.with_xref_path(&path);
|
||||
}
|
||||
block.common.handle = handle;
|
||||
block.common.owner_handle = owner;
|
||||
let _ = doc.add_entity(EntityType::Block(block));
|
||||
if let Some(br) = doc.block_records.get_mut(block_name) {
|
||||
br.block_entity_handle = handle;
|
||||
}
|
||||
}
|
||||
if end.is_null() || !matches!(doc.get_entity(end), Some(EntityType::BlockEnd(_))) {
|
||||
let handle = doc.allocate_handle();
|
||||
let mut block_end = BlockEnd::new();
|
||||
block_end.common.handle = handle;
|
||||
block_end.common.owner_handle = owner;
|
||||
let _ = doc.add_entity(EntityType::BlockEnd(block_end));
|
||||
if let Some(br) = doc.block_records.get_mut(block_name) {
|
||||
br.block_end_handle = handle;
|
||||
}
|
||||
}
|
||||
let b = Block::new(block_name, Vector3::zero());
|
||||
let _ = doc.add_entity(EntityType::Block(b));
|
||||
let _ = doc.add_entity(EntityType::BlockEnd(BlockEnd::new()));
|
||||
}
|
||||
|
||||
/// Shared symbol-remap core for XREF load-merge and BIND (SPIKE3 REFACTOR
|
||||
@@ -471,6 +573,7 @@ struct XrefSymbolMaps {
|
||||
layers: HashMap<String, String>,
|
||||
linetypes: HashMap<String, String>,
|
||||
text_styles: HashMap<String, String>,
|
||||
text_style_handles: HashMap<Handle, Handle>,
|
||||
dim_styles: HashMap<String, String>,
|
||||
blocks: HashMap<String, String>,
|
||||
br_handles: HashMap<Handle, Handle>,
|
||||
@@ -493,16 +596,55 @@ fn import_xref_symbols(
|
||||
) -> XrefSymbolMaps {
|
||||
let mut maps = XrefSymbolMaps::default();
|
||||
|
||||
// A layer's plot style and material point at objects of the file it came
|
||||
// from; carried over verbatim they point at whatever the host keeps under
|
||||
// those handles. Take the host's defaults (its layer 0) instead.
|
||||
// ponytail: every imported layer gets the default plot style and material;
|
||||
// map them by name if a reference ever carries non-default ones.
|
||||
let (host_plotstyle, host_material) = doc
|
||||
.layers
|
||||
.get("0")
|
||||
.map(|layer| (layer.plotstyle_handle, layer.material))
|
||||
.unwrap_or((Handle::NULL, Handle::NULL));
|
||||
|
||||
for layer in xref_doc.layers.iter() {
|
||||
let old = layer.name.clone();
|
||||
let new = naming.name_for(SymbolKind::Layer, &old);
|
||||
let mut cloned = layer.clone();
|
||||
cloned.name = new.clone();
|
||||
cloned.plotstyle_handle = host_plotstyle;
|
||||
cloned.material = host_material;
|
||||
cloned.set_handle(doc.allocate_handle());
|
||||
doc.layers.add_or_replace(cloned);
|
||||
maps.layers.insert(old.to_uppercase(), new);
|
||||
}
|
||||
|
||||
for ts in xref_doc.text_styles.iter() {
|
||||
// A shape-file style has no name of its own; the host's style for the
|
||||
// same shape file serves the imported linetypes.
|
||||
if ts.is_shape_file {
|
||||
let same_file = doc
|
||||
.text_styles
|
||||
.iter()
|
||||
.find(|host| host.is_shape_file && host.font_file.eq_ignore_ascii_case(&ts.font_file))
|
||||
.map(|host| host.handle);
|
||||
if let Some(host_handle) = same_file {
|
||||
maps.text_style_handles.insert(ts.handle, host_handle);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let old = ts.name.clone();
|
||||
let new = naming.name_for(SymbolKind::TextStyle, &old);
|
||||
let mut cloned = ts.clone();
|
||||
cloned.name = new.clone();
|
||||
let new_handle = doc.allocate_handle();
|
||||
cloned.set_handle(new_handle);
|
||||
cloned.xref_dependent = false;
|
||||
doc.text_styles.add_or_replace(cloned);
|
||||
maps.text_style_handles.insert(ts.handle, new_handle);
|
||||
maps.text_styles.insert(old.to_uppercase(), new);
|
||||
}
|
||||
|
||||
for lt in xref_doc.line_types.iter() {
|
||||
let old = lt.name.clone();
|
||||
if is_sentinel_linetype(&old) {
|
||||
@@ -511,22 +653,20 @@ fn import_xref_symbols(
|
||||
let new = naming.name_for(SymbolKind::Linetype, &old);
|
||||
let mut cloned = lt.clone();
|
||||
cloned.name = new.clone();
|
||||
// Shape and text segments name their style by handle: point them at
|
||||
// the imported copy, not at the source file's record.
|
||||
for element in &mut cloned.elements {
|
||||
if let Some(complex) = element.complex.as_mut() {
|
||||
if let Some(new_style) = maps.text_style_handles.get(&complex.style_handle) {
|
||||
complex.style_handle = *new_style;
|
||||
}
|
||||
}
|
||||
}
|
||||
cloned.set_handle(doc.allocate_handle());
|
||||
doc.line_types.add_or_replace(cloned);
|
||||
maps.linetypes.insert(old.to_uppercase(), new);
|
||||
}
|
||||
|
||||
for ts in xref_doc.text_styles.iter() {
|
||||
let old = ts.name.clone();
|
||||
let new = naming.name_for(SymbolKind::TextStyle, &old);
|
||||
let mut cloned = ts.clone();
|
||||
cloned.name = new.clone();
|
||||
cloned.set_handle(doc.allocate_handle());
|
||||
cloned.xref_dependent = false;
|
||||
doc.text_styles.add_or_replace(cloned);
|
||||
maps.text_styles.insert(old.to_uppercase(), new);
|
||||
}
|
||||
|
||||
for ds in xref_doc.dim_styles.iter() {
|
||||
let old = ds.name.clone();
|
||||
let new = naming.name_for(SymbolKind::DimStyle, &old);
|
||||
@@ -798,6 +938,21 @@ fn merge_xref_into_block(
|
||||
prefix: prefix.to_string(),
|
||||
};
|
||||
let maps = import_xref_symbols(doc, &xref_doc, &mut naming);
|
||||
// Dependent layers and linetypes carry the xref flag and their owning
|
||||
// reference, the way the file stores them; without it the host writes
|
||||
// plain records with a "|" in the name, which other readers reject.
|
||||
for name in maps.layers.values() {
|
||||
if let Some(layer) = doc.layers.get_mut(name) {
|
||||
layer.flags.xref_dependent = true;
|
||||
layer.xref_block_record_handle = br_handle;
|
||||
}
|
||||
}
|
||||
for name in maps.linetypes.values() {
|
||||
if let Some(linetype) = doc.line_types.get_mut(name) {
|
||||
linetype.xref_dependent = true;
|
||||
linetype.xref_block_record_handle = br_handle;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Entities (shared helper) ────────────────────────────────────────
|
||||
// The unremapped-handle count is ignored on the load path (BIND reports
|
||||
@@ -1709,7 +1864,54 @@ fn same_file(a: &Path, b: &Path) -> bool {
|
||||
/// Shared by Detach (which then also erases the BlockRecord itself) and BIND
|
||||
/// (which drops the previous merge before re-merging under `$N$` names). The
|
||||
/// xref BlockRecord itself, its INSERTs, and non-prefixed symbols stay.
|
||||
/// Drop from a save copy what resolving each xref merged into the host but
|
||||
/// the file never stores: the reference's own geometry, its nested block
|
||||
/// definitions and its text and dimension styles. Dependent layers and
|
||||
/// linetypes stay (flagged), so layer overrides kept under VISRETAIN persist.
|
||||
pub fn strip_resolved_xref_content(doc: &mut CadDocument) {
|
||||
let xrefs: Vec<(String, Handle)> = doc
|
||||
.block_records
|
||||
.iter()
|
||||
.filter(|br| br.flags.is_xref || br.flags.is_xref_overlay)
|
||||
.map(|br| (br.name.clone(), br.handle))
|
||||
.collect();
|
||||
for (name, handle) in xrefs {
|
||||
let (begin, end) = doc
|
||||
.block_records
|
||||
.get(&name)
|
||||
.map(|br| (br.block_entity_handle, br.block_end_handle))
|
||||
.unwrap_or((Handle::NULL, Handle::NULL));
|
||||
let owned: Vec<Handle> = doc
|
||||
.entities()
|
||||
.filter(|e| e.common().owner_handle == handle)
|
||||
.map(|e| e.common().handle)
|
||||
.filter(|h| *h != begin && *h != end)
|
||||
.collect();
|
||||
for h in owned {
|
||||
doc.remove_entity(h);
|
||||
}
|
||||
if let Some(br) = doc.block_records.get_mut(&name) {
|
||||
br.entity_handles.clear();
|
||||
}
|
||||
remove_dependent_symbols(doc, &name, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Text styles a kept linetype draws its shapes or text with.
|
||||
fn linetype_styles(doc: &CadDocument) -> HashSet<Handle> {
|
||||
doc.line_types
|
||||
.iter()
|
||||
.flat_map(|linetype| linetype.elements.iter())
|
||||
.filter_map(|element| element.complex.as_ref().map(|complex| complex.style_handle))
|
||||
.filter(|handle| !handle.is_null())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn remove_pipe_symbols(doc: &mut CadDocument, name: &str) {
|
||||
remove_dependent_symbols(doc, name, true);
|
||||
}
|
||||
|
||||
fn remove_dependent_symbols(doc: &mut CadDocument, name: &str, layers_too: bool) {
|
||||
let prefix = format!("{}|", name.to_uppercase());
|
||||
let is_dep = |n: &str| n.to_uppercase().starts_with(&prefix);
|
||||
// Dependent blocks first — their handles route the owned-entity cleanup.
|
||||
@@ -1746,7 +1948,7 @@ fn remove_pipe_symbols(doc: &mut CadDocument, name: &str) {
|
||||
let layers: Vec<String> = doc
|
||||
.layers
|
||||
.names()
|
||||
.filter(|n| is_dep(n))
|
||||
.filter(|n| layers_too && is_dep(n))
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
for n in layers {
|
||||
@@ -1755,21 +1957,38 @@ fn remove_pipe_symbols(doc: &mut CadDocument, name: &str) {
|
||||
let lts: Vec<String> = doc
|
||||
.line_types
|
||||
.names()
|
||||
.filter(|n| is_dep(n))
|
||||
.filter(|n| layers_too && is_dep(n))
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
for n in lts {
|
||||
doc.line_types.remove(&n);
|
||||
}
|
||||
// Styles a kept dependent linetype still draws with stay, flagged.
|
||||
let used = if layers_too {
|
||||
HashSet::default()
|
||||
} else {
|
||||
linetype_styles(doc)
|
||||
};
|
||||
let styles: Vec<String> = doc
|
||||
.text_styles
|
||||
.names()
|
||||
.filter(|n| is_dep(n))
|
||||
.map(|s| s.to_string())
|
||||
.iter()
|
||||
.filter(|style| is_dep(&style.name) && !used.contains(&style.handle))
|
||||
.map(|style| style.name.clone())
|
||||
.collect();
|
||||
for n in styles {
|
||||
doc.text_styles.remove(&n);
|
||||
}
|
||||
let owner = doc
|
||||
.block_records
|
||||
.get(name)
|
||||
.map(|br| br.handle)
|
||||
.unwrap_or(Handle::NULL);
|
||||
for style in doc.text_styles.iter_mut() {
|
||||
if is_dep(&style.name) {
|
||||
style.xref_dependent = true;
|
||||
style.xref_block_record_handle = owner;
|
||||
}
|
||||
}
|
||||
let dims: Vec<String> = doc
|
||||
.dim_styles
|
||||
.names()
|
||||
|
||||
@@ -265,6 +265,28 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"Added annotation scale {arg}." => Some(("view", "added-annotation-scale-arg")),
|
||||
"Added from GitHub" => Some(("plugins", "added-from-github")),
|
||||
"Adjust" => Some(("insert", "adjust")),
|
||||
"Attach External Reference" => Some(("insert", "xref-attach-dialog-title")),
|
||||
"Browse..." => Some(("insert", "xref-attach-browse")),
|
||||
"Reference Type" => Some(("insert", "xref-attach-reference-type")),
|
||||
"Specify On-screen" => Some(("insert", "xref-attach-specify-on-screen")),
|
||||
"Uniform Scale" => Some(("insert", "xref-attach-uniform-scale")),
|
||||
"Insertion point" => Some(("insert", "xref-attach-insertion-point")),
|
||||
"Angle:" => Some(("insert", "xref-attach-angle")),
|
||||
"Block Unit" => Some(("insert", "xref-attach-block-unit")),
|
||||
"Unit:" => Some(("insert", "xref-attach-unit")),
|
||||
"Factor:" => Some(("insert", "xref-attach-factor")),
|
||||
"Found in:" => Some(("insert", "xref-attach-found-in")),
|
||||
"Saved path:" => Some(("insert", "xref-attach-saved-path")),
|
||||
"Show Details" => Some(("insert", "xref-attach-show-details")),
|
||||
"Hide Details" => Some(("insert", "xref-attach-hide-details")),
|
||||
"Select Reference File" => Some(("insert", "xref-attach-select-file")),
|
||||
"All Reference Files" => Some(("insert", "xref-attach-all-files")),
|
||||
"Inserts references to external files such as other drawings, raster images, and underlays." => Some(("insert", "xref-attach-help")),
|
||||
"Invalid input." => Some(("insert", "xref-attach-invalid-input")),
|
||||
"Insertion point X" => Some(("insert", "xref-insertion-point-x")),
|
||||
"Insertion point Y" => Some(("insert", "xref-insertion-point-y")),
|
||||
"Insertion point Z" => Some(("insert", "xref-insertion-point-z")),
|
||||
"Layer property overrides" => Some(("insert", "xref-layer-property-overrides")),
|
||||
"Adjust Colors for Background" => Some(("properties", "adjust-colors-for-background")),
|
||||
"Advanced" => Some(("properties", "advanced")),
|
||||
"Alias" => Some(("command", "alias-title-case")),
|
||||
|
||||
@@ -23,6 +23,7 @@ mod underlay_layers;
|
||||
pub(crate) mod wblock;
|
||||
mod xadjust;
|
||||
pub(crate) mod xattach;
|
||||
pub(crate) mod xref_cmd;
|
||||
mod xclip;
|
||||
|
||||
use crate::modules::{CadModule, IconKind, RibbonGroup, RibbonItem};
|
||||
|
||||
+526
-249
@@ -1,23 +1,25 @@
|
||||
// XATTACH command — attach an external DWG/DXF file as an XREF block
|
||||
// and insert it at a picked point.
|
||||
// XATTACH placement — attach an external DWG/DXF file as an XREF block and
|
||||
// insert it.
|
||||
//
|
||||
// Workflow:
|
||||
// Step 1 (point pick): user clicks the insertion point.
|
||||
// Step 2 (text input): scale factor (default 1.0).
|
||||
// Step 3 (text input): rotation angle (default 0.0).
|
||||
// Result: BlockRecord + Block entities are created with is_xref=true,
|
||||
// then an INSERT entity is committed with the chosen scale/rotation.
|
||||
// The reference options (type, path type, fixed or on-screen scale,
|
||||
// insertion point and rotation) come from the Attach External Reference
|
||||
// dialog or from -XREF Attach / Overlay. Values left "on-screen" are asked
|
||||
// here with the reference prompts:
|
||||
// Specify insertion point or [Scale/X/Y/Z/Rotate/PScale/PX/PY/PZ/PRotate]:
|
||||
// Enter X scale factor, specify opposite corner, or [Corner/XYZ] <1>:
|
||||
// Enter Y scale factor <use X scale factor>:
|
||||
// Specify rotation angle <0>:
|
||||
// The definition is created when the INSERT is committed (see the command
|
||||
// driver), so one undo removes both.
|
||||
|
||||
use acadrust::entities::{Block, BlockEnd, Insert};
|
||||
use acadrust::entities::Insert;
|
||||
use acadrust::tables::block_record::{BlockFlags, BlockRecord};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, InputKind, WorkingPlane};
|
||||
use crate::io::xref_model::normalize_lexical;
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, WorkingPlane};
|
||||
use crate::io::xref_model::{normalize_lexical, Pathtype};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::scene::Scene;
|
||||
@@ -31,45 +33,210 @@ pub fn tool() -> ToolDef {
|
||||
}
|
||||
}
|
||||
|
||||
/// Multi-step placement state: insertion point, then scale, then rotation.
|
||||
/// Mirrors the INSERT command's scale/rotation value handling (single value
|
||||
/// typed after the point pick, bare Enter keeps the default).
|
||||
enum XAttachStep {
|
||||
Point,
|
||||
Scale { point: DVec3 },
|
||||
Rotate { point: DVec3, sx: f64, sy: f64, sz: f64 },
|
||||
/// What to attach and how the reference is stored.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct XrefAttachRequest {
|
||||
/// The drawing file as chosen (absolute).
|
||||
pub path: String,
|
||||
pub overlay: bool,
|
||||
pub path_type: Pathtype,
|
||||
}
|
||||
|
||||
pub struct XAttachCommand {
|
||||
path: String,
|
||||
block_name: String,
|
||||
plane: WorkingPlane,
|
||||
step: XAttachStep,
|
||||
/// Placement values fixed before the command starts; `None` = on-screen.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct XrefPlacement {
|
||||
pub insert: Option<DVec3>,
|
||||
pub scale: Option<[f64; 3]>,
|
||||
/// Degrees.
|
||||
pub rotation: Option<f64>,
|
||||
}
|
||||
|
||||
impl XAttachCommand {
|
||||
/// Create an XATTACH command with a path already filled in (from file-picker).
|
||||
pub fn with_path(path: String) -> Self {
|
||||
let block_name = path_to_block_name(&path);
|
||||
Self {
|
||||
path,
|
||||
block_name,
|
||||
plane: WorkingPlane::default(),
|
||||
step: XAttachStep::Point,
|
||||
impl XrefPlacement {
|
||||
/// Everything on-screen, as -XREF asks it.
|
||||
pub fn on_screen() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Preset {
|
||||
Scale,
|
||||
X,
|
||||
Y,
|
||||
Z,
|
||||
Rotate,
|
||||
PScale,
|
||||
PX,
|
||||
PY,
|
||||
PZ,
|
||||
PRotate,
|
||||
}
|
||||
|
||||
impl Preset {
|
||||
fn from_keyword(text: &str) -> Option<Self> {
|
||||
Some(match text {
|
||||
"S" | "SCALE" => Preset::Scale,
|
||||
"X" => Preset::X,
|
||||
"Y" => Preset::Y,
|
||||
"Z" => Preset::Z,
|
||||
"R" | "ROTATE" => Preset::Rotate,
|
||||
"PS" | "PSCALE" => Preset::PScale,
|
||||
"PX" => Preset::PX,
|
||||
"PY" => Preset::PY,
|
||||
"PZ" => Preset::PZ,
|
||||
"PR" | "PROTATE" => Preset::PRotate,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt(self) -> &'static str {
|
||||
match self {
|
||||
Preset::Scale => "Specify scale factor for XYZ axes:",
|
||||
Preset::X => "Specify X scale factor:",
|
||||
Preset::Y => "Specify Y scale factor:",
|
||||
Preset::Z => "Specify Z Scale factor:",
|
||||
Preset::Rotate => "Specify rotation angle:",
|
||||
Preset::PScale => "Specify preview scale factor for XYZ axes:",
|
||||
Preset::PX => "Specify preview X scale factor:",
|
||||
Preset::PY => "Specify preview Y scale factor:",
|
||||
Preset::PZ => "Specify preview Z scale factor:",
|
||||
Preset::PRotate => "Specify preview rotation angle:",
|
||||
}
|
||||
}
|
||||
|
||||
fn commit_insert(&self, point: DVec3, sx: f64, sy: f64, sz: f64, rotation: f64) -> EntityType {
|
||||
fn is_angle(self) -> bool {
|
||||
matches!(self, Preset::Rotate | Preset::PRotate)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum Step {
|
||||
Insert,
|
||||
Preset(Preset),
|
||||
XScale,
|
||||
Corner,
|
||||
YScale,
|
||||
XyzX,
|
||||
XyzY,
|
||||
XyzZ,
|
||||
Rotation,
|
||||
}
|
||||
|
||||
pub struct XAttachCommand {
|
||||
request: XrefAttachRequest,
|
||||
block_name: String,
|
||||
/// Command name shown before each prompt (ATTACH, XATTACH or -XREF).
|
||||
label: &'static str,
|
||||
plane: WorkingPlane,
|
||||
step: Step,
|
||||
point: Option<DVec3>,
|
||||
scale: [f64; 3],
|
||||
/// Scale fixed in the dialog or by the Scale/X/Y/Z options.
|
||||
scale_fixed: bool,
|
||||
/// Degrees.
|
||||
rotation: f64,
|
||||
rotation_fixed: bool,
|
||||
}
|
||||
|
||||
const NONZERO: &str = "Value must be nonzero.";
|
||||
|
||||
impl XAttachCommand {
|
||||
pub fn new(request: XrefAttachRequest, placement: XrefPlacement, label: &'static str) -> Self {
|
||||
let block_name = path_to_block_name(&request.path);
|
||||
let mut command = Self {
|
||||
request,
|
||||
block_name,
|
||||
label,
|
||||
plane: WorkingPlane::default(),
|
||||
step: Step::Insert,
|
||||
point: placement.insert,
|
||||
scale: placement.scale.unwrap_or([1.0; 3]),
|
||||
scale_fixed: placement.scale.is_some(),
|
||||
rotation: placement.rotation.unwrap_or(0.0),
|
||||
rotation_fixed: placement.rotation.is_some(),
|
||||
};
|
||||
if command.point.is_some() {
|
||||
command.step = command.next_step().unwrap_or(Step::Rotation);
|
||||
}
|
||||
command
|
||||
}
|
||||
|
||||
/// Attach with every value on-screen.
|
||||
#[cfg(test)]
|
||||
pub fn with_path(path: String) -> Self {
|
||||
Self::new(
|
||||
XrefAttachRequest {
|
||||
path,
|
||||
overlay: false,
|
||||
path_type: Pathtype::Full,
|
||||
},
|
||||
XrefPlacement::on_screen(),
|
||||
"XATTACH",
|
||||
)
|
||||
}
|
||||
|
||||
/// The finished INSERT when nothing is left to ask (all values fixed in
|
||||
/// the dialog).
|
||||
pub fn immediate(&self) -> Option<EntityType> {
|
||||
(self.point.is_some() && self.next_step().is_none()).then(|| self.commit_insert())
|
||||
}
|
||||
|
||||
/// The step after the insertion point, or `None` when it can commit.
|
||||
fn next_step(&self) -> Option<Step> {
|
||||
if !self.scale_fixed {
|
||||
Some(Step::XScale)
|
||||
} else if !self.rotation_fixed {
|
||||
Some(Step::Rotation)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) -> CmdResult {
|
||||
match self.next_step() {
|
||||
Some(step) => {
|
||||
self.step = step;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
None => CmdResult::CommitAndExit(self.commit_insert()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Scale settled at this step: the rotation (if asked) comes next.
|
||||
fn scale_done(&mut self) -> CmdResult {
|
||||
self.scale_fixed = true;
|
||||
self.advance()
|
||||
}
|
||||
|
||||
fn commit_insert(&self) -> EntityType {
|
||||
let point = self.point.unwrap_or(DVec3::ZERO);
|
||||
let mut ins = Insert::new(
|
||||
self.block_name.clone(),
|
||||
Vector3::new(point.x, point.y, point.z),
|
||||
);
|
||||
ins.set_x_scale(sx);
|
||||
ins.set_y_scale(sy);
|
||||
ins.set_z_scale(sz);
|
||||
ins.rotation = rotation;
|
||||
ins.set_x_scale(self.scale[0]);
|
||||
ins.set_y_scale(self.scale[1]);
|
||||
ins.set_z_scale(self.scale[2]);
|
||||
ins.rotation = self.rotation.to_radians();
|
||||
self.plane.place_entity(EntityType::Insert(ins))
|
||||
}
|
||||
|
||||
/// Corner pick at the X scale prompt: X and Y from the rectangle,
|
||||
/// Z follows X.
|
||||
fn corner(&mut self, pt: DVec3) -> CmdResult {
|
||||
let base = self.point.unwrap_or(DVec3::ZERO);
|
||||
let corner = self.plane.to_local(pt);
|
||||
let (sx, sy) = (corner.x - base.x, corner.y - base.y);
|
||||
if sx.abs() < 1e-12 || sy.abs() < 1e-12 {
|
||||
return CmdResult::ReportError(NONZERO.to_string());
|
||||
}
|
||||
self.scale = [sx, sy, sx.abs()];
|
||||
self.scale_done()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_number(text: &str) -> Option<f64> {
|
||||
text.trim().replace(',', ".").parse::<f64>().ok().filter(|v| v.is_finite())
|
||||
}
|
||||
|
||||
impl CadCommand for XAttachCommand {
|
||||
@@ -82,121 +249,228 @@ impl CadCommand for XAttachCommand {
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
XAttachStep::Point => t!(
|
||||
"XATTACH Specify insertion point for \"%{name}\":",
|
||||
name = self.block_name
|
||||
)
|
||||
.into_owned(),
|
||||
XAttachStep::Scale { .. } => {
|
||||
t!("XATTACH Specify scale factor <1.0> (X,Y,Z or pick Corner):").into_owned()
|
||||
let text = match self.step {
|
||||
Step::Insert => {
|
||||
"Specify insertion point or [Scale/X/Y/Z/Rotate/PScale/PX/PY/PZ/PRotate]:"
|
||||
}
|
||||
XAttachStep::Rotate { .. } => {
|
||||
t!("XATTACH Specify rotation angle <0>:").into_owned()
|
||||
Step::Preset(preset) => preset.prompt(),
|
||||
Step::XScale => {
|
||||
"Enter X scale factor, specify opposite corner, or [Corner/XYZ] <1>:"
|
||||
}
|
||||
Step::Corner => "Specify opposite corner:",
|
||||
Step::YScale => "Enter Y scale factor <use X scale factor>:",
|
||||
Step::XyzX => "Specify X scale factor or [Corner] <1>:",
|
||||
Step::XyzY => "Enter Y scale factor <use X scale factor>:",
|
||||
Step::XyzZ => "Specify Z scale factor or <use X scale factor>:",
|
||||
Step::Rotation => "Specify rotation angle <0>:",
|
||||
};
|
||||
format!("{} {}", self.label, text)
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
Step::Insert => vec![
|
||||
CmdOption::new("Scale", "S"),
|
||||
CmdOption::new("X", "X"),
|
||||
CmdOption::new("Y", "Y"),
|
||||
CmdOption::new("Z", "Z"),
|
||||
CmdOption::new("Rotate", "R"),
|
||||
CmdOption::new("PScale", "PS"),
|
||||
CmdOption::new("PX", "PX"),
|
||||
CmdOption::new("PY", "PY"),
|
||||
CmdOption::new("PZ", "PZ"),
|
||||
CmdOption::new("PRotate", "PR"),
|
||||
],
|
||||
Step::XScale => vec![CmdOption::new("Corner", "C"), CmdOption::new("XYZ", "XYZ")],
|
||||
Step::XyzX => vec![CmdOption::new("Corner", "C")],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
match self.step {
|
||||
XAttachStep::Point => InputKind::Point,
|
||||
XAttachStep::Scale { .. } | XAttachStep::Rotate { .. } => InputKind::SingleToken,
|
||||
Step::Insert | Step::Corner => InputKind::Point,
|
||||
_ => InputKind::SingleToken,
|
||||
}
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
self.step == Step::Insert
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
match self.step {
|
||||
XAttachStep::Point => {
|
||||
let point = self.plane.to_local(pt);
|
||||
self.step = XAttachStep::Scale { point };
|
||||
CmdResult::NeedPoint
|
||||
Step::Insert => {
|
||||
self.point = Some(self.plane.to_local(pt));
|
||||
self.advance()
|
||||
}
|
||||
XAttachStep::Scale { point } => {
|
||||
// Corner: derive X/Y scale from the picked opposite corner.
|
||||
let corner = self.plane.to_local(pt);
|
||||
let sx = (corner.x - point.x).abs();
|
||||
let sy = (corner.y - point.y).abs();
|
||||
self.step = XAttachStep::Rotate {
|
||||
point,
|
||||
sx: if sx < 1e-9 { 1.0 } else { sx },
|
||||
sy: if sy < 1e-9 { 1.0 } else { sy },
|
||||
sz: 1.0,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
XAttachStep::Rotate { point, sx, sy, sz } => {
|
||||
// Second point defines the rotation direction from the base.
|
||||
let base_world = self.plane.to_world(point);
|
||||
let rotation = self.plane.angle(base_world, pt).unwrap_or(0.0);
|
||||
CmdResult::CommitAndExit(self.commit_insert(point, sx, sy, sz, rotation))
|
||||
Step::XScale | Step::Corner | Step::XyzX => self.corner(pt),
|
||||
Step::Rotation => {
|
||||
let base = self.plane.to_world(self.point.unwrap_or(DVec3::ZERO));
|
||||
self.rotation = self.plane.angle(base, pt).unwrap_or(0.0).to_degrees();
|
||||
CmdResult::CommitAndExit(self.commit_insert())
|
||||
}
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let token = text.trim().to_uppercase();
|
||||
match self.step {
|
||||
XAttachStep::Point => None,
|
||||
XAttachStep::Scale { point } => {
|
||||
// Single value (uniform) or X/Y/Z triplet; invalid input is
|
||||
// consumed and re-prompts (mirrors INSERT's scale handling).
|
||||
if let Some((sx, sy, sz)) = parse_scale_spec(text) {
|
||||
self.step = XAttachStep::Rotate { point, sx, sy, sz };
|
||||
}
|
||||
Step::Insert => {
|
||||
let preset = Preset::from_keyword(&token)?;
|
||||
self.step = Step::Preset(preset);
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
XAttachStep::Rotate { point, sx, sy, sz } => {
|
||||
if let Some(rotation) = crate::entities::common::parse_typed_angle(text) {
|
||||
Some(CmdResult::CommitAndExit(
|
||||
self.commit_insert(point, sx, sy, sz, rotation),
|
||||
))
|
||||
Step::Preset(preset) => {
|
||||
let value = if preset.is_angle() {
|
||||
crate::entities::common::parse_typed_angle(text).map(f64::to_degrees)
|
||||
} else {
|
||||
Some(CmdResult::NeedPoint)
|
||||
parse_number(text)
|
||||
};
|
||||
let Some(value) = value else {
|
||||
return Some(CmdResult::NeedPoint);
|
||||
};
|
||||
if !preset.is_angle() && value == 0.0 {
|
||||
return Some(CmdResult::ReportError(NONZERO.to_string()));
|
||||
}
|
||||
match preset {
|
||||
Preset::Scale => {
|
||||
self.scale = [value; 3];
|
||||
self.scale_fixed = true;
|
||||
}
|
||||
Preset::X => {
|
||||
self.scale[0] = value;
|
||||
self.scale_fixed = true;
|
||||
}
|
||||
Preset::Y => {
|
||||
self.scale[1] = value;
|
||||
self.scale_fixed = true;
|
||||
}
|
||||
Preset::Z => {
|
||||
self.scale[2] = value;
|
||||
self.scale_fixed = true;
|
||||
}
|
||||
Preset::Rotate => {
|
||||
self.rotation = value;
|
||||
self.rotation_fixed = true;
|
||||
}
|
||||
// Preview-only values: the reference asks the real ones
|
||||
// after the insertion point as usual.
|
||||
Preset::PScale | Preset::PX | Preset::PY | Preset::PZ | Preset::PRotate => {}
|
||||
}
|
||||
self.step = Step::Insert;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::XScale | Step::XyzX if token == "C" || token == "CORNER" => {
|
||||
self.step = Step::Corner;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::XScale if token == "XYZ" => {
|
||||
self.step = Step::XyzX;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::XScale | Step::XyzX => {
|
||||
let value = parse_number(text)?;
|
||||
if value == 0.0 {
|
||||
return Some(CmdResult::ReportError(NONZERO.to_string()));
|
||||
}
|
||||
self.scale = [value, value, value.abs()];
|
||||
self.step = if self.step == Step::XScale {
|
||||
Step::YScale
|
||||
} else {
|
||||
Step::XyzY
|
||||
};
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::YScale | Step::XyzY => {
|
||||
let value = parse_number(text)?;
|
||||
if value == 0.0 {
|
||||
return Some(CmdResult::ReportError(NONZERO.to_string()));
|
||||
}
|
||||
self.scale[1] = value;
|
||||
if self.step == Step::XyzY {
|
||||
self.step = Step::XyzZ;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
Some(self.scale_done())
|
||||
}
|
||||
Step::XyzZ => {
|
||||
let value = parse_number(text)?;
|
||||
if value == 0.0 {
|
||||
return Some(CmdResult::ReportError(NONZERO.to_string()));
|
||||
}
|
||||
self.scale[2] = value;
|
||||
Some(self.scale_done())
|
||||
}
|
||||
Step::Rotation => {
|
||||
let rotation = crate::entities::common::parse_typed_angle(text)?;
|
||||
self.rotation = rotation.to_degrees();
|
||||
Some(CmdResult::CommitAndExit(self.commit_insert()))
|
||||
}
|
||||
Step::Corner => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.step {
|
||||
// Bare Enter at the insertion-point step cancels (unchanged).
|
||||
XAttachStep::Point => CmdResult::Cancel,
|
||||
// Bare Enter accepts the default scale / rotation.
|
||||
XAttachStep::Scale { point } => {
|
||||
self.step = XAttachStep::Rotate {
|
||||
point,
|
||||
sx: 1.0,
|
||||
sy: 1.0,
|
||||
sz: 1.0,
|
||||
Step::Insert => {
|
||||
CmdResult::ReportError("Point or option keyword required.".to_string())
|
||||
}
|
||||
Step::Preset(_) => {
|
||||
self.step = Step::Insert;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::XScale | Step::XyzX => {
|
||||
self.scale = [1.0; 3];
|
||||
self.step = if self.step == Step::XScale {
|
||||
Step::YScale
|
||||
} else {
|
||||
Step::XyzY
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
XAttachStep::Rotate { point, sx, sy, sz } => {
|
||||
CmdResult::CommitAndExit(self.commit_insert(point, sx, sy, sz, 0.0))
|
||||
Step::YScale => {
|
||||
self.scale[1] = self.scale[0];
|
||||
self.scale_done()
|
||||
}
|
||||
Step::XyzY => {
|
||||
self.scale[1] = self.scale[0];
|
||||
self.step = Step::XyzZ;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::XyzZ => {
|
||||
self.scale[2] = self.scale[0];
|
||||
self.scale_done()
|
||||
}
|
||||
Step::Corner => CmdResult::NeedPoint,
|
||||
Step::Rotation => {
|
||||
self.rotation = 0.0;
|
||||
CmdResult::CommitAndExit(self.commit_insert())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, _pt: DVec3) -> Vec<WireModel> {
|
||||
// XREF-Task6: PScale drag preview — no framework hook; revisit
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn xattach_path(&self) -> Option<String> {
|
||||
Some(self.path.clone())
|
||||
fn xattach_request(&self) -> Option<XrefAttachRequest> {
|
||||
Some(self.request.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive a block name from the file path: take the file stem, uppercase it.
|
||||
/// Reference name for a file: its name without the extension, case kept.
|
||||
pub fn path_to_block_name(path: &str) -> String {
|
||||
let p = std::path::Path::new(path);
|
||||
p.file_stem()
|
||||
.map(|s| s.to_string_lossy().to_uppercase())
|
||||
let normalized = path.replace('\\', "/");
|
||||
std::path::Path::new(&normalized)
|
||||
.file_stem()
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "XREF".to_string())
|
||||
}
|
||||
|
||||
/// Collision-free block name: `stem` unless taken (case-insensitive — block
|
||||
/// names are uppercase by convention), else `stem_1`, `stem_2`, ...
|
||||
/// (`$0$` numbering belongs to Bind, not Attach.)
|
||||
/// Collision-free block name: `stem` unless taken (case-insensitive), else
|
||||
/// `stem_1`, `stem_2`, ... (`$0$` numbering belongs to Bind, not Attach.)
|
||||
pub fn unique_block_name(stem: &str, taken: &[impl AsRef<str>]) -> String {
|
||||
let upper_taken: Vec<String> = taken.iter().map(|t| t.as_ref().to_uppercase()).collect();
|
||||
if !upper_taken.iter().any(|t| t == &stem.to_uppercase()) {
|
||||
@@ -215,10 +489,6 @@ pub fn unique_block_name(stem: &str, taken: &[impl AsRef<str>]) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach-default-Full store path: a relative incoming path is joined onto
|
||||
/// the host drawing's base dir (yielding an absolute path when the base dir
|
||||
/// is absolute); absolute paths and missing base dirs pass through verbatim.
|
||||
/// Separators are normalized to `/` for stable DWG storage.
|
||||
fn is_absolute_xref_path(path: &str) -> bool {
|
||||
let bytes = path.as_bytes();
|
||||
std::path::Path::new(path).is_absolute()
|
||||
@@ -229,6 +499,9 @@ fn is_absolute_xref_path(path: &str) -> bool {
|
||||
&& matches!(bytes[2], b'/' | b'\\'))
|
||||
}
|
||||
|
||||
/// Full store path: a relative incoming path is joined onto the host
|
||||
/// drawing's base dir; absolute paths and missing base dirs pass through.
|
||||
/// Separators are normalized to `/` for stable DWG storage.
|
||||
pub fn resolve_xref_store_path(path: &str, host_base_dir: Option<&std::path::Path>) -> String {
|
||||
let p = std::path::Path::new(path);
|
||||
if is_absolute_xref_path(path) {
|
||||
@@ -240,14 +513,6 @@ pub fn resolve_xref_store_path(path: &str, host_base_dir: Option<&std::path::Pat
|
||||
}
|
||||
}
|
||||
|
||||
// Host-path note (XREF-Task6): `Scene` exposes no host file path — it owns
|
||||
// the document plus view state only (verified in scene/mod.rs: no file path
|
||||
// field). The host drawing path lives one layer up, in `Tab::current_path`
|
||||
// (app/document.rs). The self-attach guard therefore runs at the command
|
||||
// driver layer, which owns both the tab path and the pending XATTACH path,
|
||||
// via this pure, unit-tested helper. `prepare_xref_block` below stays
|
||||
// side-effect-only and keeps its `-> String` return.
|
||||
|
||||
/// True when attaching `ref_path` would attach the host drawing into itself.
|
||||
/// Lexical comparison only (no filesystem touch — the target may be missing);
|
||||
/// a relative ref is resolved against `host_base_dir` (falling back to the
|
||||
@@ -271,46 +536,52 @@ pub fn is_self_attach(
|
||||
normalize_lexical(&candidate) == normalize_lexical(&host_file.to_string_lossy())
|
||||
}
|
||||
|
||||
/// Parse a typed scale: a single uniform value, or an X/Y/Z triplet separated
|
||||
/// by commas, semicolons or whitespace. Zero never scales (rejected, like
|
||||
/// INSERT); unparseable input returns None and the caller re-prompts.
|
||||
fn parse_scale_spec(text: &str) -> Option<(f64, f64, f64)> {
|
||||
let norm = text.trim().replace([',', ';'], " ");
|
||||
let parts: Vec<&str> = norm.split_whitespace().collect();
|
||||
match parts.as_slice() {
|
||||
[single] => single
|
||||
.parse::<f64>()
|
||||
.ok()
|
||||
.filter(|v| *v != 0.0)
|
||||
.map(|v| (v, v, 1.0)),
|
||||
[x, y, z] => {
|
||||
let parsed: Option<Vec<f64>> =
|
||||
[x, y, z].iter().map(|s| s.parse::<f64>().ok()).collect();
|
||||
match parsed {
|
||||
Some(v) if v.iter().all(|n| *n != 0.0) => Some((v[0], v[1], v[2])),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
/// The drawing's existing reference for `name`, if any (case-insensitive).
|
||||
pub fn existing_reference(scene: &Scene, name: &str) -> Option<String> {
|
||||
scene
|
||||
.document
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|br| {
|
||||
(br.flags.is_xref || br.flags.is_xref_overlay) && br.name.eq_ignore_ascii_case(name)
|
||||
})
|
||||
.map(|br| br.name.clone())
|
||||
}
|
||||
|
||||
/// The path to store for `request`: full, relative to the saved host, or
|
||||
/// the file name alone. A relative path needs a saved host; until then the
|
||||
/// full path is kept, as the reference does.
|
||||
pub fn stored_path(request: &XrefAttachRequest, host_file: Option<&std::path::Path>) -> String {
|
||||
let full = resolve_xref_store_path(&request.path, None);
|
||||
match request.path_type {
|
||||
Pathtype::Full => full,
|
||||
Pathtype::Relative => host_file
|
||||
.and_then(|host| {
|
||||
crate::io::xref_model::to_pathtype_result(&full, host, Pathtype::Relative).ok()
|
||||
})
|
||||
.unwrap_or(full),
|
||||
Pathtype::None => std::path::Path::new(&full.replace('\\', "/"))
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or(full),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the XREF BlockRecord + Block/EndBlock entities in the scene document
|
||||
/// for a given file path. Returns the block name.
|
||||
/// Create (or reuse) the reference definition for `request` and load its
|
||||
/// content. Returns the block name the INSERT must use.
|
||||
///
|
||||
/// This must be called before committing the INSERT so that the block
|
||||
/// definition exists when the renderer looks it up.
|
||||
///
|
||||
/// Name collisions get a `_1`, `_2`... suffix via [`unique_block_name`]; the
|
||||
/// stored path follows Attach-default-Full via [`resolve_xref_store_path`].
|
||||
/// Self-attach blocking is NOT done here — see the note on [`is_self_attach`];
|
||||
/// the driver checks it before calling.
|
||||
pub fn prepare_xref_block(
|
||||
/// A reference of the same name already in the drawing is reused ("Using
|
||||
/// existing definition."). The self-attach guard runs before this, where the
|
||||
/// host path is known.
|
||||
pub fn prepare_xref_definition(
|
||||
scene: &mut Scene,
|
||||
path: &str,
|
||||
host_base_dir: Option<&std::path::Path>,
|
||||
request: &XrefAttachRequest,
|
||||
host_file: Option<&std::path::Path>,
|
||||
) -> String {
|
||||
let stem = path_to_block_name(path);
|
||||
let stem = path_to_block_name(&request.path);
|
||||
if let Some(existing) = existing_reference(scene, &stem) {
|
||||
return existing;
|
||||
}
|
||||
let taken: Vec<String> = scene
|
||||
.document
|
||||
.block_records
|
||||
@@ -318,20 +589,13 @@ pub fn prepare_xref_block(
|
||||
.map(|b| b.name.clone())
|
||||
.collect();
|
||||
let block_name = unique_block_name(&stem, &taken);
|
||||
let store_path = stored_path(request, host_file);
|
||||
|
||||
// If a BlockRecord already exists with this name, skip creation.
|
||||
if scene.document.block_records.get(&block_name).is_some() {
|
||||
return block_name;
|
||||
}
|
||||
|
||||
let store_path = resolve_xref_store_path(path, host_base_dir);
|
||||
|
||||
// Create the BlockRecord.
|
||||
let mut br = BlockRecord::new(&block_name);
|
||||
br.handle = scene.document.allocate_handle();
|
||||
br.flags = BlockFlags {
|
||||
is_xref: true,
|
||||
is_xref_overlay: false,
|
||||
is_xref: !request.overlay,
|
||||
is_xref_overlay: request.overlay,
|
||||
anonymous: false,
|
||||
has_attributes: false,
|
||||
is_external: false,
|
||||
@@ -339,33 +603,50 @@ pub fn prepare_xref_block(
|
||||
is_xref_unloaded: false,
|
||||
};
|
||||
br.xref_path = store_path.clone();
|
||||
let key = br.handle;
|
||||
let _ = scene.document.block_records.add(br);
|
||||
|
||||
// Create BLOCK entity.
|
||||
let b = Block::new(&block_name, Vector3::zero()).with_xref_path(&store_path);
|
||||
let _ = scene.document.add_entity(EntityType::Block(b));
|
||||
let _ = scene
|
||||
.document
|
||||
.add_entity(EntityType::BlockEnd(BlockEnd::new()));
|
||||
// BLOCK / ENDBLK markers linked through the record.
|
||||
crate::io::xref::ensure_block_entities(&mut scene.document, &block_name);
|
||||
|
||||
// Resolve the XREF content immediately.
|
||||
// Every reference in this host resolves relative to the host drawing, not
|
||||
// relative to the newly-attached file. Using `store_path.parent()` here
|
||||
// made an attach unexpectedly redirect existing relative xrefs.
|
||||
if let Some(base_dir) = host_base_dir {
|
||||
let _ = crate::io::xref::resolve_xrefs(&mut scene.document, base_dir);
|
||||
} else if let Some(base_dir) = std::path::Path::new(&store_path).parent() {
|
||||
let _ = crate::io::xref::resolve_xrefs(&mut scene.document, base_dir);
|
||||
// Load this reference only. A stored relative path resolves against the
|
||||
// host folder; a full path or bare file name against the chosen file's.
|
||||
let file_dir = std::path::Path::new(&resolve_xref_store_path(&request.path, None))
|
||||
.parent()
|
||||
.map(|p| p.to_path_buf());
|
||||
let base_dir = match request.path_type {
|
||||
Pathtype::Relative => host_file.and_then(|h| h.parent()).map(|p| p.to_path_buf()),
|
||||
_ => None,
|
||||
}
|
||||
.or(file_dir);
|
||||
if let Some(base_dir) = base_dir {
|
||||
let keys: rustc_hash::FxHashSet<acadrust::types::Handle> = [key].into_iter().collect();
|
||||
let _ = crate::io::xref::resolve_xrefs_for_keys(&mut scene.document, &base_dir, &keys);
|
||||
}
|
||||
|
||||
block_name
|
||||
}
|
||||
|
||||
/// Full-path attach of `path` (kept for callers that only have a path).
|
||||
#[cfg(test)]
|
||||
pub fn prepare_xref_block(
|
||||
scene: &mut Scene,
|
||||
path: &str,
|
||||
host_base_dir: Option<&std::path::Path>,
|
||||
) -> String {
|
||||
let request = XrefAttachRequest {
|
||||
path: resolve_xref_store_path(path, host_base_dir),
|
||||
overlay: false,
|
||||
path_type: Pathtype::Full,
|
||||
};
|
||||
prepare_xref_definition(scene, &request, None)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
is_self_attach, parse_scale_spec, path_to_block_name, resolve_xref_store_path,
|
||||
unique_block_name, XAttachCommand,
|
||||
is_self_attach, path_to_block_name, resolve_xref_store_path, unique_block_name,
|
||||
XAttachCommand,
|
||||
};
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use acadrust::EntityType;
|
||||
@@ -385,8 +666,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_name_derives_from_stem() {
|
||||
assert_eq!(path_to_block_name("C:/refs/plan.dwg"), "PLAN");
|
||||
fn block_name_keeps_the_file_name_case() {
|
||||
assert_eq!(path_to_block_name("C:/refs/plan.dwg"), "plan");
|
||||
assert_eq!(path_to_block_name("C:\\refs\\Site Plan.dwg"), "Site Plan");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -407,96 +689,91 @@ mod tests {
|
||||
fn self_attach_guard_matches_normalized_paths() {
|
||||
let host = std::path::Path::new("C:/Drawings/host.dwg");
|
||||
let base = std::path::Path::new("C:/Drawings");
|
||||
// Verbatim, drive-letter case, dot segments, and base-relative forms.
|
||||
assert!(is_self_attach(host, "C:/Drawings/host.dwg", Some(base)));
|
||||
assert!(is_self_attach(host, "c:/Drawings/host.dwg", Some(base)));
|
||||
assert!(is_self_attach(host, "C:/Drawings/./host.dwg", Some(base)));
|
||||
assert!(is_self_attach(host, "host.dwg", Some(base)));
|
||||
assert!(is_self_attach(host, "C:/Drawings/host.dwg", None));
|
||||
// Anything else attaches freely.
|
||||
assert!(!is_self_attach(host, "C:/Drawings/other.dwg", Some(base)));
|
||||
assert!(!is_self_attach(host, "other.dwg", Some(base)));
|
||||
assert!(!is_self_attach(host, "other.dwg", None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_spec_parses_single_and_triplet() {
|
||||
assert_eq!(parse_scale_spec("2"), Some((2.0, 2.0, 1.0)));
|
||||
assert_eq!(parse_scale_spec("2,3,4"), Some((2.0, 3.0, 4.0)));
|
||||
assert_eq!(parse_scale_spec("2 3 4"), Some((2.0, 3.0, 4.0)));
|
||||
assert_eq!(parse_scale_spec("0"), None);
|
||||
assert_eq!(parse_scale_spec("abc"), None);
|
||||
assert_eq!(parse_scale_spec("1,2"), None);
|
||||
fn enter_at_insertion_point_asks_again() {
|
||||
let mut cmd = XAttachCommand::with_path("C:/refs/plan.dwg".to_string());
|
||||
assert!(matches!(cmd.on_enter(), CmdResult::ReportError(_)));
|
||||
assert!(cmd.prompt().contains("Specify insertion point"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_at_insertion_point_cancels() {
|
||||
let mut cmd = XAttachCommand::with_path("C:/refs/plan.dwg".to_string());
|
||||
assert!(matches!(cmd.on_enter(), CmdResult::Cancel));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placement_flow_scale_then_rotate_commits() {
|
||||
let mut cmd = XAttachCommand::with_path("C:/refs/plan.dwg".to_string());
|
||||
assert!(matches!(
|
||||
cmd.on_point(DVec3::new(10.0, 20.0, 0.0)),
|
||||
CmdResult::NeedPoint
|
||||
));
|
||||
assert!(cmd.prompt().contains("scale"));
|
||||
assert!(matches!(cmd.on_text_input("2"), Some(CmdResult::NeedPoint)));
|
||||
assert!(cmd.prompt().contains("rotation"));
|
||||
match cmd.on_enter() {
|
||||
CmdResult::CommitAndExit(EntityType::Insert(ins)) => {
|
||||
assert_eq!(ins.block_name, "PLAN");
|
||||
assert!((ins.x_scale() - 2.0).abs() < 1e-9);
|
||||
assert!((ins.y_scale() - 2.0).abs() < 1e-9);
|
||||
assert!(ins.rotation.abs() < 1e-9);
|
||||
assert!((ins.insert_point.x - 10.0).abs() < 1e-9);
|
||||
assert!((ins.insert_point.y - 20.0).abs() < 1e-9);
|
||||
}
|
||||
fn committed(result: Option<CmdResult>) -> acadrust::entities::Insert {
|
||||
match result {
|
||||
Some(CmdResult::CommitAndExit(EntityType::Insert(ins))) => ins,
|
||||
_ => panic!("expected CommitAndExit(Insert)"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placement_flow_triplet_and_typed_rotation() {
|
||||
fn x_scale_then_default_y_and_rotation() {
|
||||
let mut cmd = XAttachCommand::with_path("C:/refs/plan.dwg".to_string());
|
||||
assert!(matches!(
|
||||
cmd.on_point(DVec3::ZERO),
|
||||
CmdResult::NeedPoint
|
||||
));
|
||||
assert!(matches!(
|
||||
cmd.on_text_input("2,3,4"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
match cmd.on_text_input("90") {
|
||||
Some(CmdResult::CommitAndExit(EntityType::Insert(ins))) => {
|
||||
assert!((ins.x_scale() - 2.0).abs() < 1e-9);
|
||||
assert!((ins.y_scale() - 3.0).abs() < 1e-9);
|
||||
assert!((ins.z_scale() - 4.0).abs() < 1e-9);
|
||||
assert!((ins.rotation - std::f64::consts::FRAC_PI_2).abs() < 1e-6);
|
||||
}
|
||||
_ => panic!("expected CommitAndExit(Insert)"),
|
||||
}
|
||||
assert!(matches!(cmd.on_point(DVec3::new(10.0, 20.0, 0.0)), CmdResult::NeedPoint));
|
||||
assert!(cmd.prompt().contains("Enter X scale factor"));
|
||||
assert!(matches!(cmd.on_text_input("-2"), Some(CmdResult::NeedPoint)));
|
||||
assert!(cmd.prompt().contains("Enter Y scale factor"));
|
||||
assert!(matches!(cmd.on_enter(), CmdResult::NeedPoint));
|
||||
assert!(cmd.prompt().contains("Specify rotation angle <0>"));
|
||||
let ins = committed(Some(cmd.on_enter()));
|
||||
assert_eq!(ins.block_name, "plan");
|
||||
assert!((ins.x_scale() + 2.0).abs() < 1e-9);
|
||||
assert!((ins.y_scale() + 2.0).abs() < 1e-9);
|
||||
// Z follows the size of X, not its sign.
|
||||
assert!((ins.z_scale() - 2.0).abs() < 1e-9);
|
||||
assert!((ins.insert_point.x - 10.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placement_flow_corner_pick_sets_xy_scale() {
|
||||
fn zero_scale_is_refused() {
|
||||
let mut cmd = XAttachCommand::with_path("C:/refs/plan.dwg".to_string());
|
||||
assert!(matches!(
|
||||
cmd.on_point(DVec3::ZERO),
|
||||
CmdResult::NeedPoint
|
||||
));
|
||||
assert!(matches!(
|
||||
cmd.on_point(DVec3::new(3.0, 4.0, 0.0)),
|
||||
CmdResult::NeedPoint
|
||||
));
|
||||
match cmd.on_enter() {
|
||||
CmdResult::CommitAndExit(EntityType::Insert(ins)) => {
|
||||
assert!((ins.x_scale() - 3.0).abs() < 1e-9);
|
||||
assert!((ins.y_scale() - 4.0).abs() < 1e-9);
|
||||
}
|
||||
_ => panic!("expected CommitAndExit(Insert)"),
|
||||
}
|
||||
cmd.on_point(DVec3::ZERO);
|
||||
assert!(matches!(cmd.on_text_input("0"), Some(CmdResult::ReportError(_))));
|
||||
assert!(cmd.prompt().contains("Enter X scale factor"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xyz_option_takes_three_factors() {
|
||||
let mut cmd = XAttachCommand::with_path("C:/refs/plan.dwg".to_string());
|
||||
cmd.on_point(DVec3::ZERO);
|
||||
cmd.on_text_input("XYZ");
|
||||
cmd.on_text_input("2");
|
||||
cmd.on_text_input("3");
|
||||
cmd.on_text_input("4");
|
||||
let ins = committed(cmd.on_text_input("90"));
|
||||
assert!((ins.x_scale() - 2.0).abs() < 1e-9);
|
||||
assert!((ins.y_scale() - 3.0).abs() < 1e-9);
|
||||
assert!((ins.z_scale() - 4.0).abs() < 1e-9);
|
||||
assert!((ins.rotation - std::f64::consts::FRAC_PI_2).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corner_pick_sets_x_and_y() {
|
||||
let mut cmd = XAttachCommand::with_path("C:/refs/plan.dwg".to_string());
|
||||
cmd.on_point(DVec3::ZERO);
|
||||
assert!(matches!(cmd.on_point(DVec3::new(5.0, 7.0, 0.0)), CmdResult::NeedPoint));
|
||||
let ins = committed(Some(cmd.on_enter()));
|
||||
assert!((ins.x_scale() - 5.0).abs() < 1e-9);
|
||||
assert!((ins.y_scale() - 7.0).abs() < 1e-9);
|
||||
assert!((ins.z_scale() - 5.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_preset_skips_the_scale_prompts() {
|
||||
let mut cmd = XAttachCommand::with_path("C:/refs/plan.dwg".to_string());
|
||||
cmd.on_text_input("S");
|
||||
assert!(cmd.prompt().contains("Specify scale factor for XYZ axes"));
|
||||
cmd.on_text_input("2");
|
||||
cmd.on_point(DVec3::ZERO);
|
||||
assert!(cmd.prompt().contains("Specify rotation angle <0>"));
|
||||
let ins = committed(Some(cmd.on_enter()));
|
||||
assert!((ins.z_scale() - 2.0).abs() < 1e-9);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
// -XREF — the command-line form of the reference manager.
|
||||
//
|
||||
// Enter an option [?/Bind/Detach/Path/pathType/Unload/Reload/Overlay/Attach/Show] <Attach>:
|
||||
//
|
||||
// Each option collects its answers with the reference prompts and hands them
|
||||
// to the existing reference operations through `CmdResult::Dispatch`.
|
||||
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum Step {
|
||||
Option,
|
||||
List,
|
||||
Names(&'static str),
|
||||
File { overlay: bool },
|
||||
PathNames,
|
||||
NewPath { name: String },
|
||||
PathTypeNames,
|
||||
PathTypeKind { names: String },
|
||||
ShowNames,
|
||||
}
|
||||
|
||||
pub struct XrefCommand {
|
||||
step: Step,
|
||||
/// Saved paths by reference name, for the Path option's "Old path:" line.
|
||||
saved_paths: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl XrefCommand {
|
||||
pub fn new(saved_paths: Vec<(String, String)>) -> Self {
|
||||
Self {
|
||||
step: Step::Option,
|
||||
saved_paths,
|
||||
}
|
||||
}
|
||||
|
||||
fn choose(&mut self, token: &str) -> CmdResult {
|
||||
self.step = match token {
|
||||
"?" => Step::List,
|
||||
"B" | "BIND" => Step::Names("Bind"),
|
||||
"D" | "DETACH" => Step::Names("Detach"),
|
||||
"P" | "PATH" => Step::PathNames,
|
||||
"T" | "PATHTYPE" => Step::PathTypeNames,
|
||||
"U" | "UNLOAD" => Step::Names("Unload"),
|
||||
"R" | "RELOAD" => Step::Names("Reload"),
|
||||
"O" | "OVERLAY" => Step::File { overlay: true },
|
||||
"A" | "ATTACH" => Step::File { overlay: false },
|
||||
"S" | "SHOW" => Step::ShowNames,
|
||||
_ => {
|
||||
return CmdResult::ReportError("Invalid option keyword.".to_string());
|
||||
}
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for XrefCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"-XREF"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
let text = match &self.step {
|
||||
Step::Option => "Enter an option [?/Bind/Detach/Path/pathType/Unload/Reload/Overlay/Attach/Show] <Attach>:",
|
||||
Step::List => "Enter xref name(s) to list <*>:",
|
||||
Step::Names("Bind") => "Enter xref name(s) to bind:",
|
||||
Step::Names("Detach") => "Enter xref name(s) to detach:",
|
||||
Step::Names("Unload") => "Enter xref name(s) to unload:",
|
||||
Step::Names(_) => "Enter xref name(s) to reload:",
|
||||
Step::File { overlay: false } => "Enter name of file to attach:",
|
||||
Step::File { overlay: true } => "Enter name of file to overlay:",
|
||||
Step::PathNames => "Edit xref name(s) to edit path:",
|
||||
Step::NewPath { .. } => "Enter new path:",
|
||||
Step::PathTypeNames => "Enter xref name(s) to edit path type:",
|
||||
Step::PathTypeKind { .. } => "Enter new path type [Full/Relative/None]:",
|
||||
Step::ShowNames => "Enter xref name(s) to show:",
|
||||
};
|
||||
format!("-XREF {text}")
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
Step::Option => vec![
|
||||
CmdOption::new("?", "?"),
|
||||
CmdOption::new("Bind", "B"),
|
||||
CmdOption::new("Detach", "D"),
|
||||
CmdOption::new("Path", "P"),
|
||||
CmdOption::new("pathType", "T"),
|
||||
CmdOption::new("Unload", "U"),
|
||||
CmdOption::new("Reload", "R"),
|
||||
CmdOption::new("Overlay", "O"),
|
||||
CmdOption::new("Attach", "A"),
|
||||
CmdOption::new("Show", "S"),
|
||||
],
|
||||
Step::PathTypeKind { .. } => vec![
|
||||
CmdOption::new("Full", "F"),
|
||||
CmdOption::new("Relative", "R"),
|
||||
CmdOption::new("None", "N"),
|
||||
],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
match self.step {
|
||||
// Paths and names can hold spaces.
|
||||
Step::File { .. } | Step::NewPath { .. } => InputKind::FreeText,
|
||||
_ => InputKind::SingleToken,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let value = text.trim().trim_matches('"').to_string();
|
||||
let token = value.to_uppercase();
|
||||
Some(match self.step.clone() {
|
||||
Step::Option => self.choose(&token),
|
||||
Step::List => CmdResult::Dispatch(format!("XREFLIST {value}")),
|
||||
Step::Names(op) => CmdResult::Dispatch(format!("XREF {op} {value}")),
|
||||
Step::File { overlay } => {
|
||||
let verb = if overlay { "XREFOVERLAYFILE" } else { "XREFATTACHFILE" };
|
||||
CmdResult::Dispatch(format!("{verb} {value}"))
|
||||
}
|
||||
Step::PathNames => {
|
||||
let Some((name, old)) = self
|
||||
.saved_paths
|
||||
.iter()
|
||||
.find(|(name, _)| crate::io::xref_model::wildcard_match(name, &value))
|
||||
.cloned()
|
||||
else {
|
||||
return Some(CmdResult::CancelWithMessage(
|
||||
"No matching xref names found.".to_string(),
|
||||
));
|
||||
};
|
||||
self.step = Step::NewPath { name: name.clone() };
|
||||
CmdResult::ReportMeasurement(format!(
|
||||
"{}\n{}",
|
||||
format!("xref name: \"{}\"", name),
|
||||
format!("Old path: {}", display_path(&old))
|
||||
))
|
||||
}
|
||||
Step::NewPath { name } => CmdResult::Dispatch(format!("XREF Path {name} {value}")),
|
||||
Step::PathTypeNames => {
|
||||
self.step = Step::PathTypeKind { names: value };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::PathTypeKind { names } => {
|
||||
let kind = match token.as_str() {
|
||||
"F" | "FULL" => "Full",
|
||||
"R" | "RELATIVE" => "Relative",
|
||||
"N" | "NONE" => "None",
|
||||
_ => {
|
||||
return Some(CmdResult::ReportError(
|
||||
"Invalid option keyword.".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
CmdResult::Dispatch(format!("XREF Pathtype {kind} {names}"))
|
||||
}
|
||||
Step::ShowNames => CmdResult::Dispatch("XREFSHOW".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.step.clone() {
|
||||
Step::Option => self.choose("A"),
|
||||
Step::List => CmdResult::Dispatch("XREFLIST *".to_string()),
|
||||
Step::NewPath { .. } => {
|
||||
CmdResult::CancelWithMessage("Path unchanged.".to_string())
|
||||
}
|
||||
Step::PathTypeKind { .. } => CmdResult::NeedPoint,
|
||||
_ => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A stored path shown the way the platform writes it.
|
||||
pub fn display_path(path: &str) -> String {
|
||||
if cfg!(windows) {
|
||||
path.replace('/', "\\")
|
||||
} else {
|
||||
path.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::XrefCommand;
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
|
||||
#[test]
|
||||
fn enter_chooses_attach() {
|
||||
let mut cmd = XrefCommand::new(Vec::new());
|
||||
assert!(matches!(cmd.on_enter(), CmdResult::NeedPoint));
|
||||
assert!(cmd.prompt().contains("Enter name of file to attach:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_defaults_to_every_reference() {
|
||||
let mut cmd = XrefCommand::new(Vec::new());
|
||||
cmd.on_text_input("?");
|
||||
assert!(cmd.prompt().contains("Enter xref name(s) to list <*>:"));
|
||||
assert!(matches!(cmd.on_enter(), CmdResult::Dispatch(ref s) if s == "XREFLIST *"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlay_file_is_dispatched() {
|
||||
let mut cmd = XrefCommand::new(Vec::new());
|
||||
cmd.on_text_input("O");
|
||||
assert!(cmd.prompt().contains("Enter name of file to overlay:"));
|
||||
assert!(matches!(
|
||||
cmd.on_text_input("C:/refs/site plan.dwg"),
|
||||
Some(CmdResult::Dispatch(ref s)) if s == "XREFOVERLAYFILE C:/refs/site plan.dwg"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -2530,6 +2530,7 @@ impl super::Scene {
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::io::xref::strip_resolved_xref_content(&mut document);
|
||||
document
|
||||
}
|
||||
|
||||
|
||||
@@ -155,7 +155,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>,
|
||||
@@ -185,7 +185,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 xref_attach;
|
||||
pub mod xref_help;
|
||||
pub mod xref_manager;
|
||||
@@ -0,0 +1,414 @@
|
||||
//! Attach External Reference dialog — the options chosen after picking a
|
||||
//! drawing with ATTACH / XATTACH: reference type, scale, insertion point,
|
||||
//! path type and rotation, with the file's preview and block unit.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use iced::widget::{
|
||||
button, column, combo_box, container, image, pick_list, radio, row, text, text_input, Space,
|
||||
};
|
||||
use iced::{Element, Fill, Length, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::io::xref_model::Pathtype;
|
||||
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 the dialog.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum XrefAttachMsg {
|
||||
/// Name box: a reference already in the drawing, picked or typed.
|
||||
Name(String),
|
||||
Browse,
|
||||
Overlay(bool),
|
||||
ScaleOnScreen(bool),
|
||||
Scale(usize, String),
|
||||
Uniform(bool),
|
||||
InsertOnScreen(bool),
|
||||
Insert(usize, String),
|
||||
PathType(PathTypeChoice),
|
||||
RotationOnScreen(bool),
|
||||
Rotation(String),
|
||||
Details(bool),
|
||||
Apply,
|
||||
Help,
|
||||
}
|
||||
|
||||
/// Path type choice as the 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())
|
||||
}
|
||||
}
|
||||
|
||||
/// The dialog's working copy.
|
||||
#[derive(Clone)]
|
||||
pub struct XrefAttachState {
|
||||
/// Absolute path of the drawing to attach.
|
||||
pub path: String,
|
||||
/// Reference name (the file name without extension).
|
||||
pub name: String,
|
||||
/// References already in the drawing: name and saved path.
|
||||
pub existing: Vec<(String, String)>,
|
||||
pub name_combo: combo_box::State<String>,
|
||||
pub overlay: bool,
|
||||
pub scale_on_screen: bool,
|
||||
pub scale: [String; 3],
|
||||
pub uniform: bool,
|
||||
pub insert_on_screen: bool,
|
||||
pub insert: [String; 3],
|
||||
pub path_type: PathTypeChoice,
|
||||
pub rotation_on_screen: bool,
|
||||
pub rotation: String,
|
||||
/// Block unit of the chosen drawing and its factor to the host's unit.
|
||||
pub unit_label: String,
|
||||
pub unit_factor: String,
|
||||
pub found_in: String,
|
||||
pub saved_path: String,
|
||||
pub details: bool,
|
||||
pub preview: Option<image::Handle>,
|
||||
}
|
||||
|
||||
impl XrefAttachState {
|
||||
pub fn new(existing: Vec<(String, String)>) -> Self {
|
||||
let names = existing.iter().map(|(name, _)| name.clone()).collect();
|
||||
Self {
|
||||
path: String::new(),
|
||||
name: String::new(),
|
||||
existing,
|
||||
name_combo: combo_box::State::new(names),
|
||||
overlay: false,
|
||||
scale_on_screen: false,
|
||||
scale: ["1.0000".into(), "1.0000".into(), "1.0000".into()],
|
||||
uniform: false,
|
||||
insert_on_screen: true,
|
||||
insert: ["0.0000".into(), "0.0000".into(), "0.0000".into()],
|
||||
path_type: PathTypeChoice(Pathtype::Relative),
|
||||
rotation_on_screen: false,
|
||||
rotation: "0".into(),
|
||||
unit_label: String::new(),
|
||||
unit_factor: String::new(),
|
||||
found_in: String::new(),
|
||||
saved_path: String::new(),
|
||||
details: false,
|
||||
preview: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed X/Y/Z scale; a uniform scale takes X for all three.
|
||||
pub fn scale_values(&self) -> Option<[f64; 3]> {
|
||||
let parse = |s: &String| s.trim().parse::<f64>().ok();
|
||||
let x = parse(&self.scale[0])?;
|
||||
if self.uniform {
|
||||
return Some([x, x, x]);
|
||||
}
|
||||
Some([x, parse(&self.scale[1])?, parse(&self.scale[2])?])
|
||||
}
|
||||
|
||||
pub fn insert_point(&self) -> Option<glam::DVec3> {
|
||||
let parse = |s: &String| s.trim().parse::<f64>().ok();
|
||||
Some(glam::DVec3::new(
|
||||
parse(&self.insert[0])?,
|
||||
parse(&self.insert[1])?,
|
||||
parse(&self.insert[2])?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn msg(m: XrefAttachMsg) -> Message {
|
||||
Message::XrefAttach(m)
|
||||
}
|
||||
|
||||
fn number_field<'a>(
|
||||
label: &'a str,
|
||||
value: &'a str,
|
||||
enabled: bool,
|
||||
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(16.0)),
|
||||
field
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn read_only_field<'a>(label: String, value: &'a str, label_width: f32) -> Element<'a, Message> {
|
||||
row![
|
||||
text(label)
|
||||
.size(11)
|
||||
.style(muted_style)
|
||||
.width(Length::Fixed(label_width)),
|
||||
text_input("", value)
|
||||
.size(11)
|
||||
.padding([3, 6])
|
||||
.width(Fill)
|
||||
.style(field_style),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn view_window<'a>(
|
||||
state: &'a XrefAttachState,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
// ── Name ────────────────────────────────────────────────────────────
|
||||
let name_combo = combo_box(
|
||||
&state.name_combo,
|
||||
"",
|
||||
if state.name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(&state.name)
|
||||
},
|
||||
|chosen: String| msg(XrefAttachMsg::Name(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(XrefAttachMsg::Browse))
|
||||
.style(button_style(false))
|
||||
.padding([4, 12])
|
||||
.width(Length::Fixed(96.0)),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
]
|
||||
.spacing(3);
|
||||
|
||||
// ── Preview + reference type ────────────────────────────────────────
|
||||
let preview_body: Element<'a, Message> = match &state.preview {
|
||||
Some(handle) => image(handle.clone()).width(Fill).height(Fill).into(),
|
||||
None => Space::new().width(Fill).height(Fill).into(),
|
||||
};
|
||||
let preview = group(
|
||||
t!("Preview").into_owned(),
|
||||
container(preview_body)
|
||||
.width(Fill)
|
||||
.height(Length::Fixed(132.0))
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(iced::Background::Color(
|
||||
theme.palette().background.base.color,
|
||||
)),
|
||||
border: iced::Border {
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
color: theme.palette().background.strong.color,
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
Length::Shrink,
|
||||
);
|
||||
let reference_type = group(
|
||||
t!("Reference Type").into_owned(),
|
||||
column![
|
||||
radio(
|
||||
t!("Attachment"),
|
||||
false,
|
||||
Some(state.overlay),
|
||||
|v| msg(XrefAttachMsg::Overlay(v))
|
||||
)
|
||||
.size(14)
|
||||
.text_size(11),
|
||||
radio(t!("Overlay"), true, Some(state.overlay), |v| msg(
|
||||
XrefAttachMsg::Overlay(v)
|
||||
))
|
||||
.size(14)
|
||||
.text_size(11),
|
||||
]
|
||||
.spacing(6),
|
||||
Length::Shrink,
|
||||
);
|
||||
let left = column![preview, reference_type]
|
||||
.spacing(6)
|
||||
.width(Length::FillPortion(1));
|
||||
|
||||
// ── Scale + insertion point ─────────────────────────────────────────
|
||||
let scale_enabled = !state.scale_on_screen;
|
||||
let scale = group(
|
||||
t!("Scale").into_owned(),
|
||||
column![
|
||||
labeled_checkbox(t!("Specify On-screen"), state.scale_on_screen, |v| msg(
|
||||
XrefAttachMsg::ScaleOnScreen(v)
|
||||
)),
|
||||
number_field("X:", &state.scale[0], scale_enabled, |v| msg(
|
||||
XrefAttachMsg::Scale(0, v)
|
||||
)),
|
||||
number_field(
|
||||
"Y:",
|
||||
&state.scale[1],
|
||||
scale_enabled && !state.uniform,
|
||||
|v| msg(XrefAttachMsg::Scale(1, v))
|
||||
),
|
||||
number_field(
|
||||
"Z:",
|
||||
&state.scale[2],
|
||||
scale_enabled && !state.uniform,
|
||||
|v| msg(XrefAttachMsg::Scale(2, v))
|
||||
),
|
||||
labeled_checkbox(t!("Uniform Scale"), state.uniform, |v| msg(
|
||||
XrefAttachMsg::Uniform(v)
|
||||
)),
|
||||
]
|
||||
.spacing(5),
|
||||
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(
|
||||
XrefAttachMsg::InsertOnScreen(v)
|
||||
)),
|
||||
number_field("X:", &state.insert[0], insert_enabled, |v| msg(
|
||||
XrefAttachMsg::Insert(0, v)
|
||||
)),
|
||||
number_field("Y:", &state.insert[1], insert_enabled, |v| msg(
|
||||
XrefAttachMsg::Insert(1, v)
|
||||
)),
|
||||
number_field("Z:", &state.insert[2], insert_enabled, |v| msg(
|
||||
XrefAttachMsg::Insert(2, v)
|
||||
)),
|
||||
]
|
||||
.spacing(5),
|
||||
Length::Shrink,
|
||||
);
|
||||
let middle = column![scale, insertion]
|
||||
.spacing(6)
|
||||
.width(Length::FillPortion(1));
|
||||
|
||||
// ── Path type, rotation, block unit ─────────────────────────────────
|
||||
let path_type = group(
|
||||
t!("Path type").into_owned(),
|
||||
pick_list(Some(state.path_type), PathTypeChoice::ALL, |choice| choice.to_string())
|
||||
.on_select(|v| msg(XrefAttachMsg::PathType(v)))
|
||||
.text_size(11)
|
||||
.padding([3, 6])
|
||||
.width(Fill),
|
||||
Length::Shrink,
|
||||
);
|
||||
let mut angle = text_input("", &state.rotation)
|
||||
.size(11)
|
||||
.padding([3, 6])
|
||||
.width(Fill)
|
||||
.style(field_style);
|
||||
if !state.rotation_on_screen {
|
||||
angle = angle.on_input(|v| msg(XrefAttachMsg::Rotation(v)));
|
||||
}
|
||||
let rotation = group(
|
||||
t!("Rotation").into_owned(),
|
||||
column![
|
||||
labeled_checkbox(t!("Specify On-screen"), state.rotation_on_screen, |v| msg(
|
||||
XrefAttachMsg::RotationOnScreen(v)
|
||||
)),
|
||||
row![
|
||||
text(t!("Angle:"))
|
||||
.size(11)
|
||||
.style(muted_style)
|
||||
.width(Length::Fixed(44.0)),
|
||||
angle
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center),
|
||||
]
|
||||
.spacing(5),
|
||||
Length::Shrink,
|
||||
);
|
||||
let block_unit = group(
|
||||
t!("Block Unit").into_owned(),
|
||||
column![
|
||||
read_only_field(t!("Unit:").into_owned(), &state.unit_label, 52.0),
|
||||
read_only_field(t!("Factor:").into_owned(), &state.unit_factor, 52.0),
|
||||
]
|
||||
.spacing(5),
|
||||
Length::Shrink,
|
||||
);
|
||||
let right = column![path_type, rotation, block_unit]
|
||||
.spacing(6)
|
||||
.width(Length::FillPortion(1));
|
||||
|
||||
let body = row![left, middle, right].spacing(6).width(Fill);
|
||||
|
||||
// ── Details ─────────────────────────────────────────────────────────
|
||||
let details: Element<'a, Message> = if state.details {
|
||||
group(
|
||||
t!("Location").into_owned(),
|
||||
column![
|
||||
read_only_field(t!("Found in:").into_owned(), &state.found_in, 92.0),
|
||||
read_only_field(t!("Saved path:").into_owned(), &state.saved_path, 92.0),
|
||||
]
|
||||
.spacing(5),
|
||||
Length::Shrink,
|
||||
)
|
||||
} else {
|
||||
Space::new().height(0).into()
|
||||
};
|
||||
|
||||
// ── Footer ──────────────────────────────────────────────────────────
|
||||
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(XrefAttachMsg::Details(!state.details)))
|
||||
.style(button_style(false))
|
||||
.padding([4, 12]),
|
||||
Space::new().width(Fill),
|
||||
button(text(t!("OK")).size(11))
|
||||
.on_press(msg(XrefAttachMsg::Apply))
|
||||
.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(XrefAttachMsg::Help))
|
||||
.style(button_style(false))
|
||||
.padding([4, 12]),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center);
|
||||
|
||||
column![name_section, body, details, footer]
|
||||
.spacing(6)
|
||||
.padding([8, 10])
|
||||
.width(sizing.width)
|
||||
.into()
|
||||
}
|
||||
Reference in new issue
Block a user