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
|
@@ -7,8 +7,10 @@ module Lithop
|
|
|
7
7
|
|
|
8
8
|
def add(parser, options)
|
|
9
9
|
add_dimensions(parser, options)
|
|
10
|
+
add_tiles(parser, options)
|
|
10
11
|
add_margins(parser, options)
|
|
11
12
|
add_placement(parser, options)
|
|
13
|
+
add_adjustments(parser, options)
|
|
12
14
|
add_thickness(parser, options)
|
|
13
15
|
add_sampling(parser, options)
|
|
14
16
|
end
|
|
@@ -17,11 +19,35 @@ module Lithop
|
|
|
17
19
|
parser.on("--layout-profile NAME", "Layout profile: standard") do |value|
|
|
18
20
|
options[:layout_profile] = value
|
|
19
21
|
end
|
|
20
|
-
parser.on("--width MM", Float, "Model width (default:
|
|
21
|
-
|
|
22
|
+
parser.on("--width MM", Float, "Model width (default: #{Settings::DEFAULTS.fetch(:width)})") do |value|
|
|
23
|
+
options[:width] = value
|
|
24
|
+
end
|
|
25
|
+
parser.on("--height MM", Float, "Model height (default: #{Settings::DEFAULTS.fetch(:height)})") do |value|
|
|
26
|
+
options[:height] = value
|
|
27
|
+
end
|
|
22
28
|
end
|
|
23
29
|
private_class_method :add_dimensions
|
|
24
30
|
|
|
31
|
+
def add_tiles(parser, options)
|
|
32
|
+
parser.on("--tile-count COUNT", Integer, "Number of 3MF tiles (default: 1)") do |value|
|
|
33
|
+
options[:tile_count] = value
|
|
34
|
+
end
|
|
35
|
+
parser.on("--tile-layout MODE", "Tile layout: auto/columns/rows/grid (default: auto)") do |value|
|
|
36
|
+
options[:tile_layout] = value.downcase
|
|
37
|
+
end
|
|
38
|
+
parser.on("--tile-rows COUNT", Integer, "Tile rows for grid layout") { |value| options[:tile_rows] = value }
|
|
39
|
+
parser.on("--tile-columns COUNT", Integer, "Tile columns for grid layout") do |value|
|
|
40
|
+
options[:tile_columns] = value
|
|
41
|
+
end
|
|
42
|
+
parser.on("--separate-z-parts", "Put base White, color regions, and top White on separate plates") do
|
|
43
|
+
options[:separate_z_parts] = true
|
|
44
|
+
end
|
|
45
|
+
parser.on("--color-region-parts COUNT", Integer, "Number of CMYW color-region Z parts (default: 3)") do |value|
|
|
46
|
+
options[:color_region_parts] = value
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
private_class_method :add_tiles
|
|
50
|
+
|
|
25
51
|
def add_margins(parser, options)
|
|
26
52
|
parser.on("--margin-top MM", Float, "Top margin (default: 0)") { |value| options[:margin_top] = value }
|
|
27
53
|
parser.on("--margin-right MM", Float, "Right margin (default: 0)") { |value| options[:margin_right] = value }
|
|
@@ -46,6 +72,19 @@ module Lithop
|
|
|
46
72
|
end
|
|
47
73
|
private_class_method :add_placement
|
|
48
74
|
|
|
75
|
+
def add_adjustments(parser, options)
|
|
76
|
+
parser.on("--brightness PERCENT", Float, "Brightness: -100..100 (default: 0)") do |value|
|
|
77
|
+
options[:brightness] = value
|
|
78
|
+
end
|
|
79
|
+
parser.on("--contrast PERCENT", Float, "Contrast: -100..100 (default: 0)") do |value|
|
|
80
|
+
options[:contrast] = value
|
|
81
|
+
end
|
|
82
|
+
parser.on("--saturation PERCENT", Float, "Saturation: -100..100 (default: 0)") do |value|
|
|
83
|
+
options[:saturation] = value
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
private_class_method :add_adjustments
|
|
87
|
+
|
|
49
88
|
def add_thickness(parser, options)
|
|
50
89
|
parser.on("--min-thickness MM", Float, "Requested minimum model thickness (default: 1.0)") do |value|
|
|
51
90
|
options[:minimum_thickness] = value
|
data/lib/lithop/cli.rb
CHANGED
|
@@ -44,7 +44,8 @@ module Lithop
|
|
|
44
44
|
project_dir:,
|
|
45
45
|
input_settings_path: settings_path,
|
|
46
46
|
calibration:,
|
|
47
|
-
progress: method(:report_estimate)
|
|
47
|
+
progress: method(:report_estimate),
|
|
48
|
+
job_progress: method(:report_job_progress)
|
|
48
49
|
).call(input)
|
|
49
50
|
@out.puts "Generated #{result.triangle_count} STL and #{result.three_mf_triangle_count} CMYW 3MF triangles"
|
|
50
51
|
@out.puts "in #{result.path}"
|
|
@@ -66,6 +67,12 @@ module Lithop
|
|
|
66
67
|
@out.puts " CMYW by material: #{counts}"
|
|
67
68
|
end
|
|
68
69
|
|
|
70
|
+
def report_job_progress(progress)
|
|
71
|
+
return unless progress.state == "running" && progress.percent.positive?
|
|
72
|
+
|
|
73
|
+
@out.puts "Progress #{progress.percent.round}%: #{progress.message}"
|
|
74
|
+
end
|
|
75
|
+
|
|
69
76
|
def load_settings(options, keys: nil)
|
|
70
77
|
resolved = CLISettings.new(cwd: @cwd).call(options, keys:)
|
|
71
78
|
[resolved.project_dir, resolved.settings, resolved.input_settings_path]
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Lithop
|
|
4
4
|
# Builds the color-model portion of an export manifest.
|
|
5
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
5
6
|
class ExportColorMetadata
|
|
6
7
|
def initialize(settings:, calibration:)
|
|
7
8
|
@settings = settings
|
|
@@ -12,6 +13,7 @@ module Lithop
|
|
|
12
13
|
def call(artifacts)
|
|
13
14
|
{
|
|
14
15
|
file: artifacts.three_mf_name,
|
|
16
|
+
files: artifacts.three_mf_names,
|
|
15
17
|
format: "3MF",
|
|
16
18
|
property: "3MF color group and Bambu Studio part extruder assignments",
|
|
17
19
|
calibration: @calibration.metadata,
|
|
@@ -23,6 +25,7 @@ module Lithop
|
|
|
23
25
|
layout_profile: @settings.layout_profile,
|
|
24
26
|
base_white_layers: @settings.base_white_layers,
|
|
25
27
|
minimum_top_white_layers: @settings.minimum_top_white_layers,
|
|
28
|
+
z_part_separation: z_part_separation,
|
|
26
29
|
thickness_mm: thickness_structure,
|
|
27
30
|
physical_material_layers_encoded: true,
|
|
28
31
|
bambu_studio_material_assignments_encoded: true,
|
|
@@ -63,6 +66,58 @@ module Lithop
|
|
|
63
66
|
}
|
|
64
67
|
end
|
|
65
68
|
|
|
69
|
+
def z_part_separation
|
|
70
|
+
structure = @settings.layer_structure
|
|
71
|
+
boundaries = structure.z_boundaries(@settings.base_white_layers + @settings.color_layer_count)
|
|
72
|
+
color_regions = structure.color_region_layer_ranges(@settings.color_region_parts)
|
|
73
|
+
.each_with_index.map do |range, index|
|
|
74
|
+
{
|
|
75
|
+
id: "color_region_#{index + 1}",
|
|
76
|
+
layer_start: range.begin - @settings.base_white_layers + 1,
|
|
77
|
+
layer_end: range.end - @settings.base_white_layers,
|
|
78
|
+
z_min: boundaries.fetch(range.begin),
|
|
79
|
+
z_max: boundaries.fetch(range.end)
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
metadata = {
|
|
83
|
+
enabled: @settings.separate_z_parts,
|
|
84
|
+
base_white: { id: "base_white", z_min: 0.0, z_max: structure.base_white_thickness },
|
|
85
|
+
color_regions:,
|
|
86
|
+
top_white: {
|
|
87
|
+
id: "top_white", z_min: structure.color_region_end,
|
|
88
|
+
z_max: @settings.effective_maximum_thickness
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
metadata[:plates] = z_plates(metadata) if @settings.separate_z_parts
|
|
92
|
+
metadata
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def z_plates(metadata)
|
|
96
|
+
base_white = metadata.fetch(:base_white)
|
|
97
|
+
regions = metadata.fetch(:color_regions).map do |region|
|
|
98
|
+
region.merge(z_min: base_white.fetch(:z_min), build_z_max: region.fetch(:z_max))
|
|
99
|
+
end
|
|
100
|
+
regions << metadata.fetch(:top_white)
|
|
101
|
+
regions.each_with_index.map do |region, index|
|
|
102
|
+
z_min = region.fetch(:z_min)
|
|
103
|
+
z_max = region.fetch(:z_max)
|
|
104
|
+
{
|
|
105
|
+
plate_id: index + 1,
|
|
106
|
+
name: plate_name(region.fetch(:id)),
|
|
107
|
+
source_z_min: z_min,
|
|
108
|
+
source_z_max: z_max,
|
|
109
|
+
build_z_min: 0.0,
|
|
110
|
+
build_z_max: (region[:build_z_max] || (z_max - z_min)).round(9)
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def plate_name(id)
|
|
116
|
+
return "Top White" if id == "top_white"
|
|
117
|
+
|
|
118
|
+
id.sub("color_region_", "Color region ")
|
|
119
|
+
end
|
|
120
|
+
|
|
66
121
|
def used_materials(artifacts)
|
|
67
122
|
Palette::MATERIALS.select { |material| artifacts.material_model.materials.include?(material) }
|
|
68
123
|
end
|
|
@@ -5,12 +5,14 @@ module Lithop
|
|
|
5
5
|
# rubocop:disable-next Metrics/ClassLength
|
|
6
6
|
class ExportGeneration
|
|
7
7
|
Result = Data.define(:files, :triangle_count, :three_mf_triangle_count, :material_count, :object_count)
|
|
8
|
+
TileEstimate = Data.define(:triangle_count, :triangle_counts_by_material)
|
|
8
9
|
|
|
9
|
-
def initialize(settings:, input_settings_path:, calibration:, progress:, instrumentation:)
|
|
10
|
+
def initialize(settings:, input_settings_path:, calibration:, progress:, job_progress:, instrumentation:)
|
|
10
11
|
@settings = settings
|
|
11
12
|
@input_settings_path = input_settings_path
|
|
12
13
|
@calibration_context = ExportCalibration.new(settings:, input: calibration)
|
|
13
14
|
@progress = progress
|
|
15
|
+
@job_progress = job_progress
|
|
14
16
|
@instrumentation = Instrumentation.new(instrumentation)
|
|
15
17
|
@metadata = ExportMetadata.new(
|
|
16
18
|
settings:, input_settings_path:, calibration: @calibration_context
|
|
@@ -18,20 +20,32 @@ module Lithop
|
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def call(source:, directory:, job_id:)
|
|
23
|
+
report_job_progress(:image_processing, 5, "Processing input image")
|
|
21
24
|
image = @instrumentation.measure(:image_processing) { ImagePipeline.new(@settings).call(source) }
|
|
25
|
+
report_job_progress(:height_selection, 20, "Selecting White heights")
|
|
22
26
|
white_heights = @instrumentation.measure(:height_selection) do
|
|
23
27
|
@calibration_context.heights(image.white_densities)
|
|
24
28
|
end
|
|
25
29
|
geometry = white_geometry(white_heights)
|
|
30
|
+
report_job_progress(:material_model, 30, "Building CMYW material model")
|
|
26
31
|
material_model = @instrumentation.measure(:material_model) do
|
|
27
32
|
MaterialModel.new(
|
|
28
33
|
cmys: image.cmys, cell_cmys: image.color_cells,
|
|
29
34
|
heights: white_heights, settings: @settings, palette: @calibration_context.palette
|
|
30
35
|
)
|
|
31
36
|
end
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
tile_layout = TileLayout.new(@settings)
|
|
38
|
+
report_job_progress(:triangle_planning, 40, "Planning mesh triangles")
|
|
39
|
+
tile_estimates = @instrumentation.measure(:triangle_planning) do
|
|
40
|
+
estimate_tiles(tile_layout, material_model, white_heights)
|
|
41
|
+
end
|
|
42
|
+
validate_total_triangle_count!(tile_estimates)
|
|
43
|
+
report_estimate(tile_estimates)
|
|
44
|
+
report_job_progress(:artifact_write, 50, "Writing model artifacts")
|
|
45
|
+
artifacts = write_artifacts(
|
|
46
|
+
directory, geometry, material_model, tile_layout, white_heights
|
|
47
|
+
)
|
|
48
|
+
report_job_progress(:metadata_write, 95, "Writing job metadata")
|
|
35
49
|
write_supporting_files(directory, source, job_id, artifacts)
|
|
36
50
|
result_for(directory, artifacts)
|
|
37
51
|
end
|
|
@@ -58,28 +72,102 @@ module Lithop
|
|
|
58
72
|
).call
|
|
59
73
|
end
|
|
60
74
|
|
|
61
|
-
def
|
|
75
|
+
def estimate_tiles(tile_layout, material_model, white_heights)
|
|
76
|
+
tile_layout.tiles.map do |tile|
|
|
77
|
+
_tile_model, plan = build_tile_plan(tile, material_model, white_heights)
|
|
78
|
+
TileEstimate.new(
|
|
79
|
+
triangle_count: plan.triangle_count,
|
|
80
|
+
triangle_counts_by_material: plan.triangle_counts_by_material
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def build_tile_plan(tile, material_model, white_heights)
|
|
86
|
+
tile_model = TileMaterialModel.new(model: material_model, tile:)
|
|
87
|
+
plan = if @settings.separate_z_parts
|
|
88
|
+
top_geometry = tile_white_geometry(
|
|
89
|
+
tile, tile_model, white_heights,
|
|
90
|
+
base_height: @settings.layer_structure.color_region_end
|
|
91
|
+
)
|
|
92
|
+
z_separated_material_mesh_plan(tile_model, top_geometry)
|
|
93
|
+
else
|
|
94
|
+
geometry = tile_white_geometry(tile, tile_model, white_heights)
|
|
95
|
+
material_mesh_plan(tile_model, geometry)
|
|
96
|
+
end
|
|
97
|
+
[tile_model, plan]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def tile_white_geometry(tile, model, heights, base_height: 0.0)
|
|
101
|
+
stride = @settings.columns + 1
|
|
102
|
+
sliced_heights = (tile.row_start..tile.row_end).flat_map do |row|
|
|
103
|
+
heights.slice((row * stride) + tile.column_start, model.columns + 1)
|
|
104
|
+
end
|
|
105
|
+
Geometry.new(
|
|
106
|
+
heights: sliced_heights,
|
|
107
|
+
columns: model.columns,
|
|
108
|
+
rows: model.rows,
|
|
109
|
+
x_boundaries: model.x_boundaries,
|
|
110
|
+
y_boundaries: model.y_boundaries,
|
|
111
|
+
optimize_bottom: true,
|
|
112
|
+
base_height:
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def z_separated_material_mesh_plan(model, top_geometry)
|
|
117
|
+
base_height = @settings.layer_structure.base_white_thickness
|
|
118
|
+
base_geometry = BoxGeometry.new(
|
|
119
|
+
width: model.x_boundaries.last,
|
|
120
|
+
height: model.y_boundaries.last,
|
|
121
|
+
z_min: 0.0,
|
|
122
|
+
z_max: base_height
|
|
123
|
+
)
|
|
124
|
+
ZSeparatedMaterialMeshPlan.new(
|
|
125
|
+
model:, base_geometry:, top_geometry:, settings: @settings,
|
|
126
|
+
maximum_triangles: @settings.maximum_triangles,
|
|
127
|
+
limit_message: ->(count) { @settings.triangle_limit_message("physical Z-separated CMYW mesh", count) }
|
|
128
|
+
).call
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def validate_total_triangle_count!(tile_estimates)
|
|
132
|
+
count = tile_estimates.sum(&:triangle_count)
|
|
133
|
+
return if count <= @settings.maximum_triangles
|
|
134
|
+
|
|
135
|
+
raise ConfigurationError, @settings.triangle_limit_message("physical CMYW tiled mesh", count)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def write_artifacts(directory, geometry, material_model, tile_layout, white_heights)
|
|
62
139
|
stl_name = "cmyk_white.stl"
|
|
63
140
|
triangle_count = @instrumentation.measure(:stl_write) do
|
|
64
141
|
STL.write(File.join(directory, stl_name), geometry.each_triangle)
|
|
65
142
|
end
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
143
|
+
FileUtils.mkdir_p(File.join(directory, "tiles")) if tile_layout.tiles.length > 1
|
|
144
|
+
tile_artifacts = tile_layout.tiles.each_with_index.map do |tile, index|
|
|
145
|
+
percent = 60 + ((index * 20.0) / tile_layout.tiles.length)
|
|
146
|
+
report_job_progress(:three_mf_write, percent, "Writing 3MF tile #{index + 1}/#{tile_layout.tiles.length}")
|
|
147
|
+
tile_model, mesh_plan = build_tile_plan(tile, material_model, white_heights)
|
|
148
|
+
name = tile_layout.tiles.one? ? "lithop.3mf" : "tiles/#{tile.id}.3mf"
|
|
149
|
+
three_mf = @instrumentation.measure(:three_mf_write) do
|
|
150
|
+
ThreeMF.write(
|
|
151
|
+
File.join(directory, name), parts: mesh_plan.parts, instrumentation: @instrumentation,
|
|
152
|
+
settings: @settings
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
mesh_plan.validate!(three_mf)
|
|
156
|
+
TileArtifact.new(tile:, name:, three_mf:, material_model: tile_model)
|
|
157
|
+
end
|
|
158
|
+
if tile_layout.tiles.length > 1
|
|
159
|
+
TileLayoutSVG.write(File.join(directory, "tile-layout.svg"), settings: @settings, layout: tile_layout)
|
|
72
160
|
end
|
|
73
|
-
|
|
161
|
+
report_job_progress(:preview_write, 85, "Writing previews")
|
|
74
162
|
@instrumentation.measure(:preview_write) do
|
|
75
163
|
ExportPreviews.write(directory, model: material_model, settings: @settings, white_heights:)
|
|
76
164
|
end
|
|
77
165
|
ExportArtifacts.new(
|
|
78
166
|
stl_name:,
|
|
79
|
-
|
|
167
|
+
tile_artifacts:,
|
|
80
168
|
stl_triangle_count: triangle_count,
|
|
81
|
-
|
|
82
|
-
|
|
169
|
+
material_model:,
|
|
170
|
+
tile_layout:
|
|
83
171
|
)
|
|
84
172
|
end
|
|
85
173
|
|
|
@@ -91,27 +179,36 @@ module Lithop
|
|
|
91
179
|
File.join(directory, "PRINT_README.md"),
|
|
92
180
|
settings: @settings,
|
|
93
181
|
material_model: artifacts.material_model,
|
|
94
|
-
calibration_profile: @calibration_context.profile
|
|
182
|
+
calibration_profile: @calibration_context.profile,
|
|
183
|
+
tile_artifacts: artifacts.tile_artifacts
|
|
95
184
|
)
|
|
96
185
|
end
|
|
97
186
|
end
|
|
98
187
|
|
|
99
|
-
def report_estimate(
|
|
188
|
+
def report_estimate(tile_estimates)
|
|
100
189
|
return unless @progress
|
|
101
190
|
|
|
191
|
+
counts = tile_estimates.each_with_object(Hash.new(0)) do |estimate, totals|
|
|
192
|
+
estimate.triangle_counts_by_material.each { |material, count| totals[material] += count }
|
|
193
|
+
end
|
|
194
|
+
|
|
102
195
|
@progress.call(
|
|
103
196
|
Export::Estimate.new(
|
|
104
197
|
stl_triangle_count: @settings.estimated_triangles,
|
|
105
|
-
three_mf_triangle_count:
|
|
106
|
-
triangle_counts_by_material:
|
|
198
|
+
three_mf_triangle_count: tile_estimates.sum(&:triangle_count),
|
|
199
|
+
triangle_counts_by_material: counts.freeze
|
|
107
200
|
)
|
|
108
201
|
)
|
|
109
202
|
end
|
|
110
203
|
|
|
204
|
+
def report_job_progress(phase, percent, message)
|
|
205
|
+
@job_progress&.call(phase:, percent:, message:)
|
|
206
|
+
end
|
|
207
|
+
|
|
111
208
|
def result_for(directory, artifacts)
|
|
112
209
|
three_mf = artifacts.three_mf
|
|
113
210
|
Result.new(
|
|
114
|
-
files: Dir.
|
|
211
|
+
files: Dir.glob("**/*", base: directory).select { |name| File.file?(File.join(directory, name)) }.sort,
|
|
115
212
|
triangle_count: artifacts.stl_triangle_count,
|
|
116
213
|
three_mf_triangle_count: three_mf.triangle_count,
|
|
117
214
|
material_count: three_mf.material_count,
|
data/lib/lithop/export/job.rb
CHANGED
|
@@ -15,35 +15,49 @@ module Lithop
|
|
|
15
15
|
# rubocop:disable-next Metrics/ParameterLists
|
|
16
16
|
def initialize(
|
|
17
17
|
settings:, project_dir:, input_settings_path: nil, calibration: nil, progress: nil,
|
|
18
|
-
instrumentation: nil, clock: Time, random: SecureRandom
|
|
18
|
+
job_progress: nil, instrumentation: nil, clock: Time, random: SecureRandom, job_id: nil
|
|
19
19
|
)
|
|
20
20
|
@settings = settings
|
|
21
21
|
@project_dir = File.expand_path(project_dir)
|
|
22
22
|
@input_settings_path = input_settings_path
|
|
23
23
|
@calibration = calibration
|
|
24
24
|
@progress = progress
|
|
25
|
+
@job_progress = job_progress
|
|
25
26
|
@clock = clock
|
|
26
27
|
@random = random
|
|
27
|
-
@
|
|
28
|
-
|
|
29
|
-
)
|
|
28
|
+
@instrumentation = instrumentation
|
|
29
|
+
@job_id = job_id
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def call(input_path)
|
|
33
33
|
source = File.expand_path(input_path)
|
|
34
|
-
job_id = "#{@clock.now.utc.strftime("%Y%m%dT%H%M%SZ")}-#{@random.hex(4)}"
|
|
35
|
-
|
|
36
|
-
@
|
|
37
|
-
end
|
|
38
|
-
Result.new(
|
|
39
|
-
job_id:,
|
|
40
|
-
path: destination,
|
|
41
|
-
files: result.files,
|
|
42
|
-
triangle_count: result.triangle_count,
|
|
43
|
-
three_mf_triangle_count: result.three_mf_triangle_count,
|
|
44
|
-
material_count: result.material_count,
|
|
45
|
-
object_count: result.object_count
|
|
34
|
+
job_id = @job_id || "#{@clock.now.utc.strftime("%Y%m%dT%H%M%SZ")}-#{@random.hex(4)}"
|
|
35
|
+
execution = JobExecution.new(
|
|
36
|
+
project_dir: @project_dir, job_id:, progress: @job_progress, clock: @clock
|
|
46
37
|
)
|
|
38
|
+
execution.call do |job|
|
|
39
|
+
generation = ExportGeneration.new(
|
|
40
|
+
settings: @settings,
|
|
41
|
+
input_settings_path: @input_settings_path,
|
|
42
|
+
calibration: @calibration,
|
|
43
|
+
progress: @progress,
|
|
44
|
+
job_progress: job.method(:progress),
|
|
45
|
+
instrumentation: @instrumentation
|
|
46
|
+
)
|
|
47
|
+
destination, generated = JobDirectory.new(project_dir: @project_dir, job_id:).call do |directory|
|
|
48
|
+
generation.call(source:, directory:, job_id:)
|
|
49
|
+
end
|
|
50
|
+
job.progress(phase: :publishing, percent: 99, message: "Artifacts published")
|
|
51
|
+
Result.new(
|
|
52
|
+
job_id:,
|
|
53
|
+
path: destination,
|
|
54
|
+
files: generated.files,
|
|
55
|
+
triangle_count: generated.triangle_count,
|
|
56
|
+
three_mf_triangle_count: generated.three_mf_triangle_count,
|
|
57
|
+
material_count: generated.material_count,
|
|
58
|
+
object_count: generated.object_count
|
|
59
|
+
)
|
|
60
|
+
end
|
|
47
61
|
end
|
|
48
62
|
end
|
|
49
63
|
end
|
|
@@ -5,6 +5,7 @@ require "json"
|
|
|
5
5
|
|
|
6
6
|
module Lithop
|
|
7
7
|
# Builds and writes the resolved settings and export manifest.
|
|
8
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
8
9
|
class ExportMetadata
|
|
9
10
|
def initialize(settings:, input_settings_path:, calibration:)
|
|
10
11
|
@settings = settings
|
|
@@ -29,8 +30,8 @@ module Lithop
|
|
|
29
30
|
|
|
30
31
|
private
|
|
31
32
|
|
|
33
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
32
34
|
def manifest_for(source, job_id, artifacts)
|
|
33
|
-
materials = material_names(used_materials(artifacts))
|
|
34
35
|
{
|
|
35
36
|
schema_version: 1,
|
|
36
37
|
job_id:,
|
|
@@ -49,9 +50,21 @@ module Lithop
|
|
|
49
50
|
},
|
|
50
51
|
input_sha256: Digest::SHA256.file(source).hexdigest,
|
|
51
52
|
filament_profile: FilamentProfile.default.to_h,
|
|
53
|
+
tile_layout: tile_layout_metadata(artifacts),
|
|
54
|
+
tiles: tile_metadata(artifacts),
|
|
52
55
|
parts: [
|
|
53
56
|
{ file: artifacts.stl_name, material: "White", role: "monochrome_reference" },
|
|
54
|
-
|
|
57
|
+
*artifacts.tile_artifacts.map do |artifact|
|
|
58
|
+
tile_materials = Palette::MATERIALS.select do |material|
|
|
59
|
+
artifact.material_model.materials.include?(material)
|
|
60
|
+
end
|
|
61
|
+
{
|
|
62
|
+
file: artifact.name,
|
|
63
|
+
tile_id: artifact.tile.id,
|
|
64
|
+
materials: material_names(tile_materials),
|
|
65
|
+
role: artifacts.tile_artifacts.one? ? "cmyw_lithophane" : "cmyw_lithophane_tile"
|
|
66
|
+
}
|
|
67
|
+
end
|
|
55
68
|
],
|
|
56
69
|
color_model: @color_metadata.call(artifacts),
|
|
57
70
|
files: artifact_files(artifacts),
|
|
@@ -62,14 +75,46 @@ module Lithop
|
|
|
62
75
|
|
|
63
76
|
def artifact_files(artifacts)
|
|
64
77
|
files = [
|
|
65
|
-
artifacts.stl_name, artifacts.
|
|
78
|
+
artifacts.stl_name, *artifacts.three_mf_names, "settings.json", "manifest.json",
|
|
66
79
|
"preview.png", "transmission.png", "thickness.png",
|
|
67
80
|
*Palette::MATERIALS.map { |material| "#{material}-mask.png" }, "PRINT_README.md"
|
|
68
81
|
]
|
|
82
|
+
files << "tile-layout.svg" if artifacts.tile_artifacts.length > 1
|
|
69
83
|
files << "calibration-profile.json" if @calibration.profile
|
|
70
84
|
files
|
|
71
85
|
end
|
|
72
86
|
|
|
87
|
+
def tile_layout_metadata(artifacts)
|
|
88
|
+
{
|
|
89
|
+
requested: @settings.tile_layout,
|
|
90
|
+
tile_count: @settings.tile_count,
|
|
91
|
+
rows: artifacts.tile_layout.rows,
|
|
92
|
+
columns: artifacts.tile_layout.columns,
|
|
93
|
+
order: "left_to_right_then_top_to_bottom",
|
|
94
|
+
local_origin: "xy_min",
|
|
95
|
+
assembled_image_top: "+Y"
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def tile_metadata(artifacts)
|
|
100
|
+
artifacts.tile_artifacts.map do |artifact|
|
|
101
|
+
tile = artifact.tile
|
|
102
|
+
used = Palette::MATERIALS.select { |material| artifact.material_model.materials.include?(material) }
|
|
103
|
+
{
|
|
104
|
+
id: tile.id,
|
|
105
|
+
file: artifact.name,
|
|
106
|
+
row: tile.row,
|
|
107
|
+
column: tile.column,
|
|
108
|
+
offset_mm: tile.offset,
|
|
109
|
+
bounds_mm: tile.bounds,
|
|
110
|
+
dimensions_mm: { width: tile.width, height: tile.height, depth: @settings.effective_maximum_thickness },
|
|
111
|
+
materials: material_names(used),
|
|
112
|
+
unused_materials: material_names(Palette::MATERIALS - used),
|
|
113
|
+
triangle_count: artifact.three_mf.triangle_count
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
73
118
|
def used_materials(artifacts)
|
|
74
119
|
Palette::MATERIALS.select { |material| artifacts.material_model.materials.include?(material) }
|
|
75
120
|
end
|
|
@@ -1,6 +1,103 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Lithop
|
|
4
|
+
# Splits the reference White carrier and CMY pigments into closed Z-region parts.
|
|
5
|
+
class ZSeparatedMaterialMeshPlan
|
|
6
|
+
Entry = Data.define(:material, :geometry, :name, :region, :build_z_offset)
|
|
7
|
+
|
|
8
|
+
def initialize(model:, base_geometry:, top_geometry:, settings:, maximum_triangles:, limit_message:)
|
|
9
|
+
@model = model
|
|
10
|
+
@base_geometry = base_geometry
|
|
11
|
+
@top_geometry = top_geometry
|
|
12
|
+
@settings = settings
|
|
13
|
+
@maximum_triangles = maximum_triangles
|
|
14
|
+
@limit_message = limit_message
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
entries = build_entries
|
|
19
|
+
entry_counts = entries.map { |entry| triangle_count(entry.geometry) }
|
|
20
|
+
total = entry_counts.sum
|
|
21
|
+
raise ConfigurationError, @limit_message.call(total) if total > @maximum_triangles
|
|
22
|
+
|
|
23
|
+
MaterialMeshPlan::Result.new(
|
|
24
|
+
parts: entries.map do |entry|
|
|
25
|
+
ThreeMF::Part.new(
|
|
26
|
+
material: entry.material, triangles: triangle_source(entry.geometry),
|
|
27
|
+
name: entry.name, region: entry.region, build_z_offset: entry.build_z_offset
|
|
28
|
+
)
|
|
29
|
+
end,
|
|
30
|
+
triangle_count: total,
|
|
31
|
+
triangle_counts_by_material: counts_by_material(entries, entry_counts)
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def build_entries
|
|
38
|
+
entries = []
|
|
39
|
+
color_ranges.each_with_index do |range, index|
|
|
40
|
+
region_model = LayerRegionModel.new(model: @model, layer_range: range)
|
|
41
|
+
region = "color_region_#{index + 1}"
|
|
42
|
+
build_z_offset = @settings.layer_structure.base_white_thickness - region_model.z_boundaries.first
|
|
43
|
+
entries << Entry.new(
|
|
44
|
+
material: :white, geometry: @base_geometry, name: "Color region #{index + 1} Base White",
|
|
45
|
+
region:, build_z_offset: 0.0
|
|
46
|
+
)
|
|
47
|
+
entries << Entry.new(
|
|
48
|
+
material: :white,
|
|
49
|
+
geometry: color_region_white_geometry(region_model),
|
|
50
|
+
name: "Color region #{index + 1} White",
|
|
51
|
+
region:,
|
|
52
|
+
build_z_offset:
|
|
53
|
+
)
|
|
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
|
+
end
|
|
66
|
+
entries << Entry.new(
|
|
67
|
+
material: :white, geometry: @top_geometry, name: "Top White",
|
|
68
|
+
region: "top_white", build_z_offset: -@settings.layer_structure.color_region_end
|
|
69
|
+
)
|
|
70
|
+
entries
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def color_ranges
|
|
74
|
+
@settings.layer_structure.color_region_layer_ranges(@settings.color_region_parts)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def color_region_white_geometry(model)
|
|
78
|
+
BoxGeometry.new(
|
|
79
|
+
width: model.x_boundaries.last,
|
|
80
|
+
height: model.y_boundaries.last,
|
|
81
|
+
z_min: model.z_boundaries.first,
|
|
82
|
+
z_max: model.z_boundaries.last
|
|
83
|
+
)
|
|
84
|
+
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
|
+
end
|
|
100
|
+
|
|
4
101
|
# Builds the reference-style overlapping White height field and contiguous CMY columns.
|
|
5
102
|
class ReferenceMaterialMeshPlan
|
|
6
103
|
def initialize(model:, white_geometry:, maximum_triangles:, limit_message:)
|