forked from 1b-agent/opencadstudio-upstream
Merge pull request #1812 from ramox81/codex/import-dgnimport
IMPORT window and DGNIMPORT / -DGNIMPORT # Conflicts: # locales/ar-SA/opencadstudio.ftl # locales/bg-BG/opencadstudio.ftl # locales/cs-CZ/opencadstudio.ftl # locales/de-DE/opencadstudio.ftl # locales/el-GR/opencadstudio.ftl # locales/en-US/opencadstudio.ftl # locales/es-ES/opencadstudio.ftl # locales/fi-FI/opencadstudio.ftl # locales/fr-FR/opencadstudio.ftl # locales/hi-IN/opencadstudio.ftl # locales/hu-HU/opencadstudio.ftl # locales/it-IT/opencadstudio.ftl # locales/ja-JP/opencadstudio.ftl # locales/ko-KR/opencadstudio.ftl # locales/lo-LA/opencadstudio.ftl # locales/nl-NL/opencadstudio.ftl # locales/pl-PL/opencadstudio.ftl # locales/pt-BR/opencadstudio.ftl # locales/ru-RU/opencadstudio.ftl # locales/tr-TR/opencadstudio.ftl # locales/zh-CN/opencadstudio.ftl # locales/zh-TW/opencadstudio.ftl
This commit is contained in:
commit
2b4c4bfdc6
39 files changed
+3210
-13
No files matched your search
@@ -6656,3 +6656,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6644,3 +6644,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6641,3 +6641,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6642,3 +6642,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6644,3 +6644,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6643,3 +6643,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6644,3 +6644,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6641,3 +6641,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6644,3 +6644,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6632,3 +6632,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6628,3 +6628,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6643,3 +6643,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6632,3 +6632,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6643,3 +6643,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6644,3 +6644,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6642,3 +6642,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6643,3 +6643,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6644,3 +6644,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6645,3 +6645,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6565,3 +6565,38 @@ txt2mtxt =
|
|||||||
.result-count = Sonuç · { $count } MText
|
.result-count = Sonuç · { $count } MText
|
||||||
.apply = Uygula
|
.apply = Uygula
|
||||||
.help = TXT2MTXT komutunun metin nesnelerini MText olarak nasıl birleştireceğini ayarlar.
|
.help = TXT2MTXT komutunun metin nesnelerini MText olarak nasıl birleştireceğini ayarlar.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Destekleniyor
|
||||||
|
.by-extension = Uzantıya göre
|
||||||
|
.acis-solver = ACIS çözücü var
|
||||||
|
.export-only = Yalnız dışa aktarma
|
||||||
|
.acis-reason = ACIS gövdeleri yalnız çizim içinde okunur; SAT dosyası içe aktarılamaz.
|
||||||
|
.step-reason = STEP dosyaları yalnız yazılır (STEPOUT), okunmaz.
|
||||||
|
.no-importer = OpenCAD Studio'da bu biçim için içe aktarıcı yok.
|
||||||
|
.recent = Son:
|
||||||
|
.group-2d = 2B / Altlık
|
||||||
|
.group-3d = 3B Model
|
||||||
|
.search = Biçim ara...
|
||||||
|
.only-supported = Yalnız OCS'nin açabildikleri
|
||||||
|
.models = __ocs_fmt_0__ model
|
||||||
|
.change = Değiştir...
|
||||||
|
.name-conflict = Ad çakışmasında
|
||||||
|
.prefix = Bağımlı tanımlara önek ekle
|
||||||
|
.ignore-duplicates = Yinelenen adları yok say
|
||||||
|
.master-unit = Ana birim
|
||||||
|
.sub-unit = Alt birim
|
||||||
|
.explode = Metin düğümlerini metin öğelerine patlat
|
||||||
|
.references = Dış DGN referansları
|
||||||
|
.bind = Blok / xref yap
|
||||||
|
.attach = DGN altlığı bağla
|
||||||
|
.references-note = DGN dosyalarının referans ekleri okunmaz; dışarıda bırakılır.
|
||||||
|
.mapping = Özellik eşleme
|
||||||
|
.mapping-standard = Standart eşleme kurulumu.
|
||||||
|
.mapping-note = Önizleme seçili kurulumun bu dosyaya etkisini gösterir.
|
||||||
|
.import-file = Dosya İçe Aktar
|
||||||
|
.dgn-import = DGN İçe Aktar
|
||||||
|
.import-dgn-file = DGN Dosyası İçe Aktar
|
||||||
|
.help-dgn = DGN modelini çizim nesneleri olarak içe aktarır: seviyeler katman, hücreler blok olur.
|
||||||
|
.help-import = Seçilen biçimdeki dosyayı o biçimin içe aktarıcısıyla içe aktarır.
|
||||||
|
.blank = Dosya adı boş olamaz
|
||||||
|
.invalid-dgn = Geçersiz veya desteklenmeyen DGN dosyası.
|
||||||
@@ -6627,3 +6627,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -6639,3 +6639,38 @@ txt2mtxt =
|
|||||||
.result-count = Result · { $count } MText
|
.result-count = Result · { $count } MText
|
||||||
.apply = Apply
|
.apply = Apply
|
||||||
.help = Sets how TXT2MTXT combines text objects into MText.
|
.help = Sets how TXT2MTXT combines text objects into MText.
|
||||||
|
dgn-import =
|
||||||
|
.supported = Supported
|
||||||
|
.by-extension = By extension
|
||||||
|
.acis-solver = ACIS solver available
|
||||||
|
.export-only = Export only
|
||||||
|
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
|
||||||
|
.step-reason = STEP files are only written (STEPOUT), not read.
|
||||||
|
.no-importer = OpenCAD Studio has no importer for this format.
|
||||||
|
.recent = Recent:
|
||||||
|
.group-2d = 2D / Underlay
|
||||||
|
.group-3d = 3D Model
|
||||||
|
.search = Search formats...
|
||||||
|
.only-supported = Only what OCS can open
|
||||||
|
.models = __ocs_fmt_0__ model(s)
|
||||||
|
.change = Change file...
|
||||||
|
.name-conflict = On name conflict
|
||||||
|
.prefix = Add prefix to dependent definitions
|
||||||
|
.ignore-duplicates = Ignore duplicate names
|
||||||
|
.master-unit = Master unit
|
||||||
|
.sub-unit = Sub unit
|
||||||
|
.explode = Explode text nodes to text elements
|
||||||
|
.references = External DGN references
|
||||||
|
.bind = Bind as block / xref
|
||||||
|
.attach = Attach as DGN underlay
|
||||||
|
.references-note = Reference attachments of DGN files are not read; they are left out.
|
||||||
|
.mapping = Property mapping
|
||||||
|
.mapping-standard = Standard mapping setup.
|
||||||
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
.import-file = Import File
|
||||||
|
.dgn-import = DGN Import
|
||||||
|
.import-dgn-file = Import DGN File
|
||||||
|
.help-dgn = Imports a DGN model as drawing objects: levels as layers, cells as blocks.
|
||||||
|
.help-import = Imports a file of the chosen format with the format's importer.
|
||||||
|
.blank = Filename cannot be blank
|
||||||
|
.invalid-dgn = Invalid or unsupported DGN file.
|
||||||
@@ -628,6 +628,12 @@ impl OpenCADStudio {
|
|||||||
self.apply_xclip(i, inserts, action);
|
self.apply_xclip(i, inserts, action);
|
||||||
}
|
}
|
||||||
CmdResult::OpenPdfImportSettings => self.open_pdf_import_settings(),
|
CmdResult::OpenPdfImportSettings => self.open_pdf_import_settings(),
|
||||||
|
CmdResult::DgnImport(request) => {
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
return self.run_dgn_import(i, request);
|
||||||
|
}
|
||||||
CmdResult::PdfImportFile(import) => {
|
CmdResult::PdfImportFile(import) => {
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
|
|||||||
@@ -0,0 +1,501 @@
|
|||||||
|
//! IMPORT (the Import File window, or a file name on the command line,
|
||||||
|
//! routed by format) and DGNIMPORT / -DGNIMPORT (the DGN Import window or
|
||||||
|
//! the command line), and the import itself: a DGN model's layers, text
|
||||||
|
//! styles, cells and objects added as one undo step, into a new drawing
|
||||||
|
//! unless the window chose the current one.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::modules::insert::dgn_import::{self, DgnImportCommand, DgnImportRequest};
|
||||||
|
use crate::ui::window::import_dialogs::{
|
||||||
|
unsupported_reason, DgnImportState, ImportDialogMsg, ImportFileState, Support, FORMATS,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// How many files the Import File window offers as recent.
|
||||||
|
const RECENT: usize = 5;
|
||||||
|
|
||||||
|
/// Picks a file and registers its bytes (the web build reads them only
|
||||||
|
/// here); `None` when the picker is cancelled.
|
||||||
|
async fn pick(title: String, filters: Vec<(String, Vec<String>)>) -> Option<String> {
|
||||||
|
let mut dialog = crate::sys::file_dialog().set_title(title.as_str());
|
||||||
|
for (name, extensions) in &filters {
|
||||||
|
let extensions: Vec<&str> = extensions.iter().map(String::as_str).collect();
|
||||||
|
dialog = dialog.add_filter(name.as_str(), &extensions);
|
||||||
|
}
|
||||||
|
let handle = dialog.pick_file().await?;
|
||||||
|
let path = crate::sys::handle_path(&handle)
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned();
|
||||||
|
let bytes = handle.read().await;
|
||||||
|
crate::scene::model::pdf_raster::register_source(&path, Arc::new(bytes));
|
||||||
|
Some(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dgn_filters() -> Vec<(String, Vec<String>)> {
|
||||||
|
vec![
|
||||||
|
("MicroStation DGN (*.dgn)".to_string(), vec!["dgn".to_string()]),
|
||||||
|
("All DGN Files (*.*)".to_string(), vec!["*".to_string()]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extension(path: &str) -> String {
|
||||||
|
std::path::Path::new(path)
|
||||||
|
.extension()
|
||||||
|
.map(|e| e.to_string_lossy().to_ascii_lowercase())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exists(path: &str) -> bool {
|
||||||
|
crate::scene::model::pdf_raster::source_bytes(path).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the file is a DGN model DGNIMPORT reads.
|
||||||
|
fn is_dgn(path: &str) -> bool {
|
||||||
|
crate::scene::model::underlay_vector::item_names(codec::entities::UnderlayType::Dgn, path)
|
||||||
|
.and_then(|models| models.into_iter().next())
|
||||||
|
.is_some_and(|model| crate::scene::model::underlay_vector::dgn_import(path, &model).is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The importer a file goes to.
|
||||||
|
enum Route {
|
||||||
|
Pdf,
|
||||||
|
Dgn,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The importer for `path` chosen as `format` (an index of [`FORMATS`]; `None`
|
||||||
|
/// follows the extension), or why there is none.
|
||||||
|
fn route(path: &str, format: Option<usize>) -> Result<Route, String> {
|
||||||
|
let f = format.map(|k| &FORMATS[k]);
|
||||||
|
if let Some(f) = f {
|
||||||
|
if let Some(reason) = unsupported_reason(f.support) {
|
||||||
|
return Err(format!("{}: {reason}", f.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ext = extension(path);
|
||||||
|
let by_extension = f.is_none_or(|f| f.support == Support::ByExtension);
|
||||||
|
let pdf = if by_extension { ext == "pdf" } else { f.is_some_and(|f| f.name == "PDF Files") };
|
||||||
|
if pdf {
|
||||||
|
return Ok(Route::Pdf);
|
||||||
|
}
|
||||||
|
let dgn_format = f.is_some_and(|f| f.name.contains("DGN"));
|
||||||
|
if (dgn_format || by_extension) && is_dgn(path) {
|
||||||
|
return Ok(Route::Dgn);
|
||||||
|
}
|
||||||
|
if dgn_format || ext == "dgn" {
|
||||||
|
return Err(crate::t!("Invalid or unsupported DGN file.").into_owned());
|
||||||
|
}
|
||||||
|
// Any other extension: the format whose patterns name it.
|
||||||
|
let named = FORMATS
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.extensions.iter().any(|e| e.eq_ignore_ascii_case(&ext)));
|
||||||
|
Err(match named.and_then(|f| unsupported_reason(f.support).map(|r| (f, r))) {
|
||||||
|
Some((f, reason)) => format!("{}: {reason}", f.name),
|
||||||
|
None => crate::t!("OpenCAD Studio has no importer for this format.").into_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenCADStudio {
|
||||||
|
/// IMPORT, DGNIMPORT and -DGNIMPORT; `None` defers to the other
|
||||||
|
/// families.
|
||||||
|
pub(super) fn dispatch_import(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||||
|
let (verb, rest) = match cmd.trim().split_once(char::is_whitespace) {
|
||||||
|
Some((verb, rest)) => (verb.to_ascii_uppercase(), rest.trim().trim_matches('"')),
|
||||||
|
None => (cmd.trim().to_ascii_uppercase(), ""),
|
||||||
|
};
|
||||||
|
let dialog = crate::sys::file_dia();
|
||||||
|
match (verb.as_str(), rest.is_empty()) {
|
||||||
|
("IMPORT", true) if dialog => {
|
||||||
|
self.open_import_file_dialog();
|
||||||
|
Some(Task::none())
|
||||||
|
}
|
||||||
|
("IMPORT", true) => {
|
||||||
|
use crate::command::{CadCommand, ValuePromptCommand};
|
||||||
|
let command = ValuePromptCommand::new("_IMPORTFILE", "Enter import file name:");
|
||||||
|
self.command_line.push_info(&command.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
|
Some(Task::none())
|
||||||
|
}
|
||||||
|
// A file named after IMPORT (or typed at its prompt).
|
||||||
|
("IMPORT" | "_IMPORTFILE", false) => Some(self.import_routed(rest, None)),
|
||||||
|
("_IMPORTFILE", true) => Some(Task::none()),
|
||||||
|
("DGNIMPORT", true) if dialog => Some(self.pick_dgn_file()),
|
||||||
|
("DGNIMPORT" | "-DGNIMPORT", _) => {
|
||||||
|
use crate::command::CadCommand;
|
||||||
|
let command = DgnImportCommand::new();
|
||||||
|
self.command_line.push_info(&command.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
|
if !rest.is_empty() {
|
||||||
|
return Some(
|
||||||
|
self.feed_command(crate::command::StepInput::Text(rest.to_string())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(Task::none())
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pick_dgn_file(&mut self) -> Task<Message> {
|
||||||
|
let title = crate::t!("Import DGN File").into_owned();
|
||||||
|
Task::perform(pick(title, dgn_filters()), |path| {
|
||||||
|
Message::ImportDialog(ImportDialogMsg::DgnPicked(path))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::app) fn open_import_file_dialog(&mut self) {
|
||||||
|
let path = self.import_dialogs.recent.first().cloned().unwrap_or_default();
|
||||||
|
self.import_dialogs.file = Some(ImportFileState {
|
||||||
|
path,
|
||||||
|
format: 0,
|
||||||
|
search: String::new(),
|
||||||
|
only_supported: false,
|
||||||
|
error: None,
|
||||||
|
});
|
||||||
|
self.active_modal = Some(crate::app::ModalKind::ImportFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The DGN Import window for a file; an error when it is no DGN model.
|
||||||
|
pub(in crate::app) fn open_dgn_import_dialog(&mut self, path: &str) -> Result<(), String> {
|
||||||
|
use crate::scene::model::underlay_vector::{dgn_import, item_names};
|
||||||
|
let models = item_names(codec::entities::UnderlayType::Dgn, path).unwrap_or_default();
|
||||||
|
let Some(import) = models.first().and_then(|m| dgn_import(path, m)) else {
|
||||||
|
return Err(crate::t!("Invalid or unsupported DGN file.").into_owned());
|
||||||
|
};
|
||||||
|
// The options chosen last stay when the file is changed.
|
||||||
|
let previous = self.import_dialogs.dgn.take();
|
||||||
|
let keep = |f: fn(&DgnImportState) -> bool, default: bool| {
|
||||||
|
previous.as_ref().map_or(default, f)
|
||||||
|
};
|
||||||
|
self.import_dialogs.dgn = Some(DgnImportState {
|
||||||
|
path: path.to_string(),
|
||||||
|
version: if import.v7 { "V7" } else { "V8" },
|
||||||
|
models: crate::ui::window::pdf_dialogs::item_thumbs(
|
||||||
|
codec::entities::UnderlayType::Dgn,
|
||||||
|
path,
|
||||||
|
),
|
||||||
|
model: 0,
|
||||||
|
current_drawing: keep(|s| s.current_drawing, false),
|
||||||
|
prefix_duplicates: keep(|s| s.prefix_duplicates, false),
|
||||||
|
sub_units: keep(|s| s.sub_units, false),
|
||||||
|
explode_text_nodes: keep(|s| s.explode_text_nodes, false),
|
||||||
|
units: import.unit_names.clone(),
|
||||||
|
mapping_tab: previous.as_ref().map_or(0, |s| s.mapping_tab),
|
||||||
|
mapping: [0, 1, 2, 3].map(|tab| dgn_import::mapping_rows(&import, tab)),
|
||||||
|
});
|
||||||
|
self.active_modal = Some(crate::app::ModalKind::DgnImport);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remember_import(&mut self, path: &str) {
|
||||||
|
let recent = &mut self.import_dialogs.recent;
|
||||||
|
recent.retain(|p| p != path);
|
||||||
|
recent.insert(0, path.to_string());
|
||||||
|
recent.truncate(RECENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// IMPORT on the command line: the file to its importer.
|
||||||
|
fn import_routed(&mut self, path: &str, format: Option<usize>) -> Task<Message> {
|
||||||
|
if !exists(path) {
|
||||||
|
self.command_line
|
||||||
|
.push_error(&format!("File {path} does not exist."));
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
match route(path, format) {
|
||||||
|
Ok(Route::Pdf) => {
|
||||||
|
self.remember_import(path);
|
||||||
|
self.dispatch_command(&format!("_PDFIMPORTFILE {path}"))
|
||||||
|
}
|
||||||
|
Ok(Route::Dgn) => {
|
||||||
|
self.remember_import(path);
|
||||||
|
if crate::sys::file_dia() {
|
||||||
|
if let Err(error) = self.open_dgn_import_dialog(path) {
|
||||||
|
self.command_line.push_error(&error);
|
||||||
|
}
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
self.dispatch_command(&format!("-DGNIMPORT {path}"))
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
self.command_line.push_error(&error);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::app) fn update_import_dialog(&mut self, message: ImportDialogMsg) -> Task<Message> {
|
||||||
|
let i = self.active_tab;
|
||||||
|
match message {
|
||||||
|
ImportDialogMsg::Help(which) => {
|
||||||
|
let help = match which {
|
||||||
|
"dgn" => crate::t!(
|
||||||
|
"Imports a DGN model as drawing objects: levels as layers, cells as blocks."
|
||||||
|
),
|
||||||
|
_ => crate::t!(
|
||||||
|
"Imports a file of the chosen format with the format's importer."
|
||||||
|
),
|
||||||
|
};
|
||||||
|
self.command_line.push_info(help.as_ref());
|
||||||
|
}
|
||||||
|
// ── Import File ──────────────────────────────────────────────
|
||||||
|
ImportDialogMsg::Path(path) => {
|
||||||
|
if let Some(s) = self.import_dialogs.file.as_mut() {
|
||||||
|
s.path = path;
|
||||||
|
s.error = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImportDialogMsg::Recent(n) => {
|
||||||
|
let path = self.import_dialogs.recent.get(n).cloned();
|
||||||
|
if let (Some(s), Some(path)) = (self.import_dialogs.file.as_mut(), path) {
|
||||||
|
s.path = path;
|
||||||
|
s.error = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImportDialogMsg::Format(k) => {
|
||||||
|
if let Some(s) = self.import_dialogs.file.as_mut() {
|
||||||
|
s.format = k.min(FORMATS.len() - 1);
|
||||||
|
s.error = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImportDialogMsg::Search(text) => {
|
||||||
|
if let Some(s) = self.import_dialogs.file.as_mut() {
|
||||||
|
s.search = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImportDialogMsg::OnlySupported(on) => {
|
||||||
|
if let Some(s) = self.import_dialogs.file.as_mut() {
|
||||||
|
s.only_supported = on;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImportDialogMsg::Browse => {
|
||||||
|
let Some(s) = self.import_dialogs.file.as_ref() else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let f = &FORMATS[s.format];
|
||||||
|
let filters = vec![(
|
||||||
|
format!("{} ({})", f.name, f.patterns),
|
||||||
|
f.extensions.iter().map(|e| e.to_string()).collect(),
|
||||||
|
)];
|
||||||
|
let title = crate::t!("Import File").into_owned();
|
||||||
|
return Task::perform(pick(title, filters), |path| {
|
||||||
|
Message::ImportDialog(ImportDialogMsg::Picked(path))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ImportDialogMsg::Picked(Some(path)) => {
|
||||||
|
if let Some(s) = self.import_dialogs.file.as_mut() {
|
||||||
|
s.path = path;
|
||||||
|
s.error = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImportDialogMsg::Picked(None) => {}
|
||||||
|
ImportDialogMsg::ImportOk => {
|
||||||
|
let Some(s) = self.import_dialogs.file.as_ref() else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let path = s.path.trim().trim_matches('"').to_string();
|
||||||
|
let format = s.format;
|
||||||
|
let error = if path.is_empty() {
|
||||||
|
Some(crate::t!("Filename cannot be blank").into_owned())
|
||||||
|
} else if !exists(&path) {
|
||||||
|
Some(format!("File {path} does not exist."))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let routed = match error {
|
||||||
|
Some(error) => Err(error),
|
||||||
|
None => route(&path, Some(format)),
|
||||||
|
};
|
||||||
|
match routed {
|
||||||
|
Err(error) => {
|
||||||
|
if let Some(s) = self.import_dialogs.file.as_mut() {
|
||||||
|
s.error = Some(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Route::Pdf) => {
|
||||||
|
self.remember_import(&path);
|
||||||
|
self.close_active_modal();
|
||||||
|
return self.dispatch_command(&format!("_PDFIMPORTFILE {path}"));
|
||||||
|
}
|
||||||
|
Ok(Route::Dgn) => {
|
||||||
|
self.remember_import(&path);
|
||||||
|
self.close_active_modal();
|
||||||
|
if let Err(error) = self.open_dgn_import_dialog(&path) {
|
||||||
|
self.command_line.push_error(&error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ── DGN Import ───────────────────────────────────────────────
|
||||||
|
ImportDialogMsg::DgnChange => return self.pick_dgn_file(),
|
||||||
|
ImportDialogMsg::DgnPicked(Some(path)) => {
|
||||||
|
if let Err(error) = self.open_dgn_import_dialog(&path) {
|
||||||
|
self.command_line.push_error(&error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImportDialogMsg::DgnPicked(None) => {}
|
||||||
|
ImportDialogMsg::DgnModel(k) => {
|
||||||
|
use crate::scene::model::underlay_vector::dgn_import;
|
||||||
|
if let Some(s) = self.import_dialogs.dgn.as_mut() {
|
||||||
|
if let Some(model) = s.models.get(k) {
|
||||||
|
if let Some(import) = dgn_import(&s.path, &model.label) {
|
||||||
|
s.mapping = [0, 1, 2, 3].map(|tab| dgn_import::mapping_rows(&import, tab));
|
||||||
|
s.units = import.unit_names.clone();
|
||||||
|
}
|
||||||
|
s.model = k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImportDialogMsg::DgnCurrent(on) => {
|
||||||
|
if let Some(s) = self.import_dialogs.dgn.as_mut() {
|
||||||
|
s.current_drawing = on;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImportDialogMsg::DgnPrefix(on) => {
|
||||||
|
if let Some(s) = self.import_dialogs.dgn.as_mut() {
|
||||||
|
s.prefix_duplicates = on;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImportDialogMsg::DgnSub(on) => {
|
||||||
|
if let Some(s) = self.import_dialogs.dgn.as_mut() {
|
||||||
|
s.sub_units = on;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImportDialogMsg::DgnExplode(on) => {
|
||||||
|
if let Some(s) = self.import_dialogs.dgn.as_mut() {
|
||||||
|
s.explode_text_nodes = on;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImportDialogMsg::DgnMappingTab(tab) => {
|
||||||
|
if let Some(s) = self.import_dialogs.dgn.as_mut() {
|
||||||
|
s.mapping_tab = tab.min(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImportDialogMsg::DgnOk => {
|
||||||
|
let Some(s) = self.import_dialogs.dgn.as_ref() else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let request = DgnImportRequest {
|
||||||
|
path: s.path.clone(),
|
||||||
|
model: s
|
||||||
|
.models
|
||||||
|
.get(s.model)
|
||||||
|
.map(|m| m.label.clone())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
sub_units: s.sub_units,
|
||||||
|
explode_text_nodes: s.explode_text_nodes,
|
||||||
|
current_drawing: s.current_drawing,
|
||||||
|
prefix_duplicates: s.prefix_duplicates,
|
||||||
|
};
|
||||||
|
self.close_active_modal();
|
||||||
|
return self.run_dgn_import(i, request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The import: into a new drawing (or the current one), one undo step.
|
||||||
|
pub(in crate::app) fn run_dgn_import(&mut self, i: usize, request: DgnImportRequest) -> Task<Message> {
|
||||||
|
let Some(import) =
|
||||||
|
crate::scene::model::underlay_vector::dgn_import(&request.path, &request.model)
|
||||||
|
else {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("Invalid or unsupported DGN file.").as_ref());
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let converted =
|
||||||
|
dgn_import::convert(&import, request.sub_units, request.explode_text_nodes);
|
||||||
|
let mut task = Task::none();
|
||||||
|
let i = if request.current_drawing {
|
||||||
|
i
|
||||||
|
} else {
|
||||||
|
task = self.update(Message::TabNew);
|
||||||
|
self.active_tab
|
||||||
|
};
|
||||||
|
self.push_undo_snapshot(i, "DGNIMPORT");
|
||||||
|
let stem = std::path::Path::new(&request.path.replace('\\', "/"))
|
||||||
|
.file_stem()
|
||||||
|
.map(|s| s.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let scene = &mut self.tabs[i].scene;
|
||||||
|
// In the current drawing, names it already has keep the existing
|
||||||
|
// definitions, or are imported under the file's name as prefix.
|
||||||
|
let prefix = (request.current_drawing && request.prefix_duplicates)
|
||||||
|
.then(|| format!("{stem}$0$"));
|
||||||
|
let rename = |taken: bool, name: &str| match &prefix {
|
||||||
|
Some(p) if taken && name != "0" => format!("{p}{name}"),
|
||||||
|
_ => name.to_string(),
|
||||||
|
};
|
||||||
|
let mut layers: HashMap<String, String> = HashMap::new();
|
||||||
|
for (name, color) in &converted.layers {
|
||||||
|
let target = rename(scene.document.layers.contains(name), name);
|
||||||
|
if !scene.document.layers.contains(&target) {
|
||||||
|
let mut layer = codec::tables::Layer::new(target.as_str());
|
||||||
|
layer.handle = scene.document.allocate_handle();
|
||||||
|
layer.color = *color;
|
||||||
|
let _ = scene.document.layers.add(layer);
|
||||||
|
}
|
||||||
|
layers.insert(name.clone(), target);
|
||||||
|
}
|
||||||
|
let mut styles: HashMap<String, String> = HashMap::new();
|
||||||
|
for (name, font) in &converted.styles {
|
||||||
|
let target = rename(scene.document.text_styles.contains(name), name);
|
||||||
|
if !scene.document.text_styles.contains(&target) {
|
||||||
|
let mut style = codec::tables::TextStyle::new(target.as_str());
|
||||||
|
style.handle = scene.document.allocate_handle();
|
||||||
|
style.font_file = font.clone();
|
||||||
|
let _ = scene.document.text_styles.add(style);
|
||||||
|
}
|
||||||
|
styles.insert(name.clone(), target);
|
||||||
|
}
|
||||||
|
let mut blocks: HashMap<String, String> = HashMap::new();
|
||||||
|
for (name, _) in &converted.blocks {
|
||||||
|
let taken = scene.document.block_records.get(name).is_some();
|
||||||
|
blocks.insert(name.clone(), rename(taken, name));
|
||||||
|
}
|
||||||
|
let fix = |mut entity: codec::EntityType| {
|
||||||
|
let common = entity.common_mut();
|
||||||
|
if let Some(layer) = layers.get(&common.layer) {
|
||||||
|
common.layer = layer.clone();
|
||||||
|
}
|
||||||
|
match &mut entity {
|
||||||
|
codec::EntityType::Text(t) => {
|
||||||
|
if let Some(s) = styles.get(&t.style) {
|
||||||
|
t.style = s.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
codec::EntityType::MText(t) => {
|
||||||
|
if let Some(s) = styles.get(&t.style) {
|
||||||
|
t.style = s.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
codec::EntityType::Insert(insert) => {
|
||||||
|
if let Some(b) = blocks.get(&insert.block_name) {
|
||||||
|
insert.block_name = b.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
entity
|
||||||
|
};
|
||||||
|
for (name, entities) in converted.blocks {
|
||||||
|
let target = &blocks[&name];
|
||||||
|
if scene.document.block_records.get(target).is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let entities = entities.into_iter().map(&fix).collect();
|
||||||
|
let _ = scene.define_block_from_owned_entities(entities, target, glam::DVec3::ZERO);
|
||||||
|
}
|
||||||
|
for entity in converted.entities {
|
||||||
|
scene.add_entity(fix(entity));
|
||||||
|
}
|
||||||
|
if !request.current_drawing {
|
||||||
|
scene.fit_all();
|
||||||
|
}
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.command_line.push_output("Regenerating model.");
|
||||||
|
self.refresh_layer_panel();
|
||||||
|
self.refresh_properties();
|
||||||
|
task
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ mod blocks;
|
|||||||
mod blockvars;
|
mod blockvars;
|
||||||
mod bvisibility;
|
mod bvisibility;
|
||||||
mod count;
|
mod count;
|
||||||
|
mod dgn_import;
|
||||||
mod dim;
|
mod dim;
|
||||||
pub(crate) mod display;
|
pub(crate) mod display;
|
||||||
mod draw;
|
mod draw;
|
||||||
@@ -337,6 +338,9 @@ impl OpenCADStudio {
|
|||||||
if let Some(t) = self.dispatch_xref_attach(cmd, i) {
|
if let Some(t) = self.dispatch_xref_attach(cmd, i) {
|
||||||
return Some(t);
|
return Some(t);
|
||||||
}
|
}
|
||||||
|
if let Some(t) = self.dispatch_import(cmd, i) {
|
||||||
|
return Some(t);
|
||||||
|
}
|
||||||
if let Some(t) = self.dispatch_bvisibility(cmd, i) {
|
if let Some(t) = self.dispatch_bvisibility(cmd, i) {
|
||||||
return Some(t);
|
return Some(t);
|
||||||
}
|
}
|
||||||
@@ -934,6 +938,9 @@ inventory::submit!(crate::command::CommandRegistration {
|
|||||||
"NEW",
|
"NEW",
|
||||||
"QNEW",
|
"QNEW",
|
||||||
"OBJIMPORT",
|
"OBJIMPORT",
|
||||||
|
"IMPORT",
|
||||||
|
"DGNIMPORT",
|
||||||
|
"-DGNIMPORT",
|
||||||
"OPEN",
|
"OPEN",
|
||||||
"ORTHO",
|
"ORTHO",
|
||||||
"P",
|
"P",
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ pub(super) const NAMES: &[&str] = &[
|
|||||||
"txt2mtxt_settings",
|
"txt2mtxt_settings",
|
||||||
"view_background",
|
"view_background",
|
||||||
"image_adjust",
|
"image_adjust",
|
||||||
|
"import_file_dialog",
|
||||||
|
"dgn_import_dialog",
|
||||||
"refedit_dialog",
|
"refedit_dialog",
|
||||||
"pdf_layer_toggle",
|
"pdf_layer_toggle",
|
||||||
"pc_manager_toggle",
|
"pc_manager_toggle",
|
||||||
@@ -2161,6 +2163,90 @@ impl OpenCADStudio {
|
|||||||
_ => return Err(bad()),
|
_ => return Err(bad()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The Import File window, one field per call: "path=<file>",
|
||||||
|
// "format=<index or name>", "search=<text>", "filter=all|supported",
|
||||||
|
// "recent=<n>", "ok" or "cancel".
|
||||||
|
"import_file_dialog" => {
|
||||||
|
use crate::ui::window::import_dialogs::{ImportDialogMsg as I, FORMATS};
|
||||||
|
if self.active_modal != Some(crate::app::ModalKind::ImportFile) {
|
||||||
|
return Err(failure("no_dialog", "The Import File window is not open"));
|
||||||
|
}
|
||||||
|
let spec = string(req, "value")?;
|
||||||
|
let (key, value) = spec.split_once('=').unwrap_or((spec, ""));
|
||||||
|
let bad = || failure("invalid_value", spec);
|
||||||
|
if key == "cancel" {
|
||||||
|
return Ok(Task::done(Message::CloseModal));
|
||||||
|
}
|
||||||
|
Message::ImportDialog(match key {
|
||||||
|
"ok" => I::ImportOk,
|
||||||
|
"path" => I::Path(value.to_string()),
|
||||||
|
"search" => I::Search(value.to_string()),
|
||||||
|
"filter" => I::OnlySupported(match value {
|
||||||
|
"all" => false,
|
||||||
|
"supported" => true,
|
||||||
|
_ => return Err(bad()),
|
||||||
|
}),
|
||||||
|
"recent" => I::Recent(value.trim().parse().map_err(|_| bad())?),
|
||||||
|
"format" => I::Format(match value.trim().parse::<usize>() {
|
||||||
|
Ok(k) if k < FORMATS.len() => k,
|
||||||
|
_ => FORMATS
|
||||||
|
.iter()
|
||||||
|
.position(|f| f.name.eq_ignore_ascii_case(value.trim()))
|
||||||
|
.ok_or_else(bad)?,
|
||||||
|
}),
|
||||||
|
_ => return Err(bad()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// The DGN Import window, one field per call: "path=<file>" (as
|
||||||
|
// Change… picks it), "model=<n>", "target=new|current",
|
||||||
|
// "conflict=prefix|ignore", "units=master|sub", "explode=on|off",
|
||||||
|
// "mapping=layer|linetype|lineweight|color", "ok" or "cancel".
|
||||||
|
"dgn_import_dialog" => {
|
||||||
|
use crate::ui::window::import_dialogs::ImportDialogMsg as I;
|
||||||
|
if self.active_modal != Some(crate::app::ModalKind::DgnImport) {
|
||||||
|
return Err(failure("no_dialog", "The DGN Import window is not open"));
|
||||||
|
}
|
||||||
|
let spec = string(req, "value")?;
|
||||||
|
let (key, value) = spec.split_once('=').unwrap_or((spec, ""));
|
||||||
|
let bad = || failure("invalid_value", spec);
|
||||||
|
let on = |v: &str| match v {
|
||||||
|
"on" | "1" | "true" => Ok(true),
|
||||||
|
"off" | "0" | "false" => Ok(false),
|
||||||
|
_ => Err(bad()),
|
||||||
|
};
|
||||||
|
if key == "cancel" {
|
||||||
|
return Ok(Task::done(Message::CloseModal));
|
||||||
|
}
|
||||||
|
Message::ImportDialog(match key {
|
||||||
|
"ok" => I::DgnOk,
|
||||||
|
"path" => I::DgnPicked(Some(value.to_string())),
|
||||||
|
"model" => I::DgnModel(value.trim().parse().map_err(|_| bad())?),
|
||||||
|
"target" => I::DgnCurrent(match value {
|
||||||
|
"new" => false,
|
||||||
|
"current" => true,
|
||||||
|
_ => return Err(bad()),
|
||||||
|
}),
|
||||||
|
"conflict" => I::DgnPrefix(match value {
|
||||||
|
"prefix" => true,
|
||||||
|
"ignore" => false,
|
||||||
|
_ => return Err(bad()),
|
||||||
|
}),
|
||||||
|
"units" => I::DgnSub(match value {
|
||||||
|
"master" => false,
|
||||||
|
"sub" => true,
|
||||||
|
_ => return Err(bad()),
|
||||||
|
}),
|
||||||
|
"explode" => I::DgnExplode(on(value)?),
|
||||||
|
"mapping" => I::DgnMappingTab(match value {
|
||||||
|
"layer" => 0,
|
||||||
|
"linetype" => 1,
|
||||||
|
"lineweight" => 2,
|
||||||
|
"color" => 3,
|
||||||
|
_ => return Err(bad()),
|
||||||
|
}),
|
||||||
|
_ => return Err(bad()),
|
||||||
|
})
|
||||||
|
}
|
||||||
// The Image Adjust dialog, one field per call: "brightness=70",
|
// The Image Adjust dialog, one field per call: "brightness=70",
|
||||||
// "contrast=40", "fade=20" (the bar), "type=fade,abc" (typed into
|
// "contrast=40", "fade=20" (the bar), "type=fade,abc" (typed into
|
||||||
// the box), "step=brightness,+1|-1", "reset", "apply" or "cancel".
|
// the box), "step=brightness,+1|-1", "reset", "apply" or "cancel".
|
||||||
|
|||||||
@@ -610,6 +610,8 @@ pub(super) struct OpenCADStudio {
|
|||||||
underlay_layers: Option<crate::ui::window::pdf_dialogs::UnderlayLayersState>,
|
underlay_layers: Option<crate::ui::window::pdf_dialogs::UnderlayLayersState>,
|
||||||
pdf_import_settings: Option<crate::modules::insert::pdf_import::PdfImportSettings>,
|
pdf_import_settings: Option<crate::modules::insert::pdf_import::PdfImportSettings>,
|
||||||
pdf_import_file: Option<crate::ui::window::pdf_dialogs::PdfImportFileState>,
|
pdf_import_file: Option<crate::ui::window::pdf_dialogs::PdfImportFileState>,
|
||||||
|
/// IMPORT and DGNIMPORT windows.
|
||||||
|
import_dialogs: crate::ui::window::import_dialogs::ImportDialogs,
|
||||||
/// The dialog Options was opened from; it comes back when Options closes.
|
/// The dialog Options was opened from; it comes back when Options closes.
|
||||||
options_parent: Option<ModalKind>,
|
options_parent: Option<ModalKind>,
|
||||||
/// Working copy of the Attach External Reference dialog; None while closed.
|
/// Working copy of the Attach External Reference dialog; None while closed.
|
||||||
@@ -2273,6 +2275,10 @@ pub enum ModalKind {
|
|||||||
XrefHelp,
|
XrefHelp,
|
||||||
/// Sheet set wizard, properties and sheet / subset forms.
|
/// Sheet set wizard, properties and sheet / subset forms.
|
||||||
SheetSet,
|
SheetSet,
|
||||||
|
/// IMPORT's Import File window.
|
||||||
|
ImportFile,
|
||||||
|
/// DGNIMPORT's DGN Import window.
|
||||||
|
DgnImport,
|
||||||
/// Web: a crash-recovery autosave snapshot newer than the drawing's last
|
/// Web: a crash-recovery autosave snapshot newer than the drawing's last
|
||||||
/// save awaits a restore/discard decision. Distinct from the corrupt-file
|
/// save awaits a restore/discard decision. Distinct from the corrupt-file
|
||||||
/// `RecoveryPrompt` — nothing is wrong with the drawing itself.
|
/// `RecoveryPrompt` — nothing is wrong with the drawing itself.
|
||||||
@@ -4547,6 +4553,8 @@ pub enum Message {
|
|||||||
std::path::PathBuf,
|
std::path::PathBuf,
|
||||||
Result<crate::scene::model::mesh_model::MeshModel, String>,
|
Result<crate::scene::model::mesh_model::MeshModel, String>,
|
||||||
),
|
),
|
||||||
|
/// An edit in the Import File or DGN Import window.
|
||||||
|
ImportDialog(crate::ui::window::import_dialogs::ImportDialogMsg),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -4664,6 +4672,7 @@ impl OpenCADStudio {
|
|||||||
underlay_layers: None,
|
underlay_layers: None,
|
||||||
pdf_import_settings: None,
|
pdf_import_settings: None,
|
||||||
pdf_import_file: None,
|
pdf_import_file: None,
|
||||||
|
import_dialogs: Default::default(),
|
||||||
options_parent: None,
|
options_parent: None,
|
||||||
xref_attach: None,
|
xref_attach: None,
|
||||||
wblock: None,
|
wblock: None,
|
||||||
|
|||||||
@@ -251,6 +251,8 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(PdfImportFile) => self.pdf_import_file = None,
|
Some(PdfImportFile) => self.pdf_import_file = None,
|
||||||
|
Some(ImportFile) => self.import_dialogs.file = None,
|
||||||
|
Some(DgnImport) => self.import_dialogs.dgn = None,
|
||||||
Some(XrefAttach) => self.xref_attach = None,
|
Some(XrefAttach) => self.xref_attach = None,
|
||||||
Some(WriteBlock) => self.wblock = None,
|
Some(WriteBlock) => self.wblock = None,
|
||||||
Some(Hyperlink) => {
|
Some(Hyperlink) => {
|
||||||
@@ -2452,6 +2454,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Message::PdfDialog(message) => self.update_pdf_dialog(message),
|
Message::PdfDialog(message) => self.update_pdf_dialog(message),
|
||||||
|
Message::ImportDialog(message) => self.update_import_dialog(message),
|
||||||
Message::AttdefDialog(message) => self.on_attdef_dialog(message),
|
Message::AttdefDialog(message) => self.on_attdef_dialog(message),
|
||||||
Message::AttMan(message) => self.on_attman(message),
|
Message::AttMan(message) => self.on_attman(message),
|
||||||
Message::FieldDialog(message) => self.on_field_dialog(message),
|
Message::FieldDialog(message) => self.on_field_dialog(message),
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ impl OpenCADStudio {
|
|||||||
Some(K::PdfImportSettings) => crate::t!("PDF Import Settings").into_owned(),
|
Some(K::PdfImportSettings) => crate::t!("PDF Import Settings").into_owned(),
|
||||||
Some(K::Txt2MtxtSettings) => crate::tr!("txt2mtxt", "title"),
|
Some(K::Txt2MtxtSettings) => crate::tr!("txt2mtxt", "title"),
|
||||||
Some(K::PdfImportFile) => crate::t!("Import PDF").into_owned(),
|
Some(K::PdfImportFile) => crate::t!("Import PDF").into_owned(),
|
||||||
|
Some(K::ImportFile) => crate::t!("Import File").into_owned(),
|
||||||
|
Some(K::DgnImport) => crate::t!("DGN Import").into_owned(),
|
||||||
Some(K::XrefAttach) => crate::t!("Attach External Reference").into_owned(),
|
Some(K::XrefAttach) => crate::t!("Attach External Reference").into_owned(),
|
||||||
Some(K::WriteBlock) => crate::t!("Write Block").into_owned(),
|
Some(K::WriteBlock) => crate::t!("Write Block").into_owned(),
|
||||||
Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(),
|
Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(),
|
||||||
@@ -590,6 +592,19 @@ impl OpenCADStudio {
|
|||||||
crate::ui::window::pdf_dialogs::view_import_file(state, flow)
|
crate::ui::window::pdf_dialogs::view_import_file(state, flow)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
super::super::ModalKind::ImportFile => {
|
||||||
|
let state = self.import_dialogs.file.as_ref()?;
|
||||||
|
let recent = &self.import_dialogs.recent;
|
||||||
|
sized_flow(ex, 960, 620, |flow| {
|
||||||
|
crate::ui::window::import_dialogs::view_import_file(state, recent, flow)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
super::super::ModalKind::DgnImport => {
|
||||||
|
let state = self.import_dialogs.dgn.as_ref()?;
|
||||||
|
sized_flow(ex, 900, 680, |flow| {
|
||||||
|
crate::ui::window::import_dialogs::view_dgn_import(state, flow)
|
||||||
|
})
|
||||||
|
}
|
||||||
super::super::ModalKind::XrefAttach => {
|
super::super::ModalKind::XrefAttach => {
|
||||||
let state = self.xref_attach.as_ref()?;
|
let state = self.xref_attach.as_ref()?;
|
||||||
let height = if state.details { 520 } else { 450 };
|
let height = if state.details { 520 } else { 450 };
|
||||||
|
|||||||
@@ -2272,6 +2272,8 @@ pub enum CmdResult {
|
|||||||
SetTexteditMode(bool),
|
SetTexteditMode(bool),
|
||||||
/// Return a resolved point back to a suspended parent command (e.g. MTP / M2P).
|
/// Return a resolved point back to a suspended parent command (e.g. MTP / M2P).
|
||||||
ReturnPoint(DVec3),
|
ReturnPoint(DVec3),
|
||||||
|
/// Import a DGN model (DGNIMPORT); ends the command.
|
||||||
|
DgnImport(crate::modules::insert::dgn_import::DgnImportRequest),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What kind of value the active command is currently asking for. Drives
|
/// What kind of value the active command is currently asking for. Drives
|
||||||
|
|||||||
@@ -3474,6 +3474,40 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"Unable to save: file is in use." => Some(("command-line", "unable-to-save-file-is-in-use")),
|
"Unable to save: file is in use." => Some(("command-line", "unable-to-save-file-is-in-use")),
|
||||||
"Unable to verify the server certificate" => Some(("plugins", "unable-to-verify-the-server-certificate")),
|
"Unable to verify the server certificate" => Some(("plugins", "unable-to-verify-the-server-certificate")),
|
||||||
"Unavailable" => Some(("plugins", "unavailable")),
|
"Unavailable" => Some(("plugins", "unavailable")),
|
||||||
|
"Supported" => Some(("dgn-import", "supported")),
|
||||||
|
"By extension" => Some(("dgn-import", "by-extension")),
|
||||||
|
"ACIS solver available" => Some(("dgn-import", "acis-solver")),
|
||||||
|
"Export only" => Some(("dgn-import", "export-only")),
|
||||||
|
"ACIS bodies are read only inside drawings; SAT files cannot be imported." => Some(("dgn-import", "acis-reason")),
|
||||||
|
"STEP files are only written (STEPOUT), not read." => Some(("dgn-import", "step-reason")),
|
||||||
|
"OpenCAD Studio has no importer for this format." => Some(("dgn-import", "no-importer")),
|
||||||
|
"Recent:" => Some(("dgn-import", "recent")),
|
||||||
|
"2D / Underlay" => Some(("dgn-import", "group-2d")),
|
||||||
|
"3D Model" => Some(("dgn-import", "group-3d")),
|
||||||
|
"Search formats..." => Some(("dgn-import", "search")),
|
||||||
|
"Only what OCS can open" => Some(("dgn-import", "only-supported")),
|
||||||
|
"{n} model(s)" => Some(("dgn-import", "models")),
|
||||||
|
"Change file..." => Some(("dgn-import", "change")),
|
||||||
|
"On name conflict" => Some(("dgn-import", "name-conflict")),
|
||||||
|
"Add prefix to dependent definitions" => Some(("dgn-import", "prefix")),
|
||||||
|
"Ignore duplicate names" => Some(("dgn-import", "ignore-duplicates")),
|
||||||
|
"Master unit" => Some(("dgn-import", "master-unit")),
|
||||||
|
"Sub unit" => Some(("dgn-import", "sub-unit")),
|
||||||
|
"Explode text nodes to text elements" => Some(("dgn-import", "explode")),
|
||||||
|
"External DGN references" => Some(("dgn-import", "references")),
|
||||||
|
"Bind as block / xref" => Some(("dgn-import", "bind")),
|
||||||
|
"Attach as DGN underlay" => Some(("dgn-import", "attach")),
|
||||||
|
"Reference attachments of DGN files are not read; they are left out." => Some(("dgn-import", "references-note")),
|
||||||
|
"Property mapping" => Some(("dgn-import", "mapping")),
|
||||||
|
"Standard mapping setup." => Some(("dgn-import", "mapping-standard")),
|
||||||
|
"The preview shows the selected setup's effect on this file." => Some(("dgn-import", "mapping-note")),
|
||||||
|
"Import File" => Some(("dgn-import", "import-file")),
|
||||||
|
"DGN Import" => Some(("dgn-import", "dgn-import")),
|
||||||
|
"Import DGN File" => Some(("dgn-import", "import-dgn-file")),
|
||||||
|
"Imports a DGN model as drawing objects: levels as layers, cells as blocks." => Some(("dgn-import", "help-dgn")),
|
||||||
|
"Imports a file of the chosen format with the format's importer." => Some(("dgn-import", "help-import")),
|
||||||
|
"Filename cannot be blank" => Some(("dgn-import", "blank")),
|
||||||
|
"Invalid or unsupported DGN file." => Some(("dgn-import", "invalid-dgn")),
|
||||||
"PDF scale: {scale}:1" => Some(("insert", "pdfui-pdf-scale")),
|
"PDF scale: {scale}:1" => Some(("insert", "pdfui-pdf-scale")),
|
||||||
"Applied" => Some(("properties", "applied")),
|
"Applied" => Some(("properties", "applied")),
|
||||||
"No PDF underlays found." => Some(("insert", "pdfui-no-pdf-underlays")),
|
"No PDF underlays found." => Some(("insert", "pdfui-no-pdf-underlays")),
|
||||||
|
|||||||
@@ -0,0 +1,675 @@
|
|||||||
|
// DGNIMPORT / -DGNIMPORT — a DGN model (V7 or V8) as drawing objects.
|
||||||
|
//
|
||||||
|
// Enter filename for DGN import:
|
||||||
|
// Enter name of model or [?] <Default>:
|
||||||
|
// Specify conversion units [Master/Sub] <Master>:
|
||||||
|
// Specify mapping setup or [?] <Standard>:
|
||||||
|
//
|
||||||
|
// The Standard mapping setup: each level becomes the layer of its name (its
|
||||||
|
// number when unnamed), colours become the colour index of the same RGB
|
||||||
|
// (else a true colour), weights the lineweights below, V7 line styles
|
||||||
|
// Continuous. Lines, line strings and shapes, arcs, circles and ellipses,
|
||||||
|
// complex chains and shapes (polylines with arcs), text, text nodes and
|
||||||
|
// shared cells (blocks) are converted.
|
||||||
|
|
||||||
|
use codec::entities::{
|
||||||
|
Arc, Circle, Ellipse, Insert, Line, LwPolyline, LwVertex, MText, Text,
|
||||||
|
};
|
||||||
|
use codec::types::{Color, LineWeight, Vector2, Vector3};
|
||||||
|
use codec::EntityType;
|
||||||
|
|
||||||
|
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
|
||||||
|
use crate::scene::model::underlay_vector::{DgnElement, DgnGeom, DgnImport};
|
||||||
|
|
||||||
|
/// What one import reads and where it goes.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct DgnImportRequest {
|
||||||
|
pub path: String,
|
||||||
|
pub model: String,
|
||||||
|
/// Sub units (else master units) become drawing units.
|
||||||
|
pub sub_units: bool,
|
||||||
|
/// Text nodes become one TEXT per line (else one MTEXT).
|
||||||
|
pub explode_text_nodes: bool,
|
||||||
|
/// Into the current drawing (else a new one).
|
||||||
|
pub current_drawing: bool,
|
||||||
|
/// In the current drawing: layer, block and style names that already
|
||||||
|
/// exist get the file name as prefix (else the existing ones are used).
|
||||||
|
pub prefix_duplicates: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The objects of an import: layers (name, colour), text styles (name, font
|
||||||
|
/// file), blocks (name, objects) and the model's objects, in file order.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Converted {
|
||||||
|
pub layers: Vec<(String, Color)>,
|
||||||
|
pub styles: Vec<(String, String)>,
|
||||||
|
pub blocks: Vec<(String, Vec<EntityType>)>,
|
||||||
|
pub entities: Vec<EntityType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A DGN weight (0-31) as a lineweight (1/100 mm), as the Standard mapping
|
||||||
|
/// converts it.
|
||||||
|
pub fn lineweight_of(weight: u32) -> i16 {
|
||||||
|
const TABLE: [i16; 15] = [0, 13, 30, 40, 53, 70, 80, 100, 106, 120, 140, 158, 158, 158, 200];
|
||||||
|
TABLE.get(weight as usize).copied().unwrap_or(211)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An RGB colour as the colour index of the same RGB, else a true colour.
|
||||||
|
pub fn color_of(rgb: [u8; 3]) -> Color {
|
||||||
|
let index = codec::types::aci_table::nearest_aci(rgb[0], rgb[1], rgb[2]);
|
||||||
|
if codec::types::aci_table::aci_to_rgb(index) == Some((rgb[0], rgb[1], rgb[2])) {
|
||||||
|
Color::Index(index)
|
||||||
|
} else {
|
||||||
|
Color::from_rgb(rgb[0], rgb[1], rgb[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A unit's short name as words ("m" → Meters).
|
||||||
|
pub fn unit_display(short: &str) -> String {
|
||||||
|
match short.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"m" => "Meters",
|
||||||
|
"mm" => "Millimeters",
|
||||||
|
"cm" => "Centimeters",
|
||||||
|
"km" => "Kilometers",
|
||||||
|
"in" | "\"" => "Inches",
|
||||||
|
"ft" | "'" => "Feet",
|
||||||
|
"yd" => "Yards",
|
||||||
|
"mi" => "Miles",
|
||||||
|
other => return other.to_string(),
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layer_name(import: &DgnImport, level: u32) -> String {
|
||||||
|
import
|
||||||
|
.levels
|
||||||
|
.get(&level)
|
||||||
|
.map(|l| l.0.clone())
|
||||||
|
.filter(|n| !n.is_empty())
|
||||||
|
.unwrap_or_else(|| level.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when the axes `u` and `v` are of one length and at right angles.
|
||||||
|
fn circular(u: [f64; 2], v: [f64; 2]) -> bool {
|
||||||
|
let (lu, lv) = (u[0].hypot(u[1]), v[0].hypot(v[1]));
|
||||||
|
(lu - lv).abs() <= 1e-9 * lu.max(1.0) && (u[0] * v[0] + u[1] * v[1]).abs() <= 1e-9 * lu * lv
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The arc's points at `n` steps.
|
||||||
|
fn arc_points(center: [f64; 2], u: [f64; 2], v: [f64; 2], start: f64, sweep: f64) -> Vec<[f64; 2]> {
|
||||||
|
let n = ((sweep.abs() / (std::f64::consts::PI / 32.0)).ceil() as usize).clamp(2, 4096);
|
||||||
|
(0..=n)
|
||||||
|
.map(|k| {
|
||||||
|
let t = start + sweep * k as f64 / n as f64;
|
||||||
|
[
|
||||||
|
center[0] + t.cos() * u[0] + t.sin() * v[0],
|
||||||
|
center[1] + t.cos() * u[1] + t.sin() * v[1],
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A circular arc as centre, radius, start angle and signed sweep in the
|
||||||
|
/// drawing's angles.
|
||||||
|
fn circle_angles(u: [f64; 2], v: [f64; 2], start: f64, sweep: f64) -> (f64, f64, f64) {
|
||||||
|
let side = (u[0] * v[1] - u[1] * v[0]).signum();
|
||||||
|
let a0 = u[1].atan2(u[0]);
|
||||||
|
(u[0].hypot(u[1]), a0 + side * start, side * sweep)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn polyline(points: &[[f64; 2]], bulges: &[f64]) -> EntityType {
|
||||||
|
let mut pl = LwPolyline::new();
|
||||||
|
pl.plinegen = true;
|
||||||
|
pl.vertices = points
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(k, p)| {
|
||||||
|
let mut v = LwVertex::new(Vector2::new(p[0], p[1]));
|
||||||
|
v.bulge = bulges.get(k).copied().unwrap_or(0.0);
|
||||||
|
v
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
EntityType::LwPolyline(pl)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A complex chain's vertices and bulges: lines and line strings add their
|
||||||
|
/// points, circular arcs a bulge, other arcs their points.
|
||||||
|
fn chain(parts: &[DgnGeom]) -> (Vec<[f64; 2]>, Vec<f64>) {
|
||||||
|
let mut points: Vec<[f64; 2]> = Vec::new();
|
||||||
|
let mut bulges: Vec<f64> = Vec::new();
|
||||||
|
let mut add = |p: [f64; 2], bulge_before: Option<f64>| {
|
||||||
|
let same = points
|
||||||
|
.last()
|
||||||
|
.is_some_and(|q| (q[0] - p[0]).abs() < 1e-9 && (q[1] - p[1]).abs() < 1e-9);
|
||||||
|
if let (Some(b), Some(last)) = (bulge_before, bulges.last_mut()) {
|
||||||
|
*last = b;
|
||||||
|
}
|
||||||
|
if !same {
|
||||||
|
points.push(p);
|
||||||
|
bulges.push(0.0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for part in parts {
|
||||||
|
match part {
|
||||||
|
DgnGeom::Line(a, b) => {
|
||||||
|
add(*a, None);
|
||||||
|
add(*b, None);
|
||||||
|
}
|
||||||
|
DgnGeom::Points(pts) => pts.iter().for_each(|p| add(*p, None)),
|
||||||
|
DgnGeom::Arc {
|
||||||
|
center,
|
||||||
|
u,
|
||||||
|
v,
|
||||||
|
start,
|
||||||
|
sweep,
|
||||||
|
} if circular(*u, *v) => {
|
||||||
|
let at = |t: f64| {
|
||||||
|
[
|
||||||
|
center[0] + t.cos() * u[0] + t.sin() * v[0],
|
||||||
|
center[1] + t.cos() * u[1] + t.sin() * v[1],
|
||||||
|
]
|
||||||
|
};
|
||||||
|
let (_, _, signed) = circle_angles(*u, *v, *start, *sweep);
|
||||||
|
add(at(*start), None);
|
||||||
|
add(at(start + sweep), Some((signed / 4.0).tan()));
|
||||||
|
}
|
||||||
|
DgnGeom::Arc {
|
||||||
|
center,
|
||||||
|
u,
|
||||||
|
v,
|
||||||
|
start,
|
||||||
|
sweep,
|
||||||
|
} => arc_points(*center, *u, *v, *start, *sweep)
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|p| add(p, None)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(points, bulges)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_entity(geom: &DgnGeom, style: &str) -> Option<EntityType> {
|
||||||
|
let DgnGeom::Text {
|
||||||
|
text,
|
||||||
|
origin,
|
||||||
|
height,
|
||||||
|
width_factor,
|
||||||
|
rotation,
|
||||||
|
..
|
||||||
|
} = geom
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let mut t = Text::with_value(text.clone(), Vector3::new(origin[0], origin[1], 0.0))
|
||||||
|
.with_height(*height)
|
||||||
|
.with_rotation(*rotation);
|
||||||
|
t.width_factor = if *width_factor > 0.0 { *width_factor } else { 1.0 };
|
||||||
|
t.style = style.to_string();
|
||||||
|
Some(EntityType::Text(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The text style of a line of text (V7: one per font number, "dgnNNN"),
|
||||||
|
/// added to `styles` with its font file the first time.
|
||||||
|
fn text_style(geom: &DgnGeom, styles: &mut Vec<(String, String)>) -> String {
|
||||||
|
match geom {
|
||||||
|
DgnGeom::Text {
|
||||||
|
font: Some(font), ..
|
||||||
|
} => {
|
||||||
|
let name = format!("dgn{font:03}");
|
||||||
|
if !styles.iter().any(|s| s.0 == name) {
|
||||||
|
styles.push((name.clone(), format!("{name}.shx")));
|
||||||
|
}
|
||||||
|
name
|
||||||
|
}
|
||||||
|
_ => "Standard".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lines of text as one MTEXT, a paragraph a line, as the reference
|
||||||
|
/// converts them: a text node attached at its top left (a height above the
|
||||||
|
/// first baseline), a V8 text at its bottom left a third of the height
|
||||||
|
/// below its baseline.
|
||||||
|
fn mtext(lines: &[DgnGeom], style: String, node: bool) -> Option<EntityType> {
|
||||||
|
let Some(DgnGeom::Text {
|
||||||
|
origin,
|
||||||
|
height,
|
||||||
|
rotation,
|
||||||
|
..
|
||||||
|
}) = lines.first()
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let mut m = MText::new();
|
||||||
|
m.value = lines
|
||||||
|
.iter()
|
||||||
|
.filter_map(|g| match g {
|
||||||
|
DgnGeom::Text { text, .. } => Some(text.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\\P");
|
||||||
|
let up = if node { *height } else { -0.33 * height };
|
||||||
|
m.insertion_point = Vector3::new(
|
||||||
|
origin[0] - rotation.sin() * up,
|
||||||
|
origin[1] + rotation.cos() * up,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
if !node {
|
||||||
|
m.attachment_point = codec::entities::AttachmentPoint::BottomLeft;
|
||||||
|
m.line_spacing_factor = 0.8;
|
||||||
|
}
|
||||||
|
m.height = *height;
|
||||||
|
m.rotation = *rotation;
|
||||||
|
m.rectangle_width = 0.0;
|
||||||
|
m.style = style;
|
||||||
|
Some(EntityType::MText(m))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The object(s) one element converts to.
|
||||||
|
fn convert_geom(
|
||||||
|
geom: &DgnGeom,
|
||||||
|
explode_text_nodes: bool,
|
||||||
|
styles: &mut Vec<(String, String)>,
|
||||||
|
) -> Vec<EntityType> {
|
||||||
|
match geom {
|
||||||
|
DgnGeom::Line(a, b) => vec![EntityType::Line(Line::from_points(
|
||||||
|
Vector3::new(a[0], a[1], 0.0),
|
||||||
|
Vector3::new(b[0], b[1], 0.0),
|
||||||
|
))],
|
||||||
|
DgnGeom::Points(pts) if pts.len() >= 2 => vec![polyline(pts, &[])],
|
||||||
|
DgnGeom::Points(_) => Vec::new(),
|
||||||
|
DgnGeom::Arc {
|
||||||
|
center,
|
||||||
|
u,
|
||||||
|
v,
|
||||||
|
start,
|
||||||
|
sweep,
|
||||||
|
} => {
|
||||||
|
let c = Vector3::new(center[0], center[1], 0.0);
|
||||||
|
let full = sweep.abs() >= std::f64::consts::TAU - 1e-9;
|
||||||
|
if circular(*u, *v) {
|
||||||
|
let (radius, a0, signed) = circle_angles(*u, *v, *start, *sweep);
|
||||||
|
if full {
|
||||||
|
return vec![EntityType::Circle(Circle::from_center_radius(c, radius))];
|
||||||
|
}
|
||||||
|
let (from, to) = if signed >= 0.0 {
|
||||||
|
(a0, a0 + signed)
|
||||||
|
} else {
|
||||||
|
(a0 + signed, a0)
|
||||||
|
};
|
||||||
|
return vec![EntityType::Arc(Arc::from_center_radius_angles(
|
||||||
|
c,
|
||||||
|
radius,
|
||||||
|
from.rem_euclid(std::f64::consts::TAU),
|
||||||
|
to.rem_euclid(std::f64::consts::TAU),
|
||||||
|
))];
|
||||||
|
}
|
||||||
|
let (lu, lv) = (u[0].hypot(u[1]), v[0].hypot(v[1]));
|
||||||
|
if (u[0] * v[0] + u[1] * v[1]).abs() > 1e-9 * lu * lv || lu < 1e-12 || lv < 1e-12 {
|
||||||
|
// ponytail: a skewed plan projection (a 3D ellipse seen
|
||||||
|
// obliquely) becomes points; find its principal axes if
|
||||||
|
// such files matter.
|
||||||
|
return vec![polyline(&arc_points(*center, *u, *v, *start, *sweep), &[])];
|
||||||
|
}
|
||||||
|
// P(t) = c + cos t·u + sin t·v as an ellipse on its major axis
|
||||||
|
// with the minor axis turned counter-clockwise from it.
|
||||||
|
let ccw = u[0] * v[1] - u[1] * v[0] > 0.0;
|
||||||
|
let (major, ratio, map): (_, f64, Box<dyn Fn(f64) -> f64>) = if lu >= lv {
|
||||||
|
if ccw {
|
||||||
|
(*u, lv / lu, Box::new(|t| t))
|
||||||
|
} else {
|
||||||
|
(*u, lv / lu, Box::new(|t: f64| -t))
|
||||||
|
}
|
||||||
|
} else if ccw {
|
||||||
|
(*v, lu / lv, Box::new(|t: f64| t - std::f64::consts::FRAC_PI_2))
|
||||||
|
} else {
|
||||||
|
(*v, lu / lv, Box::new(|t: f64| std::f64::consts::FRAC_PI_2 - t))
|
||||||
|
};
|
||||||
|
let mut e = Ellipse::from_center_axes(c, Vector3::new(major[0], major[1], 0.0), ratio);
|
||||||
|
if !full {
|
||||||
|
let (p0, p1) = (map(*start), map(start + sweep));
|
||||||
|
let (from, to) = if p1 >= p0 { (p0, p1) } else { (p1, p0) };
|
||||||
|
e.start_parameter = from.rem_euclid(std::f64::consts::TAU);
|
||||||
|
e.end_parameter = e.start_parameter + (to - from);
|
||||||
|
}
|
||||||
|
vec![EntityType::Ellipse(e)]
|
||||||
|
}
|
||||||
|
DgnGeom::Chain(parts) => {
|
||||||
|
let (points, bulges) = chain(parts);
|
||||||
|
if points.len() >= 2 {
|
||||||
|
vec![polyline(&points, &bulges)]
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// V7 text is TEXT, V8 text an MTEXT.
|
||||||
|
DgnGeom::Text { font: Some(_), .. } => {
|
||||||
|
let style = text_style(geom, styles);
|
||||||
|
text_entity(geom, &style).into_iter().collect()
|
||||||
|
}
|
||||||
|
DgnGeom::Text { .. } => {
|
||||||
|
let style = text_style(geom, styles);
|
||||||
|
mtext(std::slice::from_ref(geom), style, false).into_iter().collect()
|
||||||
|
}
|
||||||
|
// Exploded, each line converts as a text element of its own.
|
||||||
|
DgnGeom::TextNode(lines) if explode_text_nodes => lines
|
||||||
|
.iter()
|
||||||
|
.flat_map(|g| convert_geom(g, false, styles))
|
||||||
|
.collect(),
|
||||||
|
DgnGeom::TextNode(lines) => {
|
||||||
|
let Some(first) = lines.first() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let style = text_style(first, styles);
|
||||||
|
mtext(lines, style, true).into_iter().collect()
|
||||||
|
}
|
||||||
|
DgnGeom::Cell {
|
||||||
|
name,
|
||||||
|
m,
|
||||||
|
origin,
|
||||||
|
z_scale,
|
||||||
|
} => {
|
||||||
|
let sx = m[0].hypot(m[2]);
|
||||||
|
let det = m[0] * m[3] - m[1] * m[2];
|
||||||
|
let sy = if sx > 0.0 { det / sx } else { 1.0 };
|
||||||
|
let insert = Insert::new(name.clone(), Vector3::new(origin[0], origin[1], 0.0))
|
||||||
|
.with_scale(sx, sy, *z_scale)
|
||||||
|
.with_rotation(m[2].atan2(m[0]));
|
||||||
|
vec![EntityType::Insert(insert)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_elements(
|
||||||
|
import: &DgnImport,
|
||||||
|
elements: &[DgnElement],
|
||||||
|
explode_text_nodes: bool,
|
||||||
|
in_cell: bool,
|
||||||
|
out: &mut Converted,
|
||||||
|
) -> Vec<EntityType> {
|
||||||
|
let mut entities = Vec::new();
|
||||||
|
for element in elements {
|
||||||
|
// A cell's objects are on layer 0 (V7: by block), as the reference
|
||||||
|
// defines the block.
|
||||||
|
let layer = if in_cell {
|
||||||
|
"0".to_string()
|
||||||
|
} else {
|
||||||
|
layer_name(import, element.level)
|
||||||
|
};
|
||||||
|
if !out.layers.iter().any(|l| l.0 == layer) {
|
||||||
|
let color = import
|
||||||
|
.levels
|
||||||
|
.get(&element.level)
|
||||||
|
.and_then(|l| l.1)
|
||||||
|
.map(color_of)
|
||||||
|
.unwrap_or(Color::Index(7));
|
||||||
|
out.layers.push((layer.clone(), color));
|
||||||
|
}
|
||||||
|
for mut entity in convert_geom(&element.geom, explode_text_nodes, &mut out.styles) {
|
||||||
|
let common = entity.common_mut();
|
||||||
|
common.layer = layer.clone();
|
||||||
|
common.color = element.color.map(color_of).unwrap_or(Color::ByLayer);
|
||||||
|
common.line_weight = element
|
||||||
|
.weight
|
||||||
|
.map(|w| LineWeight::Value(lineweight_of(w)))
|
||||||
|
.unwrap_or(LineWeight::ByLayer);
|
||||||
|
// The Standard mapping draws every line style Continuous.
|
||||||
|
if element.style.is_some() {
|
||||||
|
common.linetype = "Continuous".to_string();
|
||||||
|
}
|
||||||
|
if import.v7 && in_cell {
|
||||||
|
common.color = Color::ByBlock;
|
||||||
|
common.line_weight = LineWeight::ByBlock;
|
||||||
|
}
|
||||||
|
entities.push(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entities
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The import's objects, in sub units when `sub_units`.
|
||||||
|
pub fn convert(import: &DgnImport, sub_units: bool, explode_text_nodes: bool) -> Converted {
|
||||||
|
let k = if sub_units { import.sub_per_master } else { 1.0 };
|
||||||
|
let scale = |elements: &[DgnElement]| {
|
||||||
|
crate::scene::model::underlay_vector::scaled_elements(elements, k, [0.0, 0.0])
|
||||||
|
};
|
||||||
|
let mut out = Converted::default();
|
||||||
|
for (name, elements) in &import.cells {
|
||||||
|
let entities = convert_elements(import, &scale(elements), explode_text_nodes, true, &mut out);
|
||||||
|
if !name.is_empty() && !entities.is_empty() {
|
||||||
|
out.blocks.push((name.clone(), entities));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.entities = convert_elements(import, &scale(&import.elements), explode_text_nodes, false, &mut out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Standard mapping of what a file uses: level → layer, line style →
|
||||||
|
/// linetype, weight → lineweight, colour → colour, one row each.
|
||||||
|
pub fn mapping_rows(import: &DgnImport, tab: usize) -> Vec<(String, String)> {
|
||||||
|
let mut rows: Vec<(String, String)> = Vec::new();
|
||||||
|
let mut add = |dgn: String, dwg: String| {
|
||||||
|
if !rows.iter().any(|r| r.0 == dgn) {
|
||||||
|
rows.push((dgn, dwg));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let all = import
|
||||||
|
.elements
|
||||||
|
.iter()
|
||||||
|
.chain(import.cells.iter().flat_map(|c| c.1.iter()));
|
||||||
|
for e in all {
|
||||||
|
match tab {
|
||||||
|
0 => {
|
||||||
|
let name = layer_name(import, e.level);
|
||||||
|
add(name.clone(), name);
|
||||||
|
}
|
||||||
|
1 => match e.style {
|
||||||
|
Some(s) => add(s.to_string(), "Continuous".to_string()),
|
||||||
|
None => add("ByLevel".to_string(), "ByLayer".to_string()),
|
||||||
|
},
|
||||||
|
2 => match e.weight {
|
||||||
|
Some(w) => add(
|
||||||
|
w.to_string(),
|
||||||
|
format!("{:.2} mm", lineweight_of(w) as f64 / 100.0),
|
||||||
|
),
|
||||||
|
None => add("ByLevel".to_string(), "ByLayer".to_string()),
|
||||||
|
},
|
||||||
|
_ => match e.color {
|
||||||
|
Some(rgb) => add(
|
||||||
|
format!("RGB {},{},{}", rgb[0], rgb[1], rgb[2]),
|
||||||
|
match color_of(rgb) {
|
||||||
|
Color::Index(i) => i.to_string(),
|
||||||
|
_ => format!("{},{},{}", rgb[0], rgb[1], rgb[2]),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
None => add("ByLevel".to_string(), "ByLayer".to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Command line ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum Step {
|
||||||
|
Path,
|
||||||
|
Model,
|
||||||
|
ListModels,
|
||||||
|
Conversion,
|
||||||
|
Mapping,
|
||||||
|
ListMappings,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DgnImportCommand {
|
||||||
|
step: Step,
|
||||||
|
path: String,
|
||||||
|
models: Vec<String>,
|
||||||
|
model: String,
|
||||||
|
sub_units: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DgnImportCommand {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DgnImportCommand {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
step: Step::Path,
|
||||||
|
path: String::new(),
|
||||||
|
models: Vec::new(),
|
||||||
|
model: String::new(),
|
||||||
|
sub_units: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept_path(&mut self, text: &str) -> CmdResult {
|
||||||
|
let path = text.trim().trim_matches('"').to_string();
|
||||||
|
if path.is_empty() {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
|
let models = crate::scene::model::underlay_vector::item_names(
|
||||||
|
codec::entities::UnderlayType::Dgn,
|
||||||
|
&path,
|
||||||
|
)
|
||||||
|
.filter(|m| !m.is_empty() && crate::scene::model::underlay_vector::dgn_import(&path, &m[0]).is_some());
|
||||||
|
let Some(models) = models else {
|
||||||
|
return CmdResult::ReportError("Invalid or unsupported DGN file.".to_string());
|
||||||
|
};
|
||||||
|
self.path = path;
|
||||||
|
self.model = models[0].clone();
|
||||||
|
self.models = models;
|
||||||
|
self.step = Step::Model;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept_model(&mut self, text: &str) -> CmdResult {
|
||||||
|
let text = text.trim();
|
||||||
|
if text == "?" {
|
||||||
|
self.step = Step::ListModels;
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
|
let name = if text.is_empty() { self.models[0].as_str() } else { text };
|
||||||
|
match self.models.iter().find(|m| m.eq_ignore_ascii_case(name)) {
|
||||||
|
Some(found) => {
|
||||||
|
self.model = found.clone();
|
||||||
|
self.step = Step::Conversion;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
None => CmdResult::ReportError(format!("There is no {name} in {}.", self.path)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list(&mut self, pattern: &str, names: &[&str], back: Step) -> CmdResult {
|
||||||
|
let pattern = match pattern.trim() {
|
||||||
|
"" => "*",
|
||||||
|
p => p,
|
||||||
|
};
|
||||||
|
let lines: Vec<&str> = names
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|n| crate::io::xref_model::wildcard_match(n, pattern))
|
||||||
|
.collect();
|
||||||
|
self.step = back;
|
||||||
|
CmdResult::ReportMeasurement(lines.join("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept_conversion(&mut self, text: &str) -> CmdResult {
|
||||||
|
let t = text.trim().to_ascii_uppercase();
|
||||||
|
self.sub_units = match t.as_str() {
|
||||||
|
"" => false,
|
||||||
|
_ if "MASTER".starts_with(&t) => false,
|
||||||
|
_ if "SUB".starts_with(&t) => true,
|
||||||
|
_ => return CmdResult::ReportError("Invalid option keyword.".to_string()),
|
||||||
|
};
|
||||||
|
self.step = Step::Mapping;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept_mapping(&mut self, text: &str) -> CmdResult {
|
||||||
|
let text = text.trim();
|
||||||
|
if text == "?" {
|
||||||
|
self.step = Step::ListMappings;
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
|
if !text.is_empty() && !text.eq_ignore_ascii_case("Standard") {
|
||||||
|
return CmdResult::ReportError(format!("{text} not found."));
|
||||||
|
}
|
||||||
|
CmdResult::DgnImport(DgnImportRequest {
|
||||||
|
path: self.path.clone(),
|
||||||
|
model: self.model.clone(),
|
||||||
|
sub_units: self.sub_units,
|
||||||
|
explode_text_nodes: false,
|
||||||
|
current_drawing: false,
|
||||||
|
prefix_duplicates: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for DgnImportCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"DGNIMPORT"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
match self.step {
|
||||||
|
Step::Path => "Enter filename for DGN import:".to_string(),
|
||||||
|
Step::Model => format!("Enter name of model or [?] <{}>:", self.models[0]),
|
||||||
|
Step::ListModels => "Enter model(s) to list <*>:".to_string(),
|
||||||
|
Step::Conversion => "Specify conversion units [Master/Sub] <Master>:".to_string(),
|
||||||
|
Step::Mapping => "Specify mapping setup or [?] <Standard>:".to_string(),
|
||||||
|
Step::ListMappings => "Enter mapping setup(s) to list <*>:".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
match self.step {
|
||||||
|
Step::Model | Step::Mapping => vec![CmdOption::new("?", "?")],
|
||||||
|
Step::Conversion => vec![CmdOption::new("Master", "M"), CmdOption::new("Sub", "S")],
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_kind(&self) -> InputKind {
|
||||||
|
match self.step {
|
||||||
|
Step::Path | Step::Model => InputKind::FreeText,
|
||||||
|
_ => InputKind::SingleToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, _pt: glam::DVec3) -> CmdResult {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
|
Some(match self.step {
|
||||||
|
Step::Path => self.accept_path(text),
|
||||||
|
Step::Model => self.accept_model(text),
|
||||||
|
Step::ListModels => {
|
||||||
|
let models = self.models.clone();
|
||||||
|
let names: Vec<&str> = models.iter().map(String::as_str).collect();
|
||||||
|
self.list(text, &names, Step::Model)
|
||||||
|
}
|
||||||
|
Step::Conversion => self.accept_conversion(text),
|
||||||
|
Step::Mapping => self.accept_mapping(text),
|
||||||
|
Step::ListMappings => {
|
||||||
|
match self.list(text, &["Standard"], Step::Mapping) {
|
||||||
|
CmdResult::ReportMeasurement(lines) => {
|
||||||
|
CmdResult::ReportMeasurement(format!("Import mapping setups:\n{lines}"))
|
||||||
|
}
|
||||||
|
other => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
match self.step {
|
||||||
|
Step::Path => CmdResult::Cancel,
|
||||||
|
_ => self.on_text_input("").unwrap_or(CmdResult::NeedPoint),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ pub(crate) mod block_cmdline;
|
|||||||
mod content_browser;
|
mod content_browser;
|
||||||
pub(crate) mod create_block;
|
pub(crate) mod create_block;
|
||||||
mod design_center;
|
mod design_center;
|
||||||
|
pub(crate) mod dgn_import;
|
||||||
pub(crate) mod edit_block;
|
pub(crate) mod edit_block;
|
||||||
pub(crate) mod image_transparency;
|
pub(crate) mod image_transparency;
|
||||||
pub(crate) mod insert_block;
|
pub(crate) mod insert_block;
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use super::model::{
|
use super::model::{
|
||||||
arc_cubics, bspline_points, dgn_color_table, paths_bounds, Path, PathBuilder, Segment, Sheet,
|
arc_cubics, bspline_points, dgn_color_table, paths_bounds, scaled_elements, DgnCollector,
|
||||||
SubPath, Text, DGN_DEFAULT_COLORS,
|
DgnElement, DgnGeom, DgnImport, Path, PathBuilder, Segment, Sheet, SubPath, Text,
|
||||||
|
DGN_DEFAULT_COLORS,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A 32-bit integer stored as two little-endian words, high word first.
|
/// A 32-bit integer stored as two little-endian words, high word first.
|
||||||
@@ -123,6 +124,8 @@ struct Raw {
|
|||||||
paths: Vec<Path>,
|
paths: Vec<Path>,
|
||||||
texts: Vec<Text>,
|
texts: Vec<Text>,
|
||||||
instances: Vec<Instance>,
|
instances: Vec<Instance>,
|
||||||
|
/// The same run element by element, for DGNIMPORT.
|
||||||
|
elements: Vec<DgnElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -199,6 +202,7 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
raw.paths.push(path);
|
raw.paths.push(path);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let mut collect = DgnCollector::default();
|
||||||
for e in run {
|
for e in run {
|
||||||
let kind = e[1] & 0x7f;
|
let kind = e[1] & 0x7f;
|
||||||
if e[1] & 0x80 != 0 || e.len() < 36 {
|
if e[1] & 0x80 != 0 || e.len() < 36 {
|
||||||
@@ -206,6 +210,13 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
}
|
}
|
||||||
let symbology = uint16(e, 34).unwrap_or(0);
|
let symbology = uint16(e, 34).unwrap_or(0);
|
||||||
let color = colors[(symbology >> 8) as usize];
|
let color = colors[(symbology >> 8) as usize];
|
||||||
|
let element = |geom: DgnGeom| DgnElement {
|
||||||
|
level: level(e) as u32,
|
||||||
|
color: Some(color),
|
||||||
|
weight: Some(((symbology >> 3) & 31) as u32),
|
||||||
|
style: Some((symbology & 7) as u32),
|
||||||
|
geom,
|
||||||
|
};
|
||||||
// One pixel wide, as V8 models draw: the element weight is a
|
// One pixel wide, as V8 models draw: the element weight is a
|
||||||
// lineweight, not shown while lineweights are off.
|
// lineweight, not shown while lineweights are off.
|
||||||
let stroke = Some((color, -1.0));
|
let stroke = Some((color, -1.0));
|
||||||
@@ -213,8 +224,14 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
let int_pt =
|
let int_pt =
|
||||||
|i: usize| -> Option<[f64; 2]> { Some([int32(e, i)? as f64, int32(e, i + 4)? as f64]) };
|
|i: usize| -> Option<[f64; 2]> { Some([int32(e, i)? as f64, int32(e, i + 4)? as f64]) };
|
||||||
match kind {
|
match kind {
|
||||||
|
// A text node: its count of text components.
|
||||||
|
7 => collect.open(
|
||||||
|
uint16(e, 38).unwrap_or(0) as usize,
|
||||||
|
element(DgnGeom::TextNode(Vec::new())),
|
||||||
|
),
|
||||||
12 | 14 => {
|
12 | 14 => {
|
||||||
let n = uint16(e, 38).unwrap_or(0) as usize;
|
let n = uint16(e, 38).unwrap_or(0) as usize;
|
||||||
|
collect.open(n, element(DgnGeom::Chain(Vec::new())));
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
complex = Some((
|
complex = Some((
|
||||||
n,
|
n,
|
||||||
@@ -227,6 +244,7 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
}
|
}
|
||||||
3 => {
|
3 => {
|
||||||
if let (Some(a), Some(b)) = (int_pt(36), int_pt(44)) {
|
if let (Some(a), Some(b)) = (int_pt(36), int_pt(44)) {
|
||||||
|
collect.push(element(DgnGeom::Line(a, b)));
|
||||||
push(
|
push(
|
||||||
&mut raw,
|
&mut raw,
|
||||||
&mut complex,
|
&mut complex,
|
||||||
@@ -245,6 +263,7 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
let n = uint16(e, 36).unwrap_or(0) as usize;
|
let n = uint16(e, 36).unwrap_or(0) as usize;
|
||||||
let pts: Vec<[f64; 2]> = (0..n).filter_map(|k| int_pt(38 + k * 8)).collect();
|
let pts: Vec<[f64; 2]> = (0..n).filter_map(|k| int_pt(38 + k * 8)).collect();
|
||||||
if pts.len() >= 2 {
|
if pts.len() >= 2 {
|
||||||
|
collect.push(element(DgnGeom::Points(pts.clone())));
|
||||||
push(
|
push(
|
||||||
&mut raw,
|
&mut raw,
|
||||||
&mut complex,
|
&mut complex,
|
||||||
@@ -290,6 +309,15 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
let Some((a, b, r, ox, oy, start, sweep)) = arc else {
|
let Some((a, b, r, ox, oy, start, sweep)) = arc else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
let turn = (r as f64 / 360000.0).to_radians();
|
||||||
|
let (c, s) = (turn.cos(), turn.sin());
|
||||||
|
collect.push(element(DgnGeom::Arc {
|
||||||
|
center: [ox, oy],
|
||||||
|
u: [a * c, a * s],
|
||||||
|
v: [-b * s, b * c],
|
||||||
|
start: start.to_radians(),
|
||||||
|
sweep: sweep.to_radians(),
|
||||||
|
}));
|
||||||
let pieces = arc_cubics(
|
let pieces = arc_cubics(
|
||||||
[ox, oy],
|
[ox, oy],
|
||||||
a,
|
a,
|
||||||
@@ -326,7 +354,7 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let (height, width) = (height as f64 * 6.0 / 1000.0, width as f64 * 6.0 / 1000.0);
|
let (height, width) = (height as f64 * 6.0 / 1000.0, width as f64 * 6.0 / 1000.0);
|
||||||
raw.texts.push(Text {
|
let text = Text {
|
||||||
text: String::from_utf8_lossy(chars)
|
text: String::from_utf8_lossy(chars)
|
||||||
.trim_end_matches('\0')
|
.trim_end_matches('\0')
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -336,7 +364,16 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
rotation: (rotation as f64 / 360000.0).to_radians(),
|
rotation: (rotation as f64 / 360000.0).to_radians(),
|
||||||
color,
|
color,
|
||||||
font: "txt".to_string(),
|
font: "txt".to_string(),
|
||||||
});
|
};
|
||||||
|
collect.push(element(DgnGeom::Text {
|
||||||
|
text: text.text.clone(),
|
||||||
|
origin,
|
||||||
|
height,
|
||||||
|
width_factor: text.width_factor,
|
||||||
|
rotation: text.rotation,
|
||||||
|
font: Some(e[36] as u32),
|
||||||
|
}));
|
||||||
|
raw.texts.push(text);
|
||||||
}
|
}
|
||||||
27 => {
|
27 => {
|
||||||
// B-spline curve header: order, pole count.
|
// B-spline curve header: order, pole count.
|
||||||
@@ -369,6 +406,10 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
full.extend(interior);
|
full.extend(interior);
|
||||||
full.extend(vec![1.0; order]);
|
full.extend(vec![1.0; order]);
|
||||||
let pts = bspline_points(order, &poles, &full, 24 * count);
|
let pts = bspline_points(order, &poles, &full, 24 * count);
|
||||||
|
collect.push(DgnElement {
|
||||||
|
color: Some(color),
|
||||||
|
..element(DgnGeom::Points(pts.clone()))
|
||||||
|
});
|
||||||
push(
|
push(
|
||||||
&mut raw,
|
&mut raw,
|
||||||
&mut complex,
|
&mut complex,
|
||||||
@@ -399,6 +440,12 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
) else {
|
) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
collect.push(element(DgnGeom::Cell {
|
||||||
|
name: name.clone(),
|
||||||
|
m: [m0, m1, m2, m3],
|
||||||
|
origin: [ox as f64, oy as f64],
|
||||||
|
z_scale: 1.0,
|
||||||
|
}));
|
||||||
raw.instances.push(Instance {
|
raw.instances.push(Instance {
|
||||||
name,
|
name,
|
||||||
m: [m0, m1, m2, m3],
|
m: [m0, m1, m2, m3],
|
||||||
@@ -408,6 +455,8 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
collect.close();
|
||||||
|
raw.elements = collect.elements;
|
||||||
raw
|
raw
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,3 +681,67 @@ pub fn model(bytes: &[u8], hidden: &[String]) -> Option<Sheet> {
|
|||||||
})?;
|
})?;
|
||||||
Some(sheet)
|
Some(sheet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A unit name of the design file header (two characters, zero padded).
|
||||||
|
fn unit_name(b: &[u8], at: usize) -> String {
|
||||||
|
b.get(at..at + 2)
|
||||||
|
.map(|n| String::from_utf8_lossy(n).trim_matches(char::from(0)).trim().to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The model element by element (DGNIMPORT): in master units with the
|
||||||
|
/// global origin at 0,0, shared cell definitions in their own units scaled
|
||||||
|
/// to master units, and every level by number with its name.
|
||||||
|
pub fn import(bytes: &[u8]) -> Option<DgnImport> {
|
||||||
|
if !is_v7(bytes) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let u = units(bytes)?;
|
||||||
|
let all = elements(bytes);
|
||||||
|
let colors = color_table(&all).unwrap_or_else(|| DGN_DEFAULT_COLORS.to_vec());
|
||||||
|
let names = level_names(&all);
|
||||||
|
let per_unit = u.uor_per_sub * u.sub_per_master;
|
||||||
|
let s = 1.0 / per_unit;
|
||||||
|
let mut cells = Vec::new();
|
||||||
|
let mut top: Vec<&[u8]> = Vec::new();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < all.len() {
|
||||||
|
let e = all[i];
|
||||||
|
if e[1] & 0x7f == 34 && e[0] & 0x80 == 0 {
|
||||||
|
let name = e
|
||||||
|
.get(164..180)
|
||||||
|
.map(|n| {
|
||||||
|
String::from_utf8_lossy(n)
|
||||||
|
.trim_end_matches('\0')
|
||||||
|
.trim()
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut j = i + 1;
|
||||||
|
while j < all.len() && all[j][0] & 0x80 != 0 {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
let def = read_run(&all[i + 1..j], &colors).elements;
|
||||||
|
cells.push((name, scaled_elements(&def, s, [0.0, 0.0])));
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
top.push(e);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
let raw = read_run(&top, &colors);
|
||||||
|
let levels = (0..64u32)
|
||||||
|
.map(|l| {
|
||||||
|
let name = names.get(&(l as u8)).cloned().unwrap_or_else(|| l.to_string());
|
||||||
|
(l, (name, None))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Some(DgnImport {
|
||||||
|
elements: scaled_elements(&raw.elements, s, [-u.origin[0] * s, -u.origin[1] * s]),
|
||||||
|
cells,
|
||||||
|
levels,
|
||||||
|
sub_per_master: u.sub_per_master,
|
||||||
|
unit_names: (unit_name(bytes, 1120), unit_name(bytes, 1122)),
|
||||||
|
v7: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -8,8 +8,8 @@ use std::collections::HashMap;
|
|||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
use super::model::{
|
use super::model::{
|
||||||
arc_cubics, dgn_color_table, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text,
|
arc_cubics, dgn_color_table, paths_bounds, scaled_elements, DgnCollector, DgnElement, DgnGeom,
|
||||||
DGN_DEFAULT_COLORS,
|
DgnImport, Path, PathBuilder, Segment, Sheet, SubPath, Text, DGN_DEFAULT_COLORS,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Compound file ───────────────────────────────────────────────────────────
|
// ── Compound file ───────────────────────────────────────────────────────────
|
||||||
@@ -392,6 +392,8 @@ struct Raw {
|
|||||||
paths: Vec<Path>,
|
paths: Vec<Path>,
|
||||||
texts: Vec<Text>,
|
texts: Vec<Text>,
|
||||||
instances: Vec<Instance>,
|
instances: Vec<Instance>,
|
||||||
|
/// The same run element by element, for DGNIMPORT.
|
||||||
|
elements: Vec<DgnElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -551,11 +553,21 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
let q = xf_vector(xf, v);
|
let q = xf_vector(xf, v);
|
||||||
[q[0], q[1]]
|
[q[0], q[1]]
|
||||||
};
|
};
|
||||||
for e in run {
|
let mut collect = DgnCollector::default();
|
||||||
|
for (index, e) in run.iter().enumerate() {
|
||||||
if e.len() < 104 || styles.hidden.contains(&u32_at(e, 12).unwrap_or(0)) {
|
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);
|
||||||
|
// Colour and weight words: 0xFFFFFFFF takes the level's; line style
|
||||||
|
// 0x7FFFFFFF (or 0xFFFFFFFF).
|
||||||
|
let element = |geom: DgnGeom| DgnElement {
|
||||||
|
level: u32_at(e, 12).unwrap_or(0),
|
||||||
|
color: (u32_at(e, 52) != Some(u32::MAX)).then_some(color),
|
||||||
|
weight: u32_at(e, 48).filter(|w| *w != u32::MAX).map(|w| w & 31),
|
||||||
|
style: u32_at(e, 44).filter(|s| *s != u32::MAX && *s != 0x7FFF_FFFF),
|
||||||
|
geom,
|
||||||
|
};
|
||||||
let stroke = Some((color, -1.0));
|
let stroke = Some((color, -1.0));
|
||||||
let d3 = is_3d(e);
|
let d3 = is_3d(e);
|
||||||
// A point: x, y (and z in a 3D element).
|
// A point: x, y (and z in a 3D element).
|
||||||
@@ -567,8 +579,20 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
])
|
])
|
||||||
};
|
};
|
||||||
match kind(e) {
|
match kind(e) {
|
||||||
|
// A text node: grouped when its count is followed by that many
|
||||||
|
// text components.
|
||||||
|
7 => {
|
||||||
|
let n = u32_at(e, 104).unwrap_or(0) as usize;
|
||||||
|
let texts = run.get(index + 1..index + 1 + n).is_some_and(|parts| {
|
||||||
|
parts.iter().all(|p| p.len() >= 104 && kind(p) == 17 && is_component(p))
|
||||||
|
});
|
||||||
|
if texts {
|
||||||
|
collect.open(n, element(DgnGeom::TextNode(Vec::new())));
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
collect.open(n, element(DgnGeom::Chain(Vec::new())));
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
let fill = (kind(e) == 14
|
let fill = (kind(e) == 14
|
||||||
&& e.windows(4).any(|w| w == [0x07, 0x10, 0x41, 0x00]))
|
&& e.windows(4).any(|w| w == [0x07, 0x10, 0x41, 0x00]))
|
||||||
@@ -578,6 +602,7 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
}
|
}
|
||||||
3 => {
|
3 => {
|
||||||
if let (Some(a), Some(b)) = (p3(104), p3(if d3 { 128 } else { 120 })) {
|
if let (Some(a), Some(b)) = (p3(104), p3(if d3 { 128 } else { 120 })) {
|
||||||
|
collect.push(element(DgnGeom::Line(plan(a), plan(b))));
|
||||||
push(
|
push(
|
||||||
&mut raw,
|
&mut raw,
|
||||||
&mut complex,
|
&mut complex,
|
||||||
@@ -600,6 +625,7 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
.map(plan)
|
.map(plan)
|
||||||
.collect();
|
.collect();
|
||||||
if pts.len() >= 2 {
|
if pts.len() >= 2 {
|
||||||
|
collect.push(element(DgnGeom::Points(pts.clone())));
|
||||||
let mut pb = PathBuilder::default();
|
let mut pb = PathBuilder::default();
|
||||||
pb.move_to(pts[0]);
|
pb.move_to(pts[0]);
|
||||||
for p in &pts[1..] {
|
for p in &pts[1..] {
|
||||||
@@ -675,6 +701,16 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
sweep
|
sweep
|
||||||
};
|
};
|
||||||
let closed = sweep.abs() >= std::f64::consts::TAU - 1e-9;
|
let closed = sweep.abs() >= std::f64::consts::TAU - 1e-9;
|
||||||
|
{
|
||||||
|
let (pu, pv) = (axis(u), axis(v));
|
||||||
|
collect.push(element(DgnGeom::Arc {
|
||||||
|
center: plan(c),
|
||||||
|
u: [a * pu[0], a * pu[1]],
|
||||||
|
v: [b * pv[0], b * pv[1]],
|
||||||
|
start,
|
||||||
|
sweep,
|
||||||
|
}));
|
||||||
|
}
|
||||||
push(
|
push(
|
||||||
&mut raw,
|
&mut raw,
|
||||||
&mut complex,
|
&mut complex,
|
||||||
@@ -730,11 +766,20 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let (height, width) = (height * 6.0 / 1000.0 * sv, width * 6.0 / 1000.0 * su);
|
let (height, width) = (height * 6.0 / 1000.0 * sv, width * 6.0 / 1000.0 * su);
|
||||||
|
let width_factor = if height > 0.0 { width / height } else { 1.0 };
|
||||||
|
collect.push(element(DgnGeom::Text {
|
||||||
|
text: text.clone(),
|
||||||
|
origin: plan(origin),
|
||||||
|
height,
|
||||||
|
width_factor,
|
||||||
|
rotation: pu[1].atan2(pu[0]),
|
||||||
|
font: None,
|
||||||
|
}));
|
||||||
raw.texts.push(Text {
|
raw.texts.push(Text {
|
||||||
text,
|
text,
|
||||||
origin: plan(origin),
|
origin: plan(origin),
|
||||||
height,
|
height,
|
||||||
width_factor: if height > 0.0 { width / height } else { 1.0 },
|
width_factor,
|
||||||
rotation: pu[1].atan2(pu[0]),
|
rotation: pu[1].atan2(pu[0]),
|
||||||
color,
|
color,
|
||||||
font: "txt".to_string(),
|
font: "txt".to_string(),
|
||||||
@@ -756,6 +801,12 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
[m[3], m[4], m[5], o[1]],
|
[m[3], m[4], m[5], o[1]],
|
||||||
[m[6], m[7], m[8], o[2]],
|
[m[6], m[7], m[8], o[2]],
|
||||||
];
|
];
|
||||||
|
collect.push(element(DgnGeom::Cell {
|
||||||
|
name: name.clone(),
|
||||||
|
m: [m[0], m[1], m[3], m[4]],
|
||||||
|
origin: [o[0], o[1]],
|
||||||
|
z_scale: m[8],
|
||||||
|
}));
|
||||||
raw.instances.push(Instance {
|
raw.instances.push(Instance {
|
||||||
name: name.to_ascii_uppercase(),
|
name: name.to_ascii_uppercase(),
|
||||||
xf: xf_compose(xf, &local),
|
xf: xf_compose(xf, &local),
|
||||||
@@ -775,6 +826,8 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
fill,
|
fill,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
collect.close();
|
||||||
|
raw.elements = collect.elements;
|
||||||
raw
|
raw
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -921,6 +974,17 @@ pub fn model_names(bytes: &[u8]) -> Option<Vec<String>> {
|
|||||||
/// name and colour index. The default level (64) is listed as "0", as the
|
/// name and colour index. The default level (64) is listed as "0", as the
|
||||||
/// reference names it.
|
/// reference names it.
|
||||||
fn level_table(nm: &[&[u8]]) -> Vec<(u32, String, u32)> {
|
fn level_table(nm: &[&[u8]]) -> Vec<(u32, String, u32)> {
|
||||||
|
let mut out = level_table_raw(nm);
|
||||||
|
for (id, name, _) in &mut out {
|
||||||
|
if *id == 64 && name.eq_ignore_ascii_case("Default") {
|
||||||
|
*name = "0".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The level table with the file's own names.
|
||||||
|
fn level_table_raw(nm: &[&[u8]]) -> Vec<(u32, String, u32)> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let mut in_levels = false;
|
let mut in_levels = false;
|
||||||
for e in nm {
|
for e in nm {
|
||||||
@@ -930,10 +994,7 @@ fn level_table(nm: &[&[u8]]) -> Vec<(u32, String, u32)> {
|
|||||||
let (Some(id), Some(color)) = (u32_at(e, 32), u32_at(e, 72)) else {
|
let (Some(id), Some(color)) = (u32_at(e, 32), u32_at(e, 72)) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let mut name = marked_string(e, 32).unwrap_or_default();
|
let 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.push((id, name, color));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -1072,6 +1133,93 @@ fn stored_range(graphics: &[&[u8]]) -> Option<[f64; 4]> {
|
|||||||
(r[0] <= r[2]).then_some(r)
|
(r[0] <= r[2]).then_some(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The short name of a unit of `metres` per unit ("m", "mm", "ft" …), empty
|
||||||
|
/// for one without a common name.
|
||||||
|
fn unit_name(metres: f64) -> String {
|
||||||
|
const NAMES: [(f64, &str); 8] = [
|
||||||
|
(1.0, "m"),
|
||||||
|
(0.001, "mm"),
|
||||||
|
(0.01, "cm"),
|
||||||
|
(1000.0, "km"),
|
||||||
|
(0.0254, "in"),
|
||||||
|
(0.3048, "ft"),
|
||||||
|
(0.9144, "yd"),
|
||||||
|
(1609.344, "mi"),
|
||||||
|
];
|
||||||
|
NAMES
|
||||||
|
.iter()
|
||||||
|
.find(|(m, _)| (metres / m - 1.0).abs() < 1e-6)
|
||||||
|
.map(|(_, n)| n.to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The named model element by element (DGNIMPORT): in master units with
|
||||||
|
/// the global origin at 0,0, shared cell definitions in master units, and
|
||||||
|
/// every level by id with its own name and colour.
|
||||||
|
pub fn import(bytes: &[u8], name: &str) -> Option<DgnImport> {
|
||||||
|
let cfb = Cfb::open(bytes)?;
|
||||||
|
let all = models(&cfb);
|
||||||
|
let (_, graphics, header) = all
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.0.eq_ignore_ascii_case(name))
|
||||||
|
.or_else(|| all.first())?;
|
||||||
|
let non_model = non_model(&cfb);
|
||||||
|
let nm = elements(&non_model);
|
||||||
|
let colors = nm
|
||||||
|
.iter()
|
||||||
|
.find(|e| kind(e) == 5)
|
||||||
|
.and_then(|e| dgn_color_table(e, 37))
|
||||||
|
.unwrap_or_else(|| DGN_DEFAULT_COLORS.to_vec());
|
||||||
|
let table = level_table_raw(&nm);
|
||||||
|
let styles = Styles {
|
||||||
|
levels: table.iter().map(|l| (l.0, l.2)).collect(),
|
||||||
|
hidden: Default::default(),
|
||||||
|
extended: extended_colors(&cfb),
|
||||||
|
colors,
|
||||||
|
};
|
||||||
|
let levels = table
|
||||||
|
.iter()
|
||||||
|
.map(|(id, name, color)| {
|
||||||
|
let rgb = styles.colors.get(*color as usize).copied();
|
||||||
|
(*id, (name.clone(), rgb))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let (per_unit, sub_uor) = units(header);
|
||||||
|
let s = 1.0 / per_unit;
|
||||||
|
let mut cells = Vec::new();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < nm.len() {
|
||||||
|
if kind(nm[i]) == 34 && !is_component(nm[i]) {
|
||||||
|
let cell = marked_string(nm[i], 248).unwrap_or_default();
|
||||||
|
let mut j = i + 1;
|
||||||
|
while j < nm.len() && is_component(nm[j]) {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
let def = read_run(&nm[i + 1..j], &styles, &IDENTITY).elements;
|
||||||
|
cells.push((cell, scaled_elements(&def, s, [0.0, 0.0])));
|
||||||
|
i = j;
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let raw = read_run(&elements(graphics), &styles, &IDENTITY);
|
||||||
|
let origin = global_origin(header);
|
||||||
|
// Unit definitions: numerator / denominator per metre, master at 4180,
|
||||||
|
// sub at 4196.
|
||||||
|
let metres = |at: usize| match (f64_at(header, at), f64_at(header, at + 8)) {
|
||||||
|
(Some(n), Some(d)) if n > 0.0 && d > 0.0 => unit_name(d / n),
|
||||||
|
_ => String::new(),
|
||||||
|
};
|
||||||
|
Some(DgnImport {
|
||||||
|
elements: scaled_elements(&raw.elements, s, [-origin[0] * s, -origin[1] * s]),
|
||||||
|
cells,
|
||||||
|
levels,
|
||||||
|
sub_per_master: per_unit / sub_uor,
|
||||||
|
unit_names: (metres(4180), metres(4196)),
|
||||||
|
v7: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod cfb_memory_tests {
|
mod cfb_memory_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -20,10 +20,21 @@ use std::sync::{Arc, Mutex, OnceLock};
|
|||||||
|
|
||||||
use codec::entities::UnderlayType;
|
use codec::entities::UnderlayType;
|
||||||
|
|
||||||
pub use model::{Path, Segment, Sheet, SubPath};
|
pub use model::{scaled_elements, DgnElement, DgnGeom, DgnImport, Path, Segment, Sheet, SubPath};
|
||||||
|
|
||||||
use super::pdf_raster::{source_bytes, PdfPage};
|
use super::pdf_raster::{source_bytes, PdfPage};
|
||||||
|
|
||||||
|
/// A DGN model element by element, for DGNIMPORT; `None` when the file is
|
||||||
|
/// not a V7 or V8 design file.
|
||||||
|
pub fn dgn_import(path: &str, item: &str) -> Option<DgnImport> {
|
||||||
|
let bytes = source_bytes(path)?;
|
||||||
|
if dgn7::is_v7(&bytes) {
|
||||||
|
dgn7::import(&bytes)
|
||||||
|
} else {
|
||||||
|
dgn8::import(&bytes, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Sheet names (DWF) or model names (DGN) of a file.
|
/// Sheet names (DWF) or model names (DGN) of a file.
|
||||||
pub fn item_names(kind: UnderlayType, path: &str) -> Option<Vec<String>> {
|
pub fn item_names(kind: UnderlayType, path: &str) -> Option<Vec<String>> {
|
||||||
let bytes = source_bytes(path)?;
|
let bytes = source_bytes(path)?;
|
||||||
|
|||||||
@@ -532,6 +532,182 @@ pub fn dgn_color_table(e: &[u8], first: usize) -> Option<Vec<[u8; 3]>> {
|
|||||||
Some(out)
|
Some(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A DGN element as drawing geometry (what DGNIMPORT converts): its level,
|
||||||
|
/// symbology and shape, in the units of the run that read it.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct DgnElement {
|
||||||
|
/// Level number (V7) or id (V8).
|
||||||
|
pub level: u32,
|
||||||
|
/// Colour; `None` takes the level's.
|
||||||
|
pub color: Option<[u8; 3]>,
|
||||||
|
/// Weight 0-31; `None` takes the level's.
|
||||||
|
pub weight: Option<u32>,
|
||||||
|
/// Line style; `None` takes the level's.
|
||||||
|
pub style: Option<u32>,
|
||||||
|
pub geom: DgnGeom,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum DgnGeom {
|
||||||
|
Line([f64; 2], [f64; 2]),
|
||||||
|
/// A line string, curve or B-spline (sampled), or a shape (its points
|
||||||
|
/// end where they start).
|
||||||
|
Points(Vec<[f64; 2]>),
|
||||||
|
/// An elliptical arc: the point at parameter t is
|
||||||
|
/// `center + cos t · u + sin t · v`, from `start` over `sweep` (radians).
|
||||||
|
Arc {
|
||||||
|
center: [f64; 2],
|
||||||
|
u: [f64; 2],
|
||||||
|
v: [f64; 2],
|
||||||
|
start: f64,
|
||||||
|
sweep: f64,
|
||||||
|
},
|
||||||
|
/// A complex chain or shape: its components in order.
|
||||||
|
Chain(Vec<DgnGeom>),
|
||||||
|
Text {
|
||||||
|
text: String,
|
||||||
|
origin: [f64; 2],
|
||||||
|
height: f64,
|
||||||
|
width_factor: f64,
|
||||||
|
rotation: f64,
|
||||||
|
/// Font number (V7); `None` when the file's font is not read.
|
||||||
|
font: Option<u32>,
|
||||||
|
},
|
||||||
|
/// A text node: its lines.
|
||||||
|
TextNode(Vec<DgnGeom>),
|
||||||
|
/// A shared cell instance: x' = m[0]·x + m[1]·y + origin.x,
|
||||||
|
/// y' = m[2]·x + m[3]·y + origin.y.
|
||||||
|
Cell {
|
||||||
|
name: String,
|
||||||
|
m: [f64; 4],
|
||||||
|
origin: [f64; 2],
|
||||||
|
/// Scale along z (1 in a 2D file).
|
||||||
|
z_scale: f64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DgnGeom {
|
||||||
|
/// The geometry through x' = s·x + t (lengths scaled by `s`).
|
||||||
|
pub fn scaled(&self, s: f64, t: [f64; 2]) -> DgnGeom {
|
||||||
|
let p = |q: [f64; 2]| [q[0] * s + t[0], q[1] * s + t[1]];
|
||||||
|
let v = |q: [f64; 2]| [q[0] * s, q[1] * s];
|
||||||
|
match self {
|
||||||
|
DgnGeom::Line(a, b) => DgnGeom::Line(p(*a), p(*b)),
|
||||||
|
DgnGeom::Points(pts) => DgnGeom::Points(pts.iter().map(|q| p(*q)).collect()),
|
||||||
|
DgnGeom::Arc {
|
||||||
|
center,
|
||||||
|
u,
|
||||||
|
v: w,
|
||||||
|
start,
|
||||||
|
sweep,
|
||||||
|
} => DgnGeom::Arc {
|
||||||
|
center: p(*center),
|
||||||
|
u: v(*u),
|
||||||
|
v: v(*w),
|
||||||
|
start: *start,
|
||||||
|
sweep: *sweep,
|
||||||
|
},
|
||||||
|
DgnGeom::Chain(parts) => DgnGeom::Chain(parts.iter().map(|g| g.scaled(s, t)).collect()),
|
||||||
|
DgnGeom::TextNode(parts) => {
|
||||||
|
DgnGeom::TextNode(parts.iter().map(|g| g.scaled(s, t)).collect())
|
||||||
|
}
|
||||||
|
DgnGeom::Text {
|
||||||
|
text,
|
||||||
|
origin,
|
||||||
|
height,
|
||||||
|
width_factor,
|
||||||
|
rotation,
|
||||||
|
font,
|
||||||
|
} => DgnGeom::Text {
|
||||||
|
text: text.clone(),
|
||||||
|
origin: p(*origin),
|
||||||
|
height: height * s,
|
||||||
|
width_factor: *width_factor,
|
||||||
|
rotation: *rotation,
|
||||||
|
font: *font,
|
||||||
|
},
|
||||||
|
DgnGeom::Cell {
|
||||||
|
name,
|
||||||
|
m,
|
||||||
|
origin,
|
||||||
|
z_scale,
|
||||||
|
} => DgnGeom::Cell {
|
||||||
|
name: name.clone(),
|
||||||
|
m: *m,
|
||||||
|
origin: p(*origin),
|
||||||
|
z_scale: *z_scale,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collects elements in file order, gathering the components of a complex
|
||||||
|
/// chain, shape or text node into their header's element.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct DgnCollector {
|
||||||
|
pub elements: Vec<DgnElement>,
|
||||||
|
/// The open complex element and how many components it still takes.
|
||||||
|
open: Option<(usize, DgnElement)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DgnCollector {
|
||||||
|
/// A complex chain or shape (or a text node) header with `count`
|
||||||
|
/// components.
|
||||||
|
pub fn open(&mut self, count: usize, header: DgnElement) {
|
||||||
|
self.close();
|
||||||
|
if count > 0 {
|
||||||
|
self.open = Some((count, header));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, element: DgnElement) {
|
||||||
|
if let Some((left, header)) = self.open.as_mut() {
|
||||||
|
if let DgnGeom::Chain(parts) | DgnGeom::TextNode(parts) = &mut header.geom {
|
||||||
|
parts.push(element.geom);
|
||||||
|
}
|
||||||
|
*left -= 1;
|
||||||
|
if *left == 0 {
|
||||||
|
self.close();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.elements.push(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ends a complex element cut short with the components it has.
|
||||||
|
pub fn close(&mut self) {
|
||||||
|
if let Some((_, header)) = self.open.take() {
|
||||||
|
self.elements.push(header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What DGNIMPORT reads from a model: its elements and the shared cells
|
||||||
|
/// they place (in master units, the global origin at 0,0), the levels by
|
||||||
|
/// number or id (name, colour) and the sub units per master unit.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct DgnImport {
|
||||||
|
pub elements: Vec<DgnElement>,
|
||||||
|
pub cells: Vec<(String, Vec<DgnElement>)>,
|
||||||
|
pub levels: std::collections::HashMap<u32, (String, Option<[u8; 3]>)>,
|
||||||
|
pub sub_per_master: f64,
|
||||||
|
/// Master and sub unit names, when the file has them.
|
||||||
|
pub unit_names: (String, String),
|
||||||
|
/// A V7 file (its elements carry their own line style and weight).
|
||||||
|
pub v7: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The elements through x' = s·x + t.
|
||||||
|
pub fn scaled_elements(elements: &[DgnElement], s: f64, t: [f64; 2]) -> Vec<DgnElement> {
|
||||||
|
elements
|
||||||
|
.iter()
|
||||||
|
.map(|e| DgnElement {
|
||||||
|
geom: e.geom.scaled(s, t),
|
||||||
|
..e.clone()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// A line of text the host draws with its own font.
|
/// A line of text the host draws with its own font.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Text {
|
pub struct Text {
|
||||||
|
|||||||
@@ -0,0 +1,639 @@
|
|||||||
|
//! Import File (IMPORT: the format first, then the file, routed to the
|
||||||
|
//! format's importer) and DGN Import (DGNIMPORT: model, target drawing,
|
||||||
|
//! conversion units and the mapping setup's effect on the file).
|
||||||
|
|
||||||
|
use iced::widget::{button, column, container, image, row, scrollable, text, text_input, Space};
|
||||||
|
use iced::{Background, Border, Element, Fill, Length, Theme};
|
||||||
|
|
||||||
|
use crate::app::Message;
|
||||||
|
use crate::t;
|
||||||
|
use crate::ui::style::common::muted_style;
|
||||||
|
use crate::ui::style::form::{button_style, dialog_button, dialog_button_styled_opt, field_style};
|
||||||
|
use crate::ui::window::pdf_dialogs::{card, well_style, PageThumb};
|
||||||
|
|
||||||
|
/// One edit in the Import File or DGN Import window.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ImportDialogMsg {
|
||||||
|
// Import File
|
||||||
|
Path(String),
|
||||||
|
Browse,
|
||||||
|
/// A recent file chosen from its chip.
|
||||||
|
Recent(usize),
|
||||||
|
Format(usize),
|
||||||
|
Search(String),
|
||||||
|
OnlySupported(bool),
|
||||||
|
Picked(Option<String>),
|
||||||
|
ImportOk,
|
||||||
|
// DGN Import
|
||||||
|
DgnChange,
|
||||||
|
DgnPicked(Option<String>),
|
||||||
|
DgnModel(usize),
|
||||||
|
DgnCurrent(bool),
|
||||||
|
DgnPrefix(bool),
|
||||||
|
DgnSub(bool),
|
||||||
|
DgnExplode(bool),
|
||||||
|
DgnMappingTab(usize),
|
||||||
|
DgnOk,
|
||||||
|
Help(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The colour of a format that is read in part or only written.
|
||||||
|
const AMBER: iced::Color = iced::Color::from_rgb8(0xd9, 0xa4, 0x00);
|
||||||
|
|
||||||
|
fn msg(m: ImportDialogMsg) -> Message {
|
||||||
|
Message::ImportDialog(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether OCS imports a format, and why not when it does not.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Support {
|
||||||
|
Yes,
|
||||||
|
/// Any file: the importer follows the file's extension.
|
||||||
|
ByExtension,
|
||||||
|
/// SAT: ACIS bodies are read only inside drawings.
|
||||||
|
AcisSolverOnly,
|
||||||
|
/// STEP: written by STEPOUT, not read.
|
||||||
|
ExportOnly,
|
||||||
|
No,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ImportFormat {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub patterns: &'static str,
|
||||||
|
pub extensions: &'static [&'static str],
|
||||||
|
/// 0 2D / underlay, 1 3D model, 2 other.
|
||||||
|
pub group: u8,
|
||||||
|
pub support: Support,
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn format(
|
||||||
|
name: &'static str,
|
||||||
|
patterns: &'static str,
|
||||||
|
extensions: &'static [&'static str],
|
||||||
|
group: u8,
|
||||||
|
support: Support,
|
||||||
|
) -> ImportFormat {
|
||||||
|
ImportFormat {
|
||||||
|
name,
|
||||||
|
patterns,
|
||||||
|
extensions,
|
||||||
|
group,
|
||||||
|
support,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The reference's Import File types, in its order of groups.
|
||||||
|
pub const FORMATS: [ImportFormat; 21] = [
|
||||||
|
format("PDF Files", "*.pdf", &["pdf"], 0, Support::Yes),
|
||||||
|
format("MicroStation DGN", "*.dgn", &["dgn"], 0, Support::Yes),
|
||||||
|
format("All DGN Files", "*.*", &["*"], 0, Support::Yes),
|
||||||
|
format("Metafile", "*.wmf", &["wmf"], 0, Support::No),
|
||||||
|
format("3D Studio", "*.3ds", &["3ds"], 1, Support::No),
|
||||||
|
format("ACIS", "*.sat", &["sat"], 1, Support::AcisSolverOnly),
|
||||||
|
format(
|
||||||
|
"CATIA V4",
|
||||||
|
"*.model; *.session; *.exp; *.dlv3",
|
||||||
|
&["model", "session", "exp", "dlv3"],
|
||||||
|
1,
|
||||||
|
Support::No,
|
||||||
|
),
|
||||||
|
format(
|
||||||
|
"CATIA V5",
|
||||||
|
"*.CATPart; *.CATProduct",
|
||||||
|
&["CATPart", "CATProduct"],
|
||||||
|
1,
|
||||||
|
Support::No,
|
||||||
|
),
|
||||||
|
format("IGES", "*.igs; *.iges", &["igs", "iges"], 1, Support::No),
|
||||||
|
format("Inventor", "*.ipt; *.iam", &["ipt", "iam"], 1, Support::No),
|
||||||
|
format("JT", "*.jt", &["jt"], 1, Support::No),
|
||||||
|
format("NX", "*.prt", &["prt"], 1, Support::No),
|
||||||
|
format("Parasolid Binary", "*.x_b", &["x_b"], 1, Support::No),
|
||||||
|
format("Parasolid Text", "*.x_t", &["x_t"], 1, Support::No),
|
||||||
|
format("Pro/ENGINEER", "*.prt*; *.asm*", &["prt", "asm"], 1, Support::No),
|
||||||
|
format("Pro/ENGINEER Granite", "*.g", &["g"], 1, Support::No),
|
||||||
|
format("Pro/ENGINEER Neutral", "*.neu*", &["neu"], 1, Support::No),
|
||||||
|
format("Rhino", "*.3dm", &["3dm"], 1, Support::No),
|
||||||
|
format(
|
||||||
|
"SolidWorks",
|
||||||
|
"*.prt; *.sldprt; *.asm; *.sldasm",
|
||||||
|
&["prt", "sldprt", "asm", "sldasm"],
|
||||||
|
1,
|
||||||
|
Support::No,
|
||||||
|
),
|
||||||
|
format("STEP", "*.ste; *.stp; *.step", &["ste", "stp", "step"], 1, Support::ExportOnly),
|
||||||
|
format("All Files", "*.*", &["*"], 2, Support::ByExtension),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub struct ImportFileState {
|
||||||
|
pub path: String,
|
||||||
|
pub format: usize,
|
||||||
|
pub search: String,
|
||||||
|
pub only_supported: bool,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DgnImportState {
|
||||||
|
pub path: String,
|
||||||
|
pub version: &'static str,
|
||||||
|
pub models: Vec<PageThumb>,
|
||||||
|
pub model: usize,
|
||||||
|
pub current_drawing: bool,
|
||||||
|
pub prefix_duplicates: bool,
|
||||||
|
pub sub_units: bool,
|
||||||
|
pub explode_text_nodes: bool,
|
||||||
|
/// Master and sub unit names.
|
||||||
|
pub units: (String, String),
|
||||||
|
pub mapping_tab: usize,
|
||||||
|
/// The chosen model's mapping rows per tab (layer, linetype,
|
||||||
|
/// lineweight, colour).
|
||||||
|
pub mapping: [Vec<(String, String)>; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Both windows' working copies and the files imported this session.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ImportDialogs {
|
||||||
|
pub file: Option<ImportFileState>,
|
||||||
|
pub dgn: Option<DgnImportState>,
|
||||||
|
pub recent: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_name(path: &str) -> String {
|
||||||
|
std::path::Path::new(path)
|
||||||
|
.file_name()
|
||||||
|
.map(|n| n.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| path.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A long path as its drive, an ellipsis and the last three parts.
|
||||||
|
fn short_path(path: &str) -> String {
|
||||||
|
let parts: Vec<&str> = path.split(['/', '\\']).filter(|p| !p.is_empty()).collect();
|
||||||
|
if parts.len() <= 4 {
|
||||||
|
return path.to_string();
|
||||||
|
}
|
||||||
|
format!("{}\\…\\{}", parts[0], parts[parts.len() - 3..].join("\\"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn badge<'a>(support: Support) -> Element<'a, Message> {
|
||||||
|
let (label, tone) = match support {
|
||||||
|
Support::Yes => (t!("Supported"), 0),
|
||||||
|
Support::ByExtension => (t!("By extension"), 2),
|
||||||
|
Support::AcisSolverOnly => (t!("ACIS solver available"), 1),
|
||||||
|
Support::ExportOnly => (t!("Export only"), 1),
|
||||||
|
Support::No => (t!("None"), 2),
|
||||||
|
};
|
||||||
|
let color = move |theme: &Theme| match tone {
|
||||||
|
0 => theme.palette().success.base.color,
|
||||||
|
1 => AMBER,
|
||||||
|
_ => theme.palette().background.base.text.scale_alpha(0.55),
|
||||||
|
};
|
||||||
|
container(
|
||||||
|
text(label)
|
||||||
|
.size(10)
|
||||||
|
.style(move |theme: &Theme| text::Style {
|
||||||
|
color: Some(color(theme)),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.padding([1, 7])
|
||||||
|
.style(move |theme: &Theme| container::Style {
|
||||||
|
border: Border {
|
||||||
|
width: 1.0,
|
||||||
|
radius: 9.0.into(),
|
||||||
|
color: color(theme),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The reason a format cannot be imported.
|
||||||
|
pub fn unsupported_reason(support: Support) -> Option<std::borrow::Cow<'static, str>> {
|
||||||
|
match support {
|
||||||
|
Support::Yes | Support::ByExtension => None,
|
||||||
|
Support::AcisSolverOnly => Some(t!(
|
||||||
|
"ACIS bodies are read only inside drawings; SAT files cannot be imported."
|
||||||
|
)),
|
||||||
|
Support::ExportOnly => Some(t!("STEP files are only written (STEPOUT), not read.")),
|
||||||
|
Support::No => Some(t!("OpenCAD Studio has no importer for this format.")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tile_style(selected: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
|
||||||
|
move |theme: &Theme, status| {
|
||||||
|
let palette = theme.palette();
|
||||||
|
let hovered = matches!(status, button::Status::Hovered | button::Status::Pressed);
|
||||||
|
button::Style {
|
||||||
|
background: Some(Background::Color(if selected || hovered {
|
||||||
|
palette.background.base.color
|
||||||
|
} else {
|
||||||
|
palette.background.weak.color
|
||||||
|
})),
|
||||||
|
text_color: palette.background.base.text,
|
||||||
|
border: Border {
|
||||||
|
width: if selected { 1.5 } else { 0.0 },
|
||||||
|
radius: 6.0.into(),
|
||||||
|
color: palette.primary.base.color,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Help, Cancel and the named action (inactive while `ok` is `None`).
|
||||||
|
fn footer<'a>(help: &'static str, action: std::borrow::Cow<'static, str>, ok: Option<Message>) -> Element<'a, Message> {
|
||||||
|
row![
|
||||||
|
button(text("?").size(12))
|
||||||
|
.on_press(msg(ImportDialogMsg::Help(help)))
|
||||||
|
.style(button_style(false))
|
||||||
|
.padding([5, 11]),
|
||||||
|
Space::new().width(Fill),
|
||||||
|
dialog_button(t!("Cancel"), Message::CloseModal, false),
|
||||||
|
dialog_button_styled_opt(action, ok, button::primary),
|
||||||
|
]
|
||||||
|
.spacing(6)
|
||||||
|
.align_y(iced::Center)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chip<'a>(label: String, on: bool, message: Message) -> Element<'a, Message> {
|
||||||
|
button(text(label).size(11))
|
||||||
|
.on_press(message)
|
||||||
|
.style(button_style(on))
|
||||||
|
.padding([4, 10])
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One choice out of a few, as joined buttons; inactive ones are greyed.
|
||||||
|
fn segmented<'a>(options: Vec<(String, bool, Option<Message>)>) -> Element<'a, Message> {
|
||||||
|
let buttons = options.into_iter().map(|(label, on, message)| {
|
||||||
|
button(text(label).size(11).width(Fill).align_x(iced::Center))
|
||||||
|
.on_press_maybe(message)
|
||||||
|
.style(button_style(on))
|
||||||
|
.padding([5, 6])
|
||||||
|
.width(Fill)
|
||||||
|
.into()
|
||||||
|
});
|
||||||
|
container(iced::widget::Row::with_children(buttons.collect::<Vec<_>>()).spacing(2))
|
||||||
|
.padding(2)
|
||||||
|
.width(Fill)
|
||||||
|
.style(well_style)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_import_file<'a>(
|
||||||
|
state: &'a ImportFileState,
|
||||||
|
recent: &'a [String],
|
||||||
|
sizing: crate::ui::modal::ModalSizing,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
let mut chips = row![text(t!("Recent:")).size(11).style(muted_style)]
|
||||||
|
.spacing(6)
|
||||||
|
.align_y(iced::Center);
|
||||||
|
for (n, path) in recent.iter().enumerate() {
|
||||||
|
chips = chips.push(chip(
|
||||||
|
file_name(path),
|
||||||
|
state.path == *path,
|
||||||
|
msg(ImportDialogMsg::Recent(n)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut file = column![row![
|
||||||
|
text_input("", &state.path)
|
||||||
|
.on_input(|s| msg(ImportDialogMsg::Path(s)))
|
||||||
|
.on_submit(msg(ImportDialogMsg::ImportOk))
|
||||||
|
.size(12)
|
||||||
|
.padding([6, 8])
|
||||||
|
.width(Fill)
|
||||||
|
.style(field_style),
|
||||||
|
button(text(t!("Browse...")).size(11))
|
||||||
|
.on_press(msg(ImportDialogMsg::Browse))
|
||||||
|
.style(button_style(false))
|
||||||
|
.padding([6, 14]),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center)]
|
||||||
|
.spacing(8);
|
||||||
|
if !recent.is_empty() {
|
||||||
|
file = file.push(chips);
|
||||||
|
}
|
||||||
|
if let Some(error) = &state.error {
|
||||||
|
file = file.push(text(error.clone()).size(11).style(|theme: &Theme| text::Style {
|
||||||
|
color: Some(theme.palette().danger.base.color),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let search = state.search.trim().to_lowercase();
|
||||||
|
let shown: Vec<usize> = (0..FORMATS.len())
|
||||||
|
.filter(|&k| {
|
||||||
|
let f = &FORMATS[k];
|
||||||
|
(!state.only_supported || matches!(f.support, Support::Yes | Support::ByExtension))
|
||||||
|
&& (search.is_empty()
|
||||||
|
|| f.name.to_lowercase().contains(&search)
|
||||||
|
|| f.patterns.to_lowercase().contains(&search))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut groups = column![].spacing(6);
|
||||||
|
for (group, title) in [
|
||||||
|
(0u8, t!("2D / Underlay")),
|
||||||
|
(1, t!("3D Model")),
|
||||||
|
(2, t!("Other")),
|
||||||
|
] {
|
||||||
|
let members: Vec<usize> = shown
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|&k| FORMATS[k].group == group)
|
||||||
|
.collect();
|
||||||
|
if members.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
groups = groups.push(text(title.to_uppercase()).size(10).style(muted_style));
|
||||||
|
for line in members.chunks(3) {
|
||||||
|
let mut r = row![].spacing(6);
|
||||||
|
for &k in line {
|
||||||
|
let f = &FORMATS[k];
|
||||||
|
r = r.push(
|
||||||
|
button(
|
||||||
|
row![
|
||||||
|
column![
|
||||||
|
text(f.name).size(12),
|
||||||
|
text(f.patterns).size(10).style(muted_style),
|
||||||
|
]
|
||||||
|
.spacing(1)
|
||||||
|
.width(Fill),
|
||||||
|
badge(f.support),
|
||||||
|
]
|
||||||
|
.spacing(6)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
)
|
||||||
|
.on_press(msg(ImportDialogMsg::Format(k)))
|
||||||
|
.style(tile_style(state.format == k))
|
||||||
|
.padding([5, 8])
|
||||||
|
.width(Length::FillPortion(1)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for _ in line.len()..3 {
|
||||||
|
r = r.push(Space::new().width(Length::FillPortion(1)));
|
||||||
|
}
|
||||||
|
groups = groups.push(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let selected = &FORMATS[state.format];
|
||||||
|
let reason = unsupported_reason(selected.support);
|
||||||
|
let formats = card(
|
||||||
|
format!("{} · {}", t!("Format"), FORMATS.len()),
|
||||||
|
column![
|
||||||
|
row![
|
||||||
|
text_input(&t!("Search formats..."), &state.search)
|
||||||
|
.on_input(|s| msg(ImportDialogMsg::Search(s)))
|
||||||
|
.size(11)
|
||||||
|
.padding([5, 8])
|
||||||
|
.width(Length::Fixed(220.0))
|
||||||
|
.style(field_style),
|
||||||
|
Space::new().width(Fill),
|
||||||
|
chip(
|
||||||
|
t!("All").into_owned(),
|
||||||
|
!state.only_supported,
|
||||||
|
msg(ImportDialogMsg::OnlySupported(false))
|
||||||
|
),
|
||||||
|
chip(
|
||||||
|
t!("Only what OCS can open").into_owned(),
|
||||||
|
state.only_supported,
|
||||||
|
msg(ImportDialogMsg::OnlySupported(true))
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.spacing(6)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
scrollable(groups).height(Length::Fixed(360.0)),
|
||||||
|
text(match &reason {
|
||||||
|
Some(r) => format!("{}: {r}", selected.name),
|
||||||
|
None => String::new(),
|
||||||
|
})
|
||||||
|
.size(11)
|
||||||
|
.style(|_: &Theme| text::Style {
|
||||||
|
color: Some(AMBER),
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
.spacing(8),
|
||||||
|
);
|
||||||
|
let ok = reason.is_none().then(|| msg(ImportDialogMsg::ImportOk));
|
||||||
|
column![
|
||||||
|
card(t!("File").into_owned(), file),
|
||||||
|
formats,
|
||||||
|
footer("import", t!("Import"), ok),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.padding([10, 12])
|
||||||
|
.width(sizing.width)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_dgn_import<'a>(
|
||||||
|
state: &'a DgnImportState,
|
||||||
|
sizing: crate::ui::modal::ModalSizing,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
let top = container(
|
||||||
|
row![
|
||||||
|
container(text(file_name(&state.path)).size(12))
|
||||||
|
.padding([4, 10])
|
||||||
|
.style(|theme: &Theme| container::Style {
|
||||||
|
border: Border {
|
||||||
|
width: 1.0,
|
||||||
|
radius: 12.0.into(),
|
||||||
|
color: theme.palette().primary.base.color,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
text(format!(
|
||||||
|
"{} · {} · {}",
|
||||||
|
short_path(&state.path),
|
||||||
|
state.version,
|
||||||
|
crate::tf!("{n} model(s)", n = state.models.len())
|
||||||
|
))
|
||||||
|
.size(11)
|
||||||
|
.style(muted_style)
|
||||||
|
.width(Fill)
|
||||||
|
.wrapping(iced::advanced::text::Wrapping::None),
|
||||||
|
button(text(t!("Change file...")).size(11))
|
||||||
|
.on_press(msg(ImportDialogMsg::DgnChange))
|
||||||
|
.style(button::text)
|
||||||
|
.padding([4, 6]),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
)
|
||||||
|
.padding([8, 12])
|
||||||
|
.width(Fill)
|
||||||
|
.style(crate::ui::window::pdf_dialogs::card_style);
|
||||||
|
|
||||||
|
let tiles = state.models.iter().enumerate().map(|(k, m)| {
|
||||||
|
let chosen = k == state.model;
|
||||||
|
let picture: Element<'a, Message> = match &m.image {
|
||||||
|
Some(handle) => image(handle.clone()).width(Fill).height(Fill).into(),
|
||||||
|
None => Space::new().width(Fill).height(Fill).into(),
|
||||||
|
};
|
||||||
|
button(
|
||||||
|
column![
|
||||||
|
container(picture)
|
||||||
|
.width(Length::Fixed(132.0))
|
||||||
|
.height(Length::Fixed(80.0))
|
||||||
|
.padding(4)
|
||||||
|
.style(well_style),
|
||||||
|
text(m.label.clone())
|
||||||
|
.size(11)
|
||||||
|
.width(Length::Fixed(132.0))
|
||||||
|
.align_x(iced::Center),
|
||||||
|
]
|
||||||
|
.spacing(4),
|
||||||
|
)
|
||||||
|
.on_press(msg(ImportDialogMsg::DgnModel(k)))
|
||||||
|
.style(tile_style(chosen))
|
||||||
|
.padding(5)
|
||||||
|
.into()
|
||||||
|
});
|
||||||
|
let model = card(
|
||||||
|
t!("Model").into_owned(),
|
||||||
|
scrollable(
|
||||||
|
iced::widget::Row::with_children(tiles.collect::<Vec<_>>())
|
||||||
|
.spacing(6)
|
||||||
|
.wrap()
|
||||||
|
.vertical_spacing(6),
|
||||||
|
)
|
||||||
|
.height(Length::Fixed(118.0)),
|
||||||
|
);
|
||||||
|
let current = state.current_drawing;
|
||||||
|
let target = card(
|
||||||
|
t!("Target").into_owned(),
|
||||||
|
column![
|
||||||
|
segmented(vec![
|
||||||
|
(
|
||||||
|
t!("New Drawing").into_owned(),
|
||||||
|
!current,
|
||||||
|
Some(msg(ImportDialogMsg::DgnCurrent(false)))
|
||||||
|
),
|
||||||
|
(
|
||||||
|
t!("Current Drawing").into_owned(),
|
||||||
|
current,
|
||||||
|
Some(msg(ImportDialogMsg::DgnCurrent(true)))
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
text(t!("On name conflict")).size(10).style(muted_style),
|
||||||
|
segmented(vec![
|
||||||
|
(
|
||||||
|
t!("Add prefix to dependent definitions").into_owned(),
|
||||||
|
current && state.prefix_duplicates,
|
||||||
|
current.then(|| msg(ImportDialogMsg::DgnPrefix(true)))
|
||||||
|
),
|
||||||
|
(
|
||||||
|
t!("Ignore duplicate names").into_owned(),
|
||||||
|
current && !state.prefix_duplicates,
|
||||||
|
current.then(|| msg(ImportDialogMsg::DgnPrefix(false)))
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
.spacing(6),
|
||||||
|
);
|
||||||
|
let unit = |name: &str| {
|
||||||
|
let word = crate::modules::insert::dgn_import::unit_display(name);
|
||||||
|
if word.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(" · {}", t!(word.as_str()))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let units = card(
|
||||||
|
t!("Units").into_owned(),
|
||||||
|
column![
|
||||||
|
segmented(vec![
|
||||||
|
(
|
||||||
|
format!("{}{}", t!("Master unit"), unit(&state.units.0)),
|
||||||
|
!state.sub_units,
|
||||||
|
Some(msg(ImportDialogMsg::DgnSub(false)))
|
||||||
|
),
|
||||||
|
(
|
||||||
|
format!("{}{}", t!("Sub unit"), unit(&state.units.1)),
|
||||||
|
state.sub_units,
|
||||||
|
Some(msg(ImportDialogMsg::DgnSub(true)))
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
row![
|
||||||
|
iced::widget::toggler(state.explode_text_nodes)
|
||||||
|
.on_toggle(|on| msg(ImportDialogMsg::DgnExplode(on)))
|
||||||
|
.size(16),
|
||||||
|
text(t!("Explode text nodes to text elements")).size(11),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
]
|
||||||
|
.spacing(8),
|
||||||
|
);
|
||||||
|
// The DGN readers do not read reference attachments, so their handling
|
||||||
|
// is shown but cannot be chosen.
|
||||||
|
let references = card(
|
||||||
|
t!("External DGN references").into_owned(),
|
||||||
|
column![
|
||||||
|
segmented(vec![
|
||||||
|
(t!("Bind as block / xref").into_owned(), false, None),
|
||||||
|
(t!("Ignore").into_owned(), true, None),
|
||||||
|
(t!("Attach as DGN underlay").into_owned(), false, None),
|
||||||
|
]),
|
||||||
|
text(t!("Reference attachments of DGN files are not read; they are left out."))
|
||||||
|
.size(11)
|
||||||
|
.style(muted_style),
|
||||||
|
]
|
||||||
|
.spacing(8),
|
||||||
|
);
|
||||||
|
let tabs = [t!("Layer"), t!("Linetype"), t!("Lineweight"), t!("Color")];
|
||||||
|
let mut table = column![row![
|
||||||
|
text("DGN").size(11).style(muted_style).width(Fill),
|
||||||
|
text("DWG").size(11).style(muted_style).width(Fill),
|
||||||
|
]]
|
||||||
|
.spacing(4);
|
||||||
|
for (dgn, dwg) in &state.mapping[state.mapping_tab.min(3)] {
|
||||||
|
table = table.push(row![
|
||||||
|
text(dgn.clone()).size(11).width(Fill),
|
||||||
|
text("\u{2192}").size(11).style(muted_style).width(Length::Fixed(24.0)),
|
||||||
|
text(dwg.clone()).size(11).width(Fill),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
let mapping = card(
|
||||||
|
t!("Property mapping").into_owned(),
|
||||||
|
column![
|
||||||
|
row![chip(
|
||||||
|
"Standard".to_string(),
|
||||||
|
true,
|
||||||
|
msg(ImportDialogMsg::DgnMappingTab(state.mapping_tab))
|
||||||
|
)],
|
||||||
|
text(t!("Standard mapping setup.")).size(11).style(muted_style),
|
||||||
|
segmented(
|
||||||
|
tabs.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(k, label)| {
|
||||||
|
(
|
||||||
|
label.to_string(),
|
||||||
|
state.mapping_tab == k,
|
||||||
|
Some(msg(ImportDialogMsg::DgnMappingTab(k))),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
),
|
||||||
|
scrollable(table).height(Length::Fixed(250.0)),
|
||||||
|
text(t!("The preview shows the selected setup's effect on this file."))
|
||||||
|
.size(11)
|
||||||
|
.style(muted_style),
|
||||||
|
]
|
||||||
|
.spacing(8),
|
||||||
|
);
|
||||||
|
column![
|
||||||
|
top,
|
||||||
|
row![
|
||||||
|
column![model, target, units, references]
|
||||||
|
.spacing(10)
|
||||||
|
.width(Length::FillPortion(3)),
|
||||||
|
container(mapping).width(Length::FillPortion(2)),
|
||||||
|
]
|
||||||
|
.spacing(10),
|
||||||
|
footer("dgn", t!("Import"), Some(msg(ImportDialogMsg::DgnOk))),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.padding([10, 12])
|
||||||
|
.width(sizing.width)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ pub mod drawing_units;
|
|||||||
pub mod field_dialog;
|
pub mod field_dialog;
|
||||||
pub mod find_replace;
|
pub mod find_replace;
|
||||||
pub mod geometric_tolerance;
|
pub mod geometric_tolerance;
|
||||||
|
pub mod import_dialogs;
|
||||||
pub mod gradient_editor;
|
pub mod gradient_editor;
|
||||||
pub mod graphic_attributes;
|
pub mod graphic_attributes;
|
||||||
pub mod layer_state_manager;
|
pub mod layer_state_manager;
|
||||||
|
|||||||
Reference in new issue
Block a user