diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 6b4c40be8..3294a3489 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -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 diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index e1ad8f608..fc1bd29f7 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -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 текст diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index ff53f069c..7a772cbe5 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -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 diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 16b715346..a02d69b58 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -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 diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 5d5f8c629..d628d3b69 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -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 diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 17f8de919..b58b35734 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -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 diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index e69ec637c..1dbe7a9d2 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -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 diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 81fbf0d88..f9c4ac4ba 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -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 diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 94f1e1edf..af28e9df6 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -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 diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index e2a8dc08a..2e8616557 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -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 टेक्स्ट पहचानें diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 338e7e40b..54b3e44a7 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -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 diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index a155c96da..7d8f063bd 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -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 diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 3f4523e95..169061f1e 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -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 文字を認識 diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index e9d631f68..880d58f4d 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -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 문자 인식 diff --git a/locales/lo-LA/opencadstudio.ftl b/locales/lo-LA/opencadstudio.ftl index 92077d5ca..884e0e6d0 100644 --- a/locales/lo-LA/opencadstudio.ftl +++ b/locales/lo-LA/opencadstudio.ftl @@ -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 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 1e2a722b0..107f0ad66 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -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 diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 31eb9edcb..d0826c8d3 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -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 diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 437330dcc..ac6f48463 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -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 diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 9af234590..ba7834740 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -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 diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 8b391abf8..d38b23fcb 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -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ı diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 83c3d74e8..2461ce671 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -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 文字 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index e472e57cf..f38b013f7 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -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 文字 diff --git a/src/app/command_driver/mod.rs b/src/app/command_driver/mod.rs index a920cbee3..7d5133b36 100644 --- a/src/app/command_driver/mod.rs +++ b/src/app/command_driver/mod.rs @@ -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; diff --git a/src/app/commands/dgn_import.rs b/src/app/commands/dgn_import.rs index 1a220cd90..607b6899e 100644 --- a/src/app/commands/dgn_import.rs +++ b/src/app/commands/dgn_import.rs @@ -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) -> Result { 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)) => { diff --git a/src/app/commands/model_import.rs b/src/app/commands/model_import.rs index f5a23b5c9..d66565b7c 100644 --- a/src/app/commands/model_import.rs +++ b/src/app/commands/model_import.rs @@ -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 { + 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 = state + .objects + .iter() + .zip(&state.checked) + .filter(|(_, on)| **on) + .map(|((name, _, _), _)| name.clone()) + .collect(); + let several: Vec<(String, Vec)> = 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 = 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(); + } +} diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index 6b4f0b49c..45af4f5b4 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -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=" + // and "uncheck=" (the object's name, or "Global Settings"), + // "all", "clear", "filter=all|mesh|light|camera|general", + // "search=", "layers=object|material|color|single", + // "multi=ask|split|first|none", "path=" (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=" (as // Change… picks it), "model=", "target=new|current", // "conflict=prefix|ignore", "units=master|sub", "explode=on|off", diff --git a/src/app/mod.rs b/src/app/mod.rs index c58598e43..e23efa6d4 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -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. diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 164e8ad17..d77fb772a 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -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) => { diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 6bb78ae38..6d1d2f362 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -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| { diff --git a/src/command.rs b/src/command.rs index 602870f9b..78778f14f 100644 --- a/src/command.rs +++ b/src/command.rs @@ -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 diff --git a/src/io/mod.rs b/src/io/mod.rs index 64bfebb52..660ece8fb 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -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; diff --git a/src/io/tds_import.rs b/src/io/tds_import.rs new file mode 100644 index 000000000..46f4c2e27 --- /dev/null +++ b/src/io/tds_import.rs @@ -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)>, + /// The object colour, a palette index, when the file gives one. + pub color: Option, +} + +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, + 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 { + let mut used: Vec = 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 { + Some(u16::from_le_bytes([*self.b.get(at)?, *self.b.get(at + 1)?])) + } + fn u32(&self, at: usize) -> Option { + Some(u32::from_le_bytes(self.b.get(at..at + 4)?.try_into().ok()?)) + } + fn f32(&self, at: usize) -> Option { + 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 { + 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), +} + +pub struct TdsOptions { + /// The objects to import, by name ("Global Settings" included). + pub selected: Vec, + pub layers: LayerMode, + /// The choice for each mesh with several materials, by name. + pub multi: HashMap, + /// 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, + 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, + /// The materials the parts use, in the order they were first used. + pub materials: Vec, + pub views: Vec, + pub messages: Vec, +} + +/// 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 = 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 = (0..mesh.faces.len()).collect(); + let pieces: Vec<(Option, Vec)> = 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::().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 +} + diff --git a/src/modules/insert/mod.rs b/src/modules/insert/mod.rs index 11385add7..8946a6a18 100644 --- a/src/modules/insert/mod.rs +++ b/src/modules/insert/mod.rs @@ -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; diff --git a/src/modules/insert/tds_import.rs b/src/modules/insert/tds_import.rs new file mode 100644 index 000000000..a98e12880 --- /dev/null +++ b/src/modules/insert/tds_import.rs @@ -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. +// +// has multiple materials assigned. [Split/First/Material] : +// Select a material or [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, + pub layers: LayerMode, + /// The choice for each mesh with several materials, by name. + pub multi: HashMap, + /// 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)>, + choosing: bool, +} + +impl TdsMaterialCommand { + pub fn new(request: TdsImportRequest, queue: Vec<(String, Vec)>) -> 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] :".to_string(), + (false, Some((name, _))) => { + format!("{name} has multiple materials assigned. [Split/First/Material] :") + } + (false, None) => String::new(), + } + } + + fn options(&self) -> Vec { + 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 { + 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) + } +} diff --git a/src/ui/window/import_dialogs.rs b/src/ui/window/import_dialogs.rs index d553411e1..f98136c9c 100644 --- a/src/ui/window/import_dialogs.rs +++ b/src/ui/window/import_dialogs.rs @@ -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), + 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, + /// 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, pub dgn: Option, + pub tds: Option, pub recent: Vec, } @@ -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::>()).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,