lithop 0.2.0 → 0.4.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 +14 -0
- data/Cargo.lock +284 -0
- data/Cargo.toml +6 -0
- data/README.md +34 -13
- data/RELEASING.md +24 -6
- data/docs/README.md +6 -3
- data/docs/artifacts.md +35 -16
- data/docs/bambu-studio.md +17 -11
- data/docs/calibration.md +4 -4
- data/docs/color-model.md +20 -18
- data/docs/command.md +53 -14
- data/docs/config.md +43 -31
- data/docs/getting-started.md +17 -8
- data/docs/images/cmyw-lithophane-preview.png +0 -0
- data/docs/local-ui.md +117 -0
- data/docs/tiling.md +20 -19
- data/docs/troubleshooting.md +12 -2
- data/ext/lithop_native/Cargo.toml +17 -0
- data/ext/lithop_native/build.rs +4 -0
- data/ext/lithop_native/extconf.rb +6 -0
- data/ext/lithop_native/src/column_mesh.rs +388 -0
- data/ext/lithop_native/src/lib.rs +37 -0
- data/ext/lithop_native/src/mesh.rs +187 -0
- data/ext/lithop_native/src/palette.rs +67 -0
- data/ext/lithop_native/src/xml.rs +134 -0
- data/lib/lithop/calibration/export.rb +2 -2
- data/lib/lithop/calibration/target.rb +5 -5
- data/lib/lithop/cli/image_adjustment_options.rb +33 -0
- data/lib/lithop/cli/image_options.rb +5 -18
- data/lib/lithop/cli/options.rb +17 -4
- data/lib/lithop/cli.rb +28 -0
- data/lib/lithop/export/color_metadata.rb +46 -23
- data/lib/lithop/export/generation.rb +38 -27
- data/lib/lithop/export/job.rb +2 -2
- data/lib/lithop/export/metadata.rb +8 -3
- data/lib/lithop/export/planning.rb +82 -45
- data/lib/lithop/export/previews.rb +25 -1
- data/lib/lithop/export/print_readme.rb +20 -4
- data/lib/lithop/formats/three_mf/bambu_metadata.rb +11 -8
- data/lib/lithop/formats/three_mf/mesh.rb +72 -3
- data/lib/lithop/formats/three_mf/model_writer.rb +21 -21
- data/lib/lithop/formats/three_mf/part_writer.rb +37 -0
- data/lib/lithop/formats/three_mf.rb +1 -1
- data/lib/lithop/image/pipeline.rb +84 -31
- data/lib/lithop/image/svg_loader.rb +16 -7
- data/lib/lithop/job_execution.rb +132 -4
- data/lib/lithop/layer_structure.rb +0 -9
- data/lib/lithop/mesh/column_geometry.rb +8 -0
- data/lib/lithop/mesh/column_topology.rb +2 -0
- data/lib/lithop/model/filament_profile.rb +3 -3
- data/lib/lithop/model/material_display_map.rb +2 -1
- data/lib/lithop/model/material_model.rb +17 -5
- data/lib/lithop/model/palette.rb +21 -3
- data/lib/lithop/model/z_region_model.rb +44 -0
- data/lib/lithop/native.rb +39 -0
- data/lib/lithop/settings.rb +147 -15
- data/lib/lithop/settings_file.rb +1 -0
- data/lib/lithop/tiling.rb +3 -2
- data/lib/lithop/version.rb +1 -1
- data/lib/lithop/web/app.rb +517 -0
- data/lib/lithop/web/bambu_studio_launcher.rb +82 -0
- data/lib/lithop/web/job_launcher.rb +214 -0
- data/lib/lithop/web/job_store.rb +390 -0
- data/lib/lithop/web/preview_store.rb +182 -0
- data/lib/lithop/web/public/app.css +330 -0
- data/lib/lithop/web/public/app.js +1949 -0
- data/lib/lithop/web/server.rb +21 -0
- data/lib/lithop/web/settings_form.rb +67 -0
- data/lib/lithop/web/settings_library.rb +154 -0
- data/lib/lithop/web/views/index.erb +418 -0
- data/lib/lithop/web/worker.rb +27 -0
- data/lib/lithop.rb +3 -1
- data/sig/lithop.rbs +41 -0
- metadata +86 -3
- data/lib/lithop/model/layer_region_model.rb +0 -24
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Lithop
|
|
4
|
-
#
|
|
4
|
+
# Shared mesh counting and conversion helpers for export plans.
|
|
5
|
+
module MaterialMeshPlanningHelpers
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def triangle_count(geometry)
|
|
9
|
+
geometry.respond_to?(:triangle_count) ? geometry.triangle_count : geometry.each_triangle.count
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def triangle_source(geometry)
|
|
13
|
+
geometry.is_a?(ColumnGeometry) ? geometry : geometry.each_triangle
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def counts_by_material(entries, entry_counts)
|
|
17
|
+
entries.zip(entry_counts).each_with_object(Hash.new(0)) do |(entry, count), totals|
|
|
18
|
+
totals[entry.material] += count
|
|
19
|
+
end.freeze
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Slices the CMYW model into contiguous plates over strength-adjusted White supports.
|
|
5
24
|
class ZSeparatedMaterialMeshPlan
|
|
25
|
+
include MaterialMeshPlanningHelpers
|
|
26
|
+
|
|
6
27
|
Entry = Data.define(:material, :geometry, :name, :region, :build_z_offset)
|
|
7
28
|
|
|
8
|
-
def initialize(model:,
|
|
29
|
+
def initialize(model:, top_geometry:, settings:, maximum_triangles:, limit_message:)
|
|
9
30
|
@model = model
|
|
10
|
-
@base_geometry = base_geometry
|
|
11
31
|
@top_geometry = top_geometry
|
|
12
32
|
@settings = settings
|
|
13
33
|
@maximum_triangles = maximum_triangles
|
|
@@ -36,42 +56,73 @@ module Lithop
|
|
|
36
56
|
|
|
37
57
|
def build_entries
|
|
38
58
|
entries = []
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
build_z_offset = @settings.
|
|
43
|
-
entries
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
plate_ranges.each_with_index do |plate_range, index|
|
|
60
|
+
region = "z_part_#{index + 1}"
|
|
61
|
+
source_z_min = plate_range.begin
|
|
62
|
+
build_z_offset = @settings.z_part_base_white_thickness(index) - source_z_min
|
|
63
|
+
append_base_white_entry(entries, index:, region:)
|
|
64
|
+
append_color_entries(entries, index:, region:, build_z_offset:)
|
|
65
|
+
append_top_white_entry(entries, index:, plate_range:, region:, build_z_offset:)
|
|
66
|
+
end
|
|
67
|
+
entries
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def append_base_white_entry(entries, index:, region:)
|
|
71
|
+
range = 0.0...@settings.z_part_base_white_thickness(index)
|
|
72
|
+
entries << Entry.new(
|
|
73
|
+
material: :white, geometry: box_geometry(range),
|
|
74
|
+
name: "Z plate #{index + 1} Base White", region:, build_z_offset: 0.0
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def append_color_entries(entries, index:, region:, build_z_offset:)
|
|
79
|
+
range = color_ranges.fetch(index)
|
|
80
|
+
return unless range.begin < range.end
|
|
81
|
+
|
|
82
|
+
region_model = ZRegionModel.new(model: @model, z_range: range)
|
|
83
|
+
entries << Entry.new(
|
|
84
|
+
material: :white, geometry: color_region_white_geometry(region_model),
|
|
85
|
+
name: "Z plate #{index + 1} White", region:, build_z_offset:
|
|
86
|
+
)
|
|
87
|
+
Palette::PIGMENTS.each do |material|
|
|
88
|
+
next unless region_model.materials.include?(material)
|
|
89
|
+
|
|
47
90
|
entries << Entry.new(
|
|
48
|
-
material: :
|
|
49
|
-
|
|
50
|
-
name: "Color region #{index + 1} White",
|
|
51
|
-
region:,
|
|
52
|
-
build_z_offset:
|
|
91
|
+
material:, geometry: ColumnGeometry.new(model: region_model, material:),
|
|
92
|
+
name: "Z plate #{index + 1} #{material.to_s.capitalize}", region:, build_z_offset:
|
|
53
93
|
)
|
|
54
|
-
Palette::PIGMENTS.each do |material|
|
|
55
|
-
next unless region_model.materials.include?(material)
|
|
56
|
-
|
|
57
|
-
entries << Entry.new(
|
|
58
|
-
material:,
|
|
59
|
-
geometry: ColumnGeometry.new(model: region_model, material:),
|
|
60
|
-
name: "Color region #{index + 1} #{material.to_s.capitalize}",
|
|
61
|
-
region:,
|
|
62
|
-
build_z_offset:
|
|
63
|
-
)
|
|
64
|
-
end
|
|
65
94
|
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def append_top_white_entry(entries, index:, plate_range:, region:, build_z_offset:)
|
|
98
|
+
top_start = @settings.layer_structure.color_region_end
|
|
99
|
+
range = intersection(plate_range, top_start...@settings.z_separated_source_end)
|
|
100
|
+
return unless range
|
|
101
|
+
|
|
102
|
+
geometry = index == plate_ranges.length - 1 ? @top_geometry : box_geometry(range)
|
|
66
103
|
entries << Entry.new(
|
|
67
|
-
material: :white, geometry
|
|
68
|
-
region
|
|
104
|
+
material: :white, geometry:, name: "Z plate #{index + 1} Top White",
|
|
105
|
+
region:, build_z_offset:
|
|
69
106
|
)
|
|
70
|
-
entries
|
|
71
107
|
end
|
|
72
108
|
|
|
73
109
|
def color_ranges
|
|
74
|
-
@settings.
|
|
110
|
+
@settings.resolved_z_color_ranges
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def plate_ranges = @settings.resolved_z_part_ranges
|
|
114
|
+
|
|
115
|
+
def intersection(left, right)
|
|
116
|
+
start = [left.begin, right.begin].max
|
|
117
|
+
finish = [left.end, right.end].min
|
|
118
|
+
start < finish ? start...finish : nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def box_geometry(range)
|
|
122
|
+
BoxGeometry.new(
|
|
123
|
+
width: @model.x_boundaries.last, height: @model.y_boundaries.last,
|
|
124
|
+
z_min: range.begin, z_max: range.end
|
|
125
|
+
)
|
|
75
126
|
end
|
|
76
127
|
|
|
77
128
|
def color_region_white_geometry(model)
|
|
@@ -82,20 +133,6 @@ module Lithop
|
|
|
82
133
|
z_max: model.z_boundaries.last
|
|
83
134
|
)
|
|
84
135
|
end
|
|
85
|
-
|
|
86
|
-
def triangle_count(geometry)
|
|
87
|
-
geometry.respond_to?(:triangle_count) ? geometry.triangle_count : geometry.each_triangle.count
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def triangle_source(geometry)
|
|
91
|
-
geometry.is_a?(ColumnGeometry) ? geometry : geometry.each_triangle
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def counts_by_material(entries, entry_counts)
|
|
95
|
-
entries.zip(entry_counts).each_with_object(Hash.new(0)) do |(entry, count), totals|
|
|
96
|
-
totals[entry.material] += count
|
|
97
|
-
end.freeze
|
|
98
|
-
end
|
|
99
136
|
end
|
|
100
137
|
|
|
101
138
|
# Builds the reference-style overlapping White height field and contiguous CMY columns.
|
|
@@ -5,7 +5,7 @@ module Lithop
|
|
|
5
5
|
module ExportPreviews
|
|
6
6
|
module_function
|
|
7
7
|
|
|
8
|
-
def write(directory, model:, settings:, white_heights: nil)
|
|
8
|
+
def write(directory, model:, settings:, white_heights: nil, corrected_input: nil)
|
|
9
9
|
heights = white_heights && cell_heights(white_heights, settings.columns, settings.rows)
|
|
10
10
|
options = {
|
|
11
11
|
model:,
|
|
@@ -19,8 +19,32 @@ module Lithop
|
|
|
19
19
|
Palette::MATERIALS.each do |material|
|
|
20
20
|
Preview.write_mask(File.join(directory, "#{material}-mask.png"), model:, material:)
|
|
21
21
|
end
|
|
22
|
+
File.binwrite(File.join(directory, "corrected-input.png"), corrected_input) if corrected_input
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
def write_tiles(directory, tile_artifacts:, settings:, white_heights:)
|
|
26
|
+
return if tile_artifacts.one?
|
|
27
|
+
|
|
28
|
+
heights = cell_heights(white_heights, settings.columns, settings.rows)
|
|
29
|
+
tile_artifacts.each do |artifact|
|
|
30
|
+
tile = artifact.tile
|
|
31
|
+
tile_heights = (tile.row_start...tile.row_end).flat_map do |row|
|
|
32
|
+
offset = (row * settings.columns) + tile.column_start
|
|
33
|
+
heights.slice(offset, artifact.material_model.columns)
|
|
34
|
+
end
|
|
35
|
+
Preview.write_material(
|
|
36
|
+
File.join(directory, tile_preview_name(artifact)),
|
|
37
|
+
model: artifact.material_model,
|
|
38
|
+
minimum: settings.effective_minimum_thickness,
|
|
39
|
+
maximum: settings.effective_maximum_thickness,
|
|
40
|
+
heights: tile_heights,
|
|
41
|
+
transmission_gamma: 0.5
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def tile_preview_name(artifact) = artifact.name.sub(/\.3mf\z/, "-preview.png")
|
|
47
|
+
|
|
24
48
|
def cell_heights(vertex_heights, columns, rows)
|
|
25
49
|
stride = columns + 1
|
|
26
50
|
Array.new(columns * rows) do |index|
|
|
@@ -38,7 +38,7 @@ module Lithop
|
|
|
38
38
|
- White thickness uses #{settings.pixel_size} mm XY cells; CMYW recipes use
|
|
39
39
|
#{settings.effective_color_pixel_size} mm color cells.
|
|
40
40
|
- Use 100% rectilinear infill and verify every layer in the slicer before printing.
|
|
41
|
-
- The effective thickness range is #{settings
|
|
41
|
+
- The effective thickness range is #{output_minimum_thickness(settings)}–#{output_maximum_thickness(settings)} mm.
|
|
42
42
|
MARKDOWN
|
|
43
43
|
end
|
|
44
44
|
|
|
@@ -52,8 +52,23 @@ module Lithop
|
|
|
52
52
|
def z_part_line(settings)
|
|
53
53
|
return "- Z regions remain combined into the material parts." unless settings.separate_z_parts
|
|
54
54
|
|
|
55
|
-
"-
|
|
56
|
-
"
|
|
55
|
+
"- The separated model uses #{settings.resolved_z_part_count} contiguous Z plates with Base White " \
|
|
56
|
+
"supports of #{settings.z_part_base_white_layers_by_plate.join("/")} layers " \
|
|
57
|
+
"(#{settings.z_part_base_white_thicknesses.join("/")} mm); every plate is at least " \
|
|
58
|
+
"#{settings.z_part_minimum_build_thickness} mm thick. The assembled thickness range is " \
|
|
59
|
+
"#{settings.z_separated_output_minimum_thickness}–#{settings.z_separated_output_maximum_thickness} mm."
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def output_minimum_thickness(settings)
|
|
63
|
+
return settings.effective_minimum_thickness unless settings.separate_z_parts
|
|
64
|
+
|
|
65
|
+
settings.z_separated_output_minimum_thickness
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def output_maximum_thickness(settings)
|
|
69
|
+
return settings.effective_maximum_thickness unless settings.separate_z_parts
|
|
70
|
+
|
|
71
|
+
settings.z_separated_output_maximum_thickness
|
|
57
72
|
end
|
|
58
73
|
|
|
59
74
|
def profile_material(profile, material)
|
|
@@ -81,7 +96,8 @@ module Lithop
|
|
|
81
96
|
"- Use a #{settings.nozzle_diameter} mm nozzle, #{settings.first_layer_height} mm base layers,\n " \
|
|
82
97
|
"#{settings.layer_height} mm color layers, and #{upper_height} mm upper White layers."
|
|
83
98
|
end
|
|
84
|
-
private_class_method :model_instruction, :z_part_line, :
|
|
99
|
+
private_class_method :model_instruction, :z_part_line, :output_minimum_thickness, :profile_material,
|
|
100
|
+
:calibration_line, :thickness_method,
|
|
85
101
|
:base_layer_description, :layer_height_description
|
|
86
102
|
end
|
|
87
103
|
end
|
|
@@ -136,9 +136,7 @@ module Lithop
|
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
def source_object_id(assembly)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
assembly.region.delete_prefix("color_region_").to_i - 1
|
|
139
|
+
assembly.region.delete_prefix("z_part_").to_i - 1
|
|
142
140
|
end
|
|
143
141
|
|
|
144
142
|
def number(value) = format("%.9g", value)
|
|
@@ -150,12 +148,13 @@ module Lithop
|
|
|
150
148
|
|
|
151
149
|
def layer_ranges_xml
|
|
152
150
|
object_id = @assemblies.length
|
|
153
|
-
min_z = @settings.top_white_start
|
|
154
|
-
max_z = @settings.
|
|
151
|
+
min_z = @settings.layer_structure.top_white_start
|
|
152
|
+
max_z = @settings.effective_maximum_thickness
|
|
155
153
|
if @settings.separate_z_parts
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
source_z_min = last_plate_source_z_min
|
|
155
|
+
base_white = @settings.z_part_base_white_thickness(@settings.resolved_z_part_count - 1)
|
|
156
|
+
min_z = (min_z - source_z_min + base_white).round(9)
|
|
157
|
+
max_z = (max_z - source_z_min + base_white).round(9)
|
|
159
158
|
end
|
|
160
159
|
<<~XML
|
|
161
160
|
<?xml version="1.0" encoding="utf-8"?>
|
|
@@ -170,6 +169,10 @@ module Lithop
|
|
|
170
169
|
XML
|
|
171
170
|
end
|
|
172
171
|
|
|
172
|
+
def last_plate_source_z_min
|
|
173
|
+
@settings.resolved_z_part_ranges.last.begin
|
|
174
|
+
end
|
|
175
|
+
|
|
173
176
|
def project_settings
|
|
174
177
|
{
|
|
175
178
|
"filament_colour" => %w[#0086D6 #EC008C #FCE300 #FFFFFF],
|
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
module Lithop
|
|
4
4
|
# Indexes vertices and triangles for a 3MF mesh resource.
|
|
5
5
|
class ThreeMFMesh
|
|
6
|
-
|
|
6
|
+
NATIVE_BATCH_SIZE = 8_192
|
|
7
|
+
MAXIMUM_NATIVE_TOPOLOGY_KEY = (2**63) - 1
|
|
8
|
+
class UnsupportedNativeTopology < StandardError; end
|
|
9
|
+
|
|
10
|
+
attr_reader :vertices, :vertex_indices, :triangle_count, :native_mesh
|
|
7
11
|
|
|
8
12
|
def initialize(triangles:)
|
|
9
13
|
@triangles = triangles
|
|
10
|
-
|
|
14
|
+
Native.enabled? ? index_native_resources : index_ruby_resources
|
|
11
15
|
end
|
|
12
16
|
|
|
13
17
|
def each_indexed_triangle
|
|
@@ -18,9 +22,74 @@ module Lithop
|
|
|
18
22
|
end
|
|
19
23
|
end
|
|
20
24
|
|
|
25
|
+
def native_indexed? = !native_mesh.nil?
|
|
26
|
+
|
|
27
|
+
def vertex_count = native_indexed? ? native_mesh.vertex_count : vertices.length
|
|
28
|
+
|
|
29
|
+
def center
|
|
30
|
+
return native_mesh.center if native_indexed?
|
|
31
|
+
|
|
32
|
+
axes = vertices.transpose
|
|
33
|
+
axes.map { |values| (values.min + values.max) / 2.0 }
|
|
34
|
+
end
|
|
35
|
+
|
|
21
36
|
private
|
|
22
37
|
|
|
23
|
-
def
|
|
38
|
+
def index_native_resources
|
|
39
|
+
if @triangles.respond_to?(:native_indexed_mesh)
|
|
40
|
+
@native_mesh = @triangles.native_indexed_mesh
|
|
41
|
+
raise InputError, "3MF mesh contains no triangles" if native_mesh.vertex_count.zero?
|
|
42
|
+
|
|
43
|
+
@triangle_count = native_mesh.triangle_count
|
|
44
|
+
@vertices = nil
|
|
45
|
+
@vertex_indices = nil
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@native_mesh = Native::MeshBuilder.new
|
|
50
|
+
coordinates = []
|
|
51
|
+
topology_keys = []
|
|
52
|
+
count = 0
|
|
53
|
+
@triangles.each do |triangle|
|
|
54
|
+
validate_triangle!(triangle)
|
|
55
|
+
triangle.each do |vertex|
|
|
56
|
+
coordinates.concat(coordinates(vertex))
|
|
57
|
+
topology_keys << native_topology_key(vertex)
|
|
58
|
+
end
|
|
59
|
+
count += 1
|
|
60
|
+
flush_native_batch(coordinates, topology_keys) if (count % NATIVE_BATCH_SIZE).zero?
|
|
61
|
+
end
|
|
62
|
+
flush_native_batch(coordinates, topology_keys)
|
|
63
|
+
raise InputError, "3MF mesh contains no triangles" if native_mesh.vertex_count.zero?
|
|
64
|
+
|
|
65
|
+
@triangle_count = native_mesh.triangle_count
|
|
66
|
+
@vertices = nil
|
|
67
|
+
@vertex_indices = nil
|
|
68
|
+
rescue UnsupportedNativeTopology
|
|
69
|
+
@native_mesh = nil
|
|
70
|
+
index_ruby_resources
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def flush_native_batch(coordinates, topology_keys)
|
|
74
|
+
return if coordinates.empty?
|
|
75
|
+
|
|
76
|
+
native_mesh.add_batch(coordinates, topology_keys)
|
|
77
|
+
coordinates.clear
|
|
78
|
+
topology_keys.clear
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def native_topology_key(vertex)
|
|
82
|
+
return -1 unless vertex.respond_to?(:topology)
|
|
83
|
+
|
|
84
|
+
topology = vertex.topology
|
|
85
|
+
unless topology.is_a?(Integer) && topology.between?(0, MAXIMUM_NATIVE_TOPOLOGY_KEY)
|
|
86
|
+
raise UnsupportedNativeTopology
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
topology
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def index_ruby_resources
|
|
24
93
|
@vertices = []
|
|
25
94
|
@vertex_indices = {}
|
|
26
95
|
@triangle_count = 0
|
|
@@ -78,7 +78,7 @@ module Lithop
|
|
|
78
78
|
ThreeMFPartWriter.new(archive: @archive, profile: @profile).write(object_id, part, mesh)
|
|
79
79
|
end
|
|
80
80
|
part.triangles.release_cache! if part.triangles.respond_to?(:release_cache!)
|
|
81
|
-
vertex_count += mesh.
|
|
81
|
+
vertex_count += mesh.vertex_count
|
|
82
82
|
triangle_counts[part.material] = triangle_counts.fetch(part.material, 0) + mesh.triangle_count
|
|
83
83
|
end
|
|
84
84
|
Result.new(
|
|
@@ -96,7 +96,7 @@ module Lithop
|
|
|
96
96
|
result
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
-
# rubocop:disable-next Metrics/AbcSize
|
|
99
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength
|
|
100
100
|
def write_external_parts(object_ids, assemblies)
|
|
101
101
|
parts_by_object_id = object_ids.zip(@parts).to_h
|
|
102
102
|
vertex_count = 0
|
|
@@ -106,22 +106,26 @@ module Lithop
|
|
|
106
106
|
measure_xml do
|
|
107
107
|
@archive.write(submodel_header)
|
|
108
108
|
write_materials
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)
|
|
109
|
+
end
|
|
110
|
+
assembly.object_ids.each do |object_id|
|
|
111
|
+
part = parts_by_object_id.fetch(object_id)
|
|
112
|
+
mesh = @instrumentation.measure(:three_mf_indexing) do
|
|
113
|
+
ThreeMFMesh.new(triangles: part.triangles)
|
|
114
|
+
end
|
|
115
|
+
center = mesh_center(mesh)
|
|
116
|
+
assembly.component_origins[object_id] = component_origin(
|
|
117
|
+
center, assembly.component_origins.fetch(object_id)
|
|
118
|
+
)
|
|
119
|
+
measure_xml do
|
|
118
120
|
ThreeMFPartWriter.new(
|
|
119
121
|
archive: @archive, profile: @profile, production: true, origin: center
|
|
120
122
|
).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
123
|
end
|
|
124
|
+
part.triangles.release_cache! if part.triangles.respond_to?(:release_cache!)
|
|
125
|
+
vertex_count += mesh.vertex_count
|
|
126
|
+
triangle_counts[part.material] = triangle_counts.fetch(part.material, 0) + mesh.triangle_count
|
|
127
|
+
end
|
|
128
|
+
measure_xml do
|
|
125
129
|
@archive.write(" </resources>\n <build/>\n</model>\n")
|
|
126
130
|
end
|
|
127
131
|
end
|
|
@@ -194,9 +198,8 @@ module Lithop
|
|
|
194
198
|
|
|
195
199
|
def assembly_name(region)
|
|
196
200
|
return "Lithop CMYW lithophane" unless region
|
|
197
|
-
return "Top White" if region == "top_white"
|
|
198
201
|
|
|
199
|
-
region.sub("
|
|
202
|
+
region.sub("z_part_", "Z plate ")
|
|
200
203
|
end
|
|
201
204
|
|
|
202
205
|
def write_assembly(assembly, submodel_index = nil)
|
|
@@ -252,8 +255,7 @@ module Lithop
|
|
|
252
255
|
end
|
|
253
256
|
|
|
254
257
|
def mesh_center(mesh)
|
|
255
|
-
|
|
256
|
-
axes.map { |values| (values.min + values.max) / 2.0 }
|
|
258
|
+
mesh.center
|
|
257
259
|
end
|
|
258
260
|
|
|
259
261
|
def component_origin(center, base_origin)
|
|
@@ -261,9 +263,7 @@ module Lithop
|
|
|
261
263
|
end
|
|
262
264
|
|
|
263
265
|
def plate_index(assembly)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
assembly.region.delete_prefix("color_region_").to_i - 1
|
|
266
|
+
assembly.region.delete_prefix("z_part_").to_i - 1
|
|
267
267
|
end
|
|
268
268
|
|
|
269
269
|
def model_header
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Lithop
|
|
4
4
|
# Serializes one indexed material mesh into a 3MF model document.
|
|
5
5
|
class ThreeMFPartWriter
|
|
6
|
+
XML_BATCH_SIZE = 8_192
|
|
7
|
+
|
|
6
8
|
def initialize(archive:, profile:, production: false, origin: nil)
|
|
7
9
|
@archive = archive
|
|
8
10
|
@profile = profile
|
|
@@ -31,6 +33,9 @@ module Lithop
|
|
|
31
33
|
private
|
|
32
34
|
|
|
33
35
|
def write_vertices(mesh)
|
|
36
|
+
return write_indexed_native_vertices(mesh) if mesh.native_indexed?
|
|
37
|
+
return write_native_vertices(mesh) if Native.enabled?
|
|
38
|
+
|
|
34
39
|
mesh.vertices.each do |x, y, z|
|
|
35
40
|
@archive.write(
|
|
36
41
|
" <vertex x=\"#{number(x - @origin[0])}\" y=\"#{number(y - @origin[1])}\" " \
|
|
@@ -40,6 +45,9 @@ module Lithop
|
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
def write_triangles(mesh)
|
|
48
|
+
return write_indexed_native_triangles(mesh) if mesh.native_indexed?
|
|
49
|
+
return write_native_triangles(mesh) if Native.enabled?
|
|
50
|
+
|
|
43
51
|
count = 0
|
|
44
52
|
mesh.each_indexed_triangle do |vertices|
|
|
45
53
|
@archive.write(
|
|
@@ -52,6 +60,35 @@ module Lithop
|
|
|
52
60
|
raise InputError, "3MF triangle enumerables must be repeatable"
|
|
53
61
|
end
|
|
54
62
|
|
|
63
|
+
def write_native_vertices(mesh)
|
|
64
|
+
mesh.vertices.each_slice(XML_BATCH_SIZE) do |vertices|
|
|
65
|
+
@archive.write(Native.vertex_xml(vertices.flatten, @origin))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def write_indexed_native_vertices(mesh)
|
|
70
|
+
0.step(mesh.vertex_count - 1, XML_BATCH_SIZE) do |offset|
|
|
71
|
+
@archive.write(mesh.native_mesh.vertex_xml(offset, XML_BATCH_SIZE, @origin))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def write_native_triangles(mesh)
|
|
76
|
+
count = 0
|
|
77
|
+
mesh.each_indexed_triangle.each_slice(XML_BATCH_SIZE) do |triangles|
|
|
78
|
+
@archive.write(Native.triangle_xml(triangles.flatten))
|
|
79
|
+
count += triangles.length
|
|
80
|
+
end
|
|
81
|
+
return if count == mesh.triangle_count
|
|
82
|
+
|
|
83
|
+
raise InputError, "3MF triangle enumerables must be repeatable"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def write_indexed_native_triangles(mesh)
|
|
87
|
+
0.step(mesh.triangle_count - 1, XML_BATCH_SIZE) do |offset|
|
|
88
|
+
@archive.write(mesh.native_mesh.triangle_xml(offset, XML_BATCH_SIZE))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
55
92
|
def number(value) = format("%.9g", value)
|
|
56
93
|
end
|
|
57
94
|
end
|
|
@@ -18,7 +18,7 @@ module Lithop
|
|
|
18
18
|
MODEL_RELATIONSHIP = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"
|
|
19
19
|
MODEL_CONTENT_TYPE = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml"
|
|
20
20
|
ZIP_MUTEX = Mutex.new
|
|
21
|
-
#
|
|
21
|
+
# The Rust indexer consumes triangles once; the Ruby comparison path requires replayable input.
|
|
22
22
|
Part = Data.define(:material, :triangles, :name, :region, :build_z_offset) do
|
|
23
23
|
def initialize(material:, triangles:, name: nil, region: nil, build_z_offset: 0.0)
|
|
24
24
|
super
|