IMPORT: 3D Studio files

A .3ds file opens the 3D Studio Import window: the file's objects with
their kind and material count, filtered by kind or name, checked one by
one or all at once, where to put them on layers and what a mesh with
several materials becomes. Checked meshes come in as polyface meshes on
the chosen layers with materials made from the file's colours, cameras
as named views; a mesh with several materials is split, given its first
material, given none, or asked about on the command line. Automation
drives the window with tds_import_dialog.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-10-16 20:12:16 +03:00
1 parent 9ddc103bcc
commit 78e429732e
35 files changed
+1893 -4

No files matched your search

+35
View File
@@ -6687,6 +6687,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = استيراد DGN
.recognize-shx = التعرف على نص SHX
+35
View File
@@ -6675,6 +6675,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = Импортиране на DGN
.recognize-shx = Разпознаване на SHX текст
+35
View File
@@ -6672,6 +6672,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = Import DGN
.recognize-shx = Rozpoznat text SHX
+35
View File
@@ -6673,6 +6673,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = DGN-Import
.recognize-shx = SHX-Text erkennen
+35
View File
@@ -6675,6 +6675,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = Εισαγωγή DGN
.recognize-shx = Αναγνώριση κειμένου SHX
+35
View File
@@ -6674,6 +6674,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = DGN Import
.recognize-shx = Recognize SHX Text
+35
View File
@@ -6675,6 +6675,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = Importar DGN
.recognize-shx = Reconocer texto SHX
+35
View File
@@ -6672,6 +6672,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = DGN-tuonti
.recognize-shx = Tunnista SHX-teksti
+35
View File
@@ -6675,6 +6675,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = Import DGN
.recognize-shx = Reconnaître le texte SHX
+35
View File
@@ -6663,6 +6663,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = DGN आयात
.recognize-shx = SHX टेक्स्ट पहचानें
+35
View File
@@ -6659,6 +6659,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = DGN-importálás
.recognize-shx = SHX-szöveg felismerése
+35
View File
@@ -6674,6 +6674,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = Importa DGN
.recognize-shx = Riconosci testo SHX
+35
View File
@@ -6663,6 +6663,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = DGN 読み込み
.recognize-shx = SHX 文字を認識
+35
View File
@@ -6674,6 +6674,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = DGN 가져오기
.recognize-shx = SHX 문자 인식
+35
View File
@@ -6675,6 +6675,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = ນໍາເຂົ້າ DGN
.recognize-shx = ຮັບຮູ້ຂໍ້ຄວາມ SHX
+35
View File
@@ -6673,6 +6673,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = DGN importeren
.recognize-shx = SHX-tekst herkennen
+35
View File
@@ -6674,6 +6674,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = Import DGN
.recognize-shx = Rozpoznaj tekst SHX
+35
View File
@@ -6675,6 +6675,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = Importar DGN
.recognize-shx = Reconhecer texto SHX
+35
View File
@@ -6676,6 +6676,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = Импорт DGN
.recognize-shx = Распознать текст SHX
+35
View File
@@ -6596,6 +6596,41 @@ dgn-import =
.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ı.
import-3ds =
.title = 3D Studio İçe Aktar
.summary = { $objects } nesne, { $materials } malzeme
.change = Değiştir…
.mesh = Ağ
.light = Işık
.camera = Kamera
.general = Genel
.global = Genel Ayarlar
.all = Tümü
.name = Ad
.type = Tür
.materials = { $n } malzeme
.objects = Nesneler
.search = Ada göre ara...
.select-all = Tümünü seç
.clear = Temizle
.layers = Katmana yerleştir
.by-object = Nesneye göre
.by-material = Malzemeye göre
.by-color = Nesne rengine göre
.single = Tek katman
.note-object = Her nesne kendi adını taşıyan katmana yerleşir.
.note-material = Her nesne malzemesinin adını taşıyan katmana yerleşir.
.note-color = Her nesne renginin adını taşıyan katmana yerleşir.
.note-single = Tüm nesneler tek bir katmana yerleşir.
.multi = Birden çok malzemeli nesne
.ask = Her seferinde sor
.split = Malzemeye göre böl
.first = İlk malzemeyi ata
.none = Malzeme atama
.reason = En az bir nesne işaretleyin
.cancel = İptal
.import = İçe Aktar
.help = İşaretli nesneleri içe aktarır: ağlar malzemeleriyle çokyüzlü ağ, kameralar adlandırılmış görünüm olur.
import-panel =
.dgn-import = DGN İçe Aktar
.recognize-shx = SHX Metnini Tanı
+35
View File
@@ -6658,6 +6658,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = DGN 输入
.recognize-shx = 识别 SHX 文字
+35
View File
@@ -6670,6 +6670,41 @@ dgn-import =
.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.
import-3ds =
.title = 3D Studio Import
.summary = { $objects } objects, { $materials } materials
.change = Change...
.mesh = Mesh
.light = Light
.camera = Camera
.general = General
.global = Global Settings
.all = All
.name = Name
.type = Type
.materials = { $n } materials
.objects = Objects
.search = Search by name...
.select-all = Select all
.clear = Clear
.layers = Place on layers
.by-object = By object
.by-material = By material
.by-color = By object color
.single = Single layer
.note-object = Each object goes on a layer named after it.
.note-material = Each object goes on a layer named after its material.
.note-color = Each object goes on a layer named after its color.
.note-single = Every object goes on one layer.
.multi = Objects with several materials
.ask = Ask each time
.split = Split by material
.first = Assign the first material
.none = Don't assign a material
.reason = Check at least one object
.cancel = Cancel
.import = Import
.help = Imports the checked objects of a 3D Studio file: meshes as polyface meshes with their materials, cameras as named views.
import-panel =
.dgn-import = DGN 匯入
.recognize-shx = 辨識 SHX 文字
+5
View File
@@ -654,6 +654,11 @@ impl OpenCADStudio {
self.tabs[i].snap_result = None;
return self.run_dgn_import(i, request);
}
CmdResult::TdsImport(request) => {
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.run_tds_import(i, request);
}
CmdResult::PdfImportFile(import) => {
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
+74
View File
@@ -65,6 +65,7 @@ enum Route {
Dgn,
Model(ModelFormat),
Wmf,
Tds,
}
/// The importer for `path` chosen as `format` (an index of [`FORMATS`]; `None`
@@ -91,6 +92,7 @@ fn route(path: &str, format: Option<usize>) -> Result<Route, String> {
Some("STEP") => return Ok(Route::Model(ModelFormat::Step)),
Some("IGES") => return Ok(Route::Model(ModelFormat::Iges)),
Some("Metafile") => return Ok(Route::Wmf),
Some("3D Studio") => return Ok(Route::Tds),
_ => {}
}
let pdf = if by_extension { ext == "pdf" } else { f.is_some_and(|f| f.name == "PDF Files") };
@@ -245,6 +247,14 @@ impl OpenCADStudio {
let i = self.active_tab;
self.import_wmf(i, path, false)
}
// The options window opens whatever FILEDIA is.
Ok(Route::Tds) => {
self.remember_import(path);
if let Err(error) = self.open_tds_import_dialog(path) {
self.command_line.push_error(&error);
}
Task::none()
}
Err(error) => {
self.command_line.push_error(&error);
Task::none()
@@ -260,6 +270,7 @@ impl OpenCADStudio {
"dgn" => crate::t!(
"Imports a DGN model as drawing objects: levels as layers, cells as blocks."
),
"3ds" => std::borrow::Cow::Owned(crate::tr!("import-3ds", "help")),
_ => crate::t!(
"Imports a file of the chosen format with the format's importer."
),
@@ -362,8 +373,71 @@ impl OpenCADStudio {
self.close_active_modal();
return self.import_wmf(i, &path, false);
}
Ok(Route::Tds) => {
self.remember_import(&path);
self.close_active_modal();
if let Err(error) = self.open_tds_import_dialog(&path) {
self.command_line.push_error(&error);
}
}
}
}
// ── 3D Studio Import ─────────────────────────────────────────
ImportDialogMsg::TdsChange => {
let title = crate::t!("Import File").into_owned();
let filters = vec![("3D Studio (*.3ds)".to_string(), vec!["3ds".to_string()])];
return Task::perform(pick(title, filters), |path| {
Message::ImportDialog(ImportDialogMsg::TdsPicked(path))
});
}
ImportDialogMsg::TdsPicked(Some(path)) => {
if let Err(error) = self.open_tds_import_dialog(&path) {
self.command_line.push_error(&error);
}
}
ImportDialogMsg::TdsPicked(None) => {}
ImportDialogMsg::TdsCheck(k, on) => {
if let Some(c) = self.import_dialogs.tds.as_mut().and_then(|s| s.checked.get_mut(k)) {
*c = on;
}
}
ImportDialogMsg::TdsAll | ImportDialogMsg::TdsClear => {
let on = matches!(message, ImportDialogMsg::TdsAll);
if let Some(s) = self.import_dialogs.tds.as_mut() {
s.checked.iter_mut().for_each(|c| *c = on);
}
}
ImportDialogMsg::TdsFilter(k) => {
if let Some(s) = self.import_dialogs.tds.as_mut() {
s.filter = k.min(4);
}
}
ImportDialogMsg::TdsSearch(text) => {
if let Some(s) = self.import_dialogs.tds.as_mut() {
s.search = text;
}
}
ImportDialogMsg::TdsLayers(k) => {
if let Some(s) = self.import_dialogs.tds.as_mut() {
s.layers = k.min(3);
}
}
ImportDialogMsg::TdsMulti(k) => {
if let Some(s) = self.import_dialogs.tds.as_mut() {
s.multi = k.min(3);
}
}
ImportDialogMsg::TdsOk => {
let Some(state) = self.import_dialogs.tds.take() else {
return Task::none();
};
if !state.checked.iter().any(|c| *c) {
self.import_dialogs.tds = Some(state);
return Task::none();
}
self.close_active_modal();
return self.start_tds_import(i, state);
}
// ── DGN Import ───────────────────────────────────────────────
ImportDialogMsg::DgnChange => return self.pick_dgn_file(),
ImportDialogMsg::DgnPicked(Some(path)) => {
+214
View File
@@ -203,3 +203,217 @@ impl OpenCADStudio {
Task::none()
}
}
impl OpenCADStudio {
/// The 3D Studio Import window for a file; an error when it is no 3D
/// Studio file.
pub(in crate::app) fn open_tds_import_dialog(&mut self, path: &str) -> Result<(), String> {
let scene = crate::scene::model::pdf_raster::source_bytes(path)
.and_then(|bytes| crate::io::tds_import::read(&bytes))
.ok_or_else(|| format!("{} is not a valid 3D Studio file.", path.replace('/', "\\")))?;
let mut objects: Vec<(String, &'static str, usize)> = scene
.objects
.iter()
.map(|(name, object)| {
let materials = match object {
crate::io::tds_import::TdsObject::Mesh(mesh) => scene.mesh_materials(mesh).len(),
_ => 0,
};
(name.clone(), object.kind(), materials)
})
.collect();
objects.sort_by_key(|o| o.0.to_lowercase());
objects.insert(0, ("Global Settings".to_string(), "Global", 0));
// The layer and material choices made last stay; nothing starts
// checked.
let previous = self.import_dialogs.tds.take();
self.import_dialogs.tds = Some(crate::ui::window::import_dialogs::TdsImportState {
path: path.to_string(),
checked: vec![false; objects.len()],
objects,
materials: scene.materials.len(),
filter: 0,
search: String::new(),
layers: previous.as_ref().map_or(0, |s| s.layers),
multi: previous.as_ref().map_or(0, |s| s.multi),
});
self.active_modal = Some(crate::app::ModalKind::TdsImport);
Ok(())
}
/// The window's choices as an import: asked mesh by mesh first when set
/// to ask and some checked mesh has several materials.
pub(in crate::app) fn start_tds_import(
&mut self,
i: usize,
state: crate::ui::window::import_dialogs::TdsImportState,
) -> Task<Message> {
use crate::io::tds_import::{LayerMode, MultiMaterial, TdsObject};
use crate::modules::insert::tds_import::{TdsImportRequest, TdsMaterialCommand};
let Some(scene) = crate::scene::model::pdf_raster::source_bytes(&state.path)
.and_then(|bytes| crate::io::tds_import::read(&bytes))
else {
return Task::none();
};
let selected: Vec<String> = state
.objects
.iter()
.zip(&state.checked)
.filter(|(_, on)| **on)
.map(|((name, _, _), _)| name.clone())
.collect();
let several: Vec<(String, Vec<String>)> = scene
.objects
.iter()
.filter(|(name, _)| selected.contains(name))
.filter_map(|(name, object)| match object {
TdsObject::Mesh(mesh) => {
let materials = scene.mesh_materials(mesh);
(materials.len() > 1).then(|| (name.clone(), materials))
}
_ => None,
})
.collect();
let choice = match state.multi {
1 => Some(MultiMaterial::Split),
2 => Some(MultiMaterial::First),
3 => Some(MultiMaterial::Material(None)),
_ => None,
};
let mut request = TdsImportRequest {
path: state.path.clone(),
selected,
layers: match state.layers {
1 => LayerMode::ByMaterial,
2 => LayerMode::ByObjectColor,
3 => LayerMode::Single,
_ => LayerMode::ByObject,
},
multi: Default::default(),
compact: matches!(state.multi, 0 | 1),
};
match choice {
Some(choice) => {
for (name, _) in several {
request.multi.insert(name, choice.clone());
}
}
None if !several.is_empty() => {
use crate::command::CadCommand;
let command = TdsMaterialCommand::new(request, several);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
return Task::none();
}
None => {}
}
self.run_tds_import(i, request);
Task::none()
}
/// The import itself, one undo step: the meshes on their layers with
/// their materials, the cameras as named views.
pub(in crate::app) fn run_tds_import(
&mut self,
i: usize,
request: crate::modules::insert::tds_import::TdsImportRequest,
) {
use codec::objects::ObjectType;
let Some(scene) = crate::scene::model::pdf_raster::source_bytes(&request.path)
.and_then(|bytes| crate::io::tds_import::read(&bytes))
else {
self.command_line.push_error(&format!(
"{} is not a valid 3D Studio file.",
request.path.replace('/', "\\")
));
return;
};
let import = crate::io::tds_import::convert(
&scene,
&crate::io::tds_import::TdsOptions {
selected: request.selected.clone(),
layers: request.layers,
multi: request.multi.clone(),
compact: request.compact,
},
);
self.push_undo_snapshot(i, "IMPORT");
let document = &mut self.tabs[i].scene.document;
// Materials by name: the drawing's own when it has one, else one
// made from the file's colours.
let mut materials: std::collections::HashMap<String, codec::Handle> = Default::default();
for name in &import.materials {
let existing = document.objects.iter().find_map(|(h, o)| match o {
ObjectType::Material(m) if m.name.eq_ignore_ascii_case(name) => Some(*h),
_ => None,
});
let handle = match existing {
Some(h) => h,
None => {
let source = scene.materials.iter().find(|m| m.name == *name);
let mut material = codec::objects::Material::new();
material.handle = document.allocate_handle();
material.owner = document.header.acad_material_dict_handle;
material.name = name.clone();
let color = |rgb: Option<[u8; 3]>| match rgb {
Some([r, g, b]) => codec::objects::MaterialColor {
flag: 1,
factor: 1.0,
rgb: Some(
((0xC2u32 << 24) | (u32::from(r) << 16) | (u32::from(g) << 8) | u32::from(b))
as i32,
),
},
None => codec::objects::MaterialColor::default(),
};
material.ambient_color = color(source.and_then(|m| m.ambient));
material.diffuse_color = color(source.and_then(|m| m.diffuse));
material.specular_color = color(source.and_then(|m| m.specular));
let handle = material.handle;
let dictionary = document.header.acad_material_dict_handle;
if let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&dictionary) {
dictionary.add_entry(name.clone(), handle);
}
document.objects.insert(handle, ObjectType::Material(material));
handle
}
};
materials.insert(name.clone(), handle);
}
for part in &import.parts {
if !document.layers.contains(&part.layer) {
let mut layer = codec::tables::Layer::new(part.layer.as_str());
layer.handle = document.allocate_handle();
let _ = document.layers.add(layer);
}
}
for view in &import.views {
let mut named = codec::tables::View::new(view.name.as_str());
named.target = codec::types::Vector3::new(view.target[0], view.target[1], view.target[2]);
named.direction =
codec::types::Vector3::new(view.direction[0], view.direction[1], view.direction[2]);
named.height = view.height;
named.width = view.height;
named.lens_length = view.lens;
named.handle = match document.views.get(&view.name) {
Some(old) => old.handle,
None => document.allocate_handle(),
};
document.views.add_or_replace(named);
}
for part in import.parts {
let mut mesh = part.mesh;
mesh.common.layer = part.layer.clone();
mesh.common.material_handle = part.material.as_ref().and_then(|m| materials.get(m).copied());
if mesh.common.material_handle.is_some() {
mesh.common.material_flags = 0b11;
}
self.tabs[i].scene.add_entity(codec::EntityType::PolyfaceMesh(mesh));
}
for line in &import.messages {
self.command_line.push_output(line);
}
self.tabs[i].dirty = true;
self.refresh_properties();
}
}
+40
View File
@@ -110,6 +110,7 @@ pub(super) const NAMES: &[&str] = &[
"image_adjust",
"import_file_dialog",
"dgn_import_dialog",
"tds_import_dialog",
"refedit_dialog",
"pdf_shx_settings",
"pdf_layer_toggle",
@@ -2198,6 +2199,45 @@ impl OpenCADStudio {
_ => return Err(bad()),
})
}
// The 3D Studio Import window, one field per call: "check=<name>"
// and "uncheck=<name>" (the object's name, or "Global Settings"),
// "all", "clear", "filter=all|mesh|light|camera|general",
// "search=<text>", "layers=object|material|color|single",
// "multi=ask|split|first|none", "path=<file>" (as Change… picks
// it), "ok" or "cancel".
"tds_import_dialog" => {
use crate::ui::window::import_dialogs::ImportDialogMsg as I;
if self.active_modal != Some(crate::app::ModalKind::TdsImport) {
return Err(failure("no_dialog", "The 3D Studio 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);
if key == "cancel" {
return Ok(Task::done(Message::CloseModal));
}
let pick = |options: &[&str]| options.iter().position(|o| *o == value).ok_or_else(bad);
Message::ImportDialog(match key {
"ok" => I::TdsOk,
"all" => I::TdsAll,
"clear" => I::TdsClear,
"check" | "uncheck" => {
let k = self
.import_dialogs
.tds
.as_ref()
.and_then(|s| s.objects.iter().position(|(name, _, _)| name.eq_ignore_ascii_case(value)))
.ok_or_else(bad)?;
I::TdsCheck(k, key == "check")
}
"filter" => I::TdsFilter(pick(&["all", "mesh", "light", "camera", "general"])?),
"search" => I::TdsSearch(value.to_string()),
"layers" => I::TdsLayers(pick(&["object", "material", "color", "single"])?),
"multi" => I::TdsMulti(pick(&["ask", "split", "first", "none"])?),
"path" => I::TdsPicked(Some(value.to_string())),
_ => 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",
+2
View File
@@ -2335,6 +2335,8 @@ pub enum ModalKind {
ImportFile,
/// DGNIMPORT's DGN Import window.
DgnImport,
/// IMPORT's 3D Studio Import window.
TdsImport,
/// Web: a crash-recovery autosave snapshot newer than the drawing's last
/// save awaits a restore/discard decision. Distinct from the corrupt-file
/// `RecoveryPrompt` — nothing is wrong with the drawing itself.
+1
View File
@@ -254,6 +254,7 @@ impl OpenCADStudio {
Some(PdfImportFile) => self.pdf_import_file = None,
Some(ImportFile) => self.import_dialogs.file = None,
Some(DgnImport) => self.import_dialogs.dgn = None,
Some(TdsImport) => self.import_dialogs.tds = None,
Some(XrefAttach) => self.xref_attach = None,
Some(WriteBlock) => self.wblock = None,
Some(Hyperlink) => {
+7
View File
@@ -51,6 +51,7 @@ impl OpenCADStudio {
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::TdsImport) => crate::tr!("import-3ds", "title"),
Some(K::XrefAttach) => crate::t!("Attach External Reference").into_owned(),
Some(K::WriteBlock) => crate::t!("Write Block").into_owned(),
Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(),
@@ -613,6 +614,12 @@ impl OpenCADStudio {
crate::ui::window::import_dialogs::view_import_file(state, recent, flow)
})
}
super::super::ModalKind::TdsImport => {
let state = self.import_dialogs.tds.as_ref()?;
sized_flow(ex, 860, 560, |flow| {
crate::ui::window::import_dialogs::view_tds_import(state, flow)
})
}
super::super::ModalKind::DgnImport => {
let state = self.import_dialogs.dgn.as_ref()?;
sized_flow(ex, 900, 680, |flow| {
+2
View File
@@ -2305,6 +2305,8 @@ pub enum CmdResult {
ReturnPoint(DVec3),
/// Import a DGN model (DGNIMPORT); ends the command.
DgnImport(crate::modules::insert::dgn_import::DgnImportRequest),
/// Import a 3D Studio file with every question answered; ends the command.
TdsImport(crate::modules::insert::tds_import::TdsImportRequest),
}
/// What kind of value the active command is currently asking for. Drives
+1
View File
@@ -33,6 +33,7 @@ pub mod step;
pub mod step_import;
pub mod stl;
pub mod svg_export;
pub mod tds_import;
pub mod thumbnail;
pub mod update_check;
pub mod visual_styles;
+403
View File
@@ -0,0 +1,403 @@
//! 3D Studio (.3ds) reading: the materials, meshes, lights and cameras of a
//! file's editor section, and their conversion to drawing objects — each
//! mesh a polyface mesh on a layer chosen by object, material, object colour
//! or a single layer, with its material; each camera a named view.
use std::collections::HashMap;
use codec::entities::{PolyfaceFace, PolyfaceMesh, PolyfaceVertex};
use codec::types::Vector3;
pub struct TdsMaterial {
pub name: String,
pub ambient: Option<[u8; 3]>,
pub diffuse: Option<[u8; 3]>,
pub specular: Option<[u8; 3]>,
}
pub struct TdsMesh {
pub vertices: Vec<[f64; 3]>,
pub faces: Vec<[usize; 3]>,
/// Each material the mesh uses and the faces it covers, in file order.
pub groups: Vec<(String, Vec<usize>)>,
/// The object colour, a palette index, when the file gives one.
pub color: Option<u8>,
}
pub struct TdsCamera {
pub position: [f64; 3],
pub target: [f64; 3],
pub lens: f64,
}
pub enum TdsObject {
Mesh(TdsMesh),
Light,
Camera(TdsCamera),
}
impl TdsObject {
/// The kind as the import window lists it.
pub fn kind(&self) -> &'static str {
match self {
TdsObject::Mesh(_) => "Mesh",
TdsObject::Light => "Light",
TdsObject::Camera(_) => "Camera",
}
}
}
pub struct TdsScene {
pub materials: Vec<TdsMaterial>,
pub objects: Vec<(String, TdsObject)>,
}
impl TdsScene {
/// The materials a mesh uses, in the order the file defines them.
pub fn mesh_materials(&self, mesh: &TdsMesh) -> Vec<String> {
let mut used: Vec<String> = Vec::new();
for m in &self.materials {
if mesh.groups.iter().any(|(name, faces)| !faces.is_empty() && *name == m.name) {
used.push(m.name.clone());
}
}
for (name, faces) in &mesh.groups {
if !faces.is_empty() && !used.contains(name) {
used.push(name.clone());
}
}
used
}
}
struct Reader<'a> {
b: &'a [u8],
}
impl Reader<'_> {
fn u16(&self, at: usize) -> Option<u16> {
Some(u16::from_le_bytes([*self.b.get(at)?, *self.b.get(at + 1)?]))
}
fn u32(&self, at: usize) -> Option<u32> {
Some(u32::from_le_bytes(self.b.get(at..at + 4)?.try_into().ok()?))
}
fn f32(&self, at: usize) -> Option<f64> {
Some(f64::from(f32::from_le_bytes(self.b.get(at..at + 4)?.try_into().ok()?)))
}
fn point(&self, at: usize) -> Option<[f64; 3]> {
Some([self.f32(at)?, self.f32(at + 4)?, self.f32(at + 8)?])
}
/// A zero-terminated string and the offset after it.
fn cstr(&self, at: usize, end: usize) -> (String, usize) {
let stop = self.b[at.min(end)..end]
.iter()
.position(|c| *c == 0)
.map_or(end, |k| at + k);
(
String::from_utf8_lossy(&self.b[at.min(stop)..stop]).into_owned(),
(stop + 1).min(end),
)
}
/// The chunks between `from` and `to`: id, body start, body end.
fn chunks(&self, from: usize, to: usize) -> Vec<(u16, usize, usize)> {
let mut out = Vec::new();
let mut at = from;
while at + 6 <= to {
let (Some(id), Some(len)) = (self.u16(at), self.u32(at + 2)) else { break };
let len = len as usize;
if len < 6 || at + len > to {
break;
}
out.push((id, at + 6, at + len));
at += len;
}
out
}
/// A colour chunk's 8-bit RGB: 24-bit or float, plain before linear.
fn color(&self, from: usize, to: usize) -> Option<[u8; 3]> {
let chunks = self.chunks(from, to);
for want in [0x0011, 0x0010, 0x0012, 0x0013] {
for &(id, start, _) in &chunks {
if id != want {
continue;
}
return Some(match id {
0x0011 | 0x0012 => [*self.b.get(start)?, *self.b.get(start + 1)?, *self.b.get(start + 2)?],
_ => self.point(start)?.map(|c| (c.clamp(0.0, 1.0) * 255.0).round() as u8),
});
}
}
None
}
}
/// `None` when the bytes are no 3D Studio file.
pub fn read(bytes: &[u8]) -> Option<TdsScene> {
let r = Reader { b: bytes };
if r.u16(0)? != 0x4D4D {
return None;
}
let end = (r.u32(2)? as usize).min(bytes.len());
let mut scene = TdsScene {
materials: Vec::new(),
objects: Vec::new(),
};
for (id, start, stop) in r.chunks(6, end) {
if id != 0x3D3D {
continue;
}
for (id, start, stop) in r.chunks(start, stop) {
match id {
0xAFFF => {
let mut material = TdsMaterial {
name: String::new(),
ambient: None,
diffuse: None,
specular: None,
};
for (id, s, e) in r.chunks(start, stop) {
match id {
0xA000 => material.name = r.cstr(s, e).0,
0xA010 => material.ambient = r.color(s, e),
0xA020 => material.diffuse = r.color(s, e),
0xA030 => material.specular = r.color(s, e),
_ => {}
}
}
scene.materials.push(material);
}
0x4000 => {
let (name, body) = r.cstr(start, stop);
for (id, s, e) in r.chunks(body, stop) {
let object = match id {
0x4100 => TdsObject::Mesh(read_mesh(&r, s, e)),
0x4600 => TdsObject::Light,
0x4700 => TdsObject::Camera(TdsCamera {
position: r.point(s)?,
target: r.point(s + 12)?,
lens: r.f32(s + 28).filter(|l| *l > 0.0).unwrap_or(50.0),
}),
_ => continue,
};
scene.objects.push((name.clone(), object));
break;
}
}
_ => {}
}
}
}
Some(scene)
}
fn read_mesh(r: &Reader<'_>, from: usize, to: usize) -> TdsMesh {
let mut mesh = TdsMesh {
vertices: Vec::new(),
faces: Vec::new(),
groups: Vec::new(),
color: None,
};
for (id, s, e) in r.chunks(from, to) {
match id {
0x4110 => {
let n = r.u16(s).unwrap_or(0) as usize;
mesh.vertices = (0..n).filter_map(|k| r.point(s + 2 + 12 * k)).collect();
}
0x4120 => {
let n = r.u16(s).unwrap_or(0) as usize;
mesh.faces = (0..n)
.filter_map(|k| {
let at = s + 2 + 8 * k;
Some([r.u16(at)? as usize, r.u16(at + 2)? as usize, r.u16(at + 4)? as usize])
})
.collect();
for (id, gs, ge) in r.chunks(s + 2 + 8 * n, e) {
if id == 0x4130 {
let (name, at) = r.cstr(gs, ge);
let count = r.u16(at).unwrap_or(0) as usize;
let faces = (0..count).filter_map(|k| r.u16(at + 2 + 2 * k).map(usize::from)).collect();
mesh.groups.push((name, faces));
}
}
}
0x4165 => mesh.color = r.b.get(s).copied(),
_ => {}
}
}
mesh
}
/// Where imported objects go.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LayerMode {
#[default]
ByObject,
ByMaterial,
ByObjectColor,
Single,
}
/// What a mesh with several materials becomes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MultiMaterial {
/// One mesh per material.
Split,
/// One mesh with the first material the file defines.
First,
/// One mesh with this material, or none.
Material(Option<String>),
}
pub struct TdsOptions {
/// The objects to import, by name ("Global Settings" included).
pub selected: Vec<String>,
pub layers: LayerMode,
/// The choice for each mesh with several materials, by name.
pub multi: HashMap<String, MultiMaterial>,
/// Whether meshes keep only the vertices their faces use, in the order
/// they first use them — as they do when the import splits by
/// material or asks — rather than the file's vertex list.
pub compact: bool,
}
/// One imported mesh.
pub struct TdsPart {
pub layer: String,
pub material: Option<String>,
pub mesh: PolyfaceMesh,
}
pub struct TdsCameraView {
pub name: String,
pub target: [f64; 3],
pub direction: [f64; 3],
/// The view height at the target, from the lens.
pub height: f64,
pub lens: f64,
}
pub struct TdsImport {
pub parts: Vec<TdsPart>,
/// The materials the parts use, in the order they were first used.
pub materials: Vec<String>,
pub views: Vec<TdsCameraView>,
pub messages: Vec<String>,
}
/// A polyface mesh of `faces` of `mesh`: its vertices in file order, or
/// (`compact`) only those the faces use, in the order they first use them.
fn polyface(mesh: &TdsMesh, faces: &[usize], compact: bool) -> PolyfaceMesh {
let mut out = PolyfaceMesh::new();
let mut index: HashMap<usize, i16> = HashMap::new();
if !compact {
for (k, v) in mesh.vertices.iter().enumerate() {
index.insert(k, out.add_vertex(PolyfaceVertex::new(Vector3::new(v[0], v[1], v[2]))));
}
}
for &f in faces {
let Some(face) = mesh.faces.get(f) else { continue };
if face.iter().any(|v| *v >= mesh.vertices.len()) {
continue;
}
let ids = face.map(|v| {
*index.entry(v).or_insert_with(|| {
let p = mesh.vertices[v];
out.add_vertex(PolyfaceVertex::new(Vector3::new(p[0], p[1], p[2])))
})
});
out.add_face(PolyfaceFace::triangle(ids[0], ids[1], ids[2]));
}
out
}
/// The view height a camera's lens sees at the target distance.
// ponytail: one measured camera (50 mm at 86.46 units gives 30.457); the
// height is taken as proportional to distance over lens.
fn view_height(distance: f64, lens: f64) -> f64 {
distance * 17.6141 / lens
}
pub fn convert(scene: &TdsScene, options: &TdsOptions) -> TdsImport {
let mut out = TdsImport {
parts: Vec::new(),
materials: Vec::new(),
views: Vec::new(),
messages: Vec::new(),
};
let chosen = |name: &str| options.selected.iter().any(|s| s == name);
// The objects are taken in the order the window lists them, by name.
let mut objects: Vec<&(String, TdsObject)> = scene.objects.iter().collect();
objects.sort_by_key(|(name, _)| name.to_lowercase());
for (name, object) in objects {
if !chosen(name) {
continue;
}
match object {
TdsObject::Mesh(mesh) => {
out.messages.push(format!("Processing object {name}"));
let materials = scene.mesh_materials(mesh);
let all: Vec<usize> = (0..mesh.faces.len()).collect();
let pieces: Vec<(Option<String>, Vec<usize>)> = if materials.len() > 1 {
match options.multi.get(name).cloned().unwrap_or(MultiMaterial::Split) {
MultiMaterial::Split => materials
.iter()
.map(|m| {
let faces = mesh
.groups
.iter()
.filter(|(g, _)| g == m)
.flat_map(|(_, f)| f.iter().copied())
.collect();
(Some(m.clone()), faces)
})
.collect(),
MultiMaterial::First => vec![(materials.first().cloned(), all)],
MultiMaterial::Material(m) => vec![(m, all)],
}
} else {
vec![(materials.first().cloned(), all)]
};
let compact = options.compact || pieces.len() > 1;
for (material, faces) in pieces {
if let Some(m) = &material {
if !out.materials.contains(m) {
out.messages.push(format!("Converting material {m}"));
out.materials.push(m.clone());
}
}
let layer = match options.layers {
LayerMode::ByObject => name.to_uppercase(),
LayerMode::ByMaterial => material.as_deref().unwrap_or("0").to_uppercase(),
LayerMode::ByObjectColor => match mesh.color {
Some(c) => format!("COLOR{c:02}"),
None => "COLORNONE".to_string(),
},
LayerMode::Single => "AVLAYER".to_string(),
};
out.parts.push(TdsPart {
layer,
material,
mesh: polyface(mesh, &faces, compact),
});
}
}
TdsObject::Camera(camera) => {
let name = name.to_uppercase();
out.messages.push(format!("Creating camera {name}"));
let direction = [0, 1, 2].map(|k| camera.position[k] - camera.target[k]);
let distance = direction.iter().map(|d| d * d).sum::<f64>().sqrt();
out.views.push(TdsCameraView {
name,
target: camera.target,
direction,
height: view_height(distance, camera.lens),
lens: camera.lens,
});
}
TdsObject::Light => {}
}
}
out.messages.push("3D Studio file import completed".to_string());
out
}
+1
View File
@@ -9,6 +9,7 @@ mod content_browser;
pub(crate) mod create_block;
mod design_center;
pub(crate) mod dgn_import;
pub(crate) mod tds_import;
pub(crate) mod edit_block;
pub(crate) mod image_transparency;
pub(crate) mod insert_block;
+132
View File
@@ -0,0 +1,132 @@
// 3D Studio import: when the import is set to ask what a mesh with several
// materials becomes, this asks for each such mesh in turn.
//
// <name> has multiple materials assigned. [Split/First/Material] <Split>:
// Select a material or [None] <None>:
use std::collections::HashMap;
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
use crate::io::tds_import::{LayerMode, MultiMaterial};
/// What one 3D Studio import reads and how.
#[derive(Clone, Debug, PartialEq)]
pub struct TdsImportRequest {
pub path: String,
/// The objects to import, by name ("Global Settings" included).
pub selected: Vec<String>,
pub layers: LayerMode,
/// The choice for each mesh with several materials, by name.
pub multi: HashMap<String, MultiMaterial>,
/// Meshes keep only the vertices their faces use (the import splits or
/// asks), else the file's vertex list.
pub compact: bool,
}
pub struct TdsMaterialCommand {
request: TdsImportRequest,
/// The meshes still to ask about, with their materials.
queue: Vec<(String, Vec<String>)>,
choosing: bool,
}
impl TdsMaterialCommand {
pub fn new(request: TdsImportRequest, queue: Vec<(String, Vec<String>)>) -> Self {
Self {
request,
queue,
choosing: false,
}
}
fn answer(&mut self, choice: MultiMaterial) -> CmdResult {
if self.queue.is_empty() {
return CmdResult::Cancel;
}
let (name, _) = self.queue.remove(0);
self.request.multi.insert(name, choice);
self.choosing = false;
if self.queue.is_empty() {
CmdResult::TdsImport(self.request.clone())
} else {
CmdResult::NeedPoint
}
}
}
impl CadCommand for TdsMaterialCommand {
fn name(&self) -> &'static str {
"IMPORT"
}
fn prompt(&self) -> String {
match (self.choosing, self.queue.first()) {
(true, _) => "Select a material or [None] <None>:".to_string(),
(false, Some((name, _))) => {
format!("{name} has multiple materials assigned. [Split/First/Material] <Split>:")
}
(false, None) => String::new(),
}
}
fn options(&self) -> Vec<CmdOption> {
if self.choosing {
let mut options = vec![CmdOption::new("None", "N")];
if let Some((_, materials)) = self.queue.first() {
options.extend(materials.iter().map(|m| CmdOption::new(m, m)));
}
options
} else {
vec![
CmdOption::new("Split", "S"),
CmdOption::new("First", "F"),
CmdOption::new("Material", "M"),
]
}
}
fn input_kind(&self) -> InputKind {
if self.choosing {
InputKind::FreeText
} else {
InputKind::SingleToken
}
}
fn on_point(&mut self, _pt: glam::DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let text = text.trim();
if self.choosing {
if text.is_empty() || text.eq_ignore_ascii_case("None") || text.eq_ignore_ascii_case("N") {
return Some(self.answer(MultiMaterial::Material(None)));
}
let found = self
.queue
.first()
.and_then(|(_, materials)| materials.iter().find(|m| m.eq_ignore_ascii_case(text)))
.cloned();
return Some(match found {
Some(material) => self.answer(MultiMaterial::Material(Some(material))),
None => CmdResult::ReportError("Invalid option keyword.".to_string()),
});
}
let t = text.to_ascii_uppercase();
Some(match t.as_str() {
"" => self.answer(MultiMaterial::Split),
_ if "SPLIT".starts_with(&t) => self.answer(MultiMaterial::Split),
_ if "FIRST".starts_with(&t) => self.answer(MultiMaterial::First),
_ if "MATERIAL".starts_with(&t) => {
self.choosing = true;
CmdResult::NeedPoint
}
_ => CmdResult::ReportError("Invalid option keyword.".to_string()),
})
}
fn on_enter(&mut self) -> CmdResult {
self.on_text_input("").unwrap_or(CmdResult::NeedPoint)
}
}
+241 -4
View File
@@ -1,6 +1,8 @@
//! 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).
//! format's importer), DGN Import (DGNIMPORT: model, target drawing,
//! conversion units and the mapping setup's effect on the file) and 3D Studio
//! Import (the objects of a .3ds file to bring in, their layers and what a
//! mesh with several materials becomes).
use iced::widget::{button, column, container, image, row, scrollable, text, text_input, Space};
use iced::{Background, Border, Element, Fill, Length, Theme};
@@ -34,6 +36,17 @@ pub enum ImportDialogMsg {
DgnExplode(bool),
DgnMappingTab(usize),
DgnOk,
// 3D Studio Import
TdsChange,
TdsPicked(Option<String>),
TdsCheck(usize, bool),
TdsAll,
TdsClear,
TdsFilter(usize),
TdsSearch(String),
TdsLayers(usize),
TdsMulti(usize),
TdsOk,
Help(&'static str),
}
@@ -84,7 +97,7 @@ pub const FORMATS: [ImportFormat; 21] = [
format("MicroStation DGN", "*.dgn", &["dgn"], 0, Support::Yes),
format("All DGN Files", "*.*", &["*"], 0, Support::Yes),
format("Metafile", "*.wmf", &["wmf"], 0, Support::Yes),
format("3D Studio", "*.3ds", &["3ds"], 1, Support::No),
format("3D Studio", "*.3ds", &["3ds"], 1, Support::Yes),
format("ACIS", "*.sat", &["sat"], 1, Support::Yes),
format(
"CATIA V4",
@@ -146,11 +159,29 @@ pub struct DgnImportState {
pub mapping: [Vec<(String, String)>; 4],
}
/// Both windows' working copies and the files imported this session.
/// The objects of a 3D Studio file and the import's choices.
pub struct TdsImportState {
pub path: String,
/// "Global Settings" first, then the file's objects by name: each name,
/// its kind (Mesh, Light, Camera or Global) and the materials it uses.
pub objects: Vec<(String, &'static str, usize)>,
pub materials: usize,
pub checked: Vec<bool>,
/// 0 all, 1 meshes, 2 lights, 3 cameras, 4 global.
pub filter: usize,
pub search: String,
/// 0 by object, 1 by material, 2 by object colour, 3 single layer.
pub layers: usize,
/// 0 always ask, 1 split by material, 2 first material, 3 no material.
pub multi: usize,
}
/// The windows' working copies and the files imported this session.
#[derive(Default)]
pub struct ImportDialogs {
pub file: Option<ImportFileState>,
pub dgn: Option<DgnImportState>,
pub tds: Option<TdsImportState>,
pub recent: Vec<String>,
}
@@ -414,6 +445,212 @@ pub fn view_import_file<'a>(
.into()
}
/// One choice out of a few, as buttons stacked one above the other.
fn stacked<'a>(options: Vec<(String, bool, Message)>) -> Element<'a, Message> {
let buttons = options.into_iter().map(|(label, on, message)| {
button(text(label).size(11).width(Fill))
.on_press(message)
.style(button_style(on))
.padding([5, 10])
.width(Fill)
.into()
});
container(iced::widget::Column::with_children(buttons.collect::<Vec<_>>()).spacing(2))
.padding(2)
.width(Fill)
.style(well_style)
.into()
}
/// The kind of a 3D Studio object in the window's words.
fn tds_kind(kind: &str) -> String {
match kind {
"Mesh" => crate::tr!("import-3ds", "mesh"),
"Light" => crate::tr!("import-3ds", "light"),
"Camera" => crate::tr!("import-3ds", "camera"),
_ => crate::tr!("import-3ds", "general"),
}
}
pub fn view_tds_import<'a>(
state: &'a TdsImportState,
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(crate::tr!("import-3ds", "summary", objects = (state.objects.len().saturating_sub(1) as i64), materials = (state.materials as i64)))
.size(11)
.style(muted_style)
.width(Fill),
button(text(crate::tr!("import-3ds", "change")).size(11))
.on_press(msg(ImportDialogMsg::TdsChange))
.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 kinds = ["", "Mesh", "Light", "Camera", "Global"];
let mut filters = row![].spacing(4).align_y(iced::Center);
for (k, label) in [crate::tr!("import-3ds", "all"), crate::tr!("import-3ds", "mesh"), crate::tr!("import-3ds", "light"), crate::tr!("import-3ds", "camera"), crate::tr!("import-3ds", "general")]
.into_iter()
.enumerate()
{
filters = filters.push(chip(label, state.filter == k, msg(ImportDialogMsg::TdsFilter(k))));
}
let search = state.search.trim().to_lowercase();
let mut rows = column![row![
Space::new().width(Length::Fixed(22.0)),
text(crate::tr!("import-3ds", "name")).size(11).style(muted_style).width(Fill),
text(crate::tr!("import-3ds", "type")).size(11).style(muted_style).width(Length::Fixed(150.0)),
]]
.spacing(2);
for (k, (name, kind, materials)) in state.objects.iter().enumerate() {
let global = *kind == "Global";
if state.filter != 0 && kinds[state.filter] != *kind {
continue;
}
if !search.is_empty() && !name.to_lowercase().contains(&search) {
continue;
}
let shown_name = if global { crate::tr!("import-3ds", "global") } else { name.clone() };
let shown_kind = if global {
String::new()
} else if *kind == "Mesh" {
format!("{} · {}", tds_kind(kind), crate::tr!("import-3ds", "materials", n = (*materials as i64)))
} else {
tds_kind(kind)
};
let on = state.checked.get(k).copied().unwrap_or(false);
rows = rows.push(
row![
iced::widget::checkbox(on)
.on_toggle(move |v| msg(ImportDialogMsg::TdsCheck(k, v)))
.size(14),
text(shown_name).size(12).width(Fill),
text(shown_kind).size(11).style(muted_style).width(Length::Fixed(150.0)),
]
.spacing(8)
.align_y(iced::Center),
);
}
let objects = card(
crate::tr!("import-3ds", "objects"),
column![
row![
filters,
Space::new().width(Fill),
text_input(crate::tr!("import-3ds", "search").as_ref(), &state.search)
.on_input(|s| msg(ImportDialogMsg::TdsSearch(s)))
.size(11)
.padding([4, 8])
.width(Length::Fixed(170.0))
.style(field_style),
]
.spacing(8)
.align_y(iced::Center),
container(scrollable(rows).height(Length::Fixed(300.0)))
.padding(6)
.width(Fill)
.style(well_style),
row![
button(text(crate::tr!("import-3ds", "select-all")).size(11))
.on_press(msg(ImportDialogMsg::TdsAll))
.style(button_style(false))
.padding([4, 10]),
button(text(crate::tr!("import-3ds", "clear")).size(11))
.on_press(msg(ImportDialogMsg::TdsClear))
.style(button_style(false))
.padding([4, 10]),
]
.spacing(6),
]
.spacing(8),
);
let layer_notes = [
crate::tr!("import-3ds", "note-object"),
crate::tr!("import-3ds", "note-material"),
crate::tr!("import-3ds", "note-color"),
crate::tr!("import-3ds", "note-single"),
];
let layers = card(
crate::tr!("import-3ds", "layers"),
column![
stacked(
[crate::tr!("import-3ds", "by-object"), crate::tr!("import-3ds", "by-material"), crate::tr!("import-3ds", "by-color"), crate::tr!("import-3ds", "single")]
.into_iter()
.enumerate()
.map(|(k, label)| (label, state.layers == k, msg(ImportDialogMsg::TdsLayers(k))))
.collect()
),
text(layer_notes[state.layers.min(3)].clone()).size(11).style(muted_style),
]
.spacing(8),
);
let multi = card(
crate::tr!("import-3ds", "multi"),
stacked(
[
crate::tr!("import-3ds", "ask"),
crate::tr!("import-3ds", "split"),
crate::tr!("import-3ds", "first"),
crate::tr!("import-3ds", "none"),
]
.into_iter()
.enumerate()
.map(|(k, label)| (label, state.multi == k, msg(ImportDialogMsg::TdsMulti(k))))
.collect(),
),
);
let any = state.checked.iter().any(|c| *c);
let mut actions = row![
button(text("?").size(12))
.on_press(msg(ImportDialogMsg::Help("3ds")))
.style(button_style(false))
.padding([5, 11]),
Space::new().width(Fill),
]
.spacing(6)
.align_y(iced::Center);
if !any {
actions = actions.push(text(crate::tr!("import-3ds", "reason")).size(11).style(muted_style));
}
actions = actions
.push(dialog_button(crate::tr!("import-3ds", "cancel"), Message::CloseModal, false))
.push(dialog_button_styled_opt(
crate::tr!("import-3ds", "import"),
any.then(|| msg(ImportDialogMsg::TdsOk)),
button::primary,
));
column![
top,
row![
container(objects).width(Length::FillPortion(3)),
column![layers, multi].spacing(10).width(Length::FillPortion(2)),
]
.spacing(10),
actions,
]
.spacing(10)
.padding([10, 12])
.width(sizing.width)
.into()
}
pub fn view_dgn_import<'a>(
state: &'a DgnImportState,
sizing: crate::ui::modal::ModalSizing,