lithop 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +9 -3
- data/RELEASING.md +3 -3
- data/docs/README.md +1 -0
- data/docs/artifacts.md +19 -2
- data/docs/bambu-studio.md +2 -0
- data/docs/color-model.md +2 -0
- data/docs/command.md +23 -5
- data/docs/config.md +30 -3
- data/docs/getting-started.md +5 -3
- data/docs/tiling.md +132 -0
- data/docs/troubleshooting.md +12 -0
- data/lib/lithop/calibration/export.rb +21 -8
- data/lib/lithop/cli/calibration.rb +3 -1
- data/lib/lithop/cli/image_options.rb +41 -2
- data/lib/lithop/cli.rb +8 -1
- data/lib/lithop/export/color_metadata.rb +55 -0
- data/lib/lithop/export/generation.rb +117 -20
- data/lib/lithop/export/job.rb +30 -16
- data/lib/lithop/export/metadata.rb +48 -3
- data/lib/lithop/export/planning.rb +97 -0
- data/lib/lithop/export/print_readme.rb +22 -4
- data/lib/lithop/export/result.rb +26 -4
- data/lib/lithop/formats/three_mf/bambu_metadata.rb +111 -24
- data/lib/lithop/formats/three_mf/model_writer.rb +245 -23
- data/lib/lithop/formats/three_mf/part_writer.rb +14 -6
- data/lib/lithop/formats/three_mf.rb +8 -5
- data/lib/lithop/image/pipeline.rb +17 -0
- data/lib/lithop/job_directory.rb +41 -0
- data/lib/lithop/job_execution.rb +250 -0
- data/lib/lithop/layer_structure.rb +11 -0
- data/lib/lithop/mesh/box_geometry.rb +41 -0
- data/lib/lithop/mesh/column_geometry.rb +2 -2
- data/lib/lithop/mesh/geometry.rb +21 -13
- data/lib/lithop/model/layer_region_model.rb +24 -0
- data/lib/lithop/reference_three_mf.rb +17 -1
- data/lib/lithop/settings.rb +116 -1
- data/lib/lithop/settings_file.rb +10 -4
- data/lib/lithop/tiling.rb +126 -0
- data/lib/lithop/version.rb +1 -1
- data/lib/lithop.rb +4 -0
- data/sig/lithop.rbs +77 -2
- metadata +6 -1
|
@@ -5,7 +5,10 @@ module Lithop
|
|
|
5
5
|
module PrintReadme
|
|
6
6
|
module_function
|
|
7
7
|
|
|
8
|
-
def write(
|
|
8
|
+
def write(
|
|
9
|
+
path, settings:, material_model:, tile_artifacts:, calibration_profile: nil,
|
|
10
|
+
profile: FilamentProfile.default
|
|
11
|
+
)
|
|
9
12
|
materials = Palette::MATERIALS.select { |material| material_model.materials.include?(material) }
|
|
10
13
|
names = materials.map { |material| profile_material(profile, material).name }
|
|
11
14
|
assignments = materials.map do |material|
|
|
@@ -18,14 +21,15 @@ module Lithop
|
|
|
18
21
|
This is #{calibration_profile ? "a measured-profile calibrated" : "an uncalibrated"} CMYW lithophane
|
|
19
22
|
generated in millimetres.
|
|
20
23
|
|
|
21
|
-
-
|
|
24
|
+
- #{model_instruction(tile_artifacts, names)}
|
|
22
25
|
#{calibration_line(calibration_profile)}
|
|
23
26
|
- Bambu Studio assignments encoded for this job: #{assignments.join(", ")}.
|
|
24
27
|
- The model uses #{settings.color_layer_count} CMY/W color layers #{base_layer_description(settings)}.
|
|
25
28
|
- The backlight-to-view color-layer order is #{settings.pigment_order.map(&:capitalize).join(" -> ")} -> White.
|
|
26
29
|
- White thickness above the color region follows #{thickness_method(settings, calibration_profile)}.
|
|
30
|
+
#{z_part_line(settings)}
|
|
27
31
|
- `preview.png` shows nominal recipe colors with relative thickness shading; it is not a colorimetric prediction.
|
|
28
|
-
- `transmission.png`, `thickness.png`, and the four material masks use the same cells as
|
|
32
|
+
- `transmission.png`, `thickness.png`, and the four material masks use the same cells as the 3MF output.
|
|
29
33
|
- `cmyk_white.stl` is a separate monochrome reference model, not the color model.
|
|
30
34
|
- Product: #{profile.product_url}
|
|
31
35
|
- Guide: #{profile.reference_url}
|
|
@@ -38,6 +42,20 @@ module Lithop
|
|
|
38
42
|
MARKDOWN
|
|
39
43
|
end
|
|
40
44
|
|
|
45
|
+
def model_instruction(tile_artifacts, names)
|
|
46
|
+
return "Open `lithop.3mf` for the physical color model; it contains: #{names.join(", ")}." if tile_artifacts.one?
|
|
47
|
+
|
|
48
|
+
"Open the #{tile_artifacts.length} 3MF files under `tiles/` and assemble them using " \
|
|
49
|
+
"`tile-layout.svg` and `manifest.json`; together they contain: #{names.join(", ")}."
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def z_part_line(settings)
|
|
53
|
+
return "- Z regions remain combined into the material parts." unless settings.separate_z_parts
|
|
54
|
+
|
|
55
|
+
"- Z regions use #{settings.color_region_parts + 1} plates: each CMYW color region includes " \
|
|
56
|
+
"a Base White carrier, followed by Top White; every plate starts at Z=0."
|
|
57
|
+
end
|
|
58
|
+
|
|
41
59
|
def profile_material(profile, material)
|
|
42
60
|
profile.materials.find { |candidate| candidate.channel == material }
|
|
43
61
|
end
|
|
@@ -63,7 +81,7 @@ module Lithop
|
|
|
63
81
|
"- Use a #{settings.nozzle_diameter} mm nozzle, #{settings.first_layer_height} mm base layers,\n " \
|
|
64
82
|
"#{settings.layer_height} mm color layers, and #{upper_height} mm upper White layers."
|
|
65
83
|
end
|
|
66
|
-
private_class_method :profile_material, :calibration_line, :thickness_method,
|
|
84
|
+
private_class_method :model_instruction, :z_part_line, :profile_material, :calibration_line, :thickness_method,
|
|
67
85
|
:base_layer_description, :layer_height_description
|
|
68
86
|
end
|
|
69
87
|
end
|
data/lib/lithop/export/result.rb
CHANGED
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Lithop
|
|
4
|
+
TileArtifact = Data.define(:tile, :name, :three_mf, :material_model)
|
|
5
|
+
|
|
4
6
|
ExportArtifacts = Data.define(
|
|
5
7
|
:stl_name,
|
|
6
|
-
:
|
|
8
|
+
:tile_artifacts,
|
|
7
9
|
:stl_triangle_count,
|
|
8
|
-
:
|
|
9
|
-
:
|
|
10
|
-
)
|
|
10
|
+
:material_model,
|
|
11
|
+
:tile_layout
|
|
12
|
+
) do
|
|
13
|
+
def three_mf_names = tile_artifacts.map(&:name)
|
|
14
|
+
|
|
15
|
+
def three_mf_name
|
|
16
|
+
three_mf_names.fetch(0) if three_mf_names.one?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def three_mf
|
|
20
|
+
results = tile_artifacts.map(&:three_mf)
|
|
21
|
+
counts = results.each_with_object(Hash.new(0)) do |result, totals|
|
|
22
|
+
result.triangle_counts_by_material.each { |material, count| totals[material] += count }
|
|
23
|
+
end
|
|
24
|
+
ThreeMF::Result.new(
|
|
25
|
+
vertex_count: results.sum(&:vertex_count),
|
|
26
|
+
triangle_count: results.sum(&:triangle_count),
|
|
27
|
+
material_count: counts.length,
|
|
28
|
+
object_count: results.sum(&:object_count),
|
|
29
|
+
triangle_counts_by_material: counts.freeze
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
11
33
|
|
|
12
34
|
ExportResult = Data.define(
|
|
13
35
|
:job_id,
|
|
@@ -4,14 +4,14 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Lithop
|
|
6
6
|
# Writes Bambu Studio material-assignment metadata into a 3MF package.
|
|
7
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
7
8
|
class ThreeMFBambuMetadata
|
|
8
9
|
PATH = "Metadata/model_settings.config"
|
|
9
|
-
FIRST_MODEL_OBJECT_INDEX = 1
|
|
10
10
|
|
|
11
|
-
def initialize(archive:,
|
|
11
|
+
def initialize(archive:, object_ids:, assemblies:, parts:, profile:, settings: nil)
|
|
12
12
|
@archive = archive
|
|
13
|
-
@assembly_id = assembly_id
|
|
14
13
|
@object_ids = object_ids
|
|
14
|
+
@assemblies = assemblies
|
|
15
15
|
@parts = parts
|
|
16
16
|
@profile = profile
|
|
17
17
|
@settings = settings
|
|
@@ -20,61 +20,148 @@ module Lithop
|
|
|
20
20
|
def write
|
|
21
21
|
@archive.put_next_entry(PATH)
|
|
22
22
|
@archive.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<config>\n")
|
|
23
|
-
@
|
|
24
|
-
@archive.write(" <metadata key=\"name\" value=\"Lithop CMYW lithophane\"/>\n")
|
|
23
|
+
@assemblies.each { |assembly| write_object(assembly) }
|
|
25
24
|
if bambu_project?
|
|
26
|
-
|
|
25
|
+
write_plates
|
|
26
|
+
write_assemble if separate_z_plates?
|
|
27
|
+
end
|
|
28
|
+
@archive.write("</config>\n")
|
|
29
|
+
write_project_entries if bambu_project?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def write_object(assembly)
|
|
35
|
+
@archive.write(" <object id=\"#{assembly.id}\">\n")
|
|
36
|
+
@archive.write(" <metadata key=\"name\" value=\"#{assembly.name}\"/>\n")
|
|
37
|
+
if bambu_project?
|
|
38
|
+
@archive.write(" <metadata key=\"extruder\" value=\"#{root_extruder(assembly)}\"/>\n")
|
|
27
39
|
@archive.write(" <metadata key=\"layer_height\" value=\"#{@settings.layer_height}\"/>\n")
|
|
28
40
|
@archive.write(" <metadata key=\"sparse_infill_density\" value=\"100%\"/>\n")
|
|
29
41
|
@archive.write(" <metadata key=\"sparse_infill_pattern\" value=\"zig-zag\"/>\n")
|
|
30
42
|
end
|
|
31
|
-
@object_ids.zip(@parts).
|
|
32
|
-
|
|
43
|
+
part_map = @object_ids.zip(@parts).to_h
|
|
44
|
+
assembly.object_ids.each_with_index do |object_id, volume_id|
|
|
45
|
+
part = part_map.fetch(object_id)
|
|
46
|
+
write_part(object_id, part, assembly, volume_id)
|
|
33
47
|
end
|
|
34
48
|
@archive.write(" </object>\n")
|
|
35
|
-
write_plate if bambu_project?
|
|
36
|
-
@archive.write("</config>\n")
|
|
37
|
-
write_project_entries if bambu_project?
|
|
38
49
|
end
|
|
39
50
|
|
|
40
|
-
|
|
51
|
+
def root_extruder(assembly)
|
|
52
|
+
return 1 if @assemblies.one?
|
|
53
|
+
|
|
54
|
+
part_map = @object_ids.zip(@parts).to_h
|
|
55
|
+
material = part_map.fetch(assembly.object_ids.first).material
|
|
56
|
+
@profile.materials.index { |candidate| candidate.channel == material } + 1
|
|
57
|
+
end
|
|
41
58
|
|
|
42
|
-
def write_part(object_id,
|
|
59
|
+
def write_part(object_id, part, assembly, volume_id)
|
|
60
|
+
material = part.material
|
|
43
61
|
profile_material = @profile.materials.find { |candidate| candidate.channel == material }
|
|
44
62
|
extruder = @profile.materials.index(profile_material) + 1
|
|
45
|
-
|
|
46
|
-
@archive.write("
|
|
63
|
+
uuid = separate_z_plates? ? %( uuid="#{part_uuid(object_id)}") : ""
|
|
64
|
+
@archive.write(" <part id=\"#{object_id}\" subtype=\"normal_part\"#{uuid}>\n")
|
|
65
|
+
@archive.write(" <metadata key=\"name\" value=\"#{part.name || profile_material.name}\"/>\n")
|
|
66
|
+
if separate_z_plates?
|
|
67
|
+
origin = assembly.component_origins.fetch(object_id)
|
|
68
|
+
@archive.write(" <metadata key=\"matrix\" value=\"#{matrix(origin)}\"/>\n")
|
|
69
|
+
@archive.write(" <metadata key=\"source_file\" value=\"lithop.3mf\"/>\n")
|
|
70
|
+
@archive.write(" <metadata key=\"source_object_id\" value=\"#{source_object_id(assembly)}\"/>\n")
|
|
71
|
+
@archive.write(" <metadata key=\"source_volume_id\" value=\"#{volume_id}\"/>\n")
|
|
72
|
+
@archive.write(" <metadata key=\"source_offset_x\" value=\"#{number(origin[0])}\"/>\n")
|
|
73
|
+
@archive.write(" <metadata key=\"source_offset_y\" value=\"#{number(origin[1])}\"/>\n")
|
|
74
|
+
source_z = number(origin[2] - part.build_z_offset)
|
|
75
|
+
@archive.write(" <metadata key=\"source_offset_z\" value=\"#{source_z}\"/>\n")
|
|
76
|
+
end
|
|
47
77
|
@archive.write(" <metadata key=\"extruder\" value=\"#{extruder}\"/>\n")
|
|
48
78
|
@archive.write(" </part>\n")
|
|
49
79
|
end
|
|
50
80
|
|
|
51
81
|
def bambu_project? = @settings&.bambu_project
|
|
52
82
|
|
|
53
|
-
def
|
|
54
|
-
|
|
83
|
+
def part_uuid(object_id)
|
|
84
|
+
format("%08x-7f79-4c88-9311-43ae0fc671e7", object_id)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def write_plates
|
|
88
|
+
@assemblies.each_with_index { |assembly, index| write_plate(assembly, index + 1) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def write_plate(assembly, plate_id)
|
|
92
|
+
name = @assemblies.one? ? "lithophane" : assembly.name
|
|
93
|
+
@archive.write(<<~XML.chomp)
|
|
55
94
|
<plate>
|
|
56
|
-
<metadata key="plater_id" value="
|
|
57
|
-
<metadata key="plater_name" value="
|
|
95
|
+
<metadata key="plater_id" value="#{plate_id}"/>
|
|
96
|
+
<metadata key="plater_name" value="#{name}"/>
|
|
58
97
|
<metadata key="locked" value="false"/>
|
|
59
98
|
<model_instance>
|
|
60
|
-
<metadata key="object_id" value="#{
|
|
99
|
+
<metadata key="object_id" value="#{assembly.id}"/>
|
|
61
100
|
<metadata key="instance_id" value="0"/>
|
|
62
|
-
</model_instance>
|
|
63
|
-
</plate>
|
|
64
101
|
XML
|
|
102
|
+
@archive.write("\n <metadata key=\"identify_id\" value=\"#{identify_id(plate_id)}\"/>") if separate_z_plates?
|
|
103
|
+
@archive.write("\n </model_instance>\n</plate>\n")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def write_assemble
|
|
107
|
+
@archive.write("<assemble>\n")
|
|
108
|
+
@assemblies.each do |assembly|
|
|
109
|
+
@archive.write(
|
|
110
|
+
" <assemble_item object_id=\"#{assembly.id}\" instance_id=\"0\" " \
|
|
111
|
+
"transform=\"1 0 0 0 1 0 0 0 1 0 0 0\" offset=\"0 0 0\"/>\n"
|
|
112
|
+
)
|
|
113
|
+
assembly.object_ids.each_with_index do |object_id, volume_id|
|
|
114
|
+
transform = volume_transform(assembly.component_origins.fetch(object_id))
|
|
115
|
+
@archive.write(
|
|
116
|
+
" <assemble_item object_id=\"#{assembly.id}\" volume_id=\"#{volume_id}\" " \
|
|
117
|
+
"transform=\"#{transform}\"/>\n"
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
@archive.write("</assemble>\n")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def separate_z_plates? = @settings&.separate_z_parts && @assemblies.length > 1
|
|
125
|
+
|
|
126
|
+
def identify_id(plate_id)
|
|
127
|
+
[218, 253, 276, 287].fetch(plate_id - 1, 287 + (plate_id * 32))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def volume_transform(origin)
|
|
131
|
+
"1 0 0 0 1 0 0 0 1 #{origin.map { |value| number(value) }.join(" ")}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def matrix(origin)
|
|
135
|
+
"1 0 0 #{number(origin[0])} 0 1 0 #{number(origin[1])} 0 0 1 #{number(origin[2])} 0 0 0 1"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def source_object_id(assembly)
|
|
139
|
+
return 3 if assembly.region == "top_white"
|
|
140
|
+
|
|
141
|
+
assembly.region.delete_prefix("color_region_").to_i - 1
|
|
65
142
|
end
|
|
66
143
|
|
|
144
|
+
def number(value) = format("%.9g", value)
|
|
145
|
+
|
|
67
146
|
def write_project_entries
|
|
68
147
|
write_entry("Metadata/layer_config_ranges.xml", layer_ranges_xml)
|
|
69
148
|
write_entry("Metadata/project_settings.config", "#{JSON.pretty_generate(project_settings)}\n")
|
|
70
149
|
end
|
|
71
150
|
|
|
72
151
|
def layer_ranges_xml
|
|
152
|
+
object_id = @assemblies.length
|
|
153
|
+
min_z = @settings.top_white_start
|
|
154
|
+
max_z = @settings.maximum_thickness
|
|
155
|
+
if @settings.separate_z_parts
|
|
156
|
+
color_end = @settings.layer_structure.color_region_end
|
|
157
|
+
min_z = (min_z - color_end).round(9)
|
|
158
|
+
max_z = (max_z - color_end).round(9)
|
|
159
|
+
end
|
|
73
160
|
<<~XML
|
|
74
161
|
<?xml version="1.0" encoding="utf-8"?>
|
|
75
162
|
<objects>
|
|
76
|
-
<object id="#{
|
|
77
|
-
<range min_z="#{
|
|
163
|
+
<object id="#{object_id}">
|
|
164
|
+
<range min_z="#{min_z}" max_z="#{max_z}">
|
|
78
165
|
<option opt_key="extruder">4</option>
|
|
79
166
|
<option opt_key="layer_height">#{@settings.top_layer_height}</option>
|
|
80
167
|
</range>
|
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Lithop
|
|
4
4
|
# Serializes model resources and build items into a 3MF model document.
|
|
5
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
5
6
|
class ThreeMFModelWriter
|
|
6
7
|
CUSTOM_NAMESPACE = "https://github.com/ogom/lithop/3mf/v1"
|
|
8
|
+
BAMBU_STUDIO_NAMESPACE = "http://schemas.bambulab.com/package/2021"
|
|
9
|
+
BAMBU_STUDIO_APPLICATION = "BambuStudio-02.08.03.66"
|
|
10
|
+
PRODUCTION_NAMESPACE = "http://schemas.microsoft.com/3dmanufacturing/production/2015/06"
|
|
11
|
+
MODEL_PATH = "3D/3dmodel.model"
|
|
7
12
|
BASE_MATERIALS_ID = 1
|
|
8
13
|
COLOR_GROUP_ID = 2
|
|
9
14
|
FIRST_OBJECT_ID = 3
|
|
10
15
|
Result = Data.define(:vertex_count, :triangle_count, :triangle_counts_by_material)
|
|
16
|
+
Assembly = Data.define(:id, :name, :region, :object_ids, :component_origins)
|
|
11
17
|
|
|
12
18
|
def initialize(archive, parts, profile, instrumentation: Instrumentation.new, settings: nil)
|
|
13
19
|
@archive = archive
|
|
@@ -18,23 +24,20 @@ module Lithop
|
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
def write
|
|
27
|
+
object_ids = part_object_ids
|
|
28
|
+
assemblies = build_assemblies(object_ids)
|
|
29
|
+
return write_bambu_multi_plate(object_ids, assemblies) if bambu_multi_plate_project?
|
|
30
|
+
|
|
21
31
|
measure_xml do
|
|
32
|
+
@archive.put_next_entry(MODEL_PATH)
|
|
22
33
|
@archive.write(model_header)
|
|
23
34
|
write_materials
|
|
24
35
|
end
|
|
25
|
-
|
|
26
|
-
assembly_id = object_ids.max + 1
|
|
36
|
+
result = write_parts(object_ids)
|
|
27
37
|
measure_xml do
|
|
28
|
-
write_assembly(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
archive: @archive,
|
|
32
|
-
assembly_id:,
|
|
33
|
-
object_ids:,
|
|
34
|
-
parts: @parts,
|
|
35
|
-
profile: @profile,
|
|
36
|
-
settings: @settings
|
|
37
|
-
).write
|
|
38
|
+
assemblies.each { |assembly| write_assembly(assembly) }
|
|
39
|
+
write_build(assemblies)
|
|
40
|
+
write_bambu_metadata(object_ids, assemblies)
|
|
38
41
|
end
|
|
39
42
|
result
|
|
40
43
|
end
|
|
@@ -49,42 +52,227 @@ module Lithop
|
|
|
49
52
|
@archive.write(" </m:colorgroup>\n")
|
|
50
53
|
end
|
|
51
54
|
|
|
52
|
-
def
|
|
55
|
+
def part_object_ids
|
|
56
|
+
return @parts.each_index.map { |index| index + FIRST_OBJECT_ID }.freeze unless bambu_multi_plate_project?
|
|
57
|
+
|
|
58
|
+
next_id = 1
|
|
59
|
+
@parts.group_by(&:region).values.flat_map do |parts|
|
|
60
|
+
ids = Array.new(parts.length) do
|
|
61
|
+
value = next_id
|
|
62
|
+
next_id += 1
|
|
63
|
+
value
|
|
64
|
+
end
|
|
65
|
+
next_id += 1
|
|
66
|
+
ids
|
|
67
|
+
end.freeze
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def write_parts(object_ids)
|
|
53
71
|
vertex_count = 0
|
|
54
72
|
triangle_counts = {}
|
|
55
|
-
object_ids
|
|
56
|
-
object_id = index + FIRST_OBJECT_ID
|
|
73
|
+
object_ids.zip(@parts).each do |object_id, part|
|
|
57
74
|
mesh = @instrumentation.measure(:three_mf_indexing) do
|
|
58
75
|
ThreeMFMesh.new(triangles: part.triangles)
|
|
59
76
|
end
|
|
60
77
|
measure_xml do
|
|
61
|
-
ThreeMFPartWriter.new(archive: @archive, profile: @profile).write(object_id, part
|
|
78
|
+
ThreeMFPartWriter.new(archive: @archive, profile: @profile).write(object_id, part, mesh)
|
|
62
79
|
end
|
|
63
80
|
part.triangles.release_cache! if part.triangles.respond_to?(:release_cache!)
|
|
64
81
|
vertex_count += mesh.vertices.length
|
|
65
|
-
triangle_counts[part.material] = mesh.triangle_count
|
|
66
|
-
object_id
|
|
82
|
+
triangle_counts[part.material] = triangle_counts.fetch(part.material, 0) + mesh.triangle_count
|
|
67
83
|
end
|
|
68
|
-
|
|
84
|
+
Result.new(
|
|
69
85
|
vertex_count:,
|
|
70
86
|
triangle_count: triangle_counts.values.sum,
|
|
71
87
|
triangle_counts_by_material: triangle_counts.freeze
|
|
72
88
|
)
|
|
73
|
-
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def write_bambu_multi_plate(object_ids, assemblies)
|
|
92
|
+
result = write_external_parts(object_ids, assemblies)
|
|
93
|
+
write_root_model(assemblies)
|
|
94
|
+
write_model_relationships(assemblies)
|
|
95
|
+
write_bambu_metadata(object_ids, assemblies)
|
|
96
|
+
result
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
100
|
+
def write_external_parts(object_ids, assemblies)
|
|
101
|
+
parts_by_object_id = object_ids.zip(@parts).to_h
|
|
102
|
+
vertex_count = 0
|
|
103
|
+
triangle_counts = {}
|
|
104
|
+
assemblies.each_with_index do |assembly, index|
|
|
105
|
+
@archive.put_next_entry("3D/Objects/object_#{index + 1}.model")
|
|
106
|
+
measure_xml do
|
|
107
|
+
@archive.write(submodel_header)
|
|
108
|
+
write_materials
|
|
109
|
+
assembly.object_ids.each do |object_id|
|
|
110
|
+
part = parts_by_object_id.fetch(object_id)
|
|
111
|
+
mesh = @instrumentation.measure(:three_mf_indexing) do
|
|
112
|
+
ThreeMFMesh.new(triangles: part.triangles)
|
|
113
|
+
end
|
|
114
|
+
center = mesh_center(mesh)
|
|
115
|
+
assembly.component_origins[object_id] = component_origin(
|
|
116
|
+
center, assembly.component_origins.fetch(object_id)
|
|
117
|
+
)
|
|
118
|
+
ThreeMFPartWriter.new(
|
|
119
|
+
archive: @archive, profile: @profile, production: true, origin: center
|
|
120
|
+
).write(object_id, part, mesh)
|
|
121
|
+
part.triangles.release_cache! if part.triangles.respond_to?(:release_cache!)
|
|
122
|
+
vertex_count += mesh.vertices.length
|
|
123
|
+
triangle_counts[part.material] = triangle_counts.fetch(part.material, 0) + mesh.triangle_count
|
|
124
|
+
end
|
|
125
|
+
@archive.write(" </resources>\n <build/>\n</model>\n")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
Result.new(
|
|
129
|
+
vertex_count:,
|
|
130
|
+
triangle_count: triangle_counts.values.sum,
|
|
131
|
+
triangle_counts_by_material: triangle_counts.freeze
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def write_root_model(assemblies)
|
|
136
|
+
@archive.put_next_entry(MODEL_PATH)
|
|
137
|
+
measure_xml do
|
|
138
|
+
@archive.write(bambu_multi_plate_model_header)
|
|
139
|
+
assemblies.each_with_index { |assembly, index| write_assembly(assembly, index + 1) }
|
|
140
|
+
write_build(assemblies)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def write_model_relationships(assemblies)
|
|
145
|
+
@archive.put_next_entry("3D/_rels/3dmodel.model.rels")
|
|
146
|
+
@archive.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
|
|
147
|
+
@archive.write("<Relationships xmlns=\"#{ThreeMF::RELATIONSHIP_NAMESPACE}\">\n")
|
|
148
|
+
assemblies.each_with_index do |_assembly, index|
|
|
149
|
+
@archive.write(
|
|
150
|
+
" <Relationship Target=\"/3D/Objects/object_#{index + 1}.model\" Id=\"rel-#{index + 1}\" " \
|
|
151
|
+
"Type=\"#{ThreeMF::MODEL_RELATIONSHIP}\"/>\n"
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
@archive.write("</Relationships>\n")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def write_bambu_metadata(object_ids, assemblies)
|
|
158
|
+
ThreeMFBambuMetadata.new(
|
|
159
|
+
archive: @archive,
|
|
160
|
+
object_ids:,
|
|
161
|
+
assemblies:,
|
|
162
|
+
parts: @parts,
|
|
163
|
+
profile: @profile,
|
|
164
|
+
settings: @settings
|
|
165
|
+
).write
|
|
74
166
|
end
|
|
75
167
|
|
|
76
168
|
def measure_xml(&)
|
|
77
169
|
@instrumentation.measure(:three_mf_xml_zip, &)
|
|
78
170
|
end
|
|
79
171
|
|
|
80
|
-
def
|
|
81
|
-
|
|
172
|
+
def build_assemblies(object_ids)
|
|
173
|
+
groups = if separate_z_plates?
|
|
174
|
+
object_ids.zip(@parts).group_by { |_object_id, part| part.region }
|
|
175
|
+
else
|
|
176
|
+
{ nil => object_ids.zip(@parts) }
|
|
177
|
+
end
|
|
178
|
+
groups.map do |region, entries|
|
|
179
|
+
Assembly.new(
|
|
180
|
+
id: entries.map(&:first).max + 1,
|
|
181
|
+
name: assembly_name(region),
|
|
182
|
+
region:,
|
|
183
|
+
object_ids: entries.map(&:first).freeze,
|
|
184
|
+
component_origins: entries.to_h { |object_id, part| [object_id, [0.0, 0.0, part.build_z_offset]] }
|
|
185
|
+
)
|
|
186
|
+
end.freeze
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def separate_z_plates?
|
|
190
|
+
@settings&.separate_z_parts && @parts.all?(&:region)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def bambu_multi_plate_project? = @settings&.bambu_project && separate_z_plates?
|
|
194
|
+
|
|
195
|
+
def assembly_name(region)
|
|
196
|
+
return "Lithop CMYW lithophane" unless region
|
|
197
|
+
return "Top White" if region == "top_white"
|
|
198
|
+
|
|
199
|
+
region.sub("color_region_", "Color region ")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def write_assembly(assembly, submodel_index = nil)
|
|
203
|
+
uuid = submodel_index ? production_uuid(assembly.id) : ""
|
|
204
|
+
@archive.write(" <object id=\"#{assembly.id}\"#{uuid} type=\"model\" name=\"#{assembly.name}\">\n")
|
|
82
205
|
@archive.write(" <components>\n")
|
|
83
|
-
object_ids.
|
|
206
|
+
assembly.object_ids.each_with_index do |object_id, index|
|
|
207
|
+
transform = component_transform(assembly.component_origins.fetch(object_id))
|
|
208
|
+
path = submodel_index ? %( p:path="/3D/Objects/object_#{submodel_index}.model") : ""
|
|
209
|
+
uuid = submodel_index ? production_uuid((assembly.id * 1000) + index + 1) : ""
|
|
210
|
+
@archive.write(" <component#{path} objectid=\"#{object_id}\"#{uuid}#{transform}/>\n")
|
|
211
|
+
end
|
|
84
212
|
@archive.write(" </components>\n </object>\n")
|
|
85
213
|
end
|
|
86
214
|
|
|
215
|
+
def component_transform(origin)
|
|
216
|
+
return "" if origin.all?(&:zero?)
|
|
217
|
+
|
|
218
|
+
%( transform="1 0 0 0 1 0 0 0 1 #{origin.map { |value| format_number(value) }.join(" ")}")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def format_number(number) = format("%.9g", number)
|
|
222
|
+
|
|
223
|
+
def write_build(assemblies)
|
|
224
|
+
@archive.write(" </resources>\n <build>\n")
|
|
225
|
+
assemblies.each do |assembly|
|
|
226
|
+
attributes = if bambu_multi_plate_project?
|
|
227
|
+
%(#{build_uuid(assembly.id)} transform="#{build_transform(assembly)}" printable="1")
|
|
228
|
+
else
|
|
229
|
+
""
|
|
230
|
+
end
|
|
231
|
+
@archive.write(" <item objectid=\"#{assembly.id}\"#{attributes}/>\n")
|
|
232
|
+
end
|
|
233
|
+
@archive.write(" </build>\n</model>\n")
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def production_uuid(identifier)
|
|
237
|
+
%( p:UUID="#{format("%08x", identifier)}-61cb-4c03-9d28-80fed5dfa1dc")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def build_uuid(identifier)
|
|
241
|
+
%( p:UUID="#{format("%08x", identifier)}-b1ec-4553-aec9-835e5b724bb4")
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def build_transform(assembly)
|
|
245
|
+
x, y = [
|
|
246
|
+
[48.0, 30.0],
|
|
247
|
+
[300.160253, 37.8003226],
|
|
248
|
+
[52.2484968, -205.366274],
|
|
249
|
+
[288.0, -210.0]
|
|
250
|
+
].fetch(plate_index(assembly))
|
|
251
|
+
"1 0 0 0 1 0 0 0 1 #{format_number(x)} #{format_number(y)} 0"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def mesh_center(mesh)
|
|
255
|
+
axes = mesh.vertices.transpose
|
|
256
|
+
axes.map { |values| (values.min + values.max) / 2.0 }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def component_origin(center, base_origin)
|
|
260
|
+
[center[0], center[1], center[2] + base_origin[2]]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def plate_index(assembly)
|
|
264
|
+
return 3 if assembly.region == "top_white"
|
|
265
|
+
|
|
266
|
+
assembly.region.delete_prefix("color_region_").to_i - 1
|
|
267
|
+
end
|
|
268
|
+
|
|
87
269
|
def model_header
|
|
270
|
+
return bambu_multi_plate_model_header if bambu_multi_plate_project?
|
|
271
|
+
|
|
272
|
+
inline_model_header
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def inline_model_header
|
|
88
276
|
<<~XML
|
|
89
277
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
90
278
|
<model unit="millimeter" xml:lang="en-US" requiredextensions="m"
|
|
@@ -101,6 +289,40 @@ module Lithop
|
|
|
101
289
|
XML
|
|
102
290
|
end
|
|
103
291
|
|
|
292
|
+
def bambu_multi_plate_model_header
|
|
293
|
+
<<~XML
|
|
294
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
295
|
+
<model unit="millimeter" xml:lang="en-US" requiredextensions="p"
|
|
296
|
+
xmlns="#{ThreeMF::CORE_NAMESPACE}"
|
|
297
|
+
xmlns:BambuStudio="#{BAMBU_STUDIO_NAMESPACE}"
|
|
298
|
+
xmlns:p="#{PRODUCTION_NAMESPACE}"
|
|
299
|
+
xmlns:lithop="#{CUSTOM_NAMESPACE}">
|
|
300
|
+
<metadata name="Title">Lithop CMYW lithophane</metadata>
|
|
301
|
+
<metadata name="Application">#{BAMBU_STUDIO_APPLICATION}</metadata>
|
|
302
|
+
<metadata name="BambuStudio:3mfVersion">1</metadata>
|
|
303
|
+
<metadata name="lithop:Application" preserve="1">Lithop #{Lithop::VERSION}</metadata>
|
|
304
|
+
<metadata name="Description">Physical CMY subtractive layers with variable White luminance thickness.</metadata>
|
|
305
|
+
<metadata name="lithop:FilamentBundle" preserve="1">#{@profile.name}</metadata>
|
|
306
|
+
<metadata name="lithop:FilamentProduct" preserve="1">#{@profile.product_url}</metadata>
|
|
307
|
+
<metadata name="lithop:PrintingGuide" preserve="1">#{@profile.reference_url}</metadata>
|
|
308
|
+
<resources>
|
|
309
|
+
XML
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def submodel_header
|
|
313
|
+
<<~XML
|
|
314
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
315
|
+
<model unit="millimeter" xml:lang="en-US" requiredextensions="m p"
|
|
316
|
+
xmlns="#{ThreeMF::CORE_NAMESPACE}"
|
|
317
|
+
xmlns:m="#{ThreeMF::MATERIAL_NAMESPACE}"
|
|
318
|
+
xmlns:BambuStudio="#{BAMBU_STUDIO_NAMESPACE}"
|
|
319
|
+
xmlns:p="#{PRODUCTION_NAMESPACE}"
|
|
320
|
+
xmlns:lithop="#{CUSTOM_NAMESPACE}">
|
|
321
|
+
<metadata name="BambuStudio:3mfVersion">1</metadata>
|
|
322
|
+
<resources>
|
|
323
|
+
XML
|
|
324
|
+
end
|
|
325
|
+
|
|
104
326
|
def material_xml(material)
|
|
105
327
|
name = "#{@profile.manufacturer} #{material.name} (#{material.color_code})"
|
|
106
328
|
<<~XML
|
|
@@ -3,18 +3,23 @@
|
|
|
3
3
|
module Lithop
|
|
4
4
|
# Serializes one indexed material mesh into a 3MF model document.
|
|
5
5
|
class ThreeMFPartWriter
|
|
6
|
-
def initialize(archive:, profile:)
|
|
6
|
+
def initialize(archive:, profile:, production: false, origin: nil)
|
|
7
7
|
@archive = archive
|
|
8
8
|
@profile = profile
|
|
9
|
+
@production = production
|
|
10
|
+
@origin = origin || [0.0, 0.0, 0.0]
|
|
9
11
|
end
|
|
10
12
|
|
|
11
|
-
def write(object_id,
|
|
13
|
+
def write(object_id, part, mesh)
|
|
14
|
+
material = part.material
|
|
12
15
|
material_index = @profile.materials.index { |candidate| candidate.channel == material }
|
|
13
|
-
name = @profile.materials.fetch(material_index).name
|
|
16
|
+
name = part.name || @profile.materials.fetch(material_index).name
|
|
17
|
+
region = part.region ? " lithop:region=\"#{part.region}\"" : ""
|
|
18
|
+
uuid = @production ? %( p:UUID="00000000-0000-4000-8000-#{format("%012d", object_id)}") : ""
|
|
14
19
|
@archive.write(
|
|
15
|
-
" <object id=\"#{object_id}\" type=\"model\" name=\"#{name}\" " \
|
|
20
|
+
" <object id=\"#{object_id}\"#{uuid} type=\"model\" name=\"#{name}\" " \
|
|
16
21
|
"pid=\"#{ThreeMFModelWriter::COLOR_GROUP_ID}\" pindex=\"#{material_index}\" " \
|
|
17
|
-
"lithop:channel=\"#{material}\">\n"
|
|
22
|
+
"lithop:channel=\"#{material}\"#{region}>\n"
|
|
18
23
|
)
|
|
19
24
|
@archive.write(" <mesh>\n <vertices>\n")
|
|
20
25
|
write_vertices(mesh)
|
|
@@ -27,7 +32,10 @@ module Lithop
|
|
|
27
32
|
|
|
28
33
|
def write_vertices(mesh)
|
|
29
34
|
mesh.vertices.each do |x, y, z|
|
|
30
|
-
@archive.write(
|
|
35
|
+
@archive.write(
|
|
36
|
+
" <vertex x=\"#{number(x - @origin[0])}\" y=\"#{number(y - @origin[1])}\" " \
|
|
37
|
+
"z=\"#{number(z - @origin[2])}\"/>\n"
|
|
38
|
+
)
|
|
31
39
|
end
|
|
32
40
|
end
|
|
33
41
|
|