Initial MoonModel implementation
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "cgi"
|
||||
require "zip"
|
||||
|
||||
module MoonModel
|
||||
class ThreeMF
|
||||
CONTENT_TYPES = <<~XML.freeze
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||
<Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>
|
||||
</Types>
|
||||
XML
|
||||
RELATIONSHIPS = <<~XML.freeze
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>
|
||||
</Relationships>
|
||||
XML
|
||||
|
||||
def self.write(path, meshes, metadata: {}, palette: [])
|
||||
raise ArgumentError, "3MF needs at least one mesh" if meshes.empty?
|
||||
meshes.each { |mesh| raise ArgumentError, "invalid mesh #{mesh.name}" unless mesh.valid? }
|
||||
|
||||
materials = ordered_materials(meshes, palette)
|
||||
model = build_model(meshes, metadata, materials)
|
||||
model_settings = build_bambu_model_settings(meshes, metadata, materials)
|
||||
FileUtils.mkdir_p(File.dirname(path))
|
||||
Zip::File.open(path, create: true) do |zip|
|
||||
zip.get_output_stream("[Content_Types].xml") { |io| io.write(CONTENT_TYPES) }
|
||||
zip.get_output_stream("_rels/.rels") { |io| io.write(RELATIONSHIPS) }
|
||||
zip.get_output_stream("3D/3dmodel.model") { |io| io.write(model) }
|
||||
zip.get_output_stream("Metadata/model_settings.config") { |io| io.write(model_settings) }
|
||||
end
|
||||
path
|
||||
end
|
||||
|
||||
def self.build_model(meshes, metadata, materials = ordered_materials(meshes, []))
|
||||
material_index = materials.each_with_index.to_h { |item, index| [[item["name"], item["hex"]], index] }
|
||||
labels = mesh_labels(meshes, materials)
|
||||
xml = +%(<?xml version="1.0" encoding="UTF-8"?>\n)
|
||||
xml << %(<model unit="millimeter" xml:lang="en-US" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">\n)
|
||||
metadata.each { |key, value| xml << %(<metadata name="#{escape(key)}">#{escape(value)}</metadata>\n) }
|
||||
xml << "<resources>\n"
|
||||
unless materials.empty?
|
||||
xml << %(<basematerials id="1">\n)
|
||||
materials.each { |item| xml << %(<base name="#{escape(item["name"])}" displaycolor="#{escape(item["hex"].upcase)}"/>\n) }
|
||||
xml << "</basematerials>\n"
|
||||
end
|
||||
meshes.each_with_index do |mesh, index|
|
||||
id = index + 2
|
||||
attrs = mesh.material ? %( pid="1" pindex="#{material_index.fetch([mesh.material["name"], mesh.material["hex"]])}") : ""
|
||||
xml << %(<object id="#{id}" name="#{escape(labels[index])}" type="model"#{attrs}><mesh><vertices>\n)
|
||||
mesh.vertices.each { |x, y, z| xml << %(<vertex x="#{fmt(x)}" y="#{fmt(y)}" z="#{fmt(z)}"/>\n) }
|
||||
xml << "</vertices><triangles>\n"
|
||||
mesh.triangles.each { |a, b, c| xml << %(<triangle v1="#{a}" v2="#{b}" v3="#{c}"/>\n) }
|
||||
xml << "</triangles></mesh></object>\n"
|
||||
end
|
||||
assembly_id = meshes.length + 2
|
||||
if meshes.length > 1
|
||||
xml << %(<object id="#{assembly_id}" name="#{escape(wrapper_name(metadata))}" type="model"><components>\n)
|
||||
meshes.each_index { |index| xml << %(<component objectid="#{index + 2}"/>\n) }
|
||||
xml << "</components></object>\n"
|
||||
end
|
||||
xml << "</resources><build>\n"
|
||||
xml << %(<item objectid="#{meshes.length > 1 ? assembly_id : 2}"/>\n)
|
||||
xml << "</build></model>\n"
|
||||
xml
|
||||
end
|
||||
|
||||
def self.ordered_materials(meshes, palette)
|
||||
used = meshes.filter_map(&:material).uniq { |item| [item["name"], item["hex"]] }
|
||||
(palette + used).uniq { |item| [item["name"], item["hex"].upcase] }
|
||||
end
|
||||
|
||||
def self.mesh_labels(meshes, materials)
|
||||
meshes.map do |mesh|
|
||||
unless mesh.material
|
||||
next "#{humanize(mesh.name)} — material unassigned"
|
||||
end
|
||||
slot = materials.index { |item| item["name"] == mesh.material["name"] && item["hex"].casecmp?(mesh.material["hex"]) } + 1
|
||||
role = if mesh.name.end_with?("_structure")
|
||||
"Structure"
|
||||
elsif mesh.name == "assembly_keys"
|
||||
"Assembly keys"
|
||||
else
|
||||
"Surface"
|
||||
end
|
||||
"#{role} — #{mesh.material["name"]} (#{mesh.material["hex"].upcase}) — Filament #{slot}"
|
||||
end
|
||||
end
|
||||
|
||||
def self.build_bambu_model_settings(meshes, metadata, materials)
|
||||
labels = mesh_labels(meshes, materials)
|
||||
object_id = meshes.length > 1 ? meshes.length + 2 : 2
|
||||
xml = +%(<?xml version="1.0" encoding="UTF-8"?>\n<config>\n)
|
||||
xml << %( <object id="#{object_id}">\n)
|
||||
xml << %( <metadata key="name" value="#{escape(wrapper_name(metadata))}"/>\n)
|
||||
meshes.each_with_index do |mesh, index|
|
||||
xml << %( <part id="#{index + 2}" subtype="normal_part">\n)
|
||||
xml << %( <metadata key="name" value="#{escape(labels[index])}"/>\n)
|
||||
if mesh.material
|
||||
slot = materials.index { |item| item["name"] == mesh.material["name"] && item["hex"].casecmp?(mesh.material["hex"]) } + 1
|
||||
xml << %( <metadata key="extruder" value="#{slot}"/>\n)
|
||||
end
|
||||
xml << " </part>\n"
|
||||
end
|
||||
xml << " </object>\n <assemble/>\n</config>\n"
|
||||
xml
|
||||
end
|
||||
|
||||
def self.wrapper_name(metadata)
|
||||
title = metadata.fetch("Title", "Moon")
|
||||
part = metadata["Part"]
|
||||
part && !part.empty? ? "#{title} — #{part}" : title
|
||||
end
|
||||
|
||||
def self.humanize(value) = value.to_s.tr("_", " ").split.map(&:capitalize).join(" ")
|
||||
|
||||
def self.escape(value) = CGI.escapeHTML(value.to_s)
|
||||
def self.fmt(value) = format("%.6f", value).sub(/\.?0+\z/, "")
|
||||
private_class_method :escape, :fmt, :ordered_materials, :mesh_labels, :build_bambu_model_settings,
|
||||
:wrapper_name, :humanize
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user