From 02de08e04b38f1e8607038dc5fa464f99bfe7a78 Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 19 Feb 2024 23:38:17 +0000 Subject: [PATCH 01/23] add outline test for 3mf export --- test/exporters/test_3mf_exporter.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 test/exporters/test_3mf_exporter.rb diff --git a/test/exporters/test_3mf_exporter.rb b/test/exporters/test_3mf_exporter.rb new file mode 100644 index 0000000..f2b4753 --- /dev/null +++ b/test/exporters/test_3mf_exporter.rb @@ -0,0 +1,20 @@ +require 'minitest_helper' + +class Test3MFExporter < Minitest::Test + def test_3mf_container + # Create a test object + box = Mittsu::Mesh.new( + Mittsu::BoxGeometry.new(1.0, 1.0, 1.0) + ) + obj = Mittsu::Object3D.new + obj.add(box) + group = Mittsu::Group.new + group.add obj + + # Export to 3MF + exporter = Mittsu::ThreeMFExporter.new + file = exporter.parse(group) + + assert_equal file, 42 + end +end From fba5abc8a7e0d5dc47bf80d0fbbe5a5ed127aa61 Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 19 Feb 2024 23:42:08 +0000 Subject: [PATCH 02/23] add 3mf exporter class skeleton --- lib/mittsu.rb | 1 + lib/mittsu/exporters.rb | 1 + lib/mittsu/exporters/3mf_exporter.rb | 10 ++++++++++ 3 files changed, 12 insertions(+) create mode 100644 lib/mittsu/exporters.rb create mode 100644 lib/mittsu/exporters/3mf_exporter.rb diff --git a/lib/mittsu.rb b/lib/mittsu.rb index 71022d0..2c54979 100644 --- a/lib/mittsu.rb +++ b/lib/mittsu.rb @@ -12,3 +12,4 @@ require "mittsu/scenes" require "mittsu/textures" require "mittsu/constants" +require "mittsu/exporters" diff --git a/lib/mittsu/exporters.rb b/lib/mittsu/exporters.rb new file mode 100644 index 0000000..e0e834d --- /dev/null +++ b/lib/mittsu/exporters.rb @@ -0,0 +1 @@ +require 'mittsu/exporters/3mf_exporter' diff --git a/lib/mittsu/exporters/3mf_exporter.rb b/lib/mittsu/exporters/3mf_exporter.rb new file mode 100644 index 0000000..06ce643 --- /dev/null +++ b/lib/mittsu/exporters/3mf_exporter.rb @@ -0,0 +1,10 @@ +module Mittsu + class ThreeMFExporter + def initialize(options = {}) + end + + def parse(scene) + "ceci n'est pas une 3mf" + end + end +end From 560a171b4679c02eef97eb57eb4d5c4795035270 Mon Sep 17 00:00:00 2001 From: James Smith Date: Wed, 21 Feb 2024 12:35:14 +0000 Subject: [PATCH 03/23] rename parse to export, and add alias for compatibility --- lib/mittsu/exporters/3mf_exporter.rb | 6 +++++- test/exporters/test_3mf_exporter.rb | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/mittsu/exporters/3mf_exporter.rb b/lib/mittsu/exporters/3mf_exporter.rb index 06ce643..c3d4069 100644 --- a/lib/mittsu/exporters/3mf_exporter.rb +++ b/lib/mittsu/exporters/3mf_exporter.rb @@ -3,8 +3,12 @@ class ThreeMFExporter def initialize(options = {}) end - def parse(scene) + def export(object) "ceci n'est pas une 3mf" end + + # Parse is here for consistency with THREE.js's weird naming of exporter methods + alias_method :parse, :export + end end diff --git a/test/exporters/test_3mf_exporter.rb b/test/exporters/test_3mf_exporter.rb index f2b4753..574c226 100644 --- a/test/exporters/test_3mf_exporter.rb +++ b/test/exporters/test_3mf_exporter.rb @@ -13,8 +13,14 @@ def test_3mf_container # Export to 3MF exporter = Mittsu::ThreeMFExporter.new - file = exporter.parse(group) + file = exporter.export(group) assert_equal file, 42 end + + def test_export_method_alias + exporter = Mittsu::ThreeMFExporter.new + result = exporter.parse(Mittsu::Group.new()) + assert_kind_of(String, result) + end end From 73f003a933a511e02ef14e90b910efdc65e4e512 Mon Sep 17 00:00:00 2001 From: James Smith Date: Wed, 21 Feb 2024 12:36:47 +0000 Subject: [PATCH 04/23] add #export_uncompressed for easier testing of 3MF content --- lib/mittsu/exporters/3mf_exporter.rb | 7 ++++++- test/exporters/test_3mf_exporter.rb | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/mittsu/exporters/3mf_exporter.rb b/lib/mittsu/exporters/3mf_exporter.rb index c3d4069..4998b0b 100644 --- a/lib/mittsu/exporters/3mf_exporter.rb +++ b/lib/mittsu/exporters/3mf_exporter.rb @@ -4,11 +4,16 @@ def initialize(options = {}) end def export(object) - "ceci n'est pas une 3mf" + export_uncompressed(object) end # Parse is here for consistency with THREE.js's weird naming of exporter methods alias_method :parse, :export + private + + def export_uncompressed(object) + "ceci n'est pas une 3mf" + end end end diff --git a/test/exporters/test_3mf_exporter.rb b/test/exporters/test_3mf_exporter.rb index 574c226..ed689a5 100644 --- a/test/exporters/test_3mf_exporter.rb +++ b/test/exporters/test_3mf_exporter.rb @@ -1,7 +1,7 @@ require 'minitest_helper' class Test3MFExporter < Minitest::Test - def test_3mf_container + def test_3mf_content # Create a test object box = Mittsu::Mesh.new( Mittsu::BoxGeometry.new(1.0, 1.0, 1.0) @@ -13,7 +13,7 @@ def test_3mf_container # Export to 3MF exporter = Mittsu::ThreeMFExporter.new - file = exporter.export(group) + file = exporter.send(:export_uncompressed, group) assert_equal file, 42 end From 4057766a87c8d182ea7ed527fd1f5d4cd0516fb4 Mon Sep 17 00:00:00 2001 From: James Smith Date: Wed, 21 Feb 2024 12:37:20 +0000 Subject: [PATCH 05/23] remove extraneous Object3D and Group in export test --- test/exporters/test_3mf_exporter.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/exporters/test_3mf_exporter.rb b/test/exporters/test_3mf_exporter.rb index ed689a5..c830e06 100644 --- a/test/exporters/test_3mf_exporter.rb +++ b/test/exporters/test_3mf_exporter.rb @@ -6,14 +6,10 @@ def test_3mf_content box = Mittsu::Mesh.new( Mittsu::BoxGeometry.new(1.0, 1.0, 1.0) ) - obj = Mittsu::Object3D.new - obj.add(box) - group = Mittsu::Group.new - group.add obj # Export to 3MF exporter = Mittsu::ThreeMFExporter.new - file = exporter.send(:export_uncompressed, group) + file = exporter.send(:export_uncompressed, box) assert_equal file, 42 end From d8e3a899cb6801ad7db21230ec0b7ab671b0433f Mon Sep 17 00:00:00 2001 From: James Smith Date: Wed, 21 Feb 2024 13:19:58 +0000 Subject: [PATCH 06/23] 3MF: skeleton model file export --- lib/mittsu/exporters/3mf_exporter.rb | 15 ++++++++++++--- test/exporters/test_3mf_exporter.rb | 25 ++++++++++++++----------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/lib/mittsu/exporters/3mf_exporter.rb b/lib/mittsu/exporters/3mf_exporter.rb index 4998b0b..0617249 100644 --- a/lib/mittsu/exporters/3mf_exporter.rb +++ b/lib/mittsu/exporters/3mf_exporter.rb @@ -4,7 +4,10 @@ def initialize(options = {}) end def export(object) - export_uncompressed(object) + Dir.mktmpdir do |dir| + export_uncompressed(dir, object) + # TODO compress into single file + end end # Parse is here for consistency with THREE.js's weird naming of exporter methods @@ -12,8 +15,14 @@ def export(object) private - def export_uncompressed(object) - "ceci n'est pas une 3mf" + def export_uncompressed(dir, object) + FileUtils.mkdir_p File.join(dir, "3D") + export_3d_model_part(dir, object) + end + + def export_3d_model_part(dir, object) + filename = File.join(dir, "3D/#{object.name || SecureRandom.uuid}.model") + File.write(filename, "ceci n'est pas une 3mf") end end end diff --git a/test/exporters/test_3mf_exporter.rb b/test/exporters/test_3mf_exporter.rb index c830e06..333a5dc 100644 --- a/test/exporters/test_3mf_exporter.rb +++ b/test/exporters/test_3mf_exporter.rb @@ -1,22 +1,25 @@ require 'minitest_helper' class Test3MFExporter < Minitest::Test - def test_3mf_content - # Create a test object - box = Mittsu::Mesh.new( + def setup + @tmpdir = Dir.mktmpdir + @box = Mittsu::Mesh.new( Mittsu::BoxGeometry.new(1.0, 1.0, 1.0) ) + @box.name = "box" + @exporter = Mittsu::ThreeMFExporter.new + end - # Export to 3MF - exporter = Mittsu::ThreeMFExporter.new - file = exporter.send(:export_uncompressed, box) - - assert_equal file, 42 + def test_3mf_exports_3d_model_part + @exporter.send(:export_uncompressed, @tmpdir, @box) + assert File.exist?(File.join(@tmpdir, "3D/box.model")) end def test_export_method_alias - exporter = Mittsu::ThreeMFExporter.new - result = exporter.parse(Mittsu::Group.new()) - assert_kind_of(String, result) + assert @exporter.parse(Mittsu::Group.new()) + end + + def teardown + FileUtils.remove_entry @tmpdir end end From c03a1ebfd02581d073d32785df0fa887a49abc5d Mon Sep 17 00:00:00 2001 From: James Smith Date: Wed, 21 Feb 2024 14:41:16 +0000 Subject: [PATCH 07/23] add builder as direct dependency for writing XML files it's already an indirect dependency because of minitest, so this seemed the neatest thing to use --- mittsu.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/mittsu.gemspec b/mittsu.gemspec index ea66e7b..37a11cf 100644 --- a/mittsu.gemspec +++ b/mittsu.gemspec @@ -26,6 +26,7 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'opengl-bindings' spec.add_runtime_dependency 'ffi' spec.add_runtime_dependency 'chunky_png' + spec.add_runtime_dependency 'builder' spec.add_development_dependency "bundler" spec.add_development_dependency "rake" From 16b4f673aeb93e29c05948d13f0514556584d3ef Mon Sep 17 00:00:00 2001 From: James Smith Date: Wed, 21 Feb 2024 14:41:41 +0000 Subject: [PATCH 08/23] test building an outline XML model for 3mf --- lib/mittsu/exporters/3mf_exporter.rb | 10 +++++++++- test/exporters/test_3mf_exporter.rb | 6 +++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/mittsu/exporters/3mf_exporter.rb b/lib/mittsu/exporters/3mf_exporter.rb index 0617249..74615b1 100644 --- a/lib/mittsu/exporters/3mf_exporter.rb +++ b/lib/mittsu/exporters/3mf_exporter.rb @@ -1,3 +1,5 @@ +require 'builder' + module Mittsu class ThreeMFExporter def initialize(options = {}) @@ -22,7 +24,13 @@ def export_uncompressed(dir, object) def export_3d_model_part(dir, object) filename = File.join(dir, "3D/#{object.name || SecureRandom.uuid}.model") - File.write(filename, "ceci n'est pas une 3mf") + File.open(filename, "wb") do |file| + xml = Builder::XmlMarkup.new(target: file, indent: 2) + xml.instruct! :xml, encoding: "UTF-8" + xml.model unit: "millimeter", "xml:lang": "en-US", xmlns:"http://schemas.microsoft.com/3dmanufacturing/core/2015/02" do + + end + end end end end diff --git a/test/exporters/test_3mf_exporter.rb b/test/exporters/test_3mf_exporter.rb index 333a5dc..964a687 100644 --- a/test/exporters/test_3mf_exporter.rb +++ b/test/exporters/test_3mf_exporter.rb @@ -1,4 +1,6 @@ require 'minitest_helper' +require 'rexml/document' +require 'rexml/xpath' class Test3MFExporter < Minitest::Test def setup @@ -12,9 +14,11 @@ def setup def test_3mf_exports_3d_model_part @exporter.send(:export_uncompressed, @tmpdir, @box) - assert File.exist?(File.join(@tmpdir, "3D/box.model")) + REXML::Document.new File.read(File.join(@tmpdir, "3D/box.model")) + assert_equal "millimeter", REXML::XPath.first(xml, "/model/@unit").value end + def test_export_method_alias assert @exporter.parse(Mittsu::Group.new()) end From abcd9891574963a1cc4600e8d8e749e592881c93 Mon Sep 17 00:00:00 2001 From: James Smith Date: Thu, 22 Feb 2024 15:15:11 +0000 Subject: [PATCH 09/23] create 3MF OPC package metadata files --- lib/mittsu/exporters/3mf_exporter.rb | 38 +++++++++++++++++++++++++--- test/exporters/test_3mf_exporter.rb | 12 +++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/lib/mittsu/exporters/3mf_exporter.rb b/lib/mittsu/exporters/3mf_exporter.rb index 74615b1..d5dad4b 100644 --- a/lib/mittsu/exporters/3mf_exporter.rb +++ b/lib/mittsu/exporters/3mf_exporter.rb @@ -18,19 +18,49 @@ def export(object) private def export_uncompressed(dir, object) - FileUtils.mkdir_p File.join(dir, "3D") - export_3d_model_part(dir, object) + export_content_types(dir) + filename = export_3d_model_part(dir, object) + export_rels(dir, [filename]) end - def export_3d_model_part(dir, object) - filename = File.join(dir, "3D/#{object.name || SecureRandom.uuid}.model") + def export_content_types(dir) + filename = File.join(dir, "[Content_Types].xml") + File.open(filename, "wb") do |file| + xml = Builder::XmlMarkup.new(target: file, indent: 2) + xml.instruct! :xml, encoding: "UTF-8" + xml.Types xmlns: "http://schemas.openxmlformats.org/package/2006/content-types" do + xml.Default Extension: "rels", ContentType: "application/vnd.openxmlformats-package.relationships+xml" + xml.Default Extension: "model", ContentType: "application/vnd.ms-package.3dmanufacturing-3dmodel+xml" + end + end + end + + def export_rels(dir, models) + FileUtils.mkdir_p File.join(dir, "_rels") + filename = File.join(dir, "_rels/.rels") File.open(filename, "wb") do |file| + xml = Builder::XmlMarkup.new(target: file, indent: 2) + xml.instruct! :xml, encoding: "UTF-8" + xml.Relationships xmlns: "http://schemas.openxmlformats.org/package/2006/relationships" do + models.each do |name| + xml.Relationship Target: "/3D/#{name}.model", Id: name, ContentType: "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel" + end + end + end + end + + def export_3d_model_part(dir, object) + FileUtils.mkdir_p File.join(dir, "3D") + filename = object.name.parameterize || SecureRandom.uuid + pathname = File.join(dir, "3D/#{filename}.model") + File.open(pathname, "wb") do |file| xml = Builder::XmlMarkup.new(target: file, indent: 2) xml.instruct! :xml, encoding: "UTF-8" xml.model unit: "millimeter", "xml:lang": "en-US", xmlns:"http://schemas.microsoft.com/3dmanufacturing/core/2015/02" do end end + filename end end end diff --git a/test/exporters/test_3mf_exporter.rb b/test/exporters/test_3mf_exporter.rb index 964a687..ef164a7 100644 --- a/test/exporters/test_3mf_exporter.rb +++ b/test/exporters/test_3mf_exporter.rb @@ -16,8 +16,20 @@ def test_3mf_exports_3d_model_part @exporter.send(:export_uncompressed, @tmpdir, @box) REXML::Document.new File.read(File.join(@tmpdir, "3D/box.model")) assert_equal "millimeter", REXML::XPath.first(xml, "/model/@unit").value + + def test_3mf_exports_content_types + @exporter.send(:export_uncompressed, @tmpdir, @box) + xml = REXML::Document.new File.read(File.join(@tmpdir, "[Content_Types].xml")) + assert_equal "application/vnd.openxmlformats-package.relationships+xml", + REXML::XPath.first(xml, "/Types/Default[@Extension='rels']/@ContentType").value end + def test_3mf_exports_rels + @exporter.send(:export_uncompressed, @tmpdir, @box) + xml = REXML::Document.new File.read(File.join(@tmpdir, "_rels/.rels")) + assert_equal "/3D/box.model", + REXML::XPath.first(xml, "/Relationships/Relationship/@Target").value + end def test_export_method_alias assert @exporter.parse(Mittsu::Group.new()) From e44571f53c8edd5ab99348ed909983b9fb3d2f20 Mon Sep 17 00:00:00 2001 From: James Smith Date: Thu, 22 Feb 2024 15:15:44 +0000 Subject: [PATCH 10/23] store actual mesh data in 3MF --- lib/mittsu/exporters/3mf_exporter.rb | 25 ++++++++++++++++++++++++- test/exporters/test_3mf_exporter.rb | 6 ++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/mittsu/exporters/3mf_exporter.rb b/lib/mittsu/exporters/3mf_exporter.rb index d5dad4b..6fc05a5 100644 --- a/lib/mittsu/exporters/3mf_exporter.rb +++ b/lib/mittsu/exporters/3mf_exporter.rb @@ -57,7 +57,30 @@ def export_3d_model_part(dir, object) xml = Builder::XmlMarkup.new(target: file, indent: 2) xml.instruct! :xml, encoding: "UTF-8" xml.model unit: "millimeter", "xml:lang": "en-US", xmlns:"http://schemas.microsoft.com/3dmanufacturing/core/2015/02" do - + objectIDs = [] + xml.resources do + uuid = SecureRandom.uuid + objectIDs << uuid + xml.object id: uuid, type: "model" do + xml.mesh do + xml.vertices do + object.geometry.vertices.each do |vertex| + xml.vertex x: vertex.x, y: vertex.y, z: vertex.z + end + end + xml.triangles do + object.geometry.faces.each do |face| + xml.triangle v1: face.a, v2: face.b, v3: face.c + end + end + end + end + end + xml.build do + objectIDs.each do |id| + xml.item objectid: id + end + end end end filename diff --git a/test/exporters/test_3mf_exporter.rb b/test/exporters/test_3mf_exporter.rb index ef164a7..345b0da 100644 --- a/test/exporters/test_3mf_exporter.rb +++ b/test/exporters/test_3mf_exporter.rb @@ -14,8 +14,10 @@ def setup def test_3mf_exports_3d_model_part @exporter.send(:export_uncompressed, @tmpdir, @box) - REXML::Document.new File.read(File.join(@tmpdir, "3D/box.model")) + xml = REXML::Document.new File.read(File.join(@tmpdir, "3D/box.model")) assert_equal "millimeter", REXML::XPath.first(xml, "/model/@unit").value + assert_equal "model", REXML::XPath.first(xml, "/model/resources/object/@type").value + end def test_3mf_exports_content_types @exporter.send(:export_uncompressed, @tmpdir, @box) @@ -32,7 +34,7 @@ def test_3mf_exports_rels end def test_export_method_alias - assert @exporter.parse(Mittsu::Group.new()) + assert @exporter.parse(@box) end def teardown From e876535b29bc72cfb87037fc2740218f247dd96a Mon Sep 17 00:00:00 2001 From: James Smith Date: Fri, 23 Feb 2024 10:34:47 +0000 Subject: [PATCH 11/23] add zip compression for 3mf output --- lib/mittsu/exporters/3mf_exporter.rb | 9 +++++++-- mittsu.gemspec | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/mittsu/exporters/3mf_exporter.rb b/lib/mittsu/exporters/3mf_exporter.rb index 6fc05a5..ad9d2ee 100644 --- a/lib/mittsu/exporters/3mf_exporter.rb +++ b/lib/mittsu/exporters/3mf_exporter.rb @@ -1,4 +1,5 @@ require 'builder' +require 'zip/filesystem' module Mittsu class ThreeMFExporter @@ -7,9 +8,13 @@ def initialize(options = {}) def export(object) Dir.mktmpdir do |dir| - export_uncompressed(dir, object) - # TODO compress into single file + Zip::File.open("output.3mf", Zip::File::CREATE) do |zip| + Dir.glob("**/*", File::FNM_DOTMATCH, base: dir) do |f| + zip.add(f, File.join(dir, f)) + end + end end + true end # Parse is here for consistency with THREE.js's weird naming of exporter methods diff --git a/mittsu.gemspec b/mittsu.gemspec index 37a11cf..645b9df 100644 --- a/mittsu.gemspec +++ b/mittsu.gemspec @@ -27,6 +27,7 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'ffi' spec.add_runtime_dependency 'chunky_png' spec.add_runtime_dependency 'builder' + spec.add_runtime_dependency 'rubyzip' spec.add_development_dependency "bundler" spec.add_development_dependency "rake" From 6fe4fc78c22bac1c974a914495a551043f51a4ed Mon Sep 17 00:00:00 2001 From: James Smith Date: Fri, 23 Feb 2024 14:19:25 +0000 Subject: [PATCH 12/23] test vertex/face output in 3MF model file --- test/exporters/test_3mf_exporter.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/exporters/test_3mf_exporter.rb b/test/exporters/test_3mf_exporter.rb index 345b0da..7f2877d 100644 --- a/test/exporters/test_3mf_exporter.rb +++ b/test/exporters/test_3mf_exporter.rb @@ -17,11 +17,8 @@ def test_3mf_exports_3d_model_part xml = REXML::Document.new File.read(File.join(@tmpdir, "3D/box.model")) assert_equal "millimeter", REXML::XPath.first(xml, "/model/@unit").value assert_equal "model", REXML::XPath.first(xml, "/model/resources/object/@type").value - end - - def test_3mf_exports_content_types - @exporter.send(:export_uncompressed, @tmpdir, @box) - xml = REXML::Document.new File.read(File.join(@tmpdir, "[Content_Types].xml")) + assert_equal 8, REXML::XPath.match(xml, "/model/resources/object/mesh/vertices/vertex").count + assert_equal 12, REXML::XPath.match(xml, "/model/resources/object/mesh/triangles/triangle").count assert_equal "application/vnd.openxmlformats-package.relationships+xml", REXML::XPath.first(xml, "/Types/Default[@Extension='rels']/@ContentType").value end From d81f515706369a9de263b63cf0381bb46f9278c3 Mon Sep 17 00:00:00 2001 From: James Smith Date: Fri, 23 Feb 2024 14:20:31 +0000 Subject: [PATCH 13/23] refactor 3mf creation to require fewer temporary files on disk --- lib/mittsu/exporters/3mf_exporter.rb | 62 +++++++++++++++------------- test/exporters/test_3mf_exporter.rb | 17 +++++--- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/lib/mittsu/exporters/3mf_exporter.rb b/lib/mittsu/exporters/3mf_exporter.rb index ad9d2ee..9a9d1ed 100644 --- a/lib/mittsu/exporters/3mf_exporter.rb +++ b/lib/mittsu/exporters/3mf_exporter.rb @@ -6,13 +6,24 @@ class ThreeMFExporter def initialize(options = {}) end - def export(object) - Dir.mktmpdir do |dir| - Zip::File.open("output.3mf", Zip::File::CREATE) do |zip| - Dir.glob("**/*", File::FNM_DOTMATCH, base: dir) do |f| - zip.add(f, File.join(dir, f)) - end + def export(object, filename) + Zip::OutputStream.open(filename) do |zip| + # OPC content types file + store(zip, "[Content_Types].xml", content_types_file) + # Add models + models = [object] + model_names = models.map do |model| + # Set a model name if there isn't one + model.name ||= SecureRandom.uuid + # Make model name filesystem-safe + name = model.name.parameterize + # Store model + store(zip, "3D/#{name}.model", model_file(model)) + # Remember the name for later + name end + # Add OPC rels file with list of contained models + store(zip, "_rels/.rels", rels_file(model_names)) end true end @@ -22,17 +33,20 @@ def export(object) private - def export_uncompressed(dir, object) - export_content_types(dir) - filename = export_3d_model_part(dir, object) - export_rels(dir, [filename]) + def store(zip, filename, data) + zip.put_next_entry(filename) + zip.write(data) + end + + def build &block + xml = Builder::XmlMarkup.new + xml.instruct! :xml, encoding: "UTF-8" + yield xml + xml.target! end - def export_content_types(dir) - filename = File.join(dir, "[Content_Types].xml") - File.open(filename, "wb") do |file| - xml = Builder::XmlMarkup.new(target: file, indent: 2) - xml.instruct! :xml, encoding: "UTF-8" + def content_types_file + build do |xml| xml.Types xmlns: "http://schemas.openxmlformats.org/package/2006/content-types" do xml.Default Extension: "rels", ContentType: "application/vnd.openxmlformats-package.relationships+xml" xml.Default Extension: "model", ContentType: "application/vnd.ms-package.3dmanufacturing-3dmodel+xml" @@ -40,12 +54,8 @@ def export_content_types(dir) end end - def export_rels(dir, models) - FileUtils.mkdir_p File.join(dir, "_rels") - filename = File.join(dir, "_rels/.rels") - File.open(filename, "wb") do |file| - xml = Builder::XmlMarkup.new(target: file, indent: 2) - xml.instruct! :xml, encoding: "UTF-8" + def rels_file(models) + build do |xml| xml.Relationships xmlns: "http://schemas.openxmlformats.org/package/2006/relationships" do models.each do |name| xml.Relationship Target: "/3D/#{name}.model", Id: name, ContentType: "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel" @@ -54,13 +64,8 @@ def export_rels(dir, models) end end - def export_3d_model_part(dir, object) - FileUtils.mkdir_p File.join(dir, "3D") - filename = object.name.parameterize || SecureRandom.uuid - pathname = File.join(dir, "3D/#{filename}.model") - File.open(pathname, "wb") do |file| - xml = Builder::XmlMarkup.new(target: file, indent: 2) - xml.instruct! :xml, encoding: "UTF-8" + def model_file(object) + build do |xml| xml.model unit: "millimeter", "xml:lang": "en-US", xmlns:"http://schemas.microsoft.com/3dmanufacturing/core/2015/02" do objectIDs = [] xml.resources do @@ -88,7 +93,6 @@ def export_3d_model_part(dir, object) end end end - filename end end end diff --git a/test/exporters/test_3mf_exporter.rb b/test/exporters/test_3mf_exporter.rb index 7f2877d..58e1755 100644 --- a/test/exporters/test_3mf_exporter.rb +++ b/test/exporters/test_3mf_exporter.rb @@ -12,20 +12,25 @@ def setup @exporter = Mittsu::ThreeMFExporter.new end - def test_3mf_exports_3d_model_part - @exporter.send(:export_uncompressed, @tmpdir, @box) - xml = REXML::Document.new File.read(File.join(@tmpdir, "3D/box.model")) + def test_3mf_model_file + file = @exporter.send(:model_file, @box) + xml = REXML::Document.new file assert_equal "millimeter", REXML::XPath.first(xml, "/model/@unit").value assert_equal "model", REXML::XPath.first(xml, "/model/resources/object/@type").value assert_equal 8, REXML::XPath.match(xml, "/model/resources/object/mesh/vertices/vertex").count assert_equal 12, REXML::XPath.match(xml, "/model/resources/object/mesh/triangles/triangle").count + end + + def test_content_types_file + file = @exporter.send(:content_types_file) + xml = REXML::Document.new file assert_equal "application/vnd.openxmlformats-package.relationships+xml", REXML::XPath.first(xml, "/Types/Default[@Extension='rels']/@ContentType").value end - def test_3mf_exports_rels - @exporter.send(:export_uncompressed, @tmpdir, @box) - xml = REXML::Document.new File.read(File.join(@tmpdir, "_rels/.rels")) + def test_rels_file + file = @exporter.send(:rels_file, ["box"]) + xml = REXML::Document.new file assert_equal "/3D/box.model", REXML::XPath.first(xml, "/Relationships/Relationship/@Target").value end From 3ce76049a253074f996eecadb5bb713ee80bbfbd Mon Sep 17 00:00:00 2001 From: James Smith Date: Fri, 23 Feb 2024 14:21:52 +0000 Subject: [PATCH 14/23] remove temp directory from 3mf tests --- test/exporters/test_3mf_exporter.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/exporters/test_3mf_exporter.rb b/test/exporters/test_3mf_exporter.rb index 58e1755..93e0a39 100644 --- a/test/exporters/test_3mf_exporter.rb +++ b/test/exporters/test_3mf_exporter.rb @@ -4,7 +4,6 @@ class Test3MFExporter < Minitest::Test def setup - @tmpdir = Dir.mktmpdir @box = Mittsu::Mesh.new( Mittsu::BoxGeometry.new(1.0, 1.0, 1.0) ) @@ -36,10 +35,11 @@ def test_rels_file end def test_export_method_alias - assert @exporter.parse(@box) + Dir.mktmpdir do |dir| + filename = File.join(dir, "test.3mf") + @exporter.parse(@box, filename) + assert File.exist?(filename) + end end - def teardown - FileUtils.remove_entry @tmpdir - end end From 05ef5007791ab6a6b61242a407ea2f25b0b04f48 Mon Sep 17 00:00:00 2001 From: James Smith Date: Fri, 23 Feb 2024 14:50:05 +0000 Subject: [PATCH 15/23] include exporters in simplecov --- test/minitest_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/minitest_helper.rb b/test/minitest_helper.rb index 5a84cac..7c7e343 100644 --- a/test/minitest_helper.rb +++ b/test/minitest_helper.rb @@ -11,6 +11,7 @@ add_filter "/test/" add_group 'Cameras', 'lib/mittsu/cameras' add_group 'Core', 'lib/mittsu/core' + add_group 'Exporters', 'lib/mittsu/exporters' add_group 'Extras', 'lib/mittsu/extras' add_group 'Lights', 'lib/mittsu/lights' add_group 'Loaders', 'lib/mittsu/loaders' From f0b1b16f2b14ac443eff7763f12b79efbba89d18 Mon Sep 17 00:00:00 2001 From: James Smith Date: Fri, 23 Feb 2024 14:58:03 +0000 Subject: [PATCH 16/23] remove model name parameterization --- lib/mittsu/exporters/3mf_exporter.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/mittsu/exporters/3mf_exporter.rb b/lib/mittsu/exporters/3mf_exporter.rb index 9a9d1ed..24599de 100644 --- a/lib/mittsu/exporters/3mf_exporter.rb +++ b/lib/mittsu/exporters/3mf_exporter.rb @@ -15,12 +15,10 @@ def export(object, filename) model_names = models.map do |model| # Set a model name if there isn't one model.name ||= SecureRandom.uuid - # Make model name filesystem-safe - name = model.name.parameterize # Store model - store(zip, "3D/#{name}.model", model_file(model)) + store(zip, "3D/#{model.name}.model", model_file(model)) # Remember the name for later - name + model.name end # Add OPC rels file with list of contained models store(zip, "_rels/.rels", rels_file(model_names)) From 11db7451cea0b034c0e29caeba751089cc5c1b27 Mon Sep 17 00:00:00 2001 From: James Smith Date: Fri, 23 Feb 2024 15:11:04 +0000 Subject: [PATCH 17/23] add example for exporting 3MF --- .gitignore | 1 + examples/10_3mf_exporter_example.rb | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 examples/10_3mf_exporter_example.rb diff --git a/.gitignore b/.gitignore index 470dc23..f6e1f58 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ /test/reports *.gem .tool-versions +/examples/output.3mf diff --git a/examples/10_3mf_exporter_example.rb b/examples/10_3mf_exporter_example.rb new file mode 100644 index 0000000..895fc08 --- /dev/null +++ b/examples/10_3mf_exporter_example.rb @@ -0,0 +1,7 @@ +require_relative './example_helper' + +loader = Mittsu::OBJLoader.new +object = loader.load(File.expand_path('../male02.obj', __FILE__)) + +exporter = Mittsu::ThreeMFExporter.new +exporter.export(object, File.expand_path('../output.3mf', __FILE__)) From eae599c2496976de21f5e8a275a546966cebb634 Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 26 Feb 2024 10:46:02 +0000 Subject: [PATCH 18/23] traverse all meshes in an object for 3mf output --- lib/mittsu/exporters/3mf_exporter.rb | 40 ++++++++++++++++++---------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/lib/mittsu/exporters/3mf_exporter.rb b/lib/mittsu/exporters/3mf_exporter.rb index 24599de..0e4da65 100644 --- a/lib/mittsu/exporters/3mf_exporter.rb +++ b/lib/mittsu/exporters/3mf_exporter.rb @@ -67,20 +67,9 @@ def model_file(object) xml.model unit: "millimeter", "xml:lang": "en-US", xmlns:"http://schemas.microsoft.com/3dmanufacturing/core/2015/02" do objectIDs = [] xml.resources do - uuid = SecureRandom.uuid - objectIDs << uuid - xml.object id: uuid, type: "model" do - xml.mesh do - xml.vertices do - object.geometry.vertices.each do |vertex| - xml.vertex x: vertex.x, y: vertex.y, z: vertex.z - end - end - xml.triangles do - object.geometry.faces.each do |face| - xml.triangle v1: face.a, v2: face.b, v3: face.c - end - end + object.traverse do |x| + if (x.is_a? Mesh) + objectIDs << build_object(xml, x) end end end @@ -92,5 +81,28 @@ def model_file(object) end end end + + def build_object(xml, object) + uuid = SecureRandom.uuid + xml.object id: uuid, type: "model" do + build_mesh_element(xml, object.geometry) + end + uuid + end + + def build_mesh_element(xml, geometry) + xml.mesh do + xml.vertices do + geometry.vertices.each do |vertex| + xml.vertex x: vertex.x, y: vertex.y, z: vertex.z + end + end + xml.triangles do + geometry.faces.each do |face| + xml.triangle v1: face.a, v2: face.b, v3: face.c + end + end + end + end end end From aa8893bbee6a16f0fef4e06b055483a2463c233d Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 26 Feb 2024 11:00:31 +0000 Subject: [PATCH 19/23] test that grouped meshes are all exported --- test/exporters/test_3mf_exporter.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/exporters/test_3mf_exporter.rb b/test/exporters/test_3mf_exporter.rb index 93e0a39..7e6730a 100644 --- a/test/exporters/test_3mf_exporter.rb +++ b/test/exporters/test_3mf_exporter.rb @@ -20,6 +20,16 @@ def test_3mf_model_file assert_equal 12, REXML::XPath.match(xml, "/model/resources/object/mesh/triangles/triangle").count end + def test_grouped_meshes + group = Mittsu::Group.new + group.add(@box) + group.add(Mittsu::Mesh.new(Mittsu::SphereGeometry.new())) + file = @exporter.send(:model_file, group) + xml = REXML::Document.new file + assert_equal 2, REXML::XPath.match(xml, "/model/resources/object/mesh").count + assert_equal 2, REXML::XPath.match(xml, "/model/build/item").count + end + def test_content_types_file file = @exporter.send(:content_types_file) xml = REXML::Document.new file From 49f8356e885d0615993ada4a9b1d690a2e69dec2 Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 26 Feb 2024 12:08:53 +0000 Subject: [PATCH 20/23] check that geometry is exported correct for multi-mesh 3MF models --- test/exporters/test_3mf_exporter.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/exporters/test_3mf_exporter.rb b/test/exporters/test_3mf_exporter.rb index 7e6730a..f4cd7fb 100644 --- a/test/exporters/test_3mf_exporter.rb +++ b/test/exporters/test_3mf_exporter.rb @@ -27,6 +27,10 @@ def test_grouped_meshes file = @exporter.send(:model_file, group) xml = REXML::Document.new file assert_equal 2, REXML::XPath.match(xml, "/model/resources/object/mesh").count + assert_equal 8, REXML::XPath.match(xml, "/model/resources/object[1]/mesh/vertices/vertex").count + assert_equal 12, REXML::XPath.match(xml, "/model/resources/object[1]/mesh/triangles/triangle").count + assert_equal 63, REXML::XPath.match(xml, "/model/resources/object[2]/mesh/vertices/vertex").count + assert_equal 88, REXML::XPath.match(xml, "/model/resources/object[2]/mesh/triangles/triangle").count assert_equal 2, REXML::XPath.match(xml, "/model/build/item").count end From 9e2c05644138dbb10f7334f37bda72e1245cf75e Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 26 Feb 2024 14:58:12 +0000 Subject: [PATCH 21/23] add simple OBJ example file and use it in 3MF conversion example --- .gitignore | 2 +- examples/10_3mf_exporter_example.rb | 4 +- examples/mittsu.obj | 2082 +++++++++++++++++++++++++++ 3 files changed, 2085 insertions(+), 3 deletions(-) create mode 100644 examples/mittsu.obj diff --git a/.gitignore b/.gitignore index f6e1f58..4d7ff8e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,4 @@ /test/reports *.gem .tool-versions -/examples/output.3mf +/examples/mittsu-export.3mf diff --git a/examples/10_3mf_exporter_example.rb b/examples/10_3mf_exporter_example.rb index 895fc08..078a760 100644 --- a/examples/10_3mf_exporter_example.rb +++ b/examples/10_3mf_exporter_example.rb @@ -1,7 +1,7 @@ require_relative './example_helper' loader = Mittsu::OBJLoader.new -object = loader.load(File.expand_path('../male02.obj', __FILE__)) +object = loader.load(File.expand_path('../mittsu.obj', __FILE__)) exporter = Mittsu::ThreeMFExporter.new -exporter.export(object, File.expand_path('../output.3mf', __FILE__)) +exporter.export(object, File.expand_path('../mittsu-export.3mf', __FILE__)) diff --git a/examples/mittsu.obj b/examples/mittsu.obj new file mode 100644 index 0000000..9dcfca4 --- /dev/null +++ b/examples/mittsu.obj @@ -0,0 +1,2082 @@ +# Object Export From Tinkercad Server 2015 + +mtllib obj.mtl + +o obj_0 +v -35.32 3.63 0 +v -35.32 3.63 10 +v -35.32 23.47 10 +v -35.32 23.47 0 +v 36.24 18.53 0 +v 38.72 18.53 0 +v 36.04 8.47 0 +v 36.151 9.17 0 +v 36.218 9.937 0 +v 36.24 10.77 0 +v 38.72 3.63 0 +v 33.036 5.43 0 +v 33.656 5.55 0 +v 34.2 5.75 0 +v 34.662 6.017 0 +v 35.062 6.363 0 +v 35.4 6.79 0 +v 35.667 7.279 0 +v 35.88 7.839 0 +v 36.36 5.61 0 +v 36.22 5.61 0 +v 35.978 5.239 0 +v 35.704 4.899 0 +v 35.4 4.59 0 +v 35.082 4.326 0 +v 36.72 3.63 0 +v 34.742 4.092 0 +v 34.38 3.89 0 +v 34 3.73 0 +v 33.607 3.597 0 +v 33.2 3.49 0 +v 32.782 3.412 0 +v 26.94 18.53 0 +v 29.4 18.53 0 +v 20.62 16.45 0 +v 21.2 18.49 0 +v 21.909 18.283 0 +v 22.596 18.037 0 +v 23.26 17.75 0 +v 20.004 16.606 0 +v 19.384 16.699 0 +v 18.76 16.73 0 +v 17.931 16.679 0 +v 17.258 16.526 0 +v 16.66 18.53 0 +v 17.322 18.674 0 +v 18.029 18.761 0 +v 18.78 18.79 0 +v 19.636 18.757 0 +v 20.442 18.657 0 +v 29.4 8.91 0 +v 26.94 8.79 0 +v 29.48 7.834 0 +v 29.72 6.954 0 +v 30.12 6.27 0 +v 30.687 5.781 0 +v 31.427 5.488 0 +v 32.34 5.39 0 +v 32.356 3.366 0 +v 31.92 3.35 0 +v 31.138 3.383 0 +v 30.418 3.483 0 +v 29.76 3.65 0 +v 29.169 3.901 0 +v 28.649 4.228 0 +v 28.2 4.63 0 +v 27.809 5.112 0 +v 27.489 5.679 0 +v 27.24 6.33 0 +v 27.073 7.057 0 +v 26.973 7.877 0 +v 22.42 15.75 0 +v 21.829 16.006 0 +v 21.229 16.239 0 +v 16.74 16.27 0 +v 16.533 13.706 0 +v 16.76 13.49 0 +v 17.04 13.277 0 +v 17.373 13.063 0 +v 17.76 12.85 0 +v 18.22 12.63 0 +v 18.747 12.397 0 +v 20.84 8.27 0 +v 20.733 8.494 0 +v 20.573 8.714 0 +v 20.531 11.599 0 +v 21.06 11.33 0 +v 21.542 11.05 0 +v 21.976 10.743 0 +v 22.36 10.41 0 +v 22.68 10.059 0 +v 22.947 9.672 0 +v 23.16 9.25 0 +v -8.8 22.53 0 +v 23.327 8.792 0 +v 23.427 8.272 0 +v -8.811 22.794 0 +v 20.36 8.93 0 +v -8.844 23.028 0 +v 20.091 9.157 0 +v -8.9 23.23 0 +v 19.764 9.383 0 +v -8.989 23.417 0 +v -9.096 23.577 0 +v 19.38 9.61 0 +v 18.927 9.846 0 +v -9.22 23.71 0 +v -9.36 23.83 0 +v 18.393 10.099 0 +v -9.513 23.923 0 +v 19.34 12.15 0 +v 19.958 11.872 0 +v -9.68 23.99 0 +v 17.78 10.37 0 +v 17.16 10.648 0 +v 16.58 10.921 0 +v -9.858 24.046 0 +v 20.48 6.15 0 +v 20.667 6.377 0 +v 20.8 6.63 0 +v -10.044 24.079 0 +v 20.9 6.906 0 +v -10.24 24.09 0 +v 20.96 7.199 0 +v 20.98 7.51 0 +v -10.24 20.99 0 +v -9.86 21.032 0 +v 20.964 7.781 0 +v -9.52 21.159 0 +v 20.918 8.034 0 +v -9.22 21.37 0 +v -8.987 21.668 0 +v 23.46 7.69 0 +v 23.416 6.999 0 +v 23.282 6.366 0 +v -8.847 22.054 0 +v 23.06 5.79 0 +v 22.756 5.283 0 +v 22.376 4.83 0 +v 21.92 4.43 0 +v 21.393 4.101 0 +v -11.633 22.043 0 +v -11.493 21.65 0 +v -11.26 21.35 0 +v 20.8 3.834 0 +v -10.969 21.15 0 +v 16.56 5.49 0 +v -10.629 21.03 0 +v 16.96 5.434 0 +v 17.36 5.401 0 +v 17.76 5.39 0 +v 18.316 5.406 0 +v -10.629 24.048 0 +v 18.809 5.452 0 +v -10.969 23.921 0 +v 19.24 5.53 0 +v -11.26 23.71 0 +v 19.627 5.648 0 +v 19.96 5.788 0 +v -11.493 23.419 0 +v 20.24 5.95 0 +v -11.633 23.026 0 +v -11.68 22.53 0 +v 20.14 3.63 0 +v 19.418 3.474 0 +v 18.638 3.381 0 +v 17.8 3.35 0 +v 16.86 3.377 0 +v -9.02 18.53 0 +v -11.48 18.53 0 +v 13.98 16.53 0 +v 14.267 16.997 0 +v 14.62 17.41 0 +v 15.04 17.77 0 +v 15.522 18.077 0 +v 16.062 18.33 0 +v 11.54 18.53 0 +v 11.54 16.75 0 +v 5.32 16.75 0 +v 7.78 16.75 0 +v -0.393 5.463 10 +v 5.32 18.81 0 +v 6.36 21.93 0 +v 7.78 21.93 0 +v 7.78 18.53 0 +v -1.64 8.05 10 +v 3.22 16.75 0 +v 3.22 17.81 0 +v -1.593 7.241 10 +v 2.12 18.53 0 +v 2.12 16.75 0 +v 16.373 15.932 0 +v 6.44 4.31 10 +v 16.153 15.506 0 +v 6.809 3.988 10 +v 16.08 14.99 0 +v 7.222 3.741 10 +v 16.098 14.683 0 +v 7.68 3.57 10 +v 16.151 14.403 0 +v 8.173 3.448 10 +v 13.62 14.83 0 +v 13.66 15.446 0 +v 13.78 16.012 0 +v 2.24 5.67 10 +v 16.24 14.15 0 +v 16.36 13.926 0 +v 0.2 5.39 10 +v 0.369 5.392 10 +v 14.389 12.461 0 +v 0.556 5.399 10 +v 14.116 12.848 0 +v 13.9 13.27 0 +v 13.744 13.737 0 +v 13.651 14.257 0 +v 0.958 5.437 10 +v 16.04 11.19 0 +v 15.547 11.479 0 +v 15.107 11.786 0 +v 14.72 12.11 0 +v 13.4 6.55 0 +v 0.76 5.41 10 +v 13.689 6.406 0 +v 13.996 6.266 0 +v 14.32 6.13 0 +v 1.34 5.49 10 +v 1.531 5.517 10 +v 14.671 5.99 0 +v 15.031 5.863 0 +v 1.151 5.463 10 +v 15.4 5.75 0 +v 15.778 5.65 0 +v 16.164 5.563 0 +v 1.704 5.543 10 +v 1.86 5.57 10 +v 16.013 3.457 0 +v 15.26 3.59 0 +v 14.582 3.766 0 +v 13.962 3.999 0 +v 13.4 4.29 0 +v 2.009 5.608 10 +v 2.136 5.641 10 +v 10.378 5.437 0 +v 10.571 5.463 0 +v 0.047 3.354 10 +v 10.76 5.49 0 +v 10.951 5.517 0 +v 0.273 3.368 10 +v 11.124 5.543 0 +v 0.5 3.39 10 +v 11.28 5.57 0 +v 11.429 5.608 0 +v 0.736 3.419 10 +v 11.556 5.641 0 +v 0.962 3.452 10 +v 11.66 5.67 0 +v 1.18 3.49 10 +v 9.027 5.463 0 +v 9.62 5.39 0 +v 1.398 3.532 10 +v 9.789 5.392 0 +v 9.976 5.399 0 +v 1.598 3.579 10 +v 10.18 5.41 0 +v 1.956 3.674 10 +v 1.78 3.63 10 +v 8.2 6.05 0 +v 8.553 5.683 0 +v 2.109 3.728 10 +v 2.24 3.79 10 +v 11.376 3.674 0 +v 11.2 3.63 0 +v 11.66 3.79 0 +v 11.529 3.728 0 +v 11.018 3.579 0 +v 10.818 3.532 0 +v 10.6 3.49 0 +v 10.382 3.452 0 +v 10.156 3.419 0 +v 9.92 3.39 0 +v 9.693 3.368 0 +v 9.467 3.354 0 +v 9.24 3.35 0 +v 8.693 3.374 0 +v 7.78 8.05 0 +v 7.827 7.241 0 +v 7.967 6.574 0 +v 5.32 8.05 0 +v 5.351 7.181 0 +v 5.444 6.414 0 +v 5.6 5.75 0 +v 5.827 5.19 0 +v 6.107 4.71 0 +v 6.44 4.31 0 +v 6.809 3.988 0 +v 7.222 3.741 0 +v 7.68 3.57 0 +v 8.173 3.448 0 +v 2.24 5.67 0 +v 0.2 5.39 0 +v 0.369 5.392 0 +v 0.556 5.399 0 +v 0.958 5.437 0 +v 0.76 5.41 0 +v 1.531 5.517 0 +v 1.34 5.49 0 +v 1.151 5.463 0 +v 1.704 5.543 0 +v 1.86 5.57 0 +v 2.009 5.608 0 +v 2.136 5.641 0 +v 0.047 3.354 0 +v 0.273 3.368 0 +v 0.5 3.39 0 +v 0.736 3.419 0 +v 0.962 3.452 0 +v 1.18 3.49 0 +v 1.398 3.532 0 +v 1.598 3.579 0 +v 1.956 3.674 0 +v 1.78 3.63 0 +v 2.109 3.728 0 +v 2.24 3.79 0 +v -1.64 16.75 10 +v -1.64 16.75 0 +v -1.64 18.53 0 +v -1.64 18.53 10 +v -1.64 21.93 0 +v -1.64 21.93 10 +v -3.06 21.93 0 +v -3.06 21.93 10 +v -4.1 18.81 0 +v -6.2 17.81 0 +v -6.2 17.81 10 +v -4.1 18.81 10 +v -6.2 16.75 0 +v -6.2 16.75 10 +v -4.1 16.75 0 +v -4.1 16.75 10 +v -8.8 22.53 10 +v -8.811 22.794 10 +v -8.844 23.028 10 +v -8.9 23.23 10 +v -8.989 23.417 10 +v -9.096 23.577 10 +v -9.22 23.71 10 +v -9.36 23.83 10 +v -9.513 23.923 10 +v -9.68 23.99 10 +v -9.858 24.046 10 +v -10.044 24.079 10 +v -10.24 24.09 10 +v -9.86 21.032 10 +v -10.24 20.99 10 +v -9.52 21.159 10 +v -9.22 21.37 10 +v -8.987 21.668 10 +v -8.847 22.054 10 +v -11.633 22.043 10 +v -11.493 21.65 10 +v -11.26 21.35 10 +v -10.969 21.15 10 +v -10.629 21.03 10 +v -10.629 24.048 10 +v -10.969 23.921 10 +v -11.26 23.71 10 +v 13.962 3.999 10 +v 13.4 4.29 10 +v 14.582 3.766 10 +v 15.26 3.59 10 +v 16.013 3.457 10 +v 11.66 5.67 10 +v 10.378 5.437 10 +v 10.76 5.49 10 +v 10.951 5.517 10 +v 10.571 5.463 10 +v 11.124 5.543 10 +v 11.28 5.57 10 +v 11.429 5.608 10 +v 11.556 5.641 10 +v 9.027 5.463 10 +v 9.62 5.39 10 +v 9.789 5.392 10 +v 9.976 5.399 10 +v 10.18 5.41 10 +v 8.2 6.05 10 +v 8.553 5.683 10 +v 8.693 3.374 10 +v 9.24 3.35 10 +v 9.467 3.354 10 +v 9.693 3.368 10 +v 9.92 3.39 10 +v 10.156 3.419 10 +v 10.382 3.452 10 +v 10.6 3.49 10 +v 10.818 3.532 10 +v 11.018 3.579 10 +v 11.376 3.674 10 +v 11.2 3.63 10 +v 11.529 3.728 10 +v 11.66 3.79 10 +v 36.36 5.61 10 +v 36.22 5.61 10 +v 35.082 4.326 10 +v 35.4 4.59 10 +v 35.704 4.899 10 +v 35.978 5.239 10 +v 34.662 6.017 10 +v 36.72 3.63 10 +v 32.782 3.412 10 +v 33.2 3.49 10 +v 33.607 3.597 10 +v 34 3.73 10 +v 34.38 3.89 10 +v 34.742 4.092 10 +v 29.4 18.53 10 +v 26.94 18.53 10 +v 20.62 16.45 10 +v 23.26 17.75 10 +v 22.596 18.037 10 +v 21.909 18.283 10 +v 21.2 18.49 10 +v 20.004 16.606 10 +v 19.384 16.699 10 +v 18.76 16.73 10 +v 17.931 16.679 10 +v 17.258 16.526 10 +v 20.442 18.657 10 +v 19.636 18.757 10 +v 18.78 18.79 10 +v 18.029 18.761 10 +v 17.322 18.674 10 +v 16.66 18.53 10 +v 29.4 8.91 10 +v 26.94 8.79 10 +v 29.48 7.834 10 +v 29.72 6.954 10 +v 30.12 6.27 10 +v 30.687 5.781 10 +v 31.427 5.488 10 +v 32.34 5.39 10 +v 29.169 3.901 10 +v 29.76 3.65 10 +v 30.418 3.483 10 +v 31.138 3.383 10 +v 31.92 3.35 10 +v 32.356 3.366 10 +v 27.489 5.679 10 +v 27.809 5.112 10 +v 28.2 4.63 10 +v 28.649 4.228 10 +v 26.973 7.877 10 +v -1.453 6.574 10 +v 27.073 7.057 10 +v 27.24 6.33 10 +v 21.829 16.006 10 +v 22.42 15.75 10 +v -1.22 6.05 10 +v 21.229 16.239 10 +v -0.867 5.683 10 +v -4.069 7.181 10 +v 16.74 16.27 10 +v 16.533 13.706 10 +v -3.976 6.414 10 +v 16.76 13.49 10 +v -3.82 5.75 10 +v 17.04 13.277 10 +v 17.373 13.063 10 +v -3.593 5.19 10 +v 17.76 12.85 10 +v -3.313 4.71 10 +v 18.22 12.63 10 +v 18.747 12.397 10 +v -2.98 4.31 10 +v 20.84 8.27 10 +v -2.611 3.988 10 +v 20.733 8.494 10 +v -2.198 3.741 10 +v 20.573 8.714 10 +v -1.74 3.57 10 +v -1.247 3.448 10 +v -0.727 3.374 10 +v 22.36 10.41 10 +v 21.976 10.743 10 +v -0.18 3.35 10 +v 21.542 11.05 10 +v 21.06 11.33 10 +v 20.531 11.599 10 +v 23.427 8.272 10 +v 23.327 8.792 10 +v 23.16 9.25 10 +v 22.947 9.672 10 +v 22.68 10.059 10 +v -4.1 8.05 10 +v 20.36 8.93 10 +v 20.091 9.157 10 +v 19.764 9.383 10 +v 19.38 9.61 10 +v 18.927 9.846 10 +v 18.393 10.099 10 +v 16.58 10.921 10 +v 17.16 10.648 10 +v 17.78 10.37 10 +v 19.958 11.872 10 +v 19.34 12.15 10 +v 20.667 6.377 10 +v 20.48 6.15 10 +v 20.8 6.63 10 +v 20.9 6.906 10 +v 20.96 7.199 10 +v 20.98 7.51 10 +v 20.964 7.781 10 +v 20.918 8.034 10 +v 23.282 6.366 10 +v 23.416 6.999 10 +v 23.46 7.69 10 +v 21.393 4.101 10 +v 21.92 4.43 10 +v 22.376 4.83 10 +v 22.756 5.283 10 +v 23.06 5.79 10 +v 20.8 3.834 10 +v 16.56 5.49 10 +v 16.96 5.434 10 +v 17.36 5.401 10 +v 17.76 5.39 10 +v 18.316 5.406 10 +v 18.809 5.452 10 +v 19.24 5.53 10 +v -0.393 5.463 0 +v 19.627 5.648 10 +v -1.64 8.05 0 +v 19.96 5.788 10 +v -1.593 7.241 0 +v 20.24 5.95 10 +v -1.453 6.574 0 +v -1.22 6.05 0 +v 17.8 3.35 10 +v 18.638 3.381 10 +v 19.418 3.474 10 +v 20.14 3.63 10 +v -0.867 5.683 0 +v 16.86 3.377 10 +v -4.069 7.181 0 +v 13.98 16.53 10 +v 16.062 18.33 10 +v 15.522 18.077 10 +v 15.04 17.77 10 +v 14.62 17.41 10 +v 14.267 16.997 10 +v 11.54 18.53 10 +v -3.976 6.414 0 +v 11.54 16.75 10 +v 7.78 16.75 10 +v 5.32 16.75 10 +v -3.82 5.75 0 +v 5.32 18.81 10 +v -3.593 5.19 0 +v 7.78 21.93 10 +v 6.36 21.93 10 +v 7.78 18.53 10 +v -3.313 4.71 0 +v 3.22 17.81 10 +v 3.22 16.75 10 +v -2.98 4.31 0 +v 2.12 18.53 10 +v 2.12 16.75 10 +v 16.373 15.932 10 +v 16.153 15.506 10 +v -2.611 3.988 0 +v 16.08 14.99 10 +v -2.198 3.741 0 +v 16.098 14.683 10 +v -1.74 3.57 0 +v 16.151 14.403 10 +v -1.247 3.448 0 +v 13.78 16.012 10 +v 13.66 15.446 10 +v -0.727 3.374 0 +v 13.62 14.83 10 +v 16.24 14.15 10 +v -0.18 3.35 0 +v 16.36 13.926 10 +v -4.1 8.05 0 +v 13.651 14.257 10 +v 13.744 13.737 10 +v 13.9 13.27 10 +v 14.116 12.848 10 +v 14.389 12.461 10 +v -11.48 3.63 0 +v -11.48 3.63 10 +v -9.02 3.63 0 +v -9.02 3.63 10 +v 14.72 12.11 10 +v 15.107 11.786 10 +v 15.547 11.479 10 +v 16.04 11.19 10 +v -18.942 19.654 0 +v -18.98 19.01 0 +v -18.98 19.01 10 +v -18.942 19.654 10 +v 13.689 6.406 10 +v 13.4 6.55 10 +v -19.004 18.452 0 +v -19.004 18.452 10 +v -18.909 20.308 0 +v -18.909 20.308 10 +v -19.024 17.872 0 +v -19.024 17.872 10 +v -18.88 20.97 0 +v -18.88 20.97 10 +v -19.04 17.27 0 +v -19.062 16.69 0 +v -19.062 16.69 10 +v -19.04 17.27 10 +v 13.996 6.266 10 +v 14.32 6.13 10 +v 14.671 5.99 10 +v 15.031 5.863 10 +v -19 20.97 10 +v -19 20.97 0 +v 15.4 5.75 10 +v -16.54 23.47 0 +v -16.54 23.47 10 +v 15.778 5.65 10 +v 16.164 5.563 10 +v -20.3 23.47 0 +v -20.3 23.47 10 +v -31.4 23.47 0 +v -31.4 23.47 10 +v -19.076 16.15 0 +v -19.076 16.15 10 +v -19.08 15.65 0 +v -19.08 15.65 10 +v -19.08 3.63 0 +v -19.08 3.63 10 +v -16.54 3.63 0 +v -16.54 3.63 10 +v -25.06 3.63 10 +v -25.06 3.63 0 +v -26 7.29 10 +v -26 7.29 0 +v -25.92 7.29 0 +v -25.92 7.29 10 +v -27.14 3.63 0 +v -27.14 3.63 10 +v -32.8 3.63 0 +v -32.8 3.63 10 +v -32.8 15.49 0 +v -32.8 15.49 10 +v -32.96 20.99 0 +v -33.06 20.99 0 +v -33.009 20.323 0 +v -32.962 19.657 0 +v -32.92 18.99 0 +v -32.893 18.417 0 +v -32.867 17.817 0 +v -32.84 17.19 0 +v -32.818 16.583 0 +v -32.804 16.017 0 +v -11.493 23.419 10 +v 38.72 18.53 10 +v -11.633 23.026 10 +v 38.72 3.63 10 +v -11.68 22.53 10 +v 36.24 18.53 10 +v 36.04 8.47 10 +v 36.151 9.17 10 +v -11.48 18.53 10 +v -9.02 18.53 10 +v 36.218 9.937 10 +v 36.24 10.77 10 +v 35.062 6.363 10 +v 35.4 6.79 10 +v 35.667 7.279 10 +v 35.88 7.839 10 +v 33.036 5.43 10 +v 33.656 5.55 10 +v 34.2 5.75 10 +v 7.78 8.05 10 +v 7.827 7.241 10 +v 7.967 6.574 10 +v 5.32 8.05 10 +v 5.351 7.181 10 +v 5.444 6.414 10 +v 5.6 5.75 10 +v 5.827 5.19 10 +v 6.107 4.71 10 +v -32.804 16.017 10 +v -32.818 16.583 10 +v -32.96 20.99 10 +v -32.84 17.19 10 +v -32.867 17.817 10 +v -32.893 18.417 10 +v -32.92 18.99 10 +v -32.962 19.657 10 +v -33.009 20.323 10 +v -33.06 20.99 10 +# 698 vertices + +g group_0_15277357 + +usemtl color_15277357 +s 0 + +f 1 2 3 +f 1 3 4 +f 9 10 11 +f 6 11 10 +f 5 6 10 +f 8 9 11 +f 13 29 12 +f 14 27 13 +f 15 24 14 +f 16 22 15 +f 18 21 17 +f 17 21 16 +f 19 20 18 +f 27 14 25 +f 20 19 11 +f 21 18 20 +f 23 15 22 +f 24 15 23 +f 25 14 24 +f 22 16 21 +f 7 11 19 +f 8 11 7 +f 20 11 26 +f 29 13 28 +f 30 12 29 +f 31 12 30 +f 27 28 13 +f 33 34 51 +f 39 73 38 +f 37 38 74 +f 74 35 37 +f 36 37 35 +f 35 40 36 +f 42 48 41 +f 41 49 40 +f 43 47 42 +f 47 43 46 +f 48 42 47 +f 49 41 48 +f 50 40 49 +f 36 40 50 +f 43 44 46 +f 45 46 44 +f 52 51 71 +f 53 71 51 +f 51 52 33 +f 64 65 55 +f 54 70 53 +f 57 62 56 +f 56 64 55 +f 63 56 62 +f 61 62 57 +f 60 61 57 +f 64 56 63 +f 32 59 58 +f 31 32 58 +f 12 31 58 +f 57 58 60 +f 59 60 58 +f 66 67 55 +f 68 54 67 +f 65 66 55 +f 69 54 68 +f 70 54 69 +f 71 53 70 +f 55 67 54 +f 74 38 73 +f 72 73 39 +f 175 176 75 +f 44 75 176 +f 77 78 218 +f 217 218 78 +f 81 115 80 +f 80 116 79 +f 111 109 82 +f 82 114 81 +f 85 91 84 +f 91 85 90 +f 85 98 90 +f 89 90 98 +f 98 100 89 +f 92 84 91 +f 93 83 92 +f 95 83 93 +f 96 130 95 +f 133 128 96 +f 88 89 100 +f 87 88 100 +f 86 87 102 +f 100 102 87 +f 106 112 105 +f 105 86 102 +f 109 111 106 +f 86 105 112 +f 111 112 106 +f 114 82 109 +f 115 81 114 +f 116 80 115 +f 217 79 116 +f 119 139 118 +f 120 138 119 +f 125 134 124 +f 135 137 122 +f 130 96 128 +f 357 356 132 +f 135 124 134 +f 134 125 133 +f 83 95 130 +f 84 92 83 +f 136 358 132 +f 138 120 137 +f 118 140 161 +f 145 159 141 +f 139 119 138 +f 120 122 137 +f 128 133 125 +f 122 124 135 +f 146 362 144 +f 362 146 148 +f 150 168 149 +f 151 167 150 +f 154 166 152 +f 147 149 168 +f 145 164 158 +f 152 167 151 +f 161 141 159 +f 158 159 145 +f 139 140 118 +f 141 161 140 +f 166 154 165 +f 154 156 165 +f 167 152 166 +f 164 165 156 +f 156 158 164 +f 169 170 669 +f 172 173 192 +f 174 192 173 +f 175 75 174 +f 45 44 176 +f 184 185 183 +f 182 183 185 +f 182 185 179 +f 178 180 185 +f 177 178 185 +f 180 179 185 +f 188 182 179 +f 187 188 179 +f 325 326 191 +f 171 172 194 +f 192 194 172 +f 192 174 75 +f 202 203 196 +f 198 202 196 +f 200 215 198 +f 194 196 203 +f 203 204 194 +f 171 194 204 +f 206 213 200 +f 220 210 76 +f 213 206 212 +f 214 200 213 +f 202 198 215 +f 215 200 214 +f 219 220 76 +f 76 77 219 +f 218 219 77 +f 78 79 217 +f 210 212 207 +f 206 207 212 +f 207 76 210 +f 224 240 223 +f 221 223 240 +f 225 240 224 +f 228 239 225 +f 238 239 229 +f 237 238 231 +f 236 237 233 +f 233 147 236 +f 229 231 238 +f 228 229 239 +f 240 225 239 +f 231 232 237 +f 232 233 237 +f 168 236 147 +f 168 150 167 +f 247 249 274 +f 274 271 247 +f 273 274 249 +f 251 273 249 +f 252 273 251 +f 254 273 252 +f 256 273 254 +f 244 246 275 +f 272 275 246 +f 271 272 247 +f 246 247 272 +f 262 280 261 +f 283 284 258 +f 259 282 258 +f 261 281 259 +f 284 298 258 +f 275 276 244 +f 243 244 276 +f 276 277 243 +f 264 243 277 +f 277 278 264 +f 278 279 264 +f 262 264 279 +f 280 262 279 +f 281 261 280 +f 282 259 281 +f 268 258 298 +f 283 258 282 +f 180 285 179 +f 296 295 268 +f 286 287 290 +f 684 288 289 +f 296 197 195 +f 297 199 197 +f 297 197 296 +f 199 297 201 +f 298 201 297 +f 270 323 205 +f 300 181 208 +f 208 209 300 +f 301 300 209 +f 302 301 209 +f 302 209 211 +f 222 304 211 +f 302 211 304 +f 216 230 303 +f 222 216 304 +f 303 304 216 +f 305 306 226 +f 305 226 227 +f 307 303 230 +f 226 306 230 +f 307 230 306 +f 227 234 305 +f 308 305 234 +f 309 308 234 +f 309 234 235 +f 235 241 309 +f 310 309 241 +f 242 310 241 +f 299 242 205 +f 313 248 245 +f 313 314 248 +f 250 248 314 +f 315 253 250 +f 315 250 314 +f 253 315 255 +f 316 255 315 +f 255 316 257 +f 317 257 316 +f 317 318 257 +f 260 257 318 +f 319 263 260 +f 319 260 318 +f 266 321 265 +f 320 265 321 +f 319 321 263 +f 266 263 321 +f 265 320 269 +f 269 322 270 +f 323 270 322 +f 293 294 193 +f 193 294 195 +f 295 195 294 +f 295 296 195 +f 299 205 323 +f 311 310 242 +f 299 311 242 +f 313 245 312 +f 322 269 320 +f 190 326 327 +f 327 326 328 +f 327 328 329 +f 328 330 331 +f 328 331 329 +f 332 333 334 +f 332 334 335 +f 333 336 337 +f 333 337 334 +f 330 332 335 +f 330 335 331 +f 337 336 339 +f 338 339 336 +f 126 353 354 +f 363 148 354 +f 238 237 369 +f 238 369 367 +f 370 369 237 +f 370 237 371 +f 236 371 237 +f 249 247 377 +f 378 249 377 +f 254 379 380 +f 261 259 383 +f 384 261 383 +f 268 386 387 +f 381 258 387 +f 254 380 372 +f 373 264 385 +f 393 279 394 +f 274 400 398 +f 400 274 401 +f 283 389 388 +f 389 283 390 +f 279 393 392 +f 278 394 279 +f 395 394 277 +f 278 277 394 +f 406 408 405 +f 407 408 406 +f 408 679 405 +f 404 405 679 +f 277 396 395 +f 396 276 397 +f 272 398 399 +f 678 677 413 +f 414 415 678 +f 679 678 415 +f 404 679 415 +f 412 677 411 +f 399 397 272 +f 275 272 397 +f 418 459 421 +f 422 418 421 +f 420 421 459 +f 459 456 420 +f 422 423 418 +f 422 428 423 +f 428 429 423 +f 424 423 429 +f 430 431 425 +f 426 425 431 +f 431 432 426 +f 427 426 432 +f 433 427 432 +f 425 424 430 +f 429 430 424 +f 435 434 417 +f 416 417 434 +f 435 452 434 +f 436 434 452 +f 436 454 437 +f 448 449 437 +f 438 437 449 +f 443 444 439 +f 440 439 444 +f 444 445 440 +f 445 446 440 +f 441 440 446 +f 410 411 441 +f 447 410 441 +f 446 447 441 +f 450 438 449 +f 442 438 451 +f 451 438 450 +f 442 443 439 +f 439 438 442 +f 452 454 436 +f 454 455 437 +f 448 437 455 +f 457 419 456 +f 542 537 460 +f 419 420 456 +f 433 546 427 +f 462 427 546 +f 465 596 467 +f 463 595 465 +f 597 501 468 +f 468 467 597 +f 470 468 501 +f 501 502 470 +f 388 284 283 +f 282 390 283 +f 490 491 475 +f 391 390 282 +f 391 281 392 +f 279 392 280 +f 582 312 485 +f 486 496 484 +f 487 496 486 +f 483 484 495 +f 483 495 479 +f 477 493 479 +f 491 492 475 +f 477 475 492 +f 493 477 492 +f 483 479 493 +f 495 484 496 +f 324 186 532 +f 325 324 532 +f 496 487 497 +f 488 497 487 +f 532 186 189 +f 532 189 534 +f 453 536 189 +f 534 189 536 +f 498 497 488 +f 458 537 453 +f 536 453 537 +f 488 504 498 +f 499 498 504 +f 458 460 537 +f 544 552 461 +f 505 473 500 +f 500 499 505 +f 504 505 499 +f 503 500 473 +f 473 472 503 +f 502 503 472 +f 472 470 502 +f 542 460 181 +f 464 461 552 +f 506 520 508 +f 552 556 466 +f 552 466 464 +f 508 521 509 +f 510 509 514 +f 556 558 469 +f 556 469 466 +f 514 515 510 +f 471 469 562 +f 558 562 469 +f 511 510 515 +f 471 562 474 +f 565 474 562 +f 515 516 511 +f 512 511 516 +f 489 490 513 +f 475 513 490 +f 516 489 512 +f 513 512 489 +f 476 474 565 +f 572 478 476 +f 478 572 480 +f 574 480 572 +f 519 507 518 +f 520 506 519 +f 521 508 520 +f 514 509 521 +f 507 519 506 +f 507 535 518 +f 517 533 522 +f 576 481 480 +f 576 480 574 +f 77 463 465 +f 150 149 525 +f 481 576 482 +f 579 482 576 +f 467 77 465 +f 525 526 150 +f 151 150 526 +f 482 579 485 +f 582 485 579 +f 152 151 526 +f 152 526 527 +f 523 626 371 +f 245 485 312 +f 527 528 152 +f 154 152 528 +f 338 584 339 +f 524 523 543 +f 156 154 528 +f 156 528 529 +f 494 544 461 +f 527 526 538 +f 525 538 526 +f 529 158 156 +f 181 300 530 +f 528 527 539 +f 538 539 527 +f 529 528 540 +f 161 159 535 +f 540 541 529 +f 531 522 533 +f 535 533 517 +f 517 518 535 +f 535 118 161 +f 371 236 168 +f 491 93 492 +f 531 529 541 +f 541 522 531 +f 540 528 539 +f 538 525 543 +f 92 493 492 +f 577 569 545 +f 568 550 569 +f 90 483 493 +f 181 530 542 +f 90 484 483 +f 545 569 550 +f 550 568 549 +f 549 568 548 +f 547 548 462 +f 546 547 462 +f 172 171 545 +f 683 680 555 +f 557 561 560 +f 559 560 561 +f 488 487 86 +f 87 86 487 +f 563 555 557 +f 561 557 555 +f 564 555 563 +f 555 554 561 +f 551 561 554 +f 551 554 553 +f 86 504 488 +f 567 566 324 +f 327 324 566 +f 45 176 546 +f 176 175 547 +f 176 547 546 +f 175 174 548 +f 175 548 547 +f 462 548 568 +f 570 476 565 +f 174 173 549 +f 174 549 548 +f 570 572 476 +f 173 172 550 +f 173 550 549 +f 577 578 569 +f 172 545 550 +f 571 569 578 +f 177 551 178 +f 553 178 551 +f 578 580 571 +f 573 571 580 +f 580 585 573 +f 575 573 585 +f 585 586 575 +f 553 554 180 +f 553 180 178 +f 179 555 564 +f 179 564 187 +f 494 339 584 +f 191 567 324 +f 191 324 325 +f 584 544 494 +f 185 184 559 +f 185 559 561 +f 590 591 170 +f 184 183 560 +f 184 560 559 +f 586 587 575 +f 581 575 587 +f 587 588 581 +f 583 581 588 +f 588 589 583 +f 463 583 589 +f 594 463 589 +f 595 463 594 +f 592 593 591 +f 592 591 590 +f 169 593 592 +f 183 182 557 +f 183 557 560 +f 596 465 595 +f 597 467 596 +f 598 599 600 +f 598 600 601 +f 561 551 185 +f 177 185 551 +f 599 604 605 +f 599 605 600 +f 563 557 188 +f 182 188 557 +f 188 187 564 +f 188 564 563 +f 566 191 190 +f 267 268 295 +f 294 267 295 +f 293 267 294 +f 287 267 292 +f 293 292 267 +f 286 289 285 +f 289 286 290 +f 288 285 289 +f 179 285 288 +f 291 287 292 +f 291 290 287 +f 297 296 268 +f 298 297 268 +f 604 609 605 +f 606 598 601 +f 606 601 607 +f 604 608 609 +f 314 313 301 +f 608 612 609 +f 498 105 497 +f 302 314 301 +f 610 606 607 +f 610 607 611 +f 612 613 614 +f 612 614 615 +f 612 615 609 +f 603 368 602 +f 616 602 368 +f 303 317 304 +f 610 611 620 +f 610 620 621 +f 617 616 368 +f 368 367 617 +f 623 624 638 +f 307 318 303 +f 304 315 302 +f 618 617 367 +f 628 624 627 +f 623 627 624 +f 619 618 367 +f 367 369 619 +f 305 321 306 +f 306 319 307 +f 72 457 456 +f 323 322 308 +f 627 643 644 +f 627 644 628 +f 72 419 457 +f 309 323 308 +f 622 619 369 +f 369 370 622 +f 625 622 370 +f 626 625 370 +f 543 523 371 +f 525 524 543 +f 370 371 626 +f 311 323 310 +f 632 614 631 +f 613 631 614 +f 631 633 634 +f 631 634 632 +f 299 323 311 +f 309 310 323 +f 308 322 305 +f 633 635 636 +f 633 636 634 +f 321 305 320 +f 637 638 636 +f 637 636 635 +f 638 637 623 +f 391 392 383 +f 620 639 640 +f 620 640 621 +f 392 393 384 +f 393 394 385 +f 630 641 642 +f 630 642 629 +f 373 385 395 +f 396 397 376 +f 375 374 399 +f 643 642 641 +f 643 641 644 +f 320 305 322 +f 319 306 321 +f 318 307 319 +f 317 303 318 +f 316 304 317 +f 315 304 316 +f 579 576 530 +f 314 302 315 +f 190 191 326 +f 332 338 333 +f 639 646 645 +f 639 645 640 +f 4 630 629 +f 336 333 338 +f 332 326 338 +f 328 326 330 +f 332 330 326 +f 325 338 326 +f 78 77 467 +f 468 78 467 +f 630 4 3 +f 117 113 121 +f 123 121 113 +f 123 113 110 +f 123 110 108 +f 123 108 107 +f 123 107 104 +f 123 104 103 +f 123 103 101 +f 123 101 99 +f 123 99 97 +f 94 163 97 +f 136 163 94 +f 80 468 470 +f 132 163 136 +f 131 163 132 +f 127 126 163 +f 142 163 126 +f 470 472 80 +f 2 1 647 +f 2 647 648 +f 82 472 473 +f 649 650 648 +f 649 648 647 +f 378 377 401 +f 191 566 567 +f 190 327 566 +f 602 223 221 +f 157 97 160 +f 155 97 157 +f 160 97 162 +f 162 97 163 +f 153 97 155 +f 123 97 153 +f 131 129 163 +f 148 142 126 +f 129 127 163 +f 142 148 146 +f 142 146 144 +f 142 144 143 +f 170 169 592 +f 602 224 223 +f 584 532 544 +f 225 224 617 +f 300 301 313 +f 617 228 225 +f 552 544 534 +f 325 532 338 +f 536 552 534 +f 532 534 544 +f 537 558 536 +f 229 228 619 +f 542 570 537 +f 338 532 584 +f 619 622 229 +f 231 229 622 +f 232 231 622 +f 232 622 625 +f 530 300 312 +f 562 537 565 +f 558 537 562 +f 556 536 558 +f 552 536 556 +f 582 579 530 +f 313 312 300 +f 625 233 232 +f 570 542 572 +f 565 537 570 +f 574 572 542 +f 576 574 542 +f 542 530 576 +f 312 582 530 +f 592 590 170 +f 598 623 599 +f 604 599 623 +f 608 604 623 +f 606 623 598 +f 367 368 239 +f 240 239 368 +f 612 623 613 +f 239 238 367 +f 612 608 623 +f 623 606 610 +f 627 623 610 +f 627 610 621 +f 643 627 621 +f 635 633 637 +f 623 637 633 +f 623 633 631 +f 623 631 613 +f 643 640 642 +f 645 642 640 +f 621 640 643 +f 407 22 21 +f 645 651 642 +f 373 243 264 +f 629 642 651 +f 375 247 374 +f 246 374 247 +f 573 198 571 +f 244 243 376 +f 651 652 629 +f 374 246 376 +f 244 376 246 +f 4 629 652 +f 31 411 410 +f 375 377 247 +f 545 171 577 +f 251 249 378 +f 379 252 378 +f 251 378 252 +f 4 652 653 +f 4 653 654 +f 4 654 655 +f 4 655 656 +f 4 656 657 +f 4 657 658 +f 4 658 659 +f 4 659 660 +f 4 660 649 +f 647 1 649 +f 4 649 1 +f 206 575 581 +f 379 378 401 +f 380 379 401 +f 372 380 401 +f 583 207 581 +f 193 195 386 +f 389 390 381 +f 11 6 664 +f 662 664 6 +f 382 381 390 +f 669 670 169 +f 390 391 382 +f 383 382 391 +f 669 170 591 +f 586 585 214 +f 682 681 685 +f 670 593 169 +f 667 676 664 +f 687 688 386 +f 386 682 687 +f 214 587 586 +f 662 666 672 +f 671 664 672 +f 668 664 671 +f 662 672 664 +f 389 381 388 +f 688 193 386 +f 408 407 673 +f 407 403 673 +f 594 589 220 +f 674 673 403 +f 675 674 403 +f 403 402 675 +f 676 675 402 +f 668 667 664 +f 402 664 676 +f 569 194 568 +f 441 411 677 +f 384 383 392 +f 394 395 385 +f 395 396 373 +f 376 373 396 +f 374 376 397 +f 397 399 374 +f 399 398 375 +f 398 400 375 +f 377 375 400 +f 401 377 400 +f 385 384 393 +f 388 381 201 +f 680 554 555 +f 413 414 678 +f 412 413 677 +f 198 573 575 +f 683 684 680 +f 409 664 402 +f 171 204 577 +f 687 682 686 +f 685 686 682 +f 387 386 195 +f 684 685 681 +f 681 680 684 +f 204 578 577 +f 381 387 201 +f 199 201 387 +f 195 197 387 +f 197 199 387 +f 181 460 481 +f 482 485 181 +f 206 581 207 +f 485 245 181 +f 208 181 245 +f 245 248 208 +f 207 583 463 +f 121 123 351 +f 227 226 266 +f 235 234 270 +f 205 242 270 +f 242 241 270 +f 241 235 270 +f 257 260 216 +f 230 216 260 +f 260 263 230 +f 226 230 263 +f 263 266 226 +f 266 265 227 +f 265 269 227 +f 234 227 269 +f 270 234 269 +f 216 222 257 +f 255 257 222 +f 337 339 334 +f 335 327 331 +f 335 334 339 +f 339 327 335 +f 324 327 339 +f 329 331 327 +f 379 254 252 +f 352 665 351 +f 341 351 340 +f 342 349 341 +f 357 358 351 +f 343 349 342 +f 344 349 343 +f 345 349 344 +f 346 349 345 +f 347 349 346 +f 348 349 347 +f 350 341 349 +f 351 341 350 +f 142 143 359 +f 595 594 220 +f 340 351 358 +f 353 355 351 +f 355 356 351 +f 357 351 356 +f 382 259 381 +f 258 381 259 +f 360 143 361 +f 382 383 259 +f 144 362 361 +f 262 261 384 +f 148 363 362 +f 365 366 364 +f 366 661 364 +f 661 663 364 +f 352 364 663 +f 352 663 665 +f 359 351 665 +f 360 351 359 +f 361 351 360 +f 362 363 351 +f 363 354 351 +f 354 353 351 +f 361 362 351 +f 385 264 384 +f 262 384 264 +f 669 591 670 +f 593 670 591 +f 126 354 148 +f 192 568 194 +f 339 494 324 +f 324 494 186 +f 571 196 569 +f 352 123 153 +f 352 153 364 +f 386 267 682 +f 287 682 267 +f 365 364 155 +f 153 155 364 +f 480 481 460 +f 481 482 181 +f 365 155 366 +f 157 366 155 +f 660 689 650 +f 660 650 649 +f 268 387 258 +f 189 186 461 +f 661 366 160 +f 157 160 366 +f 659 690 689 +f 659 689 660 +f 461 186 494 +f 372 273 256 +f 651 645 646 +f 651 646 691 +f 661 160 663 +f 162 663 160 +f 461 464 189 +f 464 466 453 +f 373 376 243 +f 658 692 690 +f 658 690 659 +f 665 663 163 +f 162 163 663 +f 657 693 692 +f 657 692 658 +f 665 163 359 +f 142 359 163 +f 458 453 469 +f 466 469 453 +f 469 471 458 +f 471 474 458 +f 474 476 458 +f 460 458 476 +f 476 478 460 +f 478 480 460 +f 656 694 693 +f 656 693 657 +f 340 358 94 +f 698 652 691 +f 655 695 694 +f 655 694 656 +f 209 208 248 +f 248 250 209 +f 211 209 250 +f 250 253 211 +f 222 211 253 +f 253 255 222 +f 340 94 341 +f 97 341 94 +f 453 189 464 +f 654 696 695 +f 654 695 655 +f 97 99 341 +f 342 341 99 +f 651 691 652 +f 609 615 624 +f 605 609 624 +f 653 697 696 +f 653 696 654 +f 600 605 624 +f 342 99 343 +f 101 343 99 +f 652 698 697 +f 652 697 653 +f 103 344 343 +f 103 343 101 +f 473 505 82 +f 607 601 624 +f 601 600 624 +f 615 614 624 +f 614 632 624 +f 256 254 372 +f 620 611 628 +f 624 628 611 +f 611 607 624 +f 344 103 345 +f 104 345 103 +f 489 95 490 +f 628 644 620 +f 93 491 490 +f 93 490 95 +f 104 107 345 +f 346 345 107 +f 632 634 624 +f 638 624 634 +f 638 634 636 +f 92 492 93 +f 630 691 641 +f 107 108 347 +f 107 347 346 +f 620 644 639 +f 644 641 639 +f 639 641 646 +f 348 347 110 +f 108 110 347 +f 493 91 90 +f 348 110 349 +f 113 349 110 +f 268 267 386 +f 646 641 691 +f 350 349 117 +f 113 117 349 +f 698 691 630 +f 117 121 351 +f 117 351 350 +f 284 388 201 +f 284 201 298 +f 87 487 486 +f 3 698 630 +f 352 351 123 +f 127 353 126 +f 353 127 355 +f 129 355 127 +f 281 391 282 +f 690 3 689 +f 692 3 690 +f 650 689 3 +f 693 3 692 +f 694 3 693 +f 695 3 694 +f 696 3 695 +f 697 3 696 +f 698 3 697 +f 3 2 650 +f 648 650 2 +f 355 129 356 +f 131 356 129 +f 280 392 281 +f 475 83 130 +f 6 5 666 +f 6 666 662 +f 131 132 356 +f 357 132 358 +f 8 7 667 +f 8 667 668 +f 136 94 358 +f 9 8 668 +f 9 668 671 +f 10 9 671 +f 10 671 672 +f 360 359 143 +f 672 666 10 +f 5 10 666 +f 144 361 143 +f 578 204 203 +f 203 580 578 +f 202 215 580 +f 27 415 414 +f 14 678 679 +f 14 679 408 +f 404 415 27 +f 26 11 409 +f 463 76 207 +f 100 98 496 +f 585 215 214 +f 675 18 17 +f 587 214 213 +f 100 496 102 +f 497 102 496 +f 20 409 402 +f 213 588 587 +f 676 667 7 +f 588 210 589 +f 115 502 501 +f 678 13 677 +f 403 20 402 +f 102 497 105 +f 105 498 499 +f 15 14 408 +f 21 20 403 +f 194 569 196 +f 21 403 407 +f 673 16 408 +f 502 115 503 +f 571 198 196 +f 200 198 575 +f 499 500 109 +f 200 575 206 +f 109 500 503 +f 73 72 456 +f 412 411 31 +f 203 202 580 +f 276 396 277 +f 275 397 276 +f 271 398 272 +f 585 580 215 +f 17 673 674 +f 398 271 274 +f 674 675 17 +f 12 677 13 +f 588 213 212 +f 14 13 678 +f 212 210 588 +f 15 408 16 +f 673 17 16 +f 401 274 273 +f 675 676 18 +f 19 18 676 +f 220 589 210 +f 372 401 273 +f 676 7 19 +f 220 219 595 +f 554 285 180 +f 219 218 596 +f 219 596 595 +f 404 27 25 +f 683 555 288 +f 555 179 288 +f 596 218 597 +f 217 597 218 +f 404 25 405 +f 24 405 25 +f 554 680 285 +f 116 501 217 +f 406 405 23 +f 24 23 405 +f 681 286 680 +f 285 680 286 +f 603 602 221 +f 287 286 682 +f 681 682 286 +f 23 22 406 +f 407 406 22 +f 409 20 26 +f 224 602 616 +f 664 409 11 +f 410 447 32 +f 288 684 683 +f 31 410 32 +f 617 224 616 +f 30 412 31 +f 289 290 685 +f 289 685 684 +f 30 29 412 +f 413 412 29 +f 413 29 414 +f 28 414 29 +f 686 685 291 +f 290 291 685 +f 414 28 27 +f 228 617 618 +f 687 686 292 +f 291 292 686 +f 51 34 416 +f 34 33 417 +f 34 417 416 +f 292 293 688 +f 292 688 687 +f 619 228 618 +f 293 193 688 +f 456 459 73 +f 74 73 459 +f 40 418 423 +f 40 423 424 +f 240 368 221 +f 459 418 74 +f 233 625 626 +f 419 72 39 +f 603 221 368 +f 75 44 462 +f 626 523 233 +f 431 46 432 +f 46 433 432 +f 462 568 75 +f 192 75 568 +f 35 74 418 +f 463 77 76 +f 79 78 468 +f 468 80 79 +f 81 80 472 +f 472 82 81 +f 489 96 95 +f 425 426 43 +f 43 426 427 +f 43 427 44 +f 91 493 92 +f 428 49 429 +f 484 90 89 +f 430 429 48 +f 49 48 429 +f 486 484 88 +f 89 88 484 +f 48 431 430 +f 88 87 486 +f 35 418 40 +f 84 83 475 +f 84 475 477 +f 420 419 38 +f 39 38 419 +f 477 479 84 +f 85 84 479 +f 420 38 421 +f 37 421 38 +f 37 36 422 +f 37 422 421 +f 479 495 85 +f 36 428 422 +f 86 112 504 +f 424 41 40 +f 425 42 424 +f 41 424 42 +f 505 504 111 +f 112 111 504 +f 42 425 43 +f 111 82 505 +f 98 85 495 +f 427 462 44 +f 495 496 98 +f 36 50 428 +f 50 49 428 +f 217 501 597 +f 431 48 47 +f 47 46 431 +f 433 46 45 +f 501 116 115 +f 45 546 433 +f 416 434 51 +f 105 499 106 +f 51 434 436 +f 435 417 52 +f 33 52 417 +f 114 503 115 +f 452 435 71 +f 499 109 106 +f 503 114 109 +f 54 436 437 +f 54 437 438 +f 57 439 440 +f 58 57 440 +f 58 440 441 +f 441 677 58 +f 59 32 447 +f 435 52 71 +f 454 69 455 +f 69 448 455 +f 138 137 520 +f 145 141 522 +f 147 233 523 +f 524 149 523 +f 147 523 149 +f 525 149 524 +f 65 64 451 +f 158 529 531 +f 63 443 442 +f 159 158 531 +f 159 531 533 +f 443 63 444 +f 62 444 63 +f 533 535 159 +f 61 445 444 +f 61 444 62 +f 507 118 535 +f 445 61 446 +f 60 446 61 +f 168 543 371 +f 446 60 447 +f 59 447 60 +f 167 538 543 +f 167 543 168 +f 538 167 539 +f 166 539 167 +f 539 166 540 +f 165 540 166 +f 164 541 540 +f 164 540 165 +f 71 70 454 +f 71 454 452 +f 70 69 454 +f 69 68 448 +f 506 118 507 +f 449 448 67 +f 68 67 448 +f 67 66 450 +f 67 450 449 +f 450 66 451 +f 65 451 66 +f 53 51 436 +f 54 53 436 +f 55 54 438 +f 130 512 513 +f 438 439 55 +f 56 55 439 +f 439 57 56 +f 130 513 475 +f 677 12 58 +f 442 451 64 +f 64 63 442 +f 519 139 520 +f 119 118 506 +f 120 119 508 +f 506 508 119 +f 122 120 508 +f 122 508 509 +f 509 510 122 +f 124 122 510 +f 510 511 124 +f 125 124 511 +f 511 512 125 +f 128 125 512 +f 512 130 128 +f 514 135 515 +f 134 515 135 +f 133 516 515 +f 133 515 134 +f 489 516 96 +f 133 96 516 +f 517 522 141 +f 517 141 518 +f 140 518 141 +f 139 519 518 +f 139 518 140 +f 138 520 139 +f 521 520 137 +f 514 521 137 +f 514 137 135 +f 522 541 164 +f 145 522 164 +# 1368 faces + + #end of obj_0 + From a9f84f13c92534169f68ded19ec1e46735ab3953 Mon Sep 17 00:00:00 2001 From: James Smith Date: Wed, 28 Feb 2024 10:03:14 +0000 Subject: [PATCH 22/23] require rexml in dev for testing 3mf output --- mittsu.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/mittsu.gemspec b/mittsu.gemspec index 645b9df..d63bf2c 100644 --- a/mittsu.gemspec +++ b/mittsu.gemspec @@ -36,4 +36,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'pry' spec.add_development_dependency 'benchmark-ips' spec.add_development_dependency 'simplecov', '0.17.1' + spec.add_development_dependency 'rexml' end From 57986ea60449962c3cd43e54eee0b70e3254aeff Mon Sep 17 00:00:00 2001 From: James Smith Date: Mon, 13 May 2024 11:04:23 +0100 Subject: [PATCH 23/23] sanitize filenames in 3mf output --- lib/mittsu/exporters/3mf_exporter.rb | 9 +++++++-- test/exporters/test_3mf_exporter.rb | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/mittsu/exporters/3mf_exporter.rb b/lib/mittsu/exporters/3mf_exporter.rb index 0e4da65..ee6b1c7 100644 --- a/lib/mittsu/exporters/3mf_exporter.rb +++ b/lib/mittsu/exporters/3mf_exporter.rb @@ -16,9 +16,10 @@ def export(object, filename) # Set a model name if there isn't one model.name ||= SecureRandom.uuid # Store model - store(zip, "3D/#{model.name}.model", model_file(model)) + name = filesystem_safe_name(model) + store(zip, "3D/#{name}.model", model_file(model)) # Remember the name for later - model.name + name end # Add OPC rels file with list of contained models store(zip, "_rels/.rels", rels_file(model_names)) @@ -31,6 +32,10 @@ def export(object, filename) private + def filesystem_safe_name(model) + model.name.delete("/\\?*:|\"<>") + end + def store(zip, filename, data) zip.put_next_entry(filename) zip.write(data) diff --git a/test/exporters/test_3mf_exporter.rb b/test/exporters/test_3mf_exporter.rb index f4cd7fb..10b061d 100644 --- a/test/exporters/test_3mf_exporter.rb +++ b/test/exporters/test_3mf_exporter.rb @@ -11,6 +11,12 @@ def setup @exporter = Mittsu::ThreeMFExporter.new end + def test_filename_sanitization + @box.name = "" + name = @exporter.send(:filesystem_safe_name, @box) + assert_equal "box", name + end + def test_3mf_model_file file = @exporter.send(:model_file, @box) xml = REXML::Document.new file