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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/Cargo.lock +284 -0
  4. data/Cargo.toml +6 -0
  5. data/README.md +34 -13
  6. data/RELEASING.md +24 -6
  7. data/docs/README.md +6 -3
  8. data/docs/artifacts.md +35 -16
  9. data/docs/bambu-studio.md +17 -11
  10. data/docs/calibration.md +4 -4
  11. data/docs/color-model.md +20 -18
  12. data/docs/command.md +53 -14
  13. data/docs/config.md +43 -31
  14. data/docs/getting-started.md +17 -8
  15. data/docs/images/cmyw-lithophane-preview.png +0 -0
  16. data/docs/local-ui.md +117 -0
  17. data/docs/tiling.md +20 -19
  18. data/docs/troubleshooting.md +12 -2
  19. data/ext/lithop_native/Cargo.toml +17 -0
  20. data/ext/lithop_native/build.rs +4 -0
  21. data/ext/lithop_native/extconf.rb +6 -0
  22. data/ext/lithop_native/src/column_mesh.rs +388 -0
  23. data/ext/lithop_native/src/lib.rs +37 -0
  24. data/ext/lithop_native/src/mesh.rs +187 -0
  25. data/ext/lithop_native/src/palette.rs +67 -0
  26. data/ext/lithop_native/src/xml.rs +134 -0
  27. data/lib/lithop/calibration/export.rb +2 -2
  28. data/lib/lithop/calibration/target.rb +5 -5
  29. data/lib/lithop/cli/image_adjustment_options.rb +33 -0
  30. data/lib/lithop/cli/image_options.rb +5 -18
  31. data/lib/lithop/cli/options.rb +17 -4
  32. data/lib/lithop/cli.rb +28 -0
  33. data/lib/lithop/export/color_metadata.rb +46 -23
  34. data/lib/lithop/export/generation.rb +38 -27
  35. data/lib/lithop/export/job.rb +2 -2
  36. data/lib/lithop/export/metadata.rb +8 -3
  37. data/lib/lithop/export/planning.rb +82 -45
  38. data/lib/lithop/export/previews.rb +25 -1
  39. data/lib/lithop/export/print_readme.rb +20 -4
  40. data/lib/lithop/formats/three_mf/bambu_metadata.rb +11 -8
  41. data/lib/lithop/formats/three_mf/mesh.rb +72 -3
  42. data/lib/lithop/formats/three_mf/model_writer.rb +21 -21
  43. data/lib/lithop/formats/three_mf/part_writer.rb +37 -0
  44. data/lib/lithop/formats/three_mf.rb +1 -1
  45. data/lib/lithop/image/pipeline.rb +84 -31
  46. data/lib/lithop/image/svg_loader.rb +16 -7
  47. data/lib/lithop/job_execution.rb +132 -4
  48. data/lib/lithop/layer_structure.rb +0 -9
  49. data/lib/lithop/mesh/column_geometry.rb +8 -0
  50. data/lib/lithop/mesh/column_topology.rb +2 -0
  51. data/lib/lithop/model/filament_profile.rb +3 -3
  52. data/lib/lithop/model/material_display_map.rb +2 -1
  53. data/lib/lithop/model/material_model.rb +17 -5
  54. data/lib/lithop/model/palette.rb +21 -3
  55. data/lib/lithop/model/z_region_model.rb +44 -0
  56. data/lib/lithop/native.rb +39 -0
  57. data/lib/lithop/settings.rb +147 -15
  58. data/lib/lithop/settings_file.rb +1 -0
  59. data/lib/lithop/tiling.rb +3 -2
  60. data/lib/lithop/version.rb +1 -1
  61. data/lib/lithop/web/app.rb +517 -0
  62. data/lib/lithop/web/bambu_studio_launcher.rb +82 -0
  63. data/lib/lithop/web/job_launcher.rb +214 -0
  64. data/lib/lithop/web/job_store.rb +390 -0
  65. data/lib/lithop/web/preview_store.rb +182 -0
  66. data/lib/lithop/web/public/app.css +330 -0
  67. data/lib/lithop/web/public/app.js +1949 -0
  68. data/lib/lithop/web/server.rb +21 -0
  69. data/lib/lithop/web/settings_form.rb +67 -0
  70. data/lib/lithop/web/settings_library.rb +154 -0
  71. data/lib/lithop/web/views/index.erb +418 -0
  72. data/lib/lithop/web/worker.rb +27 -0
  73. data/lib/lithop.rb +3 -1
  74. data/sig/lithop.rbs +41 -0
  75. metadata +86 -3
  76. 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
@@ -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
- unless color_region_parts.is_a?(Integer) && color_region_parts.positive? &&
123
- color_region_parts <= color_layer_count
124
- raise ConfigurationError, "color_region_parts must be an integer from 1 to color_layer_count"
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
- minimum_thickness: 1.0,
257
- maximum_thickness: 2.14689422,
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.1,
270
- base_white_layers: 2,
271
- top_layer_height: 0.04,
272
- top_white_start: 1.0,
273
- minimum_top_white_layers: 0,
274
- exact_maximum_thickness: true,
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: PIGMENTS,
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 effective_minimum_thickness > layer_structure.color_region_end
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
@@ -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="#fff" stroke="#000" stroke-width="0.4"/>
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lithop
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end