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,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.
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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", "
|
|
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, "
|
|
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:
|
|
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:
|
|
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
|
data/lib/lithop/cli/options.rb
CHANGED
|
@@ -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:
|
|
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:
|
|
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.
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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: "
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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:
|
|
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: (
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
77
|
+
def plan_tiles(tile_layout, material_model, white_heights)
|
|
76
78
|
tile_layout.tiles.map do |tile|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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:
|
|
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:,
|
|
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
|
-
|
|
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 =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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:
|
|
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
|
-
|
|
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)
|
data/lib/lithop/export/job.rb
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|