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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
module Lithop
|
|
6
|
+
# Loads optional acceleration implemented by the bundled Rust extension.
|
|
7
|
+
module Native
|
|
8
|
+
class << self
|
|
9
|
+
def available? = @available == true
|
|
10
|
+
|
|
11
|
+
def enabled? = available? && ENV["LITHOP_DISABLE_NATIVE"] != "1"
|
|
12
|
+
|
|
13
|
+
def backend = enabled? ? :rust : :ruby
|
|
14
|
+
|
|
15
|
+
def loaded_extension_version
|
|
16
|
+
extension_version if available? && respond_to?(:extension_version)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def diagnostics
|
|
20
|
+
{
|
|
21
|
+
backend:,
|
|
22
|
+
available: available?,
|
|
23
|
+
enabled: enabled?,
|
|
24
|
+
extension_version: loaded_extension_version,
|
|
25
|
+
lithop_version: Lithop::VERSION,
|
|
26
|
+
ruby: RUBY_DESCRIPTION,
|
|
27
|
+
platform: RbConfig::CONFIG.fetch("host")
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
require "lithop_native/lithop_native"
|
|
36
|
+
Lithop::Native.instance_variable_set(:@available, true)
|
|
37
|
+
rescue LoadError
|
|
38
|
+
Lithop::Native.instance_variable_set(:@available, false)
|
|
39
|
+
end
|
data/lib/lithop/settings.rb
CHANGED
|
@@ -119,10 +119,9 @@ module Lithop
|
|
|
119
119
|
|
|
120
120
|
def validate_z_parts!
|
|
121
121
|
raise ConfigurationError, "separate_z_parts must be a boolean" unless [true, false].include?(separate_z_parts)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
end
|
|
122
|
+
return if color_region_parts.is_a?(Integer) && color_region_parts.between?(1, Settings::MAXIMUM_Z_PARTS)
|
|
123
|
+
|
|
124
|
+
raise ConfigurationError, "color_region_parts must be an integer from 1 to #{Settings::MAXIMUM_Z_PARTS}"
|
|
126
125
|
end
|
|
127
126
|
|
|
128
127
|
def validate_layer_structure!
|
|
@@ -187,6 +186,17 @@ module Lithop
|
|
|
187
186
|
|
|
188
187
|
raise ConfigurationError, "#{key} must be a finite percentage from -100 to 100"
|
|
189
188
|
end
|
|
189
|
+
unless yellow_mix_percent.is_a?(Numeric) && yellow_mix_percent.finite? &&
|
|
190
|
+
yellow_mix_percent.between?(0.0, 100.0)
|
|
191
|
+
raise ConfigurationError, "yellow_mix_percent must be a finite percentage from 0 to 100"
|
|
192
|
+
end
|
|
193
|
+
unless Settings::YELLOW_MIX_COLORS.include?(yellow_mix_color)
|
|
194
|
+
raise ConfigurationError,
|
|
195
|
+
"yellow_mix_color must be one of: #{Settings::YELLOW_MIX_COLORS.join(", ")}"
|
|
196
|
+
end
|
|
197
|
+
return if [true, false].include?(suppress_pale_colors)
|
|
198
|
+
|
|
199
|
+
raise ConfigurationError, "suppress_pale_colors must be a boolean"
|
|
190
200
|
end
|
|
191
201
|
|
|
192
202
|
def validate_nozzle_diameter!
|
|
@@ -227,16 +237,27 @@ module Lithop
|
|
|
227
237
|
# Validated dimensions, print parameters, and output limits for a generation job.
|
|
228
238
|
# rubocop:disable-next Metrics/ClassLength
|
|
229
239
|
class Settings
|
|
240
|
+
MAXIMUM_Z_PARTS = 3
|
|
241
|
+
MINIMUM_Z_PART_THICKNESS = 0.32
|
|
242
|
+
MINIMUM_Z_SOURCE_THICKNESS = MINIMUM_Z_PART_THICKNESS * MAXIMUM_Z_PARTS
|
|
243
|
+
Z_SEPARATED_COLOR_LAYER_COUNT = 8
|
|
244
|
+
Z_SEPARATED_FIRST_LAYER_HEIGHT = 0.1
|
|
245
|
+
Z_SEPARATED_LAYER_HEIGHT = 0.08
|
|
246
|
+
Z_PART_BASE_WHITE_LAYERS = 1
|
|
247
|
+
Z_PART_MINIMUM_TOTAL_LAYERS = 4
|
|
230
248
|
include SettingsValidation
|
|
231
249
|
|
|
232
250
|
MICRONS_PER_MM = LayerStructure::MICRONS_PER_MM
|
|
233
251
|
COLOR_MODELS = %w[cmy].freeze
|
|
234
252
|
NOZZLE_DIAMETERS = [0.2, 0.4].freeze
|
|
235
|
-
COLOR_LAYER_COUNTS = [4, 5, 7].freeze
|
|
253
|
+
COLOR_LAYER_COUNTS = [4, 5, 7, 8].freeze
|
|
236
254
|
LAYOUT_PROFILES = ["standard"].freeze
|
|
237
255
|
IMAGE_FITS = %w[cover contain].freeze
|
|
238
256
|
TILE_LAYOUTS = %w[auto columns rows grid].freeze
|
|
257
|
+
YELLOW_MIX_COLORS = %w[cyan magenta].freeze
|
|
239
258
|
PIGMENTS = %i[cyan magenta yellow].freeze
|
|
259
|
+
PIGMENT_ORDERS = PIGMENTS.permutation.map(&:freeze).freeze
|
|
260
|
+
DEFAULT_PIGMENT_ORDER = %i[yellow magenta cyan].freeze
|
|
240
261
|
MINIMUM_TOP_WHITE_LAYERS = LayerStructure::MINIMUM_TOP_WHITE_LAYERS
|
|
241
262
|
DEFAULTS = {
|
|
242
263
|
layout_profile: "standard",
|
|
@@ -253,8 +274,11 @@ module Lithop
|
|
|
253
274
|
brightness: 0.0,
|
|
254
275
|
contrast: 0.0,
|
|
255
276
|
saturation: 0.0,
|
|
256
|
-
|
|
257
|
-
|
|
277
|
+
suppress_pale_colors: false,
|
|
278
|
+
yellow_mix_color: "cyan",
|
|
279
|
+
yellow_mix_percent: 0.0,
|
|
280
|
+
minimum_thickness: 0.74,
|
|
281
|
+
maximum_thickness: 1.94,
|
|
258
282
|
pixel_size: 0.22,
|
|
259
283
|
color_pixel_size: nil,
|
|
260
284
|
grid_columns: 637,
|
|
@@ -266,16 +290,16 @@ module Lithop
|
|
|
266
290
|
separate_z_parts: false,
|
|
267
291
|
color_region_parts: 3,
|
|
268
292
|
first_layer_height: 0.1,
|
|
269
|
-
layer_height: 0.
|
|
270
|
-
base_white_layers:
|
|
271
|
-
top_layer_height: 0.
|
|
272
|
-
top_white_start:
|
|
273
|
-
minimum_top_white_layers:
|
|
274
|
-
exact_maximum_thickness:
|
|
293
|
+
layer_height: 0.08,
|
|
294
|
+
base_white_layers: 1,
|
|
295
|
+
top_layer_height: 0.08,
|
|
296
|
+
top_white_start: nil,
|
|
297
|
+
minimum_top_white_layers: 1,
|
|
298
|
+
exact_maximum_thickness: false,
|
|
275
299
|
nozzle_diameter: 0.2,
|
|
276
300
|
color_model: "cmy",
|
|
277
301
|
color_layer_count: 7,
|
|
278
|
-
pigment_order:
|
|
302
|
+
pigment_order: DEFAULT_PIGMENT_ORDER,
|
|
279
303
|
bambu_project: true,
|
|
280
304
|
maximum_input_pixels: 20_000_000,
|
|
281
305
|
maximum_triangles: 5_000_000
|
|
@@ -290,6 +314,7 @@ module Lithop
|
|
|
290
314
|
settings = DEFAULTS.merge(values).merge(layout_profile: profile)
|
|
291
315
|
DEFAULTS.each_key { |key| instance_variable_set("@#{key}", settings.fetch(key)) }
|
|
292
316
|
@pigment_order = normalize_pigment_order(@pigment_order)
|
|
317
|
+
apply_z_separated_layer_structure! if @separate_z_parts
|
|
293
318
|
validate!
|
|
294
319
|
@layer_structure = LayerStructure.new(
|
|
295
320
|
first_layer_height:, layer_height:, color_layer_count:, minimum_thickness:, maximum_thickness:,
|
|
@@ -304,6 +329,7 @@ module Lithop
|
|
|
304
329
|
|
|
305
330
|
validate_physical_minimum!
|
|
306
331
|
validate_separated_top_white!
|
|
332
|
+
validate_z_separated_thickness_range!
|
|
307
333
|
end
|
|
308
334
|
|
|
309
335
|
def without_default_grid(values)
|
|
@@ -315,6 +341,18 @@ module Lithop
|
|
|
315
341
|
end
|
|
316
342
|
private :without_default_grid
|
|
317
343
|
|
|
344
|
+
def apply_z_separated_layer_structure!
|
|
345
|
+
@color_layer_count = Z_SEPARATED_COLOR_LAYER_COUNT
|
|
346
|
+
@first_layer_height = Z_SEPARATED_FIRST_LAYER_HEIGHT
|
|
347
|
+
@layer_height = Z_SEPARATED_LAYER_HEIGHT
|
|
348
|
+
@top_layer_height = Z_SEPARATED_LAYER_HEIGHT
|
|
349
|
+
@top_white_start = ((@base_white_layers * @first_layer_height) +
|
|
350
|
+
(@color_layer_count * @layer_height)).round(9)
|
|
351
|
+
@minimum_thickness = [@minimum_thickness, @top_white_start + @top_layer_height].max.round(9)
|
|
352
|
+
@exact_maximum_thickness = false
|
|
353
|
+
end
|
|
354
|
+
private :apply_z_separated_layer_structure!
|
|
355
|
+
|
|
318
356
|
def columns = sampling_grid.columns
|
|
319
357
|
def rows = sampling_grid.rows
|
|
320
358
|
def effective_color_pixel_size = color_pixel_size || pixel_size
|
|
@@ -334,6 +372,77 @@ module Lithop
|
|
|
334
372
|
def resolved_tile_rows = resolved_tile_shape.fetch(0)
|
|
335
373
|
def resolved_tile_columns = resolved_tile_shape.fetch(1)
|
|
336
374
|
|
|
375
|
+
def resolved_z_part_boundaries
|
|
376
|
+
@resolved_z_part_boundaries ||= begin
|
|
377
|
+
start = layer_structure.base_white_thickness
|
|
378
|
+
source_thickness = z_separated_minimum_thickness / color_region_parts
|
|
379
|
+
boundaries = Array.new(color_region_parts) do |index|
|
|
380
|
+
(start + (index * source_thickness)).round(9)
|
|
381
|
+
end
|
|
382
|
+
[*boundaries, z_separated_source_end].freeze
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def resolved_z_part_ranges
|
|
387
|
+
@resolved_z_part_ranges ||= resolved_z_part_boundaries.each_cons(2).map do |start, finish|
|
|
388
|
+
start...finish
|
|
389
|
+
end.freeze
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def resolved_z_color_ranges
|
|
393
|
+
color_start = layer_structure.color_region_start
|
|
394
|
+
color_end = layer_structure.color_region_end
|
|
395
|
+
resolved_z_part_ranges.map do |range|
|
|
396
|
+
[range.begin, color_start].max...[range.end, color_end].min
|
|
397
|
+
end.freeze
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def resolved_z_part_count = color_region_parts
|
|
401
|
+
|
|
402
|
+
def z_separated_minimum_thickness
|
|
403
|
+
MINIMUM_Z_SOURCE_THICKNESS.round(9)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def z_separated_source_end
|
|
407
|
+
(layer_structure.base_white_thickness + z_separated_minimum_thickness).round(9)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def z_part_base_white_layers(index = 0)
|
|
411
|
+
return Z_PART_BASE_WHITE_LAYERS unless separate_z_parts
|
|
412
|
+
|
|
413
|
+
source_layers = (z_part_minimum_source_thickness(index) / layer_height).round
|
|
414
|
+
[Z_PART_BASE_WHITE_LAYERS, Z_PART_MINIMUM_TOTAL_LAYERS - source_layers].max
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def z_part_base_white_thickness(index = 0)
|
|
418
|
+
layers = z_part_base_white_layers(index)
|
|
419
|
+
(first_layer_height + ((layers - 1) * layer_height)).round(9)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def z_part_minimum_build_thickness
|
|
423
|
+
resolved_z_part_ranges.each_index.map do |index|
|
|
424
|
+
(z_part_minimum_source_thickness(index) + z_part_base_white_thickness(index)).round(9)
|
|
425
|
+
end.min
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def z_separated_output_minimum_thickness
|
|
429
|
+
without_source_base = effective_minimum_thickness - layer_structure.base_white_thickness
|
|
430
|
+
(without_source_base + z_part_base_white_thicknesses.sum).round(9)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def z_separated_output_maximum_thickness
|
|
434
|
+
without_source_base = effective_maximum_thickness - layer_structure.base_white_thickness
|
|
435
|
+
(without_source_base + z_part_base_white_thicknesses.sum).round(9)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def z_part_base_white_layers_by_plate
|
|
439
|
+
resolved_z_part_ranges.each_index.map { |index| z_part_base_white_layers(index) }.freeze
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def z_part_base_white_thicknesses
|
|
443
|
+
resolved_z_part_ranges.each_index.map { |index| z_part_base_white_thickness(index) }.freeze
|
|
444
|
+
end
|
|
445
|
+
|
|
337
446
|
def estimated_triangles
|
|
338
447
|
(4 * columns * rows) + (4 * (columns + rows))
|
|
339
448
|
end
|
|
@@ -372,6 +481,15 @@ module Lithop
|
|
|
372
481
|
effective_color_pixel_size: effective_color_pixel_size,
|
|
373
482
|
effective_minimum_thickness: effective_minimum_thickness,
|
|
374
483
|
effective_maximum_thickness: effective_maximum_thickness,
|
|
484
|
+
minimum_z_part_thickness: MINIMUM_Z_PART_THICKNESS,
|
|
485
|
+
z_part_base_white_layers: Z_PART_BASE_WHITE_LAYERS,
|
|
486
|
+
z_part_base_white_thickness: z_part_base_white_thickness,
|
|
487
|
+
z_part_minimum_build_thickness: z_part_minimum_build_thickness,
|
|
488
|
+
resolved_z_part_count: resolved_z_part_count,
|
|
489
|
+
z_separated_minimum_thickness: z_separated_minimum_thickness,
|
|
490
|
+
z_separated_output_minimum_thickness: z_separated_output_minimum_thickness,
|
|
491
|
+
z_separated_output_maximum_thickness: z_separated_output_maximum_thickness,
|
|
492
|
+
z_separated_source_end: z_separated_source_end,
|
|
375
493
|
columns: columns, rows: rows, pixel_size_x: pixel_size_x, pixel_size_y: pixel_size_y,
|
|
376
494
|
resolved_tile_rows: resolved_tile_rows, resolved_tile_columns: resolved_tile_columns
|
|
377
495
|
)
|
|
@@ -379,6 +497,12 @@ module Lithop
|
|
|
379
497
|
|
|
380
498
|
private
|
|
381
499
|
|
|
500
|
+
def z_part_minimum_source_thickness(index)
|
|
501
|
+
range = resolved_z_part_ranges.fetch(index)
|
|
502
|
+
source_end = [range.end, effective_minimum_thickness].min
|
|
503
|
+
[source_end - range.begin, 0.0].max.round(9)
|
|
504
|
+
end
|
|
505
|
+
|
|
382
506
|
def automatic_tile_shape
|
|
383
507
|
factors = factor_shapes.map do |row_count, column_count|
|
|
384
508
|
tile_width = width.fdiv(column_count)
|
|
@@ -402,9 +526,17 @@ module Lithop
|
|
|
402
526
|
|
|
403
527
|
def validate_separated_top_white!
|
|
404
528
|
return unless separate_z_parts
|
|
405
|
-
return if
|
|
529
|
+
return if z_separated_source_end > layer_structure.color_region_end
|
|
406
530
|
|
|
407
531
|
raise ConfigurationError, "separate_z_parts requires a positive top White thickness"
|
|
408
532
|
end
|
|
533
|
+
|
|
534
|
+
def validate_z_separated_thickness_range!
|
|
535
|
+
return unless separate_z_parts
|
|
536
|
+
return if z_separated_source_end <= effective_maximum_thickness
|
|
537
|
+
|
|
538
|
+
raise ConfigurationError,
|
|
539
|
+
"Z separation requires maximum_thickness of at least #{z_separated_source_end} mm"
|
|
540
|
+
end
|
|
409
541
|
end
|
|
410
542
|
end
|
data/lib/lithop/settings_file.rb
CHANGED
|
@@ -39,6 +39,7 @@ module Lithop
|
|
|
39
39
|
values[:image_fit] = values[:image_fit].downcase if values[:image_fit].is_a?(String)
|
|
40
40
|
values[:layout_profile] = values[:layout_profile].downcase if values[:layout_profile].is_a?(String)
|
|
41
41
|
values[:tile_layout] = values[:tile_layout].downcase if values[:tile_layout].is_a?(String)
|
|
42
|
+
values[:yellow_mix_color] = values[:yellow_mix_color].downcase if values[:yellow_mix_color].is_a?(String)
|
|
42
43
|
values
|
|
43
44
|
end
|
|
44
45
|
private_class_method :normalize_values
|
data/lib/lithop/tiling.rb
CHANGED
|
@@ -91,13 +91,14 @@ module Lithop
|
|
|
91
91
|
File.write(path, document(settings:, layout:))
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
-
def document(settings:, layout:)
|
|
94
|
+
def document(settings:, layout:, background: true)
|
|
95
95
|
rectangles = layout.tiles.map { |tile| tile_element(settings, tile) }.join("\n")
|
|
96
|
+
fill = background ? "#fff" : "none"
|
|
96
97
|
<<~SVG
|
|
97
98
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
98
99
|
<svg xmlns="http://www.w3.org/2000/svg" width="#{settings.width}mm" height="#{settings.height}mm"
|
|
99
100
|
viewBox="0 0 #{settings.width} #{settings.height}">
|
|
100
|
-
<rect width="#{settings.width}" height="#{settings.height}" fill="#
|
|
101
|
+
<rect width="#{settings.width}" height="#{settings.height}" fill="#{fill}" stroke="#000" stroke-width="0.4"/>
|
|
101
102
|
#{rectangles}
|
|
102
103
|
</svg>
|
|
103
104
|
SVG
|
data/lib/lithop/version.rb
CHANGED