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,134 @@
1
+ use crate::COMPONENTS;
2
+ use magnus::{Error, Ruby};
3
+ use std::fmt::Write;
4
+
5
+ const NUMBER_FORMAT: &[u8] = b"%.9g\0";
6
+
7
+ fn append_number(output: &mut String, value: f64) -> Result<(), &'static str> {
8
+ if !value.is_finite() {
9
+ return Err("3MF coordinates must be finite");
10
+ }
11
+
12
+ let mut buffer = [0_u8; 64];
13
+ let length = unsafe {
14
+ libc::snprintf(
15
+ buffer.as_mut_ptr().cast(),
16
+ buffer.len(),
17
+ NUMBER_FORMAT.as_ptr().cast(),
18
+ value,
19
+ )
20
+ };
21
+ if length < 0 || length as usize >= buffer.len() {
22
+ return Err("could not format a 3MF coordinate");
23
+ }
24
+ let formatted = std::str::from_utf8(&buffer[..length as usize])
25
+ .map_err(|_| "could not format a 3MF coordinate")?;
26
+ output.push_str(formatted);
27
+ Ok(())
28
+ }
29
+
30
+ fn build_vertex_xml(vertices: &[f64], origin: &[f64]) -> Result<String, &'static str> {
31
+ if vertices.len() % COMPONENTS != 0 {
32
+ return Err("3MF vertices must contain XYZ triples");
33
+ }
34
+ if origin.len() != COMPONENTS || origin.iter().any(|value| !value.is_finite()) {
35
+ return Err("3MF origin must contain three finite values");
36
+ }
37
+
38
+ let mut output = String::with_capacity((vertices.len() / COMPONENTS) * 72);
39
+ for vertex in vertices.chunks_exact(COMPONENTS) {
40
+ output.push_str(" <vertex x=\"");
41
+ append_number(&mut output, vertex[0] - origin[0])?;
42
+ output.push_str("\" y=\"");
43
+ append_number(&mut output, vertex[1] - origin[1])?;
44
+ output.push_str("\" z=\"");
45
+ append_number(&mut output, vertex[2] - origin[2])?;
46
+ output.push_str("\"/>\n");
47
+ }
48
+ Ok(output)
49
+ }
50
+
51
+ pub(crate) fn build_vertex_records(
52
+ vertices: &[[f64; COMPONENTS]],
53
+ origin: &[f64],
54
+ ) -> Result<String, &'static str> {
55
+ if origin.len() != COMPONENTS || origin.iter().any(|value| !value.is_finite()) {
56
+ return Err("3MF origin must contain three finite values");
57
+ }
58
+ let mut output = String::with_capacity(vertices.len() * 72);
59
+ for vertex in vertices {
60
+ output.push_str(" <vertex x=\"");
61
+ append_number(&mut output, vertex[0] - origin[0])?;
62
+ output.push_str("\" y=\"");
63
+ append_number(&mut output, vertex[1] - origin[1])?;
64
+ output.push_str("\" z=\"");
65
+ append_number(&mut output, vertex[2] - origin[2])?;
66
+ output.push_str("\"/>\n");
67
+ }
68
+ Ok(output)
69
+ }
70
+
71
+ pub(crate) fn vertex_xml(
72
+ ruby: &Ruby,
73
+ vertices: Vec<f64>,
74
+ origin: Vec<f64>,
75
+ ) -> Result<String, Error> {
76
+ build_vertex_xml(&vertices, &origin)
77
+ .map_err(|message| Error::new(ruby.exception_arg_error(), message))
78
+ }
79
+
80
+ fn build_triangle_xml(indices: &[usize]) -> Result<String, &'static str> {
81
+ if indices.len() % COMPONENTS != 0 {
82
+ return Err("3MF triangle indices must contain triples");
83
+ }
84
+
85
+ let mut output = String::with_capacity((indices.len() / COMPONENTS) * 56);
86
+ for triangle in indices.chunks_exact(COMPONENTS) {
87
+ writeln!(
88
+ output,
89
+ " <triangle v1=\"{}\" v2=\"{}\" v3=\"{}\"/>",
90
+ triangle[0], triangle[1], triangle[2]
91
+ )
92
+ .map_err(|_| "could not format 3MF triangle indices")?;
93
+ }
94
+ Ok(output)
95
+ }
96
+
97
+ pub(crate) fn build_triangle_records(
98
+ triangles: &[[u32; COMPONENTS]],
99
+ ) -> Result<String, &'static str> {
100
+ let mut output = String::with_capacity(triangles.len() * 56);
101
+ for triangle in triangles {
102
+ writeln!(
103
+ output,
104
+ " <triangle v1=\"{}\" v2=\"{}\" v3=\"{}\"/>",
105
+ triangle[0], triangle[1], triangle[2]
106
+ )
107
+ .map_err(|_| "could not format 3MF triangle indices")?;
108
+ }
109
+ Ok(output)
110
+ }
111
+
112
+ pub(crate) fn triangle_xml(ruby: &Ruby, indices: Vec<usize>) -> Result<String, Error> {
113
+ build_triangle_xml(&indices).map_err(|message| Error::new(ruby.exception_arg_error(), message))
114
+ }
115
+
116
+ #[cfg(test)]
117
+ mod tests {
118
+ use super::{build_triangle_xml, build_vertex_xml};
119
+
120
+ #[test]
121
+ fn formats_vertex_xml_with_ruby_compatible_precision() {
122
+ let xml = build_vertex_xml(&[1.2345678912, 2.0, 3.0], &[1.0, 0.0, 0.5]).unwrap();
123
+ assert_eq!(
124
+ xml,
125
+ " <vertex x=\"0.234567891\" y=\"2\" z=\"2.5\"/>\n"
126
+ );
127
+ }
128
+
129
+ #[test]
130
+ fn formats_triangle_xml() {
131
+ let xml = build_triangle_xml(&[12, 3, 45]).unwrap();
132
+ assert_eq!(xml, " <triangle v1=\"12\" v2=\"3\" v3=\"45\"/>\n");
133
+ }
134
+ }
@@ -28,9 +28,9 @@ module Lithop
28
28
  )
29
29
  execution.call do |job|
30
30
  destination, generated = JobDirectory.new(project_dir: @project_dir, job_id:).call do |directory|
31
- generate(directory, job_id, job)
31
+ generate(directory, job_id, job).tap { job.checkpoint! }
32
32
  end
33
- job.progress(phase: :publishing, percent: 99, message: "Calibration artifacts published")
33
+ job.publication_committed!(message: "Calibration artifacts published")
34
34
  Result.new(job_id:, path: destination, files: generated.fetch(:files),
35
35
  triangle_count: generated.fetch(:triangle_count), patch_count: generated.fetch(:patch_count))
36
36
  end
@@ -7,12 +7,12 @@ module Lithop
7
7
  private
8
8
 
9
9
  def validate_structure!
10
- unless @settings.color_layer_count == 7 &&
11
- @settings.pigment_order == Palette::PIGMENTS
12
- raise ConfigurationError,
13
- "calibration target requires seven layers in cyan,magenta,yellow pigment order"
14
- end
10
+ return validate_white_levels! if @settings.color_layer_count == 7
11
+
12
+ raise ConfigurationError, "calibration target requires seven color layers"
13
+ end
15
14
 
15
+ def validate_white_levels!
16
16
  levels = @settings.layer_structure.normal_layer_count(@settings.effective_maximum_thickness) -
17
17
  @settings.layer_structure.normal_layer_count(@settings.effective_minimum_thickness) + 1
18
18
  return if levels >= Target::WHITE_STAGE_COUNT
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lithop
4
+ # Adds sRGB, pale-color, and Yellow transmission adjustments.
5
+ module CLIImageAdjustmentOptions
6
+ module_function
7
+
8
+ def add(parser, options)
9
+ parser.on("--brightness PERCENT", Float, "Brightness: -100..100 (default: 0)") do |value|
10
+ options[:brightness] = value
11
+ end
12
+ parser.on("--contrast PERCENT", Float, "Contrast: -100..100 (default: 0)") do |value|
13
+ options[:contrast] = value
14
+ end
15
+ parser.on("--saturation PERCENT", Float, "Saturation: -100..100 (default: 0)") do |value|
16
+ options[:saturation] = value
17
+ end
18
+ parser.on("--suppress-pale-colors", "Convert very pale colors to White") do
19
+ options[:suppress_pale_colors] = true
20
+ end
21
+ parser.on("--no-suppress-pale-colors", "Keep very pale colors as CMY candidates (default)") do
22
+ options[:suppress_pale_colors] = false
23
+ end
24
+ parser.on("--yellow-mix-color COLOR", "Pigment mixed into Yellow: cyan/magenta (default: cyan)") do |value|
25
+ options[:yellow_mix_color] = value.downcase
26
+ end
27
+ parser.on(
28
+ "--yellow-mix-percent PERCENT", Float,
29
+ "Yellow coverage replaced by mix pigment: 0..100 (default: 0)"
30
+ ) { |value| options[:yellow_mix_percent] = value }
31
+ end
32
+ end
33
+ end
@@ -10,7 +10,7 @@ module Lithop
10
10
  add_tiles(parser, options)
11
11
  add_margins(parser, options)
12
12
  add_placement(parser, options)
13
- add_adjustments(parser, options)
13
+ CLIImageAdjustmentOptions.add(parser, options)
14
14
  add_thickness(parser, options)
15
15
  add_sampling(parser, options)
16
16
  end
@@ -39,10 +39,10 @@ module Lithop
39
39
  parser.on("--tile-columns COUNT", Integer, "Tile columns for grid layout") do |value|
40
40
  options[:tile_columns] = value
41
41
  end
42
- parser.on("--separate-z-parts", "Put base White, color regions, and top White on separate plates") do
42
+ parser.on("--separate-z-parts", "Slice the complete model into contiguous Z plates") do
43
43
  options[:separate_z_parts] = true
44
44
  end
45
- parser.on("--color-region-parts COUNT", Integer, "Number of CMYW color-region Z parts (default: 3)") do |value|
45
+ parser.on("--color-region-parts COUNT", Integer, "Z plate count (1-3); thinnest plate is 0.34 mm") do |value|
46
46
  options[:color_region_parts] = value
47
47
  end
48
48
  end
@@ -72,24 +72,11 @@ module Lithop
72
72
  end
73
73
  private_class_method :add_placement
74
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
-
88
75
  def add_thickness(parser, options)
89
- parser.on("--min-thickness MM", Float, "Requested minimum model thickness (default: 1.0)") do |value|
76
+ parser.on("--min-thickness MM", Float, "Requested minimum model thickness (default: 0.74)") do |value|
90
77
  options[:minimum_thickness] = value
91
78
  end
92
- parser.on("--max-thickness MM", Float, "Requested maximum model thickness (default: 2.14689422)") do |value|
79
+ parser.on("--max-thickness MM", Float, "Requested maximum model thickness (default: 1.94)") do |value|
93
80
  options[:maximum_thickness] = value
94
81
  end
95
82
  end
@@ -36,10 +36,10 @@ module Lithop
36
36
  def calibration(options, out)
37
37
  OptionParser.new do |parser|
38
38
  parser.banner = "Usage: lithop calibrate [options]"
39
- parser.on("--min-thickness MM", Float, "Requested minimum thickness (default: 1.0)") do |value|
39
+ parser.on("--min-thickness MM", Float, "Requested minimum thickness (default: 0.74)") do |value|
40
40
  options[:minimum_thickness] = value
41
41
  end
42
- parser.on("--max-thickness MM", Float, "Requested maximum thickness (default: 2.14689422)") do |value|
42
+ parser.on("--max-thickness MM", Float, "Requested maximum thickness (default: 1.94)") do |value|
43
43
  options[:maximum_thickness] = value
44
44
  end
45
45
  add_print_settings(parser, options)
@@ -48,6 +48,19 @@ module Lithop
48
48
  end
49
49
  end
50
50
 
51
+ def ui(options, out)
52
+ OptionParser.new do |parser|
53
+ parser.banner = "Usage: lithop ui [--project-dir DIR] [--port PORT]"
54
+ parser.on("--project-dir DIR", "Project directory containing config/ and build/") do |value|
55
+ options[:project_dir] = value
56
+ end
57
+ parser.on("--port PORT", Integer, "Local UI port (default: 4567)") do |value|
58
+ options[:port] = value
59
+ end
60
+ add_information(parser, out)
61
+ end
62
+ end
63
+
51
64
  def add_print_settings(parser, options)
52
65
  parser.on("--first-layer-height MM", Float, "First layer height (default: 0.1)") do |value|
53
66
  options[:first_layer_height] = value
@@ -61,10 +74,10 @@ module Lithop
61
74
  parser.on("--color-model MODEL", "Color model (default: cmy)") do |value|
62
75
  options[:color_model] = value.downcase
63
76
  end
64
- parser.on("--color-layer-count COUNT", Integer, "CMY/W color layers: 4/5/7 (default: 7)") do |value|
77
+ parser.on("--color-layer-count COUNT", Integer, "CMY/W color layers: 4/5/7/8 (default: 7; Z split: 8)") do |value|
65
78
  options[:color_layer_count] = value
66
79
  end
67
- parser.on("--pigment-order LIST", "Pigment order, e.g. cyan,magenta,yellow") do |value|
80
+ parser.on("--pigment-order LIST", "Pigment order, e.g. yellow,magenta,cyan (default: YMC)") do |value|
68
81
  options[:pigment_order] = value
69
82
  end
70
83
  end
data/lib/lithop/cli.rb CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  require "optparse"
4
4
  require "fileutils"
5
+ require "json"
5
6
 
6
7
  module Lithop
7
8
  # Command-line interface for generating and cleaning lithophane projects.
9
+ # rubocop:disable-next Metrics/ClassLength
8
10
  class CLI
9
11
  include CLICalibration
10
12
 
@@ -18,9 +20,12 @@ module Lithop
18
20
  @cwd = File.expand_path(cwd)
19
21
  end
20
22
 
23
+ # rubocop:disable-next Metrics/AbcSize
21
24
  def run(argv)
22
25
  return clean(argv.drop(1)) if argv.first == "clean"
23
26
  return calibrate(argv.drop(1)) if argv.first == "calibrate"
27
+ return web_ui(argv.drop(1)) if argv.first == "ui"
28
+ return diagnostics(argv.drop(1)) if argv.first == "doctor"
24
29
 
25
30
  options = {}
26
31
  parser = CLIOptions.generation(options, @out)
@@ -58,6 +63,13 @@ module Lithop
58
63
 
59
64
  private
60
65
 
66
+ def diagnostics(arguments)
67
+ raise InputError, "doctor does not accept arguments" unless arguments.empty?
68
+
69
+ @out.puts JSON.pretty_generate(Native.diagnostics)
70
+ 0
71
+ end
72
+
61
73
  def report_estimate(estimate)
62
74
  @out.puts(
63
75
  "Estimated #{estimate.stl_triangle_count} STL and " \
@@ -105,5 +117,21 @@ module Lithop
105
117
  end
106
118
 
107
119
  def report_missing_build(build_dir) = 0.tap { @out.puts "No build directory: #{build_dir}" }
120
+
121
+ def web_ui(argv)
122
+ options = {}
123
+ parser = CLIOptions.ui(options, @out)
124
+ arguments = parser.parse(argv)
125
+ raise OptionParser::InvalidArgument, arguments.join(" ") unless arguments.empty?
126
+
127
+ project_dir = File.expand_path(options.fetch(:project_dir, @cwd), @cwd)
128
+ port = options.fetch(:port, 4567)
129
+ raise OptionParser::InvalidArgument, "port must be from 1 to 65535" unless port.between?(1, 65_535)
130
+
131
+ require_relative "web/server"
132
+ @out.puts "Lithop Local UI: http://127.0.0.1:#{port}"
133
+ Web::Server.run(project_dir:, port:)
134
+ 0
135
+ end
108
136
  end
109
137
  end
@@ -68,20 +68,31 @@ module Lithop
68
68
 
69
69
  def z_part_separation
70
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|
71
+ color_regions = @settings.resolved_z_color_ranges.each_with_index.filter_map do |range, index|
72
+ next unless range.begin < range.end
73
+
74
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)
75
+ id: "z_part_#{index + 1}",
76
+ z_min: range.begin,
77
+ z_max: range.end
80
78
  }
81
79
  end
82
80
  metadata = {
83
81
  enabled: @settings.separate_z_parts,
84
- base_white: { id: "base_white", z_min: 0.0, z_max: structure.base_white_thickness },
82
+ requested_parts: @settings.color_region_parts,
83
+ resolved_parts: @settings.resolved_z_part_count,
84
+ minimum_source_plate_thickness: Settings::MINIMUM_Z_PART_THICKNESS,
85
+ minimum_plate_thickness: @settings.z_part_minimum_build_thickness,
86
+ adjusted_minimum_total_thickness: @settings.z_separated_output_minimum_thickness,
87
+ base_white: {
88
+ id: "base_white", included: true,
89
+ layers_per_plate: base_white_layers_per_plate,
90
+ thickness_per_plate: base_white_thickness_per_plate,
91
+ repeated_per_plate: true
92
+ },
93
+ source_base_white: {
94
+ id: "source_base_white", included: false, z_min: 0.0, z_max: structure.base_white_thickness
95
+ },
85
96
  color_regions:,
86
97
  top_white: {
87
98
  id: "top_white", z_min: structure.color_region_end,
@@ -92,30 +103,42 @@ module Lithop
92
103
  metadata
93
104
  end
94
105
 
106
+ def base_white_layers_per_plate
107
+ return Settings::Z_PART_BASE_WHITE_LAYERS unless @settings.separate_z_parts
108
+
109
+ @settings.z_part_base_white_layers_by_plate
110
+ end
111
+
112
+ def base_white_thickness_per_plate
113
+ return @settings.z_part_base_white_thickness unless @settings.separate_z_parts
114
+
115
+ @settings.z_part_base_white_thicknesses
116
+ end
117
+
95
118
  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)
119
+ top_white = metadata.fetch(:top_white)
120
+ ranges = @settings.resolved_z_part_ranges
121
+ ranges.each_with_index.map do |range, index|
122
+ z_min = range.begin
123
+ z_max = index == ranges.length - 1 ? top_white.fetch(:z_max) : range.end
104
124
  {
105
125
  plate_id: index + 1,
106
- name: plate_name(region.fetch(:id)),
126
+ name: "Z plate #{index + 1}",
107
127
  source_z_min: z_min,
108
128
  source_z_max: z_max,
109
129
  build_z_min: 0.0,
110
- build_z_max: (region[:build_z_max] || (z_max - z_min)).round(9)
130
+ build_z_max: (z_max - z_min + @settings.z_part_base_white_thickness(index)).round(9),
131
+ minimum_build_thickness: minimum_plate_build_thickness(range, index),
132
+ base_white_layers: @settings.z_part_base_white_layers(index),
133
+ base_white_thickness: @settings.z_part_base_white_thickness(index)
111
134
  }
112
135
  end
113
136
  end
114
137
 
115
- def plate_name(id)
116
- return "Top White" if id == "top_white"
117
-
118
- id.sub("color_region_", "Color region ")
138
+ def minimum_plate_build_thickness(range, index)
139
+ source_end = [range.end, @settings.effective_minimum_thickness].min
140
+ source_thickness = [source_end - range.begin, 0.0].max
141
+ (source_thickness + @settings.z_part_base_white_thickness(index)).round(9)
119
142
  end
120
143
 
121
144
  def used_materials(artifacts)
@@ -6,6 +6,7 @@ module Lithop
6
6
  class ExportGeneration
7
7
  Result = Data.define(:files, :triangle_count, :three_mf_triangle_count, :material_count, :object_count)
8
8
  TileEstimate = Data.define(:triangle_count, :triangle_counts_by_material)
9
+ PlannedTile = Data.define(:tile, :material_model, :mesh_plan, :estimate)
9
10
 
10
11
  def initialize(settings:, input_settings_path:, calibration:, progress:, job_progress:, instrumentation:)
11
12
  @settings = settings
@@ -36,14 +37,15 @@ module Lithop
36
37
  end
37
38
  tile_layout = TileLayout.new(@settings)
38
39
  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)
40
+ planned_tiles = @instrumentation.measure(:triangle_planning) do
41
+ plan_tiles(tile_layout, material_model, white_heights)
41
42
  end
43
+ tile_estimates = planned_tiles.map(&:estimate)
42
44
  validate_total_triangle_count!(tile_estimates)
43
45
  report_estimate(tile_estimates)
44
46
  report_job_progress(:artifact_write, 50, "Writing model artifacts")
45
47
  artifacts = write_artifacts(
46
- directory, geometry, material_model, tile_layout, white_heights
48
+ directory, geometry, material_model, tile_layout, planned_tiles, white_heights, image.corrected_preview_png
47
49
  )
48
50
  report_job_progress(:metadata_write, 95, "Writing job metadata")
49
51
  write_supporting_files(directory, source, job_id, artifacts)
@@ -72,13 +74,16 @@ module Lithop
72
74
  ).call
73
75
  end
74
76
 
75
- def estimate_tiles(tile_layout, material_model, white_heights)
77
+ def plan_tiles(tile_layout, material_model, white_heights)
76
78
  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
79
+ tile_model, mesh_plan = @instrumentation.measure(:tile_plan_estimate) do
80
+ build_tile_plan(tile, material_model, white_heights)
81
+ end
82
+ estimate = TileEstimate.new(
83
+ triangle_count: mesh_plan.triangle_count,
84
+ triangle_counts_by_material: mesh_plan.triangle_counts_by_material
81
85
  )
86
+ PlannedTile.new(tile:, material_model: tile_model, mesh_plan:, estimate:)
82
87
  end
83
88
  end
84
89
 
@@ -87,7 +92,11 @@ module Lithop
87
92
  plan = if @settings.separate_z_parts
88
93
  top_geometry = tile_white_geometry(
89
94
  tile, tile_model, white_heights,
90
- base_height: @settings.layer_structure.color_region_end
95
+ base_height: [
96
+ @settings.layer_structure.color_region_end,
97
+ @settings.resolved_z_part_ranges.last.begin
98
+ ].max,
99
+ minimum_height: @settings.effective_minimum_thickness
91
100
  )
92
101
  z_separated_material_mesh_plan(tile_model, top_geometry)
93
102
  else
@@ -97,11 +106,12 @@ module Lithop
97
106
  [tile_model, plan]
98
107
  end
99
108
 
100
- def tile_white_geometry(tile, model, heights, base_height: 0.0)
109
+ def tile_white_geometry(tile, model, heights, base_height: 0.0, minimum_height: nil)
101
110
  stride = @settings.columns + 1
102
111
  sliced_heights = (tile.row_start..tile.row_end).flat_map do |row|
103
112
  heights.slice((row * stride) + tile.column_start, model.columns + 1)
104
113
  end
114
+ sliced_heights.map! { |height| [height, minimum_height].max } if minimum_height
105
115
  Geometry.new(
106
116
  heights: sliced_heights,
107
117
  columns: model.columns,
@@ -114,15 +124,8 @@ module Lithop
114
124
  end
115
125
 
116
126
  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
127
  ZSeparatedMaterialMeshPlan.new(
125
- model:, base_geometry:, top_geometry:, settings: @settings,
128
+ model:, top_geometry:, settings: @settings,
126
129
  maximum_triangles: @settings.maximum_triangles,
127
130
  limit_message: ->(count) { @settings.triangle_limit_message("physical Z-separated CMYW mesh", count) }
128
131
  ).call
@@ -135,32 +138,33 @@ module Lithop
135
138
  raise ConfigurationError, @settings.triangle_limit_message("physical CMYW tiled mesh", count)
136
139
  end
137
140
 
138
- def write_artifacts(directory, geometry, material_model, tile_layout, white_heights)
141
+ # rubocop:disable-next Metrics/ParameterLists
142
+ def write_artifacts(directory, geometry, material_model, tile_layout, planned_tiles, white_heights, corrected_input)
139
143
  stl_name = "cmyk_white.stl"
140
144
  triangle_count = @instrumentation.measure(:stl_write) do
141
145
  STL.write(File.join(directory, stl_name), geometry.each_triangle)
142
146
  end
143
147
  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
+ tile_artifacts = planned_tiles.each_with_index.map do |planned, index|
149
+ tile = planned.tile
150
+ percent = 60 + ((index * 20.0) / planned_tiles.length)
151
+ report_job_progress(:three_mf_write, percent, "Writing 3MF tile #{index + 1}/#{planned_tiles.length}")
148
152
  name = tile_layout.tiles.one? ? "lithop.3mf" : "tiles/#{tile.id}.3mf"
149
153
  three_mf = @instrumentation.measure(:three_mf_write) do
150
154
  ThreeMF.write(
151
- File.join(directory, name), parts: mesh_plan.parts, instrumentation: @instrumentation,
155
+ File.join(directory, name), parts: planned.mesh_plan.parts, instrumentation: @instrumentation,
152
156
  settings: @settings
153
157
  )
154
158
  end
155
- mesh_plan.validate!(three_mf)
156
- TileArtifact.new(tile:, name:, three_mf:, material_model: tile_model)
159
+ planned.mesh_plan.validate!(three_mf)
160
+ TileArtifact.new(tile:, name:, three_mf:, material_model: planned.material_model)
157
161
  end
158
162
  if tile_layout.tiles.length > 1
159
163
  TileLayoutSVG.write(File.join(directory, "tile-layout.svg"), settings: @settings, layout: tile_layout)
160
164
  end
161
165
  report_job_progress(:preview_write, 85, "Writing previews")
162
166
  @instrumentation.measure(:preview_write) do
163
- ExportPreviews.write(directory, model: material_model, settings: @settings, white_heights:)
167
+ write_previews(directory, material_model, tile_artifacts, white_heights, corrected_input)
164
168
  end
165
169
  ExportArtifacts.new(
166
170
  stl_name:,
@@ -171,6 +175,13 @@ module Lithop
171
175
  )
172
176
  end
173
177
 
178
+ def write_previews(directory, material_model, tile_artifacts, white_heights, corrected_input)
179
+ ExportPreviews.write(
180
+ directory, model: material_model, settings: @settings, white_heights:, corrected_input:
181
+ )
182
+ ExportPreviews.write_tiles(directory, tile_artifacts:, settings: @settings, white_heights:)
183
+ end
184
+
174
185
  def write_supporting_files(directory, source, job_id, artifacts)
175
186
  @instrumentation.measure(:metadata_write) do
176
187
  @calibration_context.write(directory)
@@ -45,9 +45,9 @@ module Lithop
45
45
  instrumentation: @instrumentation
46
46
  )
47
47
  destination, generated = JobDirectory.new(project_dir: @project_dir, job_id:).call do |directory|
48
- generation.call(source:, directory:, job_id:)
48
+ generation.call(source:, directory:, job_id:).tap { job.checkpoint! }
49
49
  end
50
- job.progress(phase: :publishing, percent: 99, message: "Artifacts published")
50
+ job.publication_committed!
51
51
  Result.new(
52
52
  job_id:,
53
53
  path: destination,
@@ -76,10 +76,13 @@ module Lithop
76
76
  def artifact_files(artifacts)
77
77
  files = [
78
78
  artifacts.stl_name, *artifacts.three_mf_names, "settings.json", "manifest.json",
79
- "preview.png", "transmission.png", "thickness.png",
79
+ "corrected-input.png", "preview.png", "transmission.png", "thickness.png",
80
80
  *Palette::MATERIALS.map { |material| "#{material}-mask.png" }, "PRINT_README.md"
81
81
  ]
82
- files << "tile-layout.svg" if artifacts.tile_artifacts.length > 1
82
+ if artifacts.tile_artifacts.length > 1
83
+ files.concat(artifacts.tile_artifacts.map { |artifact| ExportPreviews.tile_preview_name(artifact) })
84
+ files << "tile-layout.svg"
85
+ end
83
86
  files << "calibration-profile.json" if @calibration.profile
84
87
  files
85
88
  end
@@ -100,7 +103,7 @@ module Lithop
100
103
  artifacts.tile_artifacts.map do |artifact|
101
104
  tile = artifact.tile
102
105
  used = Palette::MATERIALS.select { |material| artifact.material_model.materials.include?(material) }
103
- {
106
+ metadata = {
104
107
  id: tile.id,
105
108
  file: artifact.name,
106
109
  row: tile.row,
@@ -112,6 +115,8 @@ module Lithop
112
115
  unused_materials: material_names(Palette::MATERIALS - used),
113
116
  triangle_count: artifact.three_mf.triangle_count
114
117
  }
118
+ metadata[:preview_file] = ExportPreviews.tile_preview_name(artifact) unless artifacts.tile_artifacts.one?
119
+ metadata
115
120
  end
116
121
  end
117
122