forked from 1b-agent/opencadstudio-upstream
DWF/DGN underlays: clipping, layers, text, 3D DGN, colours and zoom extents
- PDFOSNAP, DWFOSNAP and DGNOSNAP are separate; UOSNAP sets them all and reads 2 while they differ. Enable Snap switches the selected kind's. - DWFCLIP / DGNCLIP clip their own kind with the kind's prompts and message; CLIP routes DWF and DGN underlays there; Enter at a corner asks again. - The right-edge underlay tools show for DWF and DGN too (import is PDF only). - Underlay layers: DGN levels (V8 level table, "Default" listed as "0"; V7 used levels named from the level name table), hidden through the layer override data with the DGN placeholder string first; a plotted DWF has none. - V8 colour read from its colour word (by level, index, or a true colour drawn as its nearest index); 3D V8 elements drawn in plan (lines, line strings, ellipses and arcs by their orientation, text); a model's extent includes its text. - DWF 6 TrueType text (font, height, rotation from the font opcode) and DWFx glyph runs are drawn; the default W2D colour map is the measured one. - Contrast pulls lightness towards a per-kind pivot as measured (PDF 0.5, DWF 0.243, DGN 0.65). - Zoom extents includes raster images and underlays whose frames are hidden. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1cbe89ef62
commit
56ecf786f1
41 files changed
+593
-147
No files matched your search
@@ -3560,7 +3560,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3547,7 +3547,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3545,7 +3545,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3548,7 +3548,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3548,7 +3548,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3549,7 +3549,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3548,7 +3548,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3547,7 +3547,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3548,7 +3548,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3536,7 +3536,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3547,7 +3547,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3547,7 +3547,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3538,7 +3538,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3547,7 +3547,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3546,7 +3546,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3547,7 +3547,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3548,7 +3548,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3549,7 +3549,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3522,7 +3522,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vektör geometri
|
.pdfui-vector-geometry = Vektör geometri
|
||||||
.pdfui-pdf-data-to-import = İçe aktarılacak PDF verisi
|
.pdfui-pdf-data-to-import = İçe aktarılacak PDF verisi
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Katmanlarını görmek için bir altlık seçin.
|
.pdfui-select-an-underlay-to-view-its-layers = Katmanlarını görmek için bir altlık seçin.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = Bu dosya herhangi bir katman içermiyor.
|
.pdfui-this-file-does-not-contain-any-layers = Bu dosya herhangi bir katman içermiyor
|
||||||
.pdfui-search-for-layer = Katman ara
|
.pdfui-search-for-layer = Katman ara
|
||||||
.pdfui-reference-name = Referans adı:
|
.pdfui-reference-name = Referans adı:
|
||||||
.pdfui-page-size = Sayfa boyutu:
|
.pdfui-page-size = Sayfa boyutu:
|
||||||
|
|||||||
@@ -3535,7 +3535,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -3547,7 +3547,7 @@ insert =
|
|||||||
.pdfui-vector-geometry = Vector geometry
|
.pdfui-vector-geometry = Vector geometry
|
||||||
.pdfui-pdf-data-to-import = PDF data to import
|
.pdfui-pdf-data-to-import = PDF data to import
|
||||||
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
.pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers.
|
||||||
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers.
|
.pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers
|
||||||
.pdfui-search-for-layer = Search for layer
|
.pdfui-search-for-layer = Search for layer
|
||||||
.pdfui-reference-name = Reference name:
|
.pdfui-reference-name = Reference name:
|
||||||
.pdfui-page-size = Page size:
|
.pdfui-page-size = Page size:
|
||||||
|
|||||||
@@ -709,9 +709,14 @@ impl OpenCADStudio {
|
|||||||
Err(_) => self.command_line.push_error(&format!("{path} not found.")),
|
Err(_) => self.command_line.push_error(&format!("{path} not found.")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"PDFCLIP" => {
|
"PDFCLIP" | "DWFCLIP" | "DGNCLIP" => {
|
||||||
use crate::command::CadCommand;
|
use crate::command::CadCommand;
|
||||||
let command = crate::modules::insert::pdf_clip::PdfClipCommand::new();
|
let kind = match cmd {
|
||||||
|
"DWFCLIP" => codec::entities::UnderlayType::Dwf,
|
||||||
|
"DGNCLIP" => codec::entities::UnderlayType::Dgn,
|
||||||
|
_ => codec::entities::UnderlayType::Pdf,
|
||||||
|
};
|
||||||
|
let command = crate::modules::insert::pdf_clip::PdfClipCommand::for_kind(kind);
|
||||||
self.command_line.push_info(&command.prompt());
|
self.command_line.push_info(&command.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,8 +73,7 @@ impl OpenCADStudio {
|
|||||||
crate::entities::underlay::display_path(&self.pdf_stored_path(path, state.path_type.0));
|
crate::entities::underlay::display_path(&self.pdf_stored_path(path, state.path_type.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ULAYERS: every PDF underlay of the drawing, the selected one first
|
/// ULAYERS: every underlay of the drawing, the selected one first shown.
|
||||||
/// shown.
|
|
||||||
pub(in crate::app) fn open_underlay_layers_dialog(&mut self, i: usize) {
|
pub(in crate::app) fn open_underlay_layers_dialog(&mut self, i: usize) {
|
||||||
let document = &self.tabs[i].scene.document;
|
let document = &self.tabs[i].scene.document;
|
||||||
let selected: Vec<codec::Handle> = self.tabs[i]
|
let selected: Vec<codec::Handle> = self.tabs[i]
|
||||||
@@ -88,9 +87,6 @@ impl OpenCADStudio {
|
|||||||
let codec::EntityType::Underlay(u) = entity else {
|
let codec::EntityType::Underlay(u) = entity else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if u.underlay_type != codec::entities::UnderlayType::Pdf {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Some(def) = crate::entities::underlay::definition(u, document) else {
|
let Some(def) = crate::entities::underlay::definition(u, document) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -99,10 +95,13 @@ impl OpenCADStudio {
|
|||||||
if same > 0 {
|
if same > 0 {
|
||||||
name = format!("{name} ({})", same + 1);
|
name = format!("{name} ({})", same + 1);
|
||||||
}
|
}
|
||||||
let mut layers: Vec<String> = crate::scene::model::pdf_layers::layers(&def.file_path)
|
let mut layers: Vec<String> = match u.underlay_type {
|
||||||
.into_iter()
|
codec::entities::UnderlayType::Pdf => crate::scene::model::pdf_layers::layers(&def.file_path)
|
||||||
.map(|l| l.name)
|
.into_iter()
|
||||||
.collect();
|
.map(|l| l.name)
|
||||||
|
.collect(),
|
||||||
|
kind => crate::scene::model::underlay_vector::layer_names(kind, &def.file_path),
|
||||||
|
};
|
||||||
layers.sort_by_key(|n| n.to_lowercase());
|
layers.sort_by_key(|n| n.to_lowercase());
|
||||||
layers.dedup();
|
layers.dedup();
|
||||||
targets.push(LayerTarget {
|
targets.push(LayerTarget {
|
||||||
@@ -113,7 +112,7 @@ impl OpenCADStudio {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if targets.is_empty() {
|
if targets.is_empty() {
|
||||||
self.command_line.push_error("No PDF underlays found.");
|
self.command_line.push_error("No underlays found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let current = targets
|
let current = targets
|
||||||
@@ -152,7 +151,7 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
/// Writes the layers each underlay turns off, as one undo step.
|
/// Writes the layers each underlay turns off, as one undo step.
|
||||||
fn apply_underlay_layers(&mut self, i: usize, targets: Vec<LayerTarget>) {
|
fn apply_underlay_layers(&mut self, i: usize, targets: Vec<LayerTarget>) {
|
||||||
use crate::scene::model::pdf_layers::{hidden_layers, LAYER_OVERRIDE_APP};
|
use crate::scene::model::pdf_layers::{hidden_layers, DGN_OVERRIDE_PLACEHOLDER, LAYER_OVERRIDE_APP};
|
||||||
let changed: Vec<LayerTarget> = targets
|
let changed: Vec<LayerTarget> = targets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|t| match self.tabs[i].scene.document.get_entity(t.handle) {
|
.filter(|t| match self.tabs[i].scene.document.get_entity(t.handle) {
|
||||||
@@ -182,6 +181,10 @@ impl OpenCADStudio {
|
|||||||
data.remove_record(LAYER_OVERRIDE_APP);
|
data.remove_record(LAYER_OVERRIDE_APP);
|
||||||
if !target.hidden.is_empty() {
|
if !target.hidden.is_empty() {
|
||||||
let mut record = codec::xdata::ExtendedDataRecord::new(LAYER_OVERRIDE_APP);
|
let mut record = codec::xdata::ExtendedDataRecord::new(LAYER_OVERRIDE_APP);
|
||||||
|
// A DGN underlay's names follow a placeholder string.
|
||||||
|
if u.underlay_type == codec::entities::UnderlayType::Dgn {
|
||||||
|
record.add_value(codec::xdata::XDataValue::String(DGN_OVERRIDE_PLACEHOLDER.to_string()));
|
||||||
|
}
|
||||||
for name in &target.hidden {
|
for name in &target.hidden {
|
||||||
record.add_value(codec::xdata::XDataValue::String(name.clone()));
|
record.add_value(codec::xdata::XDataValue::String(name.clone()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
//! The right-edge PDF underlay and xref tools: their state comes from the
|
//! The right-edge underlay and xref tools: their state comes from the
|
||||||
//! selection, their buttons edit it.
|
//! selection, their buttons edit it.
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use codec::entities::{Underlay, UnderlayDisplayFlags, UnderlayType};
|
use codec::entities::{Underlay, UnderlayDisplayFlags};
|
||||||
use crate::ui::ribbon::UnderlayContext;
|
use crate::ui::ribbon::UnderlayContext;
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
/// The selected PDF underlays, or nothing when anything else is selected.
|
/// The selected underlays when they are all of one kind, or nothing
|
||||||
|
/// when anything else (or another kind) is selected.
|
||||||
fn selected_pdf_underlays(&self, i: usize) -> Vec<(codec::Handle, Underlay)> {
|
fn selected_pdf_underlays(&self, i: usize) -> Vec<(codec::Handle, Underlay)> {
|
||||||
let mut out = Vec::new();
|
let mut out: Vec<(codec::Handle, Underlay)> = Vec::new();
|
||||||
for (handle, entity) in self.tabs[i].scene.selected_entities() {
|
for (handle, entity) in self.tabs[i].scene.selected_entities() {
|
||||||
match entity {
|
match entity {
|
||||||
codec::EntityType::Underlay(u) if u.underlay_type == UnderlayType::Pdf => {
|
codec::EntityType::Underlay(u)
|
||||||
|
if out.first().is_none_or(|(_, first)| first.underlay_type == u.underlay_type) =>
|
||||||
|
{
|
||||||
out.push((handle, u.clone()));
|
out.push((handle, u.clone()));
|
||||||
}
|
}
|
||||||
_ => return Vec::new(),
|
_ => return Vec::new(),
|
||||||
@@ -51,9 +54,10 @@ impl OpenCADStudio {
|
|||||||
} else {
|
} else {
|
||||||
self.selected_pdf_underlays(i).first().map(|(_, u)| {
|
self.selected_pdf_underlays(i).first().map(|(_, u)| {
|
||||||
UnderlayContext {
|
UnderlayContext {
|
||||||
|
kind: u.underlay_type,
|
||||||
monochrome: u.flags.contains(UnderlayDisplayFlags::MONOCHROME),
|
monochrome: u.flags.contains(UnderlayDisplayFlags::MONOCHROME),
|
||||||
shown: u.flags.contains(UnderlayDisplayFlags::ON),
|
shown: u.flags.contains(UnderlayDisplayFlags::ON),
|
||||||
snap: crate::scene::model::pdf_vector::pdf_osnap(),
|
snap: crate::scene::model::pdf_vector::underlay_osnap(u.underlay_type),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@@ -106,9 +110,14 @@ impl OpenCADStudio {
|
|||||||
.is_some_and(|(_, u)| u.flags.contains(UnderlayDisplayFlags::ON));
|
.is_some_and(|(_, u)| u.flags.contains(UnderlayDisplayFlags::ON));
|
||||||
self.edit_selected_underlays(i, "PDFUNDERLAY", |u| u.set_on(on));
|
self.edit_selected_underlays(i, "PDFUNDERLAY", |u| u.set_on(on));
|
||||||
}
|
}
|
||||||
|
// Enable Snap switches PDFOSNAP, DWFOSNAP or DGNOSNAP.
|
||||||
"_PDFULSNAP" => {
|
"_PDFULSNAP" => {
|
||||||
let on = !crate::scene::model::pdf_vector::pdf_osnap();
|
let Some((_, underlay)) = self.selected_pdf_underlays(i).into_iter().next() else {
|
||||||
crate::scene::model::pdf_vector::set_pdf_osnap(on);
|
return Some(Task::none());
|
||||||
|
};
|
||||||
|
let kind = underlay.underlay_type;
|
||||||
|
let on = !crate::scene::model::pdf_vector::underlay_osnap(kind);
|
||||||
|
crate::scene::model::pdf_vector::set_underlay_osnap(kind, on);
|
||||||
self.tabs[i].scene.reseed_underlays();
|
self.tabs[i].scene.reseed_underlays();
|
||||||
self.sync_underlay_tab();
|
self.sync_underlay_tab();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1183,16 +1183,27 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
return Some(self.finish_dispatch(cmd));
|
return Some(self.finish_dispatch(cmd));
|
||||||
}
|
}
|
||||||
// ponytail: one flag for every underlay kind; split it if a drawing
|
|
||||||
// needs PDF snaps on while DWF/DGN snaps are off.
|
|
||||||
if matches!(name.as_str(), "PDFOSNAP" | "DWFOSNAP" | "DGNOSNAP" | "UOSNAP") {
|
if matches!(name.as_str(), "PDFOSNAP" | "DWFOSNAP" | "DGNOSNAP" | "UOSNAP") {
|
||||||
let current = i16::from(crate::scene::model::pdf_vector::pdf_osnap());
|
use crate::scene::model::pdf_vector as pv;
|
||||||
|
use codec::entities::UnderlayType;
|
||||||
|
// UOSNAP sets every kind and reads 2 while they differ.
|
||||||
|
let kind = match name.as_str() {
|
||||||
|
"PDFOSNAP" => Some(UnderlayType::Pdf),
|
||||||
|
"DWFOSNAP" => Some(UnderlayType::Dwf),
|
||||||
|
"DGNOSNAP" => Some(UnderlayType::Dgn),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let current = kind.map_or_else(pv::uosnap, |kind| i16::from(pv::underlay_osnap(kind)));
|
||||||
if let Some(value) = &value {
|
if let Some(value) = &value {
|
||||||
match value.parse::<i16>().ok().filter(|value| (0..=1).contains(value)) {
|
match value.parse::<i16>().ok().filter(|value| (0..=1).contains(value)) {
|
||||||
Some(mode) => {
|
Some(mode) => {
|
||||||
if current != mode {
|
if current != mode {
|
||||||
crate::scene::model::pdf_vector::set_pdf_osnap(mode == 1);
|
match kind {
|
||||||
|
Some(kind) => pv::set_underlay_osnap(kind, mode == 1),
|
||||||
|
None => pv::set_uosnap(mode == 1),
|
||||||
|
}
|
||||||
self.tabs[i].scene.reseed_underlays();
|
self.tabs[i].scene.reseed_underlays();
|
||||||
|
self.sync_underlay_tab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => self.command_line.push_error("Requires 0 or 1 only"),
|
None => self.command_line.push_error("Requires 0 or 1 only"),
|
||||||
|
|||||||
+1
-1
@@ -1613,7 +1613,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
} else if let Some(ctx) = self.ribbon.underlay_context() {
|
} else if let Some(ctx) = self.ribbon.underlay_context() {
|
||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
crate::ui::side_toolbar::view_with_active(
|
crate::ui::side_toolbar::view_with_active(
|
||||||
&crate::ui::ribbon::pdf_underlay_tools(),
|
&crate::ui::ribbon::pdf_underlay_tools(ctx.kind),
|
||||||
&move |id| match id {
|
&move |id| match id {
|
||||||
"_PDFULMONO" => ctx.monochrome,
|
"_PDFULMONO" => ctx.monochrome,
|
||||||
"_PDFULSHOW" => ctx.shown,
|
"_PDFULSHOW" => ctx.shown,
|
||||||
|
|||||||
@@ -2985,7 +2985,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"Page size:" => Some(("insert", "pdfui-page-size")),
|
"Page size:" => Some(("insert", "pdfui-page-size")),
|
||||||
"Reference name:" => Some(("insert", "pdfui-reference-name")),
|
"Reference name:" => Some(("insert", "pdfui-reference-name")),
|
||||||
"Search for layer" => Some(("insert", "pdfui-search-for-layer")),
|
"Search for layer" => Some(("insert", "pdfui-search-for-layer")),
|
||||||
"This file does not contain any layers." => Some(("insert", "pdfui-this-file-does-not-contain-any-layers")),
|
"This file does not contain any layers" => Some(("insert", "pdfui-this-file-does-not-contain-any-layers")),
|
||||||
"Select an underlay to view its layers." => Some(("insert", "pdfui-select-an-underlay-to-view-its-layers")),
|
"Select an underlay to view its layers." => Some(("insert", "pdfui-select-an-underlay-to-view-its-layers")),
|
||||||
"PDF data to import" => Some(("insert", "pdfui-pdf-data-to-import")),
|
"PDF data to import" => Some(("insert", "pdfui-pdf-data-to-import")),
|
||||||
"Vector geometry" => Some(("insert", "pdfui-vector-geometry")),
|
"Vector geometry" => Some(("insert", "pdfui-vector-geometry")),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// PDFCLIP / IMAGECLIP — clip a PDF underlay or a raster image to a boundary
|
// PDFCLIP / DWFCLIP / DGNCLIP / IMAGECLIP — clip an underlay or a raster
|
||||||
// (CLIP continues here for either).
|
// image to a boundary (CLIP continues here for either).
|
||||||
//
|
//
|
||||||
// Select PDF to clip: (Select image to clip:)
|
// Select PDF to clip: (DWF / DGN; Select image to clip:)
|
||||||
// Enter PDF clipping option [ON/OFF/Delete/New boundary] <New boundary>:
|
// Enter PDF clipping option [ON/OFF/Delete/New boundary] <New boundary>:
|
||||||
// (Enter image clipping option [ON/OFF/Delete/New boundary] <New>:)
|
// (Enter image clipping option [ON/OFF/Delete/New boundary] <New>:)
|
||||||
// Delete old boundary? [Yes/No] <Yes>: (when one exists; [No/Yes] for
|
// Delete old boundary? [Yes/No] <Yes>: (when one exists; [No/Yes] for
|
||||||
@@ -38,6 +38,8 @@ enum Step {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct PdfClipCommand {
|
pub struct PdfClipCommand {
|
||||||
|
/// The underlay kind PDFCLIP / DWFCLIP / DGNCLIP clips.
|
||||||
|
kind: UnderlayType,
|
||||||
step: Step,
|
step: Step,
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
underlay: Option<Underlay>,
|
underlay: Option<Underlay>,
|
||||||
@@ -55,7 +57,13 @@ const BOUNDARY: &str = "Specify clipping boundary or select invert option:";
|
|||||||
|
|
||||||
impl PdfClipCommand {
|
impl PdfClipCommand {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
Self::for_kind(UnderlayType::Pdf)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DWFCLIP / DGNCLIP (or PDFCLIP): asks for an underlay of the kind.
|
||||||
|
pub fn for_kind(kind: UnderlayType) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
kind,
|
||||||
step: Step::Select,
|
step: Step::Select,
|
||||||
handle: Handle::NULL,
|
handle: Handle::NULL,
|
||||||
underlay: None,
|
underlay: None,
|
||||||
@@ -147,7 +155,7 @@ impl PdfClipCommand {
|
|||||||
/// Create Clipping Boundary): the New boundary branch at once. Returns the
|
/// Create Clipping Boundary): the New boundary branch at once. Returns the
|
||||||
/// command and the result of its first step.
|
/// command and the result of its first step.
|
||||||
pub fn new_boundary(handle: Handle, underlay: Underlay) -> (Self, CmdResult) {
|
pub fn new_boundary(handle: Handle, underlay: Underlay) -> (Self, CmdResult) {
|
||||||
let mut command = Self::new();
|
let mut command = Self::for_kind(underlay.underlay_type);
|
||||||
command.handle = handle;
|
command.handle = handle;
|
||||||
command.underlay = Some(underlay);
|
command.underlay = Some(underlay);
|
||||||
command.step = Step::Option;
|
command.step = Step::Option;
|
||||||
@@ -157,7 +165,7 @@ impl PdfClipCommand {
|
|||||||
|
|
||||||
/// An underlay CLIP picked: straight to the clipping option.
|
/// An underlay CLIP picked: straight to the clipping option.
|
||||||
pub fn for_underlay(handle: Handle, underlay: Underlay) -> Self {
|
pub fn for_underlay(handle: Handle, underlay: Underlay) -> Self {
|
||||||
let mut command = Self::new();
|
let mut command = Self::for_kind(underlay.underlay_type);
|
||||||
command.handle = handle;
|
command.handle = handle;
|
||||||
command.underlay = Some(underlay);
|
command.underlay = Some(underlay);
|
||||||
command.step = Step::Option;
|
command.step = Step::Option;
|
||||||
@@ -255,6 +263,14 @@ impl PdfClipCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn kind_name(&self) -> &'static str {
|
||||||
|
match self.kind {
|
||||||
|
UnderlayType::Pdf => "PDF",
|
||||||
|
UnderlayType::Dwf => "DWF",
|
||||||
|
UnderlayType::Dgn => "DGN",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn rectangle(a: DVec3, b: DVec3) -> Vec<DVec3> {
|
fn rectangle(a: DVec3, b: DVec3) -> Vec<DVec3> {
|
||||||
vec![
|
vec![
|
||||||
a,
|
a,
|
||||||
@@ -267,19 +283,25 @@ impl PdfClipCommand {
|
|||||||
|
|
||||||
impl CadCommand for PdfClipCommand {
|
impl CadCommand for PdfClipCommand {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
if self.image_mode { "IMAGECLIP" } else { "PDFCLIP" }
|
match (self.image_mode, self.kind) {
|
||||||
|
(true, _) => "IMAGECLIP",
|
||||||
|
(false, UnderlayType::Pdf) => "PDFCLIP",
|
||||||
|
(false, UnderlayType::Dwf) => "DWFCLIP",
|
||||||
|
(false, UnderlayType::Dgn) => "DGNCLIP",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::Select if self.image_mode => "Select image to clip:".to_string(),
|
Step::Select if self.image_mode => "Select image to clip:".to_string(),
|
||||||
Step::Select => "Select PDF to clip:".to_string(),
|
Step::Select => format!("Select {} to clip:", self.kind_name()),
|
||||||
Step::Option if self.image_mode => {
|
Step::Option if self.image_mode => {
|
||||||
"Enter image clipping option [ON/OFF/Delete/New boundary] <New>:".to_string()
|
"Enter image clipping option [ON/OFF/Delete/New boundary] <New>:".to_string()
|
||||||
}
|
}
|
||||||
Step::Option => {
|
Step::Option => format!(
|
||||||
"Enter PDF clipping option [ON/OFF/Delete/New boundary] <New boundary>:".to_string()
|
"Enter {} clipping option [ON/OFF/Delete/New boundary] <New boundary>:",
|
||||||
}
|
self.kind_name()
|
||||||
|
),
|
||||||
Step::DeleteOld if self.image_mode => "Delete old boundary? [No/Yes] <Yes>:".to_string(),
|
Step::DeleteOld if self.image_mode => "Delete old boundary? [No/Yes] <Yes>:".to_string(),
|
||||||
Step::DeleteOld => "Delete old boundary? [Yes/No] <Yes>:".to_string(),
|
Step::DeleteOld => "Delete old boundary? [Yes/No] <Yes>:".to_string(),
|
||||||
Step::Mode => {
|
Step::Mode => {
|
||||||
@@ -359,13 +381,16 @@ impl CadCommand for PdfClipCommand {
|
|||||||
_ => CmdResult::NeedPoint,
|
_ => CmdResult::NeedPoint,
|
||||||
},
|
},
|
||||||
Step::Select => match picked {
|
Step::Select => match picked {
|
||||||
Some(EntityType::Underlay(u)) if u.underlay_type == UnderlayType::Pdf => {
|
Some(EntityType::Underlay(u)) if u.underlay_type == self.kind => {
|
||||||
self.handle = handle;
|
self.handle = handle;
|
||||||
self.underlay = Some(u);
|
self.underlay = Some(u);
|
||||||
self.step = Step::Option;
|
self.step = Step::Option;
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
_ => CmdResult::ReportError("Object selected was not a PDF underlay.".to_string()),
|
_ => CmdResult::ReportError(format!(
|
||||||
|
"Object selected was not a {} underlay.",
|
||||||
|
self.kind_name()
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
Step::Polyline => match picked {
|
Step::Polyline => match picked {
|
||||||
Some(EntityType::LwPolyline(pl)) if pl.vertices.len() >= 3 => {
|
Some(EntityType::LwPolyline(pl)) if pl.vertices.len() >= 3 => {
|
||||||
@@ -440,6 +465,8 @@ impl CadCommand for PdfClipCommand {
|
|||||||
self.apply_boundary(&points, false)
|
self.apply_boundary(&points, false)
|
||||||
}
|
}
|
||||||
Step::PolyPoints => CmdResult::NeedPoint,
|
Step::PolyPoints => CmdResult::NeedPoint,
|
||||||
|
// A corner is asked again, as the reference does.
|
||||||
|
Step::RectFirst | Step::RectSecond => CmdResult::NeedPoint,
|
||||||
_ => CmdResult::Cancel,
|
_ => CmdResult::Cancel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,5 +488,5 @@ impl CadCommand for PdfClipCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inventory::submit!(crate::command::CommandRegistration {
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
names: &["PDFCLIP", "IMAGECLIP"]
|
names: &["PDFCLIP", "DWFCLIP", "DGNCLIP", "IMAGECLIP"]
|
||||||
});
|
});
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
// underlay as PDFCLIP, a raster image as IMAGECLIP, a layout viewport as
|
// underlay as PDFCLIP, a raster image as IMAGECLIP, a layout viewport as
|
||||||
// VPCLIP, anything else is "*Invalid selection*".
|
// VPCLIP, anything else is "*Invalid selection*".
|
||||||
|
|
||||||
use codec::entities::UnderlayType;
|
|
||||||
use codec::types::Handle;
|
use codec::types::Handle;
|
||||||
use codec::EntityType;
|
use codec::EntityType;
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
@@ -488,7 +487,7 @@ impl CadCommand for ClipCommand {
|
|||||||
self.inner = Some(Box::new(XclipCommand::for_inserts(vec![handle], false)));
|
self.inner = Some(Box::new(XclipCommand::for_inserts(vec![handle], false)));
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
Some(EntityType::Underlay(u)) if u.underlay_type == UnderlayType::Pdf => {
|
Some(EntityType::Underlay(u)) => {
|
||||||
self.inner = Some(Box::new(PdfClipCommand::for_underlay(handle, u)));
|
self.inner = Some(Box::new(PdfClipCommand::for_underlay(handle, u)));
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-2
@@ -1405,7 +1405,26 @@ impl Scene {
|
|||||||
&& z[1].is_finite()
|
&& z[1].is_finite()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
if wires.is_empty() && mesh_aabbs.is_empty() && infinite_base_pts.is_empty() {
|
// Raster images and underlays draw as GPU quads; with their frames
|
||||||
|
// hidden they carry no wire outline, so fold their corners in too.
|
||||||
|
let image_corners: Vec<glam::Vec3> = self
|
||||||
|
.images
|
||||||
|
.iter()
|
||||||
|
.filter(|(h, _)| {
|
||||||
|
let handle = **h;
|
||||||
|
self.mesh_entity_visible(handle)
|
||||||
|
&& self.document.get_entity(handle).is_some_and(|entity| {
|
||||||
|
self.belongs_to_visible_block(handle, entity.common().owner_handle, layout_block)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.flat_map(|(_, image)| {
|
||||||
|
image.corners.iter().zip(image.corners_low.iter()).map(|(c, low)| {
|
||||||
|
glam::Vec3::new(c[0] + low[0], c[1] + low[1], c[2] + low[2])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.filter(|p| p.is_finite())
|
||||||
|
.collect();
|
||||||
|
if wires.is_empty() && mesh_aabbs.is_empty() && infinite_base_pts.is_empty() && image_corners.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1449,7 +1468,7 @@ impl Scene {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cents.is_empty() && mesh_aabbs.is_empty() && infinite_base_pts.is_empty() {
|
if cents.is_empty() && mesh_aabbs.is_empty() && infinite_base_pts.is_empty() && image_corners.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1516,6 +1535,10 @@ impl Scene {
|
|||||||
min = min.min(glam::Vec3::new(*ax, *ay, *az));
|
min = min.min(glam::Vec3::new(*ax, *ay, *az));
|
||||||
max = max.max(glam::Vec3::new(*bx, *by, *bz));
|
max = max.max(glam::Vec3::new(*bx, *by, *bz));
|
||||||
}
|
}
|
||||||
|
for p in &image_corners {
|
||||||
|
min = min.min(*p);
|
||||||
|
max = max.max(*p);
|
||||||
|
}
|
||||||
// Drawing holds only infinite construction geometry: fit the view to
|
// Drawing holds only infinite construction geometry: fit the view to
|
||||||
// its base points rather than leaving the camera unchanged.
|
// its base points rather than leaving the camera unchanged.
|
||||||
if min.x > max.x {
|
if min.x > max.x {
|
||||||
|
|||||||
@@ -222,15 +222,24 @@ impl ImageModel {
|
|||||||
};
|
};
|
||||||
(source, raster)
|
(source, raster)
|
||||||
}
|
}
|
||||||
kind => (
|
kind => {
|
||||||
def.file_path.clone(),
|
let hidden = super::pdf_layers::hidden_layers(u);
|
||||||
super::underlay_vector::display_raster(
|
// The adjusted-pixel memo keys on the source: one per set of
|
||||||
|
// hidden layers.
|
||||||
|
let source = if hidden.is_empty() {
|
||||||
|
def.file_path.clone()
|
||||||
|
} else {
|
||||||
|
format!("{}#{}", def.file_path, hidden.join("|"))
|
||||||
|
};
|
||||||
|
let raster = super::underlay_vector::display_raster(
|
||||||
kind,
|
kind,
|
||||||
&def.file_path,
|
&def.file_path,
|
||||||
page,
|
page,
|
||||||
|
&hidden,
|
||||||
screen_side.unwrap_or(super::underlay_vector::RASTER_SIDE),
|
screen_side.unwrap_or(super::underlay_vector::RASTER_SIDE),
|
||||||
)?,
|
)?;
|
||||||
),
|
(source, raster)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// Dark means an HSL lightness under one half: pure blue counts as
|
// Dark means an HSL lightness under one half: pure blue counts as
|
||||||
// light, (0, 128, 0) as dark.
|
// light, (0, 128, 0) as dark.
|
||||||
@@ -243,6 +252,11 @@ impl ImageModel {
|
|||||||
&raster,
|
&raster,
|
||||||
PageAdjust {
|
PageAdjust {
|
||||||
contrast: u.contrast.min(100),
|
contrast: u.contrast.min(100),
|
||||||
|
contrast_pivot: match def.underlay_type {
|
||||||
|
UnderlayType::Pdf => 500,
|
||||||
|
UnderlayType::Dwf => 243,
|
||||||
|
UnderlayType::Dgn => 650,
|
||||||
|
},
|
||||||
monochrome: u.flags.contains(UnderlayDisplayFlags::MONOCHROME),
|
monochrome: u.flags.contains(UnderlayDisplayFlags::MONOCHROME),
|
||||||
adjust_for_background: u.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND),
|
adjust_for_background: u.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND),
|
||||||
// A DGN model is drawn for a black background, so its
|
// A DGN model is drawn for a black background, so its
|
||||||
|
|||||||
@@ -231,6 +231,9 @@ fn without_key(dict: &str, key: &str) -> Option<String> {
|
|||||||
/// them.
|
/// them.
|
||||||
pub const LAYER_OVERRIDE_APP: &str = "AdeskUnderlayLayerOverrideData";
|
pub const LAYER_OVERRIDE_APP: &str = "AdeskUnderlayLayerOverrideData";
|
||||||
|
|
||||||
|
/// The first string of a DGN underlay's layer overrides, before the names.
|
||||||
|
pub const DGN_OVERRIDE_PLACEHOLDER: &str = "???DGN_Placeholder???";
|
||||||
|
|
||||||
pub fn hidden_layers(u: &codec::entities::Underlay) -> Vec<String> {
|
pub fn hidden_layers(u: &codec::entities::Underlay) -> Vec<String> {
|
||||||
u.common
|
u.common
|
||||||
.extended_data
|
.extended_data
|
||||||
@@ -239,7 +242,7 @@ pub fn hidden_layers(u: &codec::entities::Underlay) -> Vec<String> {
|
|||||||
.filter(|r| r.application_name.eq_ignore_ascii_case(LAYER_OVERRIDE_APP))
|
.filter(|r| r.application_name.eq_ignore_ascii_case(LAYER_OVERRIDE_APP))
|
||||||
.flat_map(|r| r.values.iter())
|
.flat_map(|r| r.values.iter())
|
||||||
.filter_map(|v| match v {
|
.filter_map(|v| match v {
|
||||||
codec::xdata::XDataValue::String(s) => Some(s.clone()),
|
codec::xdata::XDataValue::String(s) if s != DGN_OVERRIDE_PLACEHOLDER => Some(s.clone()),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
@@ -214,8 +214,11 @@ pub fn page_size_inches(path: &str, page: &str) -> Option<(f64, f64)> {
|
|||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct PageAdjust {
|
pub struct PageAdjust {
|
||||||
/// 0..=100; 100 keeps the colours, lower values pull their lightness
|
/// 0..=100; 100 keeps the colours, lower values pull their lightness
|
||||||
/// towards one third.
|
/// towards `contrast_pivot` (hue and saturation kept).
|
||||||
pub contrast: u8,
|
pub contrast: u8,
|
||||||
|
/// The lightness contrast 0 leaves every colour at, in thousandths:
|
||||||
|
/// measured 500 for PDF, 243 for DWF and 650 for DGN underlays.
|
||||||
|
pub contrast_pivot: u16,
|
||||||
pub monochrome: bool,
|
pub monochrome: bool,
|
||||||
/// Lighten dark, unsaturated content on a dark background (black lines
|
/// Lighten dark, unsaturated content on a dark background (black lines
|
||||||
/// and text become white), keeping coloured content.
|
/// and text become white), keeping coloured content.
|
||||||
@@ -266,11 +269,12 @@ pub fn adjusted_pixels(path: &str, page: &str, raster: &PdfPage, adjust: PageAdj
|
|||||||
rgb = hsl_to_rgb(h, s, (1.0 - l + 20.0 / 255.0).min(1.0));
|
rgb = hsl_to_rgb(h, s, (1.0 - l + 20.0 / 255.0).min(1.0));
|
||||||
}
|
}
|
||||||
if adjust.contrast < 100 {
|
if adjust.contrast < 100 {
|
||||||
// Lightness is pulled towards one third, hue and saturation kept:
|
// Lightness is pulled towards the pivot, hue and saturation kept:
|
||||||
// contrast 0 leaves every colour at a third of full lightness.
|
// contrast 0 leaves every colour at the pivot's lightness.
|
||||||
let (h, s, l) = rgb_to_hsl(rgb);
|
let (h, s, l) = rgb_to_hsl(rgb);
|
||||||
let k = adjust.contrast as f32 / 100.0;
|
let k = adjust.contrast as f32 / 100.0;
|
||||||
rgb = hsl_to_rgb(h, s, 1.0 / 3.0 + (l - 1.0 / 3.0) * k);
|
let pivot = adjust.contrast_pivot as f32 / 1000.0;
|
||||||
|
rgb = hsl_to_rgb(h, s, pivot + (l - pivot) * k);
|
||||||
}
|
}
|
||||||
for (dst, c) in px[..3].iter_mut().zip(rgb) {
|
for (dst, c) in px[..3].iter_mut().zip(rgb) {
|
||||||
*dst = (c.clamp(0.0, 1.0) * 255.0).round() as u8;
|
*dst = (c.clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||||
|
|||||||
@@ -600,15 +600,40 @@ pub fn circle_of(sp: &SubPath) -> Option<([f64; 2], f64)> {
|
|||||||
|
|
||||||
// ── Snapping ────────────────────────────────────────────────────────────────
|
// ── Snapping ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
static PDF_OSNAP: AtomicBool = AtomicBool::new(true);
|
/// PDFOSNAP, DWFOSNAP and DGNOSNAP: whether object snaps find the geometry
|
||||||
|
/// inside underlays of each kind.
|
||||||
|
static UNDERLAY_OSNAP: [AtomicBool; 3] = [AtomicBool::new(true), AtomicBool::new(true), AtomicBool::new(true)];
|
||||||
|
|
||||||
/// PDFOSNAP: whether object snaps find the geometry inside PDF underlays.
|
fn osnap_slot(kind: codec::entities::UnderlayType) -> &'static AtomicBool {
|
||||||
pub fn pdf_osnap() -> bool {
|
match kind {
|
||||||
PDF_OSNAP.load(Ordering::Relaxed)
|
codec::entities::UnderlayType::Pdf => &UNDERLAY_OSNAP[0],
|
||||||
|
codec::entities::UnderlayType::Dwf => &UNDERLAY_OSNAP[1],
|
||||||
|
codec::entities::UnderlayType::Dgn => &UNDERLAY_OSNAP[2],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_pdf_osnap(on: bool) {
|
pub fn underlay_osnap(kind: codec::entities::UnderlayType) -> bool {
|
||||||
PDF_OSNAP.store(on, Ordering::Relaxed);
|
osnap_slot(kind).load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_underlay_osnap(kind: codec::entities::UnderlayType, on: bool) {
|
||||||
|
osnap_slot(kind).store(on, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// UOSNAP: 1 when snaps find every kind of underlay geometry, 0 when none,
|
||||||
|
/// 2 when the kinds differ.
|
||||||
|
pub fn uosnap() -> i16 {
|
||||||
|
match UNDERLAY_OSNAP.iter().filter(|s| s.load(Ordering::Relaxed)).count() {
|
||||||
|
0 => 0,
|
||||||
|
n if n == UNDERLAY_OSNAP.len() => 1,
|
||||||
|
_ => 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_uosnap(on: bool) {
|
||||||
|
for slot in &UNDERLAY_OSNAP {
|
||||||
|
slot.store(on, Ordering::Relaxed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Most snap points one underlay contributes.
|
/// Most snap points one underlay contributes.
|
||||||
@@ -623,7 +648,7 @@ pub fn underlay_snap_points(
|
|||||||
document: &codec::CadDocument,
|
document: &codec::CadDocument,
|
||||||
) -> Vec<(glam::DVec3, crate::scene::model::wire_model::SnapHint)> {
|
) -> Vec<(glam::DVec3, crate::scene::model::wire_model::SnapHint)> {
|
||||||
use crate::scene::model::wire_model::SnapHint;
|
use crate::scene::model::wire_model::SnapHint;
|
||||||
if !pdf_osnap() || !u.flags.contains(codec::entities::UnderlayDisplayFlags::ON) {
|
if !underlay_osnap(u.underlay_type) || !u.flags.contains(codec::entities::UnderlayDisplayFlags::ON) {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let Some(def) = crate::entities::underlay::definition(u, document) else {
|
let Some(def) = crate::entities::underlay::definition(u, document) else {
|
||||||
@@ -675,7 +700,7 @@ fn underlay_vectors(
|
|||||||
codec::entities::UnderlayType::Pdf => {
|
codec::entities::UnderlayType::Pdf => {
|
||||||
page_vectors(&super::pdf_layers::underlay_source(u, &def.file_path), page)
|
page_vectors(&super::pdf_layers::underlay_source(u, &def.file_path), page)
|
||||||
}
|
}
|
||||||
kind => super::underlay_vector::page_vectors(kind, &def.file_path, page),
|
kind => super::underlay_vector::page_vectors(kind, &def.file_path, page, &super::pdf_layers::hidden_layers(u)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,7 +723,7 @@ pub fn underlay_snap_geometry(
|
|||||||
u: &codec::entities::Underlay,
|
u: &codec::entities::Underlay,
|
||||||
document: &codec::CadDocument,
|
document: &codec::CadDocument,
|
||||||
) -> Vec<SnapPiece> {
|
) -> Vec<SnapPiece> {
|
||||||
if !pdf_osnap() || !u.flags.contains(codec::entities::UnderlayDisplayFlags::ON) {
|
if !underlay_osnap(u.underlay_type) || !u.flags.contains(codec::entities::UnderlayDisplayFlags::ON) {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let Some(def) = crate::entities::underlay::definition(u, document) else {
|
let Some(def) = crate::entities::underlay::definition(u, document) else {
|
||||||
|
|||||||
@@ -262,6 +262,7 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
width_factor: if height > 0.0 { width / height } else { 1.0 },
|
width_factor: if height > 0.0 { width / height } else { 1.0 },
|
||||||
rotation: (rotation as f64 / 360000.0).to_radians(),
|
rotation: (rotation as f64 / 360000.0).to_radians(),
|
||||||
color,
|
color,
|
||||||
|
font: "txt".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
27 => {
|
27 => {
|
||||||
@@ -380,14 +381,65 @@ fn place(raw: &Raw, m: &Affine, defs: &HashMap<String, Raw>, depth: usize, out:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The model's geometry, in master units.
|
/// A level's number (0-63) from an element header.
|
||||||
pub fn model(bytes: &[u8]) -> Option<Sheet> {
|
fn level(e: &[u8]) -> u8 {
|
||||||
|
e[0] & 0x3f
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Level names from the level name table (type 66 elements on level 6):
|
||||||
|
/// each entry is a level number followed by its name.
|
||||||
|
// ponytail: entries are found by that pattern rather than by the table's
|
||||||
|
// record layout; read the layout if a file lists a level wrongly.
|
||||||
|
fn level_names(all: &[&[u8]]) -> HashMap<u8, String> {
|
||||||
|
let mut names = HashMap::new();
|
||||||
|
for e in all.iter().filter(|e| e[1] & 0x7f == 66 && level(e) == 6) {
|
||||||
|
for j in 36..e.len().saturating_sub(3) {
|
||||||
|
let n = u16::from_le_bytes([e[j], e[j + 1]]);
|
||||||
|
if !(1..=63).contains(&n) || (j > 36 && e[j - 1] != 0) || names.contains_key(&(n as u8)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let rest = &e[j + 2..];
|
||||||
|
let len = rest.iter().position(|c| !(c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'$'))).unwrap_or(rest.len());
|
||||||
|
if len > 0 && rest.get(len) == Some(&0) {
|
||||||
|
names.insert(n as u8, String::from_utf8_lossy(&rest[..len]).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The used levels' names (or numbers), for the underlay layers list.
|
||||||
|
pub fn layer_names(bytes: &[u8]) -> Option<Vec<String>> {
|
||||||
|
if !is_v7(bytes) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let all = elements(bytes);
|
||||||
|
let names = level_names(&all);
|
||||||
|
let mut used: Vec<u8> = all
|
||||||
|
.iter()
|
||||||
|
.filter(|e| !matches!(e[1] & 0x7f, 9 | 8 | 10 | 5 | 66 | 34) && level(e) > 0)
|
||||||
|
.map(|e| level(e))
|
||||||
|
.collect();
|
||||||
|
used.sort_unstable();
|
||||||
|
used.dedup();
|
||||||
|
Some(used.into_iter().map(|l| names.get(&l).cloned().unwrap_or_else(|| l.to_string())).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The model's geometry, in master units, without the `hidden` levels.
|
||||||
|
pub fn model(bytes: &[u8], hidden: &[String]) -> Option<Sheet> {
|
||||||
if !is_v7(bytes) {
|
if !is_v7(bytes) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let u = units(bytes)?;
|
let u = units(bytes)?;
|
||||||
let all = elements(bytes);
|
let all = elements(bytes);
|
||||||
let colors = color_table(&all).unwrap_or_else(default_colors);
|
let colors = color_table(&all).unwrap_or_else(default_colors);
|
||||||
|
let names = level_names(&all);
|
||||||
|
let off = |e: &[u8]| {
|
||||||
|
let l = level(e);
|
||||||
|
let name = names.get(&l).cloned().unwrap_or_else(|| l.to_string());
|
||||||
|
hidden.iter().any(|h| h.eq_ignore_ascii_case(&name))
|
||||||
|
};
|
||||||
|
let all: Vec<&[u8]> = all.into_iter().filter(|e| !off(e)).collect();
|
||||||
// Shared cell definitions (type 34) and their components are drawn only
|
// Shared cell definitions (type 34) and their components are drawn only
|
||||||
// where instances place them; everything else is the model itself.
|
// where instances place them; everything else is the model itself.
|
||||||
let mut defs: HashMap<String, Raw> = HashMap::new();
|
let mut defs: HashMap<String, Raw> = HashMap::new();
|
||||||
|
|||||||
@@ -210,15 +210,22 @@ struct Styles {
|
|||||||
colors: Vec<[u8; 3]>,
|
colors: Vec<[u8; 3]>,
|
||||||
/// Level id → colour index.
|
/// Level id → colour index.
|
||||||
levels: HashMap<u32, u32>,
|
levels: HashMap<u32, u32>,
|
||||||
|
/// Levels the underlay turns off: their elements are left out.
|
||||||
|
hidden: std::collections::HashSet<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Styles {
|
impl Styles {
|
||||||
fn color(&self, e: &[u8]) -> [u8; 3] {
|
fn color(&self, e: &[u8]) -> [u8; 3] {
|
||||||
let explicit = u32_at(e, 44).unwrap_or(u32::MAX);
|
// The colour word: an index into the colour table, 0xFFFFFFFF for
|
||||||
let index = if explicit < 256 {
|
// the level's colour, or a true colour — n × 256 plus the index of
|
||||||
explicit
|
// its nearest table colour.
|
||||||
} else {
|
// ponytail: a true colour draws as that nearest index; the RGB it
|
||||||
|
// stands for is not read.
|
||||||
|
let explicit = u32_at(e, 52).unwrap_or(u32::MAX);
|
||||||
|
let index = if explicit == u32::MAX {
|
||||||
self.levels.get(&u32_at(e, 12).unwrap_or(0)).copied().unwrap_or(0)
|
self.levels.get(&u32_at(e, 12).unwrap_or(0)).copied().unwrap_or(0)
|
||||||
|
} else {
|
||||||
|
explicit & 0xff
|
||||||
};
|
};
|
||||||
self.colors.get(index as usize).copied().unwrap_or([255, 255, 255])
|
self.colors.get(index as usize).copied().unwrap_or([255, 255, 255])
|
||||||
}
|
}
|
||||||
@@ -239,6 +246,32 @@ struct Instance {
|
|||||||
origin: [f64; 2],
|
origin: [f64; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A 3D element (flag 0x0800 of its properties word): three coordinates a
|
||||||
|
/// point, orientations as quaternions. Its plan view is drawn.
|
||||||
|
fn is_3d(e: &[u8]) -> bool {
|
||||||
|
u32_at(e, 40).is_some_and(|v| v & 0x0800 != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The in-plane x and y axes (projected onto the plan) of a 3D element's
|
||||||
|
/// orientation quaternion w, x, y, z stored at `at` (the rows of its
|
||||||
|
/// rotation matrix).
|
||||||
|
fn quat_axes(e: &[u8], at: usize) -> Option<([f64; 2], [f64; 2])> {
|
||||||
|
let (w, x, y, z) = (f64_at(e, at)?, f64_at(e, at + 8)?, f64_at(e, at + 16)?, f64_at(e, at + 24)?);
|
||||||
|
Some((
|
||||||
|
[1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y - w * z)],
|
||||||
|
[2.0 * (x * y + w * z), 1.0 - 2.0 * (x * x + z * z)],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An elliptical arc on axes `u` and `v` (plan projections) as cubics.
|
||||||
|
fn projected_arc(c: [f64; 2], a: f64, b: f64, u: [f64; 2], v: [f64; 2], start: f64, sweep: f64) -> Vec<Segment> {
|
||||||
|
let map = |p: [f64; 2]| [c[0] + a * p[0] * u[0] + b * p[1] * v[0], c[1] + a * p[0] * u[1] + b * p[1] * v[1]];
|
||||||
|
arc_cubics([0.0, 0.0], 1.0, 1.0, 0.0, start, sweep)
|
||||||
|
.into_iter()
|
||||||
|
.map(|[p0, p1, p2, p3]| Segment::Cubic(map(p0), map(p1), map(p2), map(p3)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
|
fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
|
||||||
let mut raw = Raw::default();
|
let mut raw = Raw::default();
|
||||||
// A complex shape or chain collects its components into one path.
|
// A complex shape or chain collects its components into one path.
|
||||||
@@ -272,12 +305,13 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for e in run {
|
for e in run {
|
||||||
if e.len() < 104 {
|
if e.len() < 104 || styles.hidden.contains(&u32_at(e, 12).unwrap_or(0)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let color = styles.color(e);
|
let color = styles.color(e);
|
||||||
let stroke = Some((color, -1.0));
|
let stroke = Some((color, -1.0));
|
||||||
let p2 = |at: usize| -> Option<[f64; 2]> { Some([f64_at(e, at)?, f64_at(e, at + 8)?]) };
|
let p2 = |at: usize| -> Option<[f64; 2]> { Some([f64_at(e, at)?, f64_at(e, at + 8)?]) };
|
||||||
|
let d3 = is_3d(e);
|
||||||
match kind(e) {
|
match kind(e) {
|
||||||
12 | 14 => {
|
12 | 14 => {
|
||||||
let n = u32_at(e, 104).unwrap_or(0) as usize;
|
let n = u32_at(e, 104).unwrap_or(0) as usize;
|
||||||
@@ -287,13 +321,14 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
3 => {
|
3 => {
|
||||||
if let (Some(a), Some(b)) = (p2(104), p2(120)) {
|
if let (Some(a), Some(b)) = (p2(104), p2(if d3 { 128 } else { 120 })) {
|
||||||
push(&mut raw, &mut complex, Path { subpaths: vec![SubPath { segments: vec![Segment::Line(a, b)], closed: false }], stroke, fill: None });
|
push(&mut raw, &mut complex, Path { subpaths: vec![SubPath { segments: vec![Segment::Line(a, b)], closed: false }], stroke, fill: None });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
4 | 6 | 11 => {
|
4 | 6 | 11 => {
|
||||||
let n = u32_at(e, 104).unwrap_or(0) as usize;
|
let n = u32_at(e, 104).unwrap_or(0) as usize;
|
||||||
let pts: Vec<[f64; 2]> = (0..n).filter_map(|k| p2(112 + k * 16)).collect();
|
let stride = if d3 { 24 } else { 16 };
|
||||||
|
let pts: Vec<[f64; 2]> = (0..n).filter_map(|k| p2(112 + k * stride)).collect();
|
||||||
if pts.len() >= 2 {
|
if pts.len() >= 2 {
|
||||||
let mut pb = PathBuilder::default();
|
let mut pb = PathBuilder::default();
|
||||||
pb.move_to(pts[0]);
|
pb.move_to(pts[0]);
|
||||||
@@ -306,6 +341,24 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
|
|||||||
push(&mut raw, &mut complex, Path { subpaths: pb.finish(), stroke, fill: None });
|
push(&mut raw, &mut complex, Path { subpaths: pb.finish(), stroke, fill: None });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15 | 16 if d3 => {
|
||||||
|
// Axes, orientation and centre: ellipse a 104, b 112,
|
||||||
|
// quaternion 120, centre 152; arc start 104, sweep 112, a 120,
|
||||||
|
// b 128, quaternion 136, centre 168.
|
||||||
|
let arc = if kind(e) == 15 {
|
||||||
|
(|| Some((f64_at(e, 104)?, f64_at(e, 112)?, quat_axes(e, 120)?, p2(152)?, 0.0, std::f64::consts::TAU)))()
|
||||||
|
} else {
|
||||||
|
(|| Some((f64_at(e, 120)?, f64_at(e, 128)?, quat_axes(e, 136)?, p2(168)?, f64_at(e, 104)?, f64_at(e, 112)?)))()
|
||||||
|
};
|
||||||
|
let Some((a, b, (u, v), c, start, sweep)) = arc else { continue };
|
||||||
|
let sweep = if sweep == 0.0 { std::f64::consts::TAU } else { sweep };
|
||||||
|
let closed = sweep.abs() >= std::f64::consts::TAU - 1e-9;
|
||||||
|
push(
|
||||||
|
&mut raw,
|
||||||
|
&mut complex,
|
||||||
|
Path { subpaths: vec![SubPath { segments: projected_arc(c, a, b, u, v, start, sweep), closed }], stroke, fill: None },
|
||||||
|
);
|
||||||
|
}
|
||||||
15 | 16 => {
|
15 | 16 => {
|
||||||
let arc = if kind(e) == 15 {
|
let arc = if kind(e) == 15 {
|
||||||
(|| Some((f64_at(e, 104)?, f64_at(e, 112)?, f64_at(e, 120)?, p2(128)?, 0.0, std::f64::consts::TAU)))()
|
(|| Some((f64_at(e, 104)?, f64_at(e, 112)?, f64_at(e, 120)?, p2(128)?, 0.0, std::f64::consts::TAU)))()
|
||||||
@@ -321,6 +374,24 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
|
|||||||
Path { subpaths: vec![SubPath { segments: pieces.into_iter().map(|[a, b, c, d]| Segment::Cubic(a, b, c, d)).collect(), closed }], stroke, fill: None },
|
Path { subpaths: vec![SubPath { segments: pieces.into_iter().map(|[a, b, c, d]| Segment::Cubic(a, b, c, d)).collect(), closed }], stroke, fill: None },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
17 if d3 => {
|
||||||
|
// Width 112, height 120, quaternion 144, origin 176; the
|
||||||
|
// characters follow the string marker.
|
||||||
|
let (Some(width), Some(height), Some((u, _)), Some(origin)) = (f64_at(e, 112), f64_at(e, 120), quat_axes(e, 144), p2(176)) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(text) = marked_string(e, 200) else { continue };
|
||||||
|
let (height, width) = (height * 6.0 / 1000.0, width * 6.0 / 1000.0);
|
||||||
|
raw.texts.push(Text {
|
||||||
|
text,
|
||||||
|
origin,
|
||||||
|
height,
|
||||||
|
width_factor: if height > 0.0 { width / height } else { 1.0 },
|
||||||
|
rotation: u[1].atan2(u[0]),
|
||||||
|
color,
|
||||||
|
font: "txt".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
17 => {
|
17 => {
|
||||||
let (Some(width), Some(height), Some(rotation), Some(origin)) = (f64_at(e, 112), f64_at(e, 120), f64_at(e, 144), p2(152)) else {
|
let (Some(width), Some(height), Some(rotation), Some(origin)) = (f64_at(e, 112), f64_at(e, 120), f64_at(e, 144), p2(152)) else {
|
||||||
continue;
|
continue;
|
||||||
@@ -331,7 +402,7 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
|
|||||||
let Some(chars) = e.get(174..170 + count.max(4)) else { continue };
|
let Some(chars) = e.get(174..170 + count.max(4)) else { continue };
|
||||||
let text = String::from_utf8_lossy(chars).trim_end_matches(char::from(0)).to_string();
|
let text = String::from_utf8_lossy(chars).trim_end_matches(char::from(0)).to_string();
|
||||||
let (height, width) = (height * 6.0 / 1000.0, width * 6.0 / 1000.0);
|
let (height, width) = (height * 6.0 / 1000.0, width * 6.0 / 1000.0);
|
||||||
raw.texts.push(Text { text, origin, height, width_factor: if height > 0.0 { width / height } else { 1.0 }, rotation, color });
|
raw.texts.push(Text { text, origin, height, width_factor: if height > 0.0 { width / height } else { 1.0 }, rotation, color, font: "txt".to_string() });
|
||||||
}
|
}
|
||||||
35 => {
|
35 => {
|
||||||
let (Some(m0), Some(m1), Some(m3), Some(m4), Some(origin)) = (f64_at(e, 160), f64_at(e, 168), f64_at(e, 184), f64_at(e, 192), p2(232)) else {
|
let (Some(m0), Some(m1), Some(m3), Some(m4), Some(origin)) = (f64_at(e, 160), f64_at(e, 168), f64_at(e, 184), f64_at(e, 192), p2(232)) else {
|
||||||
@@ -457,23 +528,59 @@ pub fn model_names(bytes: &[u8]) -> Option<Vec<String>> {
|
|||||||
(!names.is_empty()).then_some(names)
|
(!names.is_empty()).then_some(names)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn model(bytes: &[u8], name: &str) -> Option<Sheet> {
|
/// The level table (the table entries after a header of table 1): id,
|
||||||
|
/// name and colour index. The default level (64) is listed as "0", as the
|
||||||
|
/// reference names it.
|
||||||
|
fn level_table(nm: &[&[u8]]) -> Vec<(u32, String, u32)> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut in_levels = false;
|
||||||
|
for e in nm {
|
||||||
|
match kind(e) {
|
||||||
|
96 => in_levels = u32_at(e, 12) == Some(1),
|
||||||
|
95 if in_levels && e.len() >= 80 => {
|
||||||
|
let (Some(id), Some(color)) = (u32_at(e, 32), u32_at(e, 72)) else { continue };
|
||||||
|
let mut name = marked_string(e, 32).unwrap_or_default();
|
||||||
|
if id == 64 && name.eq_ignore_ascii_case("Default") {
|
||||||
|
name = "0".to_string();
|
||||||
|
}
|
||||||
|
out.push((id, name, color));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn non_model(cfb: &Cfb) -> Vec<u8> {
|
||||||
|
cfb.streams().iter().find(|(p, _)| p == "Dgn^Nm/$1").map(|(_, e)| inflate(&cfb.stream(e))).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Level names of the file, for the underlay layers list.
|
||||||
|
pub fn layer_names(bytes: &[u8]) -> Option<Vec<String>> {
|
||||||
|
let cfb = Cfb::open(bytes)?;
|
||||||
|
let nm = non_model(&cfb);
|
||||||
|
Some(level_table(&elements(&nm)).into_iter().map(|l| l.1).filter(|n| !n.is_empty()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn model(bytes: &[u8], name: &str, hidden: &[String]) -> Option<Sheet> {
|
||||||
let cfb = Cfb::open(bytes)?;
|
let cfb = Cfb::open(bytes)?;
|
||||||
let all = models(&cfb);
|
let all = models(&cfb);
|
||||||
let (_, graphics, header) = all.iter().find(|m| m.0.eq_ignore_ascii_case(name)).or_else(|| all.first())?;
|
let (_, graphics, header) = all.iter().find(|m| m.0.eq_ignore_ascii_case(name)).or_else(|| all.first())?;
|
||||||
let non_model = cfb.streams().iter().find(|(p, _)| p == "Dgn^Nm/$1").map(|(_, e)| inflate(&cfb.stream(e))).unwrap_or_default();
|
let non_model = non_model(&cfb);
|
||||||
let nm = elements(&non_model);
|
let nm = elements(&non_model);
|
||||||
let colors = nm
|
let colors = nm
|
||||||
.iter()
|
.iter()
|
||||||
.find(|e| kind(e) == 5 && e.len() >= 37 + 768)
|
.find(|e| kind(e) == 5 && e.len() >= 37 + 768)
|
||||||
.map(|e| (0..256).map(|i| [e[37 + i * 3], e[38 + i * 3], e[39 + i * 3]]).collect())
|
.map(|e| (0..256).map(|i| [e[37 + i * 3], e[38 + i * 3], e[39 + i * 3]]).collect())
|
||||||
.unwrap_or_else(|| vec![[255, 255, 255]; 256]);
|
.unwrap_or_else(|| vec![[255, 255, 255]; 256]);
|
||||||
let levels = nm
|
let table = level_table(&nm);
|
||||||
|
let levels = table.iter().map(|l| (l.0, l.2)).collect();
|
||||||
|
let hidden = table
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|e| kind(e) == 95 && e.len() >= 80)
|
.filter(|l| hidden.iter().any(|h| h.eq_ignore_ascii_case(&l.1)))
|
||||||
.filter_map(|e| Some((u32_at(e, 32)?, u32_at(e, 72)?)))
|
.map(|l| l.0)
|
||||||
.collect();
|
.collect();
|
||||||
let styles = Styles { colors, levels };
|
let styles = Styles { colors, levels, hidden };
|
||||||
// Shared cell definitions: a type-34 element and its components.
|
// Shared cell definitions: a type-34 element and its components.
|
||||||
let mut defs = HashMap::new();
|
let mut defs = HashMap::new();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::{Cursor, Read};
|
use std::io::{Cursor, Read};
|
||||||
|
|
||||||
use super::model::{arc_cubics, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath};
|
use super::model::{arc_cubics, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text, CAP_PER_EM};
|
||||||
|
|
||||||
/// The package's files, with `\` separators turned into `/`.
|
/// The package's files, with `\` separators turned into `/`.
|
||||||
fn unzip(bytes: &[u8]) -> Option<HashMap<String, Vec<u8>>> {
|
fn unzip(bytes: &[u8]) -> Option<HashMap<String, Vec<u8>>> {
|
||||||
@@ -160,37 +160,58 @@ pub fn sheet(bytes: &[u8], name: &str) -> Option<Sheet> {
|
|||||||
.find(|s| s.name.eq_ignore_ascii_case(name))
|
.find(|s| s.name.eq_ignore_ascii_case(name))
|
||||||
.or_else(|| (name.is_empty()).then(|| entries.first()).flatten())?;
|
.or_else(|| (name.is_empty()).then(|| entries.first()).flatten())?;
|
||||||
let data = files.get(&entry.graphics)?;
|
let data = files.get(&entry.graphics)?;
|
||||||
let (paths, view) = if entry.graphics.to_ascii_lowercase().ends_with(".w2d") {
|
let (paths, view, texts) = if entry.graphics.to_ascii_lowercase().ends_with(".w2d") {
|
||||||
w2d::read(data)?
|
w2d::read(data)?
|
||||||
} else {
|
} else {
|
||||||
let w2x = entry.w2x.as_ref().and_then(|p| files.get(p));
|
let w2x = entry.w2x.as_ref().and_then(|p| files.get(p));
|
||||||
xps::read(data, w2x.map(|v| v.as_slice()), entry)?
|
xps::read(data, w2x.map(|v| v.as_slice()), entry)?
|
||||||
};
|
};
|
||||||
let rect = view.or_else(|| paths_bounds(&paths))?;
|
let rect = view.or_else(|| paths_bounds(&paths))?;
|
||||||
Some(Sheet { rect, paths, texts: Vec::new(), sub_per_master: 1.0 })
|
Some(Sheet { rect, paths, texts, sub_per_master: 1.0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The default colour map of a W2D stream: black, a 6×6×6 colour cube from
|
/// The default colour map of a W2D stream: ten system colours, a 6×6×6
|
||||||
/// index 10 (red 180 steps, green 30, blue 5), greys after it.
|
/// colour cube from index 10 (red 36 steps, green 6, blue 1), a grey ramp
|
||||||
|
/// at 226-245 and ten more system colours (measured against plotted files:
|
||||||
|
/// the cube, the ramp, 7 and 248).
|
||||||
fn default_color(index: u8) -> [u8; 3] {
|
fn default_color(index: u8) -> [u8; 3] {
|
||||||
let i = index as usize;
|
let i = index as usize;
|
||||||
const LEVELS: [u8; 6] = [0, 51, 102, 153, 204, 255];
|
const LEVELS: [u8; 6] = [0, 51, 102, 153, 204, 255];
|
||||||
// ponytail: only the cube is confirmed against plotted files; the entries
|
const LOW: [[u8; 3]; 10] = [
|
||||||
// around it are a grey ramp until a file uses them.
|
[0, 0, 0],
|
||||||
|
[128, 0, 0],
|
||||||
|
[0, 128, 0],
|
||||||
|
[128, 128, 0],
|
||||||
|
[0, 0, 128],
|
||||||
|
[128, 0, 128],
|
||||||
|
[0, 128, 128],
|
||||||
|
[192, 192, 192],
|
||||||
|
[192, 220, 192],
|
||||||
|
[166, 202, 240],
|
||||||
|
];
|
||||||
|
const HIGH: [[u8; 3]; 10] = [
|
||||||
|
[255, 251, 240],
|
||||||
|
[160, 160, 164],
|
||||||
|
[128, 128, 128],
|
||||||
|
[255, 0, 0],
|
||||||
|
[0, 255, 0],
|
||||||
|
[255, 255, 0],
|
||||||
|
[0, 0, 255],
|
||||||
|
[255, 0, 255],
|
||||||
|
[0, 255, 255],
|
||||||
|
[255, 255, 255],
|
||||||
|
];
|
||||||
match i {
|
match i {
|
||||||
|
0..=9 => LOW[i],
|
||||||
10..=225 => {
|
10..=225 => {
|
||||||
let c = i - 10;
|
let c = i - 10;
|
||||||
[LEVELS[c / 36], LEVELS[(c / 6) % 6], LEVELS[c % 6]]
|
[LEVELS[c / 36], LEVELS[(c / 6) % 6], LEVELS[c % 6]]
|
||||||
}
|
}
|
||||||
0 => [0, 0, 0],
|
226..=245 => {
|
||||||
1..=9 => {
|
let g = ((i - 226) * 255 / 19) as u8;
|
||||||
let g = (i * 255 / 10) as u8;
|
|
||||||
[g, g, g]
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let g = ((i - 226) * 255 / 29) as u8;
|
|
||||||
[g, g, g]
|
[g, g, g]
|
||||||
}
|
}
|
||||||
|
_ => HIGH[i - 246],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,9 +307,13 @@ mod w2d {
|
|||||||
(r[0] < r[2] && r[1] < r[3]).then_some(r)
|
(r[0] < r[2] && r[1] < r[3]).then_some(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The stream's geometry in model units, and its plotted view.
|
/// The stream's geometry and text in model units, and its plotted view.
|
||||||
pub fn read(b: &[u8]) -> Option<(Vec<Path>, Option<[f64; 4]>)> {
|
pub fn read(b: &[u8]) -> Option<(Vec<Path>, Option<[f64; 4]>, Vec<Text>)> {
|
||||||
let mut r = Reader { b, i: 0 };
|
let mut r = Reader { b, i: 0 };
|
||||||
|
// The current font: family, em height (graphics units), rotation
|
||||||
|
// (radians) and width scale.
|
||||||
|
let mut font = (String::from("Arial"), 0.0f64, 0.0f64, 1.0f64);
|
||||||
|
let mut texts: Vec<(String, [f64; 2], (String, f64, f64, f64), [u8; 3])> = Vec::new();
|
||||||
let mut units = Units::identity();
|
let mut units = Units::identity();
|
||||||
let mut view: Option<[f64; 4]> = None;
|
let mut view: Option<[f64; 4]> = None;
|
||||||
let mut color = [0u8, 0, 0];
|
let mut color = [0u8, 0, 0];
|
||||||
@@ -322,6 +347,13 @@ mod w2d {
|
|||||||
color = [v[0] as u8, v[1] as u8, v[2] as u8];
|
color = [v[0] as u8, v[1] as u8, v[2] as u8];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// "(FontExtension 'logfont name' 'canonical name')"
|
||||||
|
"FontExtension" => {
|
||||||
|
let body = String::from_utf8_lossy(body);
|
||||||
|
if let Some(name) = body.split('\'').nth(1).filter(|n| !n.is_empty()) {
|
||||||
|
font.0 = name.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,12 +458,47 @@ mod w2d {
|
|||||||
let fill = op == b'E';
|
let fill = op == b'E';
|
||||||
raw.push((arc_path(c, major, minor, tilt * turn, s * turn, sweep), color, if fill { -1.0 } else { weight }, visible));
|
raw.push((arc_path(c, major, minor, tilt * turn, s * turn, sweep), color, if fill { -1.0 } else { weight }, visible));
|
||||||
}
|
}
|
||||||
b'x' => {
|
0x06 => {
|
||||||
// Text: relative position and a string (drawn as the
|
// Font: a field mask, then the fields it names in order.
|
||||||
// outlines that follow it, so only the position counts).
|
|
||||||
r.i += 1;
|
r.i += 1;
|
||||||
rel(&mut at, r.i32()? as f64, r.i32()? as f64);
|
let mask = r.u16()?;
|
||||||
skip_string(&mut r)?;
|
if mask & !0x07fe != 0 {
|
||||||
|
// A font name field (or one unknown here) — its
|
||||||
|
// encoding is not read, so stop.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for bit in 1..11 {
|
||||||
|
if mask & (1 << bit) == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match 1u16 << bit {
|
||||||
|
// charset, pitch, family, style
|
||||||
|
0x0002 | 0x0004 | 0x0008 | 0x0010 => {
|
||||||
|
r.u8()?;
|
||||||
|
}
|
||||||
|
0x0020 => font.1 = r.i32()? as f64,
|
||||||
|
0x0040 => font.2 = r.u16()? as f64 * std::f64::consts::TAU / 65536.0,
|
||||||
|
0x0080 => font.3 = r.u16()? as f64 / 1024.0,
|
||||||
|
// oblique, spacing
|
||||||
|
0x0100 | 0x0200 => {
|
||||||
|
r.u16()?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
r.i32()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b'x' => {
|
||||||
|
// Text at a relative position. Stroke-font text is also
|
||||||
|
// plotted as outlines with the text itself hidden, so only
|
||||||
|
// visible text is kept.
|
||||||
|
r.i += 1;
|
||||||
|
let p = rel(&mut at, r.i32()? as f64, r.i32()? as f64);
|
||||||
|
let s = read_string(&mut r)?;
|
||||||
|
if visible && font.1 > 0.0 && !s.trim().is_empty() {
|
||||||
|
texts.push((s, p, font.clone(), color));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => break,
|
_ => break,
|
||||||
}
|
}
|
||||||
@@ -453,22 +520,42 @@ mod w2d {
|
|||||||
let b = units.model([v[2], v[3]]);
|
let b = units.model([v[2], v[3]]);
|
||||||
[a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])]
|
[a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])]
|
||||||
});
|
});
|
||||||
Some((paths, view))
|
let texts = texts
|
||||||
|
.into_iter()
|
||||||
|
.map(|(text, p, (family, em, rotation, width), color)| Text {
|
||||||
|
text,
|
||||||
|
origin: units.model(p),
|
||||||
|
height: units.length(em) * CAP_PER_EM,
|
||||||
|
width_factor: width,
|
||||||
|
rotation,
|
||||||
|
color,
|
||||||
|
font: family,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Some((paths, view, texts))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A W2D string: quoted ASCII, or a count followed by UTF-16 units.
|
/// A W2D string: quoted ASCII, or a count followed by UTF-16 units.
|
||||||
fn skip_string(r: &mut Reader) -> Option<()> {
|
fn read_string(r: &mut Reader) -> Option<String> {
|
||||||
if r.b.get(r.i) == Some(&b'\'') {
|
if r.b.get(r.i) == Some(&b'\'') {
|
||||||
r.i += 1;
|
r.i += 1;
|
||||||
|
let start = r.i;
|
||||||
while *r.b.get(r.i)? != b'\'' {
|
while *r.b.get(r.i)? != b'\'' {
|
||||||
r.i += 1;
|
r.i += 1;
|
||||||
}
|
}
|
||||||
r.i += 1;
|
r.i += 1;
|
||||||
|
Some(String::from_utf8_lossy(&r.b[start..r.i - 1]).to_string())
|
||||||
} else {
|
} else {
|
||||||
let n = r.count()?;
|
let n = r.count()?;
|
||||||
|
let units: Vec<u16> = r
|
||||||
|
.b
|
||||||
|
.get(r.i..r.i + n * 2)?
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
||||||
|
.collect();
|
||||||
r.i += n * 2;
|
r.i += n * 2;
|
||||||
|
Some(String::from_utf16_lossy(&units))
|
||||||
}
|
}
|
||||||
Some(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,8 +616,8 @@ mod xps {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The fixed page's paths in model units, and the plotted view.
|
/// The fixed page's paths and text in model units, and the plotted view.
|
||||||
pub fn read(page: &[u8], w2x: Option<&[u8]>, entry: &SheetEntry) -> Option<(Vec<Path>, Option<[f64; 4]>)> {
|
pub fn read(page: &[u8], w2x: Option<&[u8]>, entry: &SheetEntry) -> Option<(Vec<Path>, Option<[f64; 4]>, Vec<Text>)> {
|
||||||
let mut units = Units::identity();
|
let mut units = Units::identity();
|
||||||
let mut view_log = None;
|
let mut view_log = None;
|
||||||
if let Some(w2x) = w2x.and_then(|b| std::str::from_utf8(b).ok()) {
|
if let Some(w2x) = w2x.and_then(|b| std::str::from_utf8(b).ok()) {
|
||||||
@@ -565,12 +652,37 @@ mod xps {
|
|||||||
[(x - offset[0]) / scale[0], (y - offset[1]) / scale[1]]
|
[(x - offset[0]) / scale[0], (y - offset[1]) / scale[1]]
|
||||||
};
|
};
|
||||||
let mut paths = Vec::new();
|
let mut paths = Vec::new();
|
||||||
|
let mut texts = Vec::new();
|
||||||
walk(doc.root_element(), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0], &mut |node, m| {
|
walk(doc.root_element(), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0], &mut |node, m| {
|
||||||
let Some(data) = attr(node, "Data") else { return };
|
|
||||||
let m = match attr(node, "RenderTransform").and_then(parse_affine) {
|
let m = match attr(node, "RenderTransform").and_then(parse_affine) {
|
||||||
Some(local) => mul(m, local),
|
Some(local) => mul(m, local),
|
||||||
None => m,
|
None => m,
|
||||||
};
|
};
|
||||||
|
if node.tag_name().name() == "Glyphs" {
|
||||||
|
// Text: its baseline origin and em size in page units (y
|
||||||
|
// down), drawn in the plotted family.
|
||||||
|
// ponytail: the embedded font is obfuscated and not read;
|
||||||
|
// DWFx text is drawn in Arial.
|
||||||
|
let num = |name: &str| attr(node, name).and_then(|v| v.parse::<f64>().ok());
|
||||||
|
let (Some(em), Some(text)) = (num("FontRenderingEmSize"), attr(node, "UnicodeString")) else { return };
|
||||||
|
let o = [num("OriginX").unwrap_or(0.0), num("OriginY").unwrap_or(0.0)];
|
||||||
|
let map = |p: [f64; 2]| units.model(to_logical(apply(m, p)));
|
||||||
|
let (a, along, up) = (map(o), map([o[0] + em, o[1]]), map([o[0], o[1] - em]));
|
||||||
|
let len = |p: [f64; 2]| ((p[0] - a[0]).powi(2) + (p[1] - a[1]).powi(2)).sqrt();
|
||||||
|
if !text.trim().is_empty() {
|
||||||
|
texts.push(Text {
|
||||||
|
text: text.to_string(),
|
||||||
|
origin: a,
|
||||||
|
height: len(up) * CAP_PER_EM,
|
||||||
|
width_factor: if len(up) > 0.0 { len(along) / len(up) } else { 1.0 },
|
||||||
|
rotation: (along[1] - a[1]).atan2(along[0] - a[0]),
|
||||||
|
color: attr(node, "Fill").and_then(parse_color).unwrap_or([0, 0, 0]),
|
||||||
|
font: "Arial".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(data) = attr(node, "Data") else { return };
|
||||||
let map = |p: [f64; 2]| units.model(to_logical(apply(m, p)));
|
let map = |p: [f64; 2]| units.model(to_logical(apply(m, p)));
|
||||||
let subpaths: Vec<SubPath> = parse_data(data).into_iter().map(|sp| map_subpath(sp, map)).collect();
|
let subpaths: Vec<SubPath> = parse_data(data).into_iter().map(|sp| map_subpath(sp, map)).collect();
|
||||||
if subpaths.is_empty() {
|
if subpaths.is_empty() {
|
||||||
@@ -591,7 +703,7 @@ mod xps {
|
|||||||
let b = units.model([v[2], v[3]]);
|
let b = units.model([v[2], v[3]]);
|
||||||
[a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])]
|
[a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])]
|
||||||
});
|
});
|
||||||
Some((paths, view))
|
Some((paths, view, texts))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn walk(node: roxmltree::Node, m: Affine, f: &mut impl FnMut(roxmltree::Node, Affine)) {
|
fn walk(node: roxmltree::Node, m: Affine, f: &mut impl FnMut(roxmltree::Node, Affine)) {
|
||||||
@@ -604,7 +716,7 @@ mod xps {
|
|||||||
};
|
};
|
||||||
walk(child, m2, f);
|
walk(child, m2, f);
|
||||||
}
|
}
|
||||||
"Path" => f(child, m),
|
"Path" | "Glyphs" => f(child, m),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,15 @@ fn sheet_cache() -> &'static Mutex<HashMap<Key, Option<Arc<Sheet>>>> {
|
|||||||
CACHE.get_or_init(Default::default)
|
CACHE.get_or_init(Default::default)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The cache key of an item seen without some layers.
|
||||||
|
fn layered_item(item: &str, hidden: &[String]) -> String {
|
||||||
|
if hidden.is_empty() {
|
||||||
|
item.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{item}#{}", hidden.join("|"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn raster_cache() -> &'static Mutex<HashMap<Key, Option<Arc<PdfPage>>>> {
|
fn raster_cache() -> &'static Mutex<HashMap<Key, Option<Arc<PdfPage>>>> {
|
||||||
static CACHE: OnceLock<Mutex<HashMap<Key, Option<Arc<PdfPage>>>>> = OnceLock::new();
|
static CACHE: OnceLock<Mutex<HashMap<Key, Option<Arc<PdfPage>>>>> = OnceLock::new();
|
||||||
CACHE.get_or_init(Default::default)
|
CACHE.get_or_init(Default::default)
|
||||||
@@ -51,10 +60,28 @@ pub fn forget(path: &str) {
|
|||||||
vector_cache().lock().unwrap_or_else(|e| e.into_inner()).retain(|k, _| k.0 != path);
|
vector_cache().lock().unwrap_or_else(|e| e.into_inner()).retain(|k, _| k.0 != path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Layer (DWF) or level (DGN) names of a file; empty when it has none.
|
||||||
|
pub fn layer_names(kind: UnderlayType, path: &str) -> Vec<String> {
|
||||||
|
let Some(bytes) = source_bytes(path) else { return Vec::new() };
|
||||||
|
match kind {
|
||||||
|
// A plotted DWF carries no layers.
|
||||||
|
UnderlayType::Dwf | UnderlayType::Pdf => None,
|
||||||
|
UnderlayType::Dgn if dgn7::is_v7(&bytes) => dgn7::layer_names(&bytes),
|
||||||
|
UnderlayType::Dgn => dgn8::layer_names(&bytes),
|
||||||
|
}
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
/// The named sheet or model (the first when the name is empty or unknown),
|
/// The named sheet or model (the first when the name is empty or unknown),
|
||||||
/// memoised. Its text is drawn as strokes with the drawing's text engine.
|
/// memoised. Its text is drawn as strokes with the drawing's text engine.
|
||||||
pub fn sheet(kind: UnderlayType, path: &str, item: &str) -> Option<Arc<Sheet>> {
|
pub fn sheet(kind: UnderlayType, path: &str, item: &str) -> Option<Arc<Sheet>> {
|
||||||
let key = (path.to_string(), item.to_string());
|
sheet_without(kind, path, item, &[])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sheet without the `hidden` layers / levels, on the full sheet's
|
||||||
|
/// rectangle (turning layers off never moves the underlay), memoised.
|
||||||
|
pub fn sheet_without(kind: UnderlayType, path: &str, item: &str, hidden: &[String]) -> Option<Arc<Sheet>> {
|
||||||
|
let key = (path.to_string(), layered_item(item, hidden));
|
||||||
if let Some(hit) = sheet_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
|
if let Some(hit) = sheet_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
|
||||||
return hit.clone();
|
return hit.clone();
|
||||||
}
|
}
|
||||||
@@ -62,11 +89,22 @@ pub fn sheet(kind: UnderlayType, path: &str, item: &str) -> Option<Arc<Sheet>> {
|
|||||||
let bytes = source_bytes(path)?;
|
let bytes = source_bytes(path)?;
|
||||||
let mut sheet = match kind {
|
let mut sheet = match kind {
|
||||||
UnderlayType::Dwf => dwf::sheet(&bytes, item)?,
|
UnderlayType::Dwf => dwf::sheet(&bytes, item)?,
|
||||||
UnderlayType::Dgn if dgn7::is_v7(&bytes) => dgn7::model(&bytes)?,
|
UnderlayType::Dgn if dgn7::is_v7(&bytes) => dgn7::model(&bytes, hidden).unwrap_or_default(),
|
||||||
UnderlayType::Dgn => dgn8::model(&bytes, item)?,
|
UnderlayType::Dgn => dgn8::model(&bytes, item, hidden).unwrap_or_default(),
|
||||||
UnderlayType::Pdf => return None,
|
UnderlayType::Pdf => return None,
|
||||||
};
|
};
|
||||||
outline_texts(&mut sheet);
|
outline_texts(&mut sheet);
|
||||||
|
// A DGN model's extent is its geometry, text included (a DWF sheet
|
||||||
|
// keeps its plotted view).
|
||||||
|
if kind == UnderlayType::Dgn {
|
||||||
|
if let Some(b) = model::paths_bounds(&sheet.paths) {
|
||||||
|
let r = sheet.rect;
|
||||||
|
sheet.rect = [r[0].min(b[0]), r[1].min(b[1]), r[2].max(b[2]), r[3].max(b[3])];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hidden.is_empty() {
|
||||||
|
sheet.rect = sheet_without(kind, path, item, &[])?.rect;
|
||||||
|
}
|
||||||
Some(sheet)
|
Some(sheet)
|
||||||
};
|
};
|
||||||
let value = read().map(Arc::new);
|
let value = read().map(Arc::new);
|
||||||
@@ -83,7 +121,7 @@ fn outline_texts(sheet: &mut Sheet) {
|
|||||||
t.rotation as f32,
|
t.rotation as f32,
|
||||||
t.width_factor as f32,
|
t.width_factor as f32,
|
||||||
0.0,
|
0.0,
|
||||||
"txt",
|
&t.font,
|
||||||
&t.text,
|
&t.text,
|
||||||
);
|
);
|
||||||
let at = |p: [f32; 2]| [p[0] as f64, p[1] as f64];
|
let at = |p: [f32; 2]| [p[0] as f64, p[1] as f64];
|
||||||
@@ -114,13 +152,13 @@ pub const RASTER_SIDE: f64 = 3072.0;
|
|||||||
/// The sheet drawn on a transparent background with `side` pixels on its
|
/// The sheet drawn on a transparent background with `side` pixels on its
|
||||||
/// longest side (at most [`RASTER_SIDE`]), straight alpha, rows from the
|
/// longest side (at most [`RASTER_SIDE`]), straight alpha, rows from the
|
||||||
/// top, memoised per size.
|
/// top, memoised per size.
|
||||||
pub fn display_raster(kind: UnderlayType, path: &str, item: &str, side: f64) -> Option<Arc<PdfPage>> {
|
pub fn display_raster(kind: UnderlayType, path: &str, item: &str, hidden: &[String], side: f64) -> Option<Arc<PdfPage>> {
|
||||||
let side = side.clamp(16.0, RASTER_SIDE).round();
|
let side = side.clamp(16.0, RASTER_SIDE).round();
|
||||||
let key = (path.to_string(), format!("{item}@{side}"));
|
let key = (path.to_string(), format!("{}@{side}", layered_item(item, hidden)));
|
||||||
if let Some(hit) = raster_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
|
if let Some(hit) = raster_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
|
||||||
return hit.clone();
|
return hit.clone();
|
||||||
}
|
}
|
||||||
let value = sheet(kind, path, item).and_then(|s| rasterize(&s, side)).map(Arc::new);
|
let value = sheet_without(kind, path, item, hidden).and_then(|s| rasterize(&s, side)).map(Arc::new);
|
||||||
raster_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
|
raster_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
@@ -198,12 +236,12 @@ fn vector_cache() -> &'static Mutex<HashMap<Key, Option<Arc<super::pdf_vector::P
|
|||||||
|
|
||||||
/// The sheet's geometry as page vectors for object snaps, in sheet units,
|
/// The sheet's geometry as page vectors for object snaps, in sheet units,
|
||||||
/// memoised.
|
/// memoised.
|
||||||
pub fn page_vectors(kind: UnderlayType, path: &str, item: &str) -> Option<Arc<super::pdf_vector::PageVectors>> {
|
pub fn page_vectors(kind: UnderlayType, path: &str, item: &str, hidden: &[String]) -> Option<Arc<super::pdf_vector::PageVectors>> {
|
||||||
let key = (path.to_string(), item.to_string());
|
let key = (path.to_string(), layered_item(item, hidden));
|
||||||
if let Some(hit) = vector_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
|
if let Some(hit) = vector_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
|
||||||
return hit.clone();
|
return hit.clone();
|
||||||
}
|
}
|
||||||
let value = sheet(kind, path, item).map(|s| Arc::new(to_page_vectors(&s)));
|
let value = sheet_without(kind, path, item, hidden).map(|s| Arc::new(to_page_vectors(&s)));
|
||||||
vector_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
|
vector_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,8 +169,17 @@ pub struct Text {
|
|||||||
pub width_factor: f64,
|
pub width_factor: f64,
|
||||||
pub rotation: f64,
|
pub rotation: f64,
|
||||||
pub color: [u8; 3],
|
pub color: [u8; 3],
|
||||||
|
/// The font the text is drawn in: a stroke font name or a TrueType
|
||||||
|
/// family.
|
||||||
|
pub font: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Height of a TrueType font's capitals per em (Arial and its kin), which
|
||||||
|
/// turns a plotted em size into a text height.
|
||||||
|
// ponytail: one ratio for every family; read the font's own metrics if a
|
||||||
|
// serif family plots noticeably off.
|
||||||
|
pub const CAP_PER_EM: f64 = 0.716;
|
||||||
|
|
||||||
/// Points of a B-spline curve: its order, control points and knot vector
|
/// Points of a B-spline curve: its order, control points and knot vector
|
||||||
/// (clamped: `order` equal knots at each end).
|
/// (clamped: `order` equal knots at each end).
|
||||||
pub fn bspline_points(order: usize, poles: &[[f64; 2]], knots: &[f64], samples: usize) -> Vec<[f64; 2]> {
|
pub fn bspline_points(order: usize, poles: &[[f64; 2]], knots: &[f64], samples: usize) -> Vec<[f64; 2]> {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Selection-driven tools: while only PDF underlays (or only xrefs) are
|
//! Selection-driven tools: while only underlays of one kind (or only xrefs) are
|
||||||
//! selected their actions show on the right-edge toolbar; they go when the
|
//! selected their actions show on the right-edge toolbar; they go when the
|
||||||
//! selection changes. Contrast and fade stay in the Properties panel.
|
//! selection changes. Contrast and fade stay in the Properties panel.
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ use crate::modules::{ModuleEvent, ToolDef};
|
|||||||
/// as on.
|
/// as on.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct UnderlayContext {
|
pub struct UnderlayContext {
|
||||||
|
pub kind: codec::entities::UnderlayType,
|
||||||
pub monochrome: bool,
|
pub monochrome: bool,
|
||||||
pub shown: bool,
|
pub shown: bool,
|
||||||
pub snap: bool,
|
pub snap: bool,
|
||||||
@@ -27,10 +28,11 @@ const CLIP: &[u8] = include_bytes!("../../../assets/icons/xclip.svg");
|
|||||||
/// The clip icon with a small red cross at its lower right.
|
/// The clip icon with a small red cross at its lower right.
|
||||||
const UNCLIP: &[u8] = include_bytes!("../../../assets/icons/xclip_remove.svg");
|
const UNCLIP: &[u8] = include_bytes!("../../../assets/icons/xclip_remove.svg");
|
||||||
|
|
||||||
/// PDF underlay tools: display, clipping, options, layers and import.
|
/// Underlay tools: display, clipping, options, layers and (PDF only)
|
||||||
pub fn pdf_underlay_tools() -> Vec<ToolDef> {
|
/// import.
|
||||||
|
pub fn pdf_underlay_tools(kind: codec::entities::UnderlayType) -> Vec<ToolDef> {
|
||||||
const FRAMES: &[u8] = include_bytes!("../../../assets/icons/underlay_frames.svg");
|
const FRAMES: &[u8] = include_bytes!("../../../assets/icons/underlay_frames.svg");
|
||||||
vec![
|
let mut tools = vec![
|
||||||
tool("_PDFULMONO", "Display in\nMonochrome", FRAMES),
|
tool("_PDFULMONO", "Display in\nMonochrome", FRAMES),
|
||||||
tool("_PDFULCLIP", "Create Clipping\nBoundary", CLIP),
|
tool("_PDFULCLIP", "Create Clipping\nBoundary", CLIP),
|
||||||
tool("_PDFULUNCLIP", "Remove\nClipping", UNCLIP),
|
tool("_PDFULUNCLIP", "Remove\nClipping", UNCLIP),
|
||||||
@@ -46,12 +48,15 @@ pub fn pdf_underlay_tools() -> Vec<ToolDef> {
|
|||||||
"Edit\nLayers",
|
"Edit\nLayers",
|
||||||
include_bytes!("../../../assets/icons/underlay_layers.svg"),
|
include_bytes!("../../../assets/icons/underlay_layers.svg"),
|
||||||
),
|
),
|
||||||
tool(
|
];
|
||||||
|
if kind == codec::entities::UnderlayType::Pdf {
|
||||||
|
tools.push(tool(
|
||||||
"_PDFULIMPORT",
|
"_PDFULIMPORT",
|
||||||
"Import As\nObjects",
|
"Import As\nObjects",
|
||||||
include_bytes!("../../../assets/icons/cui_import.svg"),
|
include_bytes!("../../../assets/icons/cui_import.svg"),
|
||||||
),
|
));
|
||||||
]
|
}
|
||||||
|
tools
|
||||||
}
|
}
|
||||||
|
|
||||||
/// External reference tools: edit, open, clipping and the palette.
|
/// External reference tools: edit, open, clipping and the palette.
|
||||||
|
|||||||
@@ -679,7 +679,7 @@ pub fn view_layers<'a>(
|
|||||||
.height(Fill)
|
.height(Fill)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
_ => container(text(t!("This file does not contain any layers.")).size(11).style(muted_style))
|
_ => container(text(t!("This file does not contain any layers")).size(11).style(muted_style))
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
.align_x(iced::Center)
|
.align_x(iced::Center)
|
||||||
|
|||||||
Reference in new issue
Block a user