lithop 0.1.0 → 0.3.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 +31 -7
- data/RELEASING.md +24 -6
- data/docs/README.md +4 -1
- data/docs/artifacts.md +26 -2
- data/docs/bambu-studio.md +7 -1
- data/docs/color-model.md +2 -0
- data/docs/command.md +48 -7
- data/docs/config.md +31 -4
- data/docs/getting-started.md +10 -5
- data/docs/images/cmyw-lithophane-preview.png +0 -0
- data/docs/local-ui.md +114 -0
- data/docs/tiling.md +134 -0
- data/docs/troubleshooting.md +22 -0
- 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 +21 -8
- data/lib/lithop/cli/calibration.rb +3 -1
- data/lib/lithop/cli/image_options.rb +41 -2
- data/lib/lithop/cli/options.rb +13 -0
- data/lib/lithop/cli.rb +36 -1
- data/lib/lithop/export/color_metadata.rb +55 -0
- data/lib/lithop/export/generation.rb +131 -21
- data/lib/lithop/export/job.rb +30 -16
- data/lib/lithop/export/metadata.rb +54 -4
- data/lib/lithop/export/planning.rb +97 -0
- data/lib/lithop/export/previews.rb +25 -1
- data/lib/lithop/export/print_readme.rb +22 -4
- data/lib/lithop/export/result.rb +26 -4
- data/lib/lithop/formats/three_mf/bambu_metadata.rb +111 -24
- data/lib/lithop/formats/three_mf/mesh.rb +72 -3
- data/lib/lithop/formats/three_mf/model_writer.rb +249 -24
- data/lib/lithop/formats/three_mf/part_writer.rb +51 -6
- data/lib/lithop/formats/three_mf.rb +9 -6
- data/lib/lithop/image/pipeline.rb +66 -10
- data/lib/lithop/image/svg_loader.rb +5 -5
- data/lib/lithop/job_directory.rb +41 -0
- data/lib/lithop/job_execution.rb +378 -0
- data/lib/lithop/layer_structure.rb +11 -0
- data/lib/lithop/mesh/box_geometry.rb +41 -0
- data/lib/lithop/mesh/column_geometry.rb +10 -2
- data/lib/lithop/mesh/column_topology.rb +2 -0
- data/lib/lithop/mesh/geometry.rb +21 -13
- data/lib/lithop/model/layer_region_model.rb +24 -0
- data/lib/lithop/model/material_model.rb +17 -5
- data/lib/lithop/model/palette.rb +21 -3
- data/lib/lithop/native.rb +39 -0
- data/lib/lithop/reference_three_mf.rb +17 -1
- data/lib/lithop/settings.rb +116 -1
- data/lib/lithop/settings_file.rb +10 -4
- data/lib/lithop/tiling.rb +127 -0
- data/lib/lithop/version.rb +1 -1
- data/lib/lithop/web/app.rb +513 -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 +319 -0
- data/lib/lithop/web/public/app.js +1847 -0
- data/lib/lithop/web/server.rb +21 -0
- data/lib/lithop/web/settings_form.rb +64 -0
- data/lib/lithop/web/settings_library.rb +154 -0
- data/lib/lithop/web/views/index.erb +364 -0
- data/lib/lithop/web/worker.rb +27 -0
- data/lib/lithop.rb +5 -0
- data/sig/lithop.rbs +102 -2
- metadata +89 -2
data/lib/lithop/export/job.rb
CHANGED
|
@@ -15,35 +15,49 @@ module Lithop
|
|
|
15
15
|
# rubocop:disable-next Metrics/ParameterLists
|
|
16
16
|
def initialize(
|
|
17
17
|
settings:, project_dir:, input_settings_path: nil, calibration: nil, progress: nil,
|
|
18
|
-
instrumentation: nil, clock: Time, random: SecureRandom
|
|
18
|
+
job_progress: nil, instrumentation: nil, clock: Time, random: SecureRandom, job_id: nil
|
|
19
19
|
)
|
|
20
20
|
@settings = settings
|
|
21
21
|
@project_dir = File.expand_path(project_dir)
|
|
22
22
|
@input_settings_path = input_settings_path
|
|
23
23
|
@calibration = calibration
|
|
24
24
|
@progress = progress
|
|
25
|
+
@job_progress = job_progress
|
|
25
26
|
@clock = clock
|
|
26
27
|
@random = random
|
|
27
|
-
@
|
|
28
|
-
|
|
29
|
-
)
|
|
28
|
+
@instrumentation = instrumentation
|
|
29
|
+
@job_id = job_id
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def call(input_path)
|
|
33
33
|
source = File.expand_path(input_path)
|
|
34
|
-
job_id = "#{@clock.now.utc.strftime("%Y%m%dT%H%M%SZ")}-#{@random.hex(4)}"
|
|
35
|
-
|
|
36
|
-
@
|
|
37
|
-
end
|
|
38
|
-
Result.new(
|
|
39
|
-
job_id:,
|
|
40
|
-
path: destination,
|
|
41
|
-
files: result.files,
|
|
42
|
-
triangle_count: result.triangle_count,
|
|
43
|
-
three_mf_triangle_count: result.three_mf_triangle_count,
|
|
44
|
-
material_count: result.material_count,
|
|
45
|
-
object_count: result.object_count
|
|
34
|
+
job_id = @job_id || "#{@clock.now.utc.strftime("%Y%m%dT%H%M%SZ")}-#{@random.hex(4)}"
|
|
35
|
+
execution = JobExecution.new(
|
|
36
|
+
project_dir: @project_dir, job_id:, progress: @job_progress, clock: @clock
|
|
46
37
|
)
|
|
38
|
+
execution.call do |job|
|
|
39
|
+
generation = ExportGeneration.new(
|
|
40
|
+
settings: @settings,
|
|
41
|
+
input_settings_path: @input_settings_path,
|
|
42
|
+
calibration: @calibration,
|
|
43
|
+
progress: @progress,
|
|
44
|
+
job_progress: job.method(:progress),
|
|
45
|
+
instrumentation: @instrumentation
|
|
46
|
+
)
|
|
47
|
+
destination, generated = JobDirectory.new(project_dir: @project_dir, job_id:).call do |directory|
|
|
48
|
+
generation.call(source:, directory:, job_id:).tap { job.checkpoint! }
|
|
49
|
+
end
|
|
50
|
+
job.publication_committed!
|
|
51
|
+
Result.new(
|
|
52
|
+
job_id:,
|
|
53
|
+
path: destination,
|
|
54
|
+
files: generated.files,
|
|
55
|
+
triangle_count: generated.triangle_count,
|
|
56
|
+
three_mf_triangle_count: generated.three_mf_triangle_count,
|
|
57
|
+
material_count: generated.material_count,
|
|
58
|
+
object_count: generated.object_count
|
|
59
|
+
)
|
|
60
|
+
end
|
|
47
61
|
end
|
|
48
62
|
end
|
|
49
63
|
end
|
|
@@ -5,6 +5,7 @@ require "json"
|
|
|
5
5
|
|
|
6
6
|
module Lithop
|
|
7
7
|
# Builds and writes the resolved settings and export manifest.
|
|
8
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
8
9
|
class ExportMetadata
|
|
9
10
|
def initialize(settings:, input_settings_path:, calibration:)
|
|
10
11
|
@settings = settings
|
|
@@ -29,8 +30,8 @@ module Lithop
|
|
|
29
30
|
|
|
30
31
|
private
|
|
31
32
|
|
|
33
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
32
34
|
def manifest_for(source, job_id, artifacts)
|
|
33
|
-
materials = material_names(used_materials(artifacts))
|
|
34
35
|
{
|
|
35
36
|
schema_version: 1,
|
|
36
37
|
job_id:,
|
|
@@ -49,9 +50,21 @@ module Lithop
|
|
|
49
50
|
},
|
|
50
51
|
input_sha256: Digest::SHA256.file(source).hexdigest,
|
|
51
52
|
filament_profile: FilamentProfile.default.to_h,
|
|
53
|
+
tile_layout: tile_layout_metadata(artifacts),
|
|
54
|
+
tiles: tile_metadata(artifacts),
|
|
52
55
|
parts: [
|
|
53
56
|
{ file: artifacts.stl_name, material: "White", role: "monochrome_reference" },
|
|
54
|
-
|
|
57
|
+
*artifacts.tile_artifacts.map do |artifact|
|
|
58
|
+
tile_materials = Palette::MATERIALS.select do |material|
|
|
59
|
+
artifact.material_model.materials.include?(material)
|
|
60
|
+
end
|
|
61
|
+
{
|
|
62
|
+
file: artifact.name,
|
|
63
|
+
tile_id: artifact.tile.id,
|
|
64
|
+
materials: material_names(tile_materials),
|
|
65
|
+
role: artifacts.tile_artifacts.one? ? "cmyw_lithophane" : "cmyw_lithophane_tile"
|
|
66
|
+
}
|
|
67
|
+
end
|
|
55
68
|
],
|
|
56
69
|
color_model: @color_metadata.call(artifacts),
|
|
57
70
|
files: artifact_files(artifacts),
|
|
@@ -62,14 +75,51 @@ module Lithop
|
|
|
62
75
|
|
|
63
76
|
def artifact_files(artifacts)
|
|
64
77
|
files = [
|
|
65
|
-
artifacts.stl_name, artifacts.
|
|
66
|
-
"preview.png", "transmission.png", "thickness.png",
|
|
78
|
+
artifacts.stl_name, *artifacts.three_mf_names, "settings.json", "manifest.json",
|
|
79
|
+
"corrected-input.png", "preview.png", "transmission.png", "thickness.png",
|
|
67
80
|
*Palette::MATERIALS.map { |material| "#{material}-mask.png" }, "PRINT_README.md"
|
|
68
81
|
]
|
|
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
|
|
69
86
|
files << "calibration-profile.json" if @calibration.profile
|
|
70
87
|
files
|
|
71
88
|
end
|
|
72
89
|
|
|
90
|
+
def tile_layout_metadata(artifacts)
|
|
91
|
+
{
|
|
92
|
+
requested: @settings.tile_layout,
|
|
93
|
+
tile_count: @settings.tile_count,
|
|
94
|
+
rows: artifacts.tile_layout.rows,
|
|
95
|
+
columns: artifacts.tile_layout.columns,
|
|
96
|
+
order: "left_to_right_then_top_to_bottom",
|
|
97
|
+
local_origin: "xy_min",
|
|
98
|
+
assembled_image_top: "+Y"
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def tile_metadata(artifacts)
|
|
103
|
+
artifacts.tile_artifacts.map do |artifact|
|
|
104
|
+
tile = artifact.tile
|
|
105
|
+
used = Palette::MATERIALS.select { |material| artifact.material_model.materials.include?(material) }
|
|
106
|
+
metadata = {
|
|
107
|
+
id: tile.id,
|
|
108
|
+
file: artifact.name,
|
|
109
|
+
row: tile.row,
|
|
110
|
+
column: tile.column,
|
|
111
|
+
offset_mm: tile.offset,
|
|
112
|
+
bounds_mm: tile.bounds,
|
|
113
|
+
dimensions_mm: { width: tile.width, height: tile.height, depth: @settings.effective_maximum_thickness },
|
|
114
|
+
materials: material_names(used),
|
|
115
|
+
unused_materials: material_names(Palette::MATERIALS - used),
|
|
116
|
+
triangle_count: artifact.three_mf.triangle_count
|
|
117
|
+
}
|
|
118
|
+
metadata[:preview_file] = ExportPreviews.tile_preview_name(artifact) unless artifacts.tile_artifacts.one?
|
|
119
|
+
metadata
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
73
123
|
def used_materials(artifacts)
|
|
74
124
|
Palette::MATERIALS.select { |material| artifacts.material_model.materials.include?(material) }
|
|
75
125
|
end
|
|
@@ -1,6 +1,103 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Lithop
|
|
4
|
+
# Splits the reference White carrier and CMY pigments into closed Z-region parts.
|
|
5
|
+
class ZSeparatedMaterialMeshPlan
|
|
6
|
+
Entry = Data.define(:material, :geometry, :name, :region, :build_z_offset)
|
|
7
|
+
|
|
8
|
+
def initialize(model:, base_geometry:, top_geometry:, settings:, maximum_triangles:, limit_message:)
|
|
9
|
+
@model = model
|
|
10
|
+
@base_geometry = base_geometry
|
|
11
|
+
@top_geometry = top_geometry
|
|
12
|
+
@settings = settings
|
|
13
|
+
@maximum_triangles = maximum_triangles
|
|
14
|
+
@limit_message = limit_message
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
entries = build_entries
|
|
19
|
+
entry_counts = entries.map { |entry| triangle_count(entry.geometry) }
|
|
20
|
+
total = entry_counts.sum
|
|
21
|
+
raise ConfigurationError, @limit_message.call(total) if total > @maximum_triangles
|
|
22
|
+
|
|
23
|
+
MaterialMeshPlan::Result.new(
|
|
24
|
+
parts: entries.map do |entry|
|
|
25
|
+
ThreeMF::Part.new(
|
|
26
|
+
material: entry.material, triangles: triangle_source(entry.geometry),
|
|
27
|
+
name: entry.name, region: entry.region, build_z_offset: entry.build_z_offset
|
|
28
|
+
)
|
|
29
|
+
end,
|
|
30
|
+
triangle_count: total,
|
|
31
|
+
triangle_counts_by_material: counts_by_material(entries, entry_counts)
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def build_entries
|
|
38
|
+
entries = []
|
|
39
|
+
color_ranges.each_with_index do |range, index|
|
|
40
|
+
region_model = LayerRegionModel.new(model: @model, layer_range: range)
|
|
41
|
+
region = "color_region_#{index + 1}"
|
|
42
|
+
build_z_offset = @settings.layer_structure.base_white_thickness - region_model.z_boundaries.first
|
|
43
|
+
entries << Entry.new(
|
|
44
|
+
material: :white, geometry: @base_geometry, name: "Color region #{index + 1} Base White",
|
|
45
|
+
region:, build_z_offset: 0.0
|
|
46
|
+
)
|
|
47
|
+
entries << Entry.new(
|
|
48
|
+
material: :white,
|
|
49
|
+
geometry: color_region_white_geometry(region_model),
|
|
50
|
+
name: "Color region #{index + 1} White",
|
|
51
|
+
region:,
|
|
52
|
+
build_z_offset:
|
|
53
|
+
)
|
|
54
|
+
Palette::PIGMENTS.each do |material|
|
|
55
|
+
next unless region_model.materials.include?(material)
|
|
56
|
+
|
|
57
|
+
entries << Entry.new(
|
|
58
|
+
material:,
|
|
59
|
+
geometry: ColumnGeometry.new(model: region_model, material:),
|
|
60
|
+
name: "Color region #{index + 1} #{material.to_s.capitalize}",
|
|
61
|
+
region:,
|
|
62
|
+
build_z_offset:
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
entries << Entry.new(
|
|
67
|
+
material: :white, geometry: @top_geometry, name: "Top White",
|
|
68
|
+
region: "top_white", build_z_offset: -@settings.layer_structure.color_region_end
|
|
69
|
+
)
|
|
70
|
+
entries
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def color_ranges
|
|
74
|
+
@settings.layer_structure.color_region_layer_ranges(@settings.color_region_parts)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def color_region_white_geometry(model)
|
|
78
|
+
BoxGeometry.new(
|
|
79
|
+
width: model.x_boundaries.last,
|
|
80
|
+
height: model.y_boundaries.last,
|
|
81
|
+
z_min: model.z_boundaries.first,
|
|
82
|
+
z_max: model.z_boundaries.last
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def triangle_count(geometry)
|
|
87
|
+
geometry.respond_to?(:triangle_count) ? geometry.triangle_count : geometry.each_triangle.count
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def triangle_source(geometry)
|
|
91
|
+
geometry.is_a?(ColumnGeometry) ? geometry : geometry.each_triangle
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def counts_by_material(entries, entry_counts)
|
|
95
|
+
entries.zip(entry_counts).each_with_object(Hash.new(0)) do |(entry, count), totals|
|
|
96
|
+
totals[entry.material] += count
|
|
97
|
+
end.freeze
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
4
101
|
# Builds the reference-style overlapping White height field and contiguous CMY columns.
|
|
5
102
|
class ReferenceMaterialMeshPlan
|
|
6
103
|
def initialize(model:, white_geometry:, maximum_triangles:, limit_message:)
|
|
@@ -5,7 +5,7 @@ module Lithop
|
|
|
5
5
|
module ExportPreviews
|
|
6
6
|
module_function
|
|
7
7
|
|
|
8
|
-
def write(directory, model:, settings:, white_heights: nil)
|
|
8
|
+
def write(directory, model:, settings:, white_heights: nil, corrected_input: nil)
|
|
9
9
|
heights = white_heights && cell_heights(white_heights, settings.columns, settings.rows)
|
|
10
10
|
options = {
|
|
11
11
|
model:,
|
|
@@ -19,8 +19,32 @@ module Lithop
|
|
|
19
19
|
Palette::MATERIALS.each do |material|
|
|
20
20
|
Preview.write_mask(File.join(directory, "#{material}-mask.png"), model:, material:)
|
|
21
21
|
end
|
|
22
|
+
File.binwrite(File.join(directory, "corrected-input.png"), corrected_input) if corrected_input
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
def write_tiles(directory, tile_artifacts:, settings:, white_heights:)
|
|
26
|
+
return if tile_artifacts.one?
|
|
27
|
+
|
|
28
|
+
heights = cell_heights(white_heights, settings.columns, settings.rows)
|
|
29
|
+
tile_artifacts.each do |artifact|
|
|
30
|
+
tile = artifact.tile
|
|
31
|
+
tile_heights = (tile.row_start...tile.row_end).flat_map do |row|
|
|
32
|
+
offset = (row * settings.columns) + tile.column_start
|
|
33
|
+
heights.slice(offset, artifact.material_model.columns)
|
|
34
|
+
end
|
|
35
|
+
Preview.write_material(
|
|
36
|
+
File.join(directory, tile_preview_name(artifact)),
|
|
37
|
+
model: artifact.material_model,
|
|
38
|
+
minimum: settings.effective_minimum_thickness,
|
|
39
|
+
maximum: settings.effective_maximum_thickness,
|
|
40
|
+
heights: tile_heights,
|
|
41
|
+
transmission_gamma: 0.5
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def tile_preview_name(artifact) = artifact.name.sub(/\.3mf\z/, "-preview.png")
|
|
47
|
+
|
|
24
48
|
def cell_heights(vertex_heights, columns, rows)
|
|
25
49
|
stride = columns + 1
|
|
26
50
|
Array.new(columns * rows) do |index|
|
|
@@ -5,7 +5,10 @@ module Lithop
|
|
|
5
5
|
module PrintReadme
|
|
6
6
|
module_function
|
|
7
7
|
|
|
8
|
-
def write(
|
|
8
|
+
def write(
|
|
9
|
+
path, settings:, material_model:, tile_artifacts:, calibration_profile: nil,
|
|
10
|
+
profile: FilamentProfile.default
|
|
11
|
+
)
|
|
9
12
|
materials = Palette::MATERIALS.select { |material| material_model.materials.include?(material) }
|
|
10
13
|
names = materials.map { |material| profile_material(profile, material).name }
|
|
11
14
|
assignments = materials.map do |material|
|
|
@@ -18,14 +21,15 @@ module Lithop
|
|
|
18
21
|
This is #{calibration_profile ? "a measured-profile calibrated" : "an uncalibrated"} CMYW lithophane
|
|
19
22
|
generated in millimetres.
|
|
20
23
|
|
|
21
|
-
-
|
|
24
|
+
- #{model_instruction(tile_artifacts, names)}
|
|
22
25
|
#{calibration_line(calibration_profile)}
|
|
23
26
|
- Bambu Studio assignments encoded for this job: #{assignments.join(", ")}.
|
|
24
27
|
- The model uses #{settings.color_layer_count} CMY/W color layers #{base_layer_description(settings)}.
|
|
25
28
|
- The backlight-to-view color-layer order is #{settings.pigment_order.map(&:capitalize).join(" -> ")} -> White.
|
|
26
29
|
- White thickness above the color region follows #{thickness_method(settings, calibration_profile)}.
|
|
30
|
+
#{z_part_line(settings)}
|
|
27
31
|
- `preview.png` shows nominal recipe colors with relative thickness shading; it is not a colorimetric prediction.
|
|
28
|
-
- `transmission.png`, `thickness.png`, and the four material masks use the same cells as
|
|
32
|
+
- `transmission.png`, `thickness.png`, and the four material masks use the same cells as the 3MF output.
|
|
29
33
|
- `cmyk_white.stl` is a separate monochrome reference model, not the color model.
|
|
30
34
|
- Product: #{profile.product_url}
|
|
31
35
|
- Guide: #{profile.reference_url}
|
|
@@ -38,6 +42,20 @@ module Lithop
|
|
|
38
42
|
MARKDOWN
|
|
39
43
|
end
|
|
40
44
|
|
|
45
|
+
def model_instruction(tile_artifacts, names)
|
|
46
|
+
return "Open `lithop.3mf` for the physical color model; it contains: #{names.join(", ")}." if tile_artifacts.one?
|
|
47
|
+
|
|
48
|
+
"Open the #{tile_artifacts.length} 3MF files under `tiles/` and assemble them using " \
|
|
49
|
+
"`tile-layout.svg` and `manifest.json`; together they contain: #{names.join(", ")}."
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def z_part_line(settings)
|
|
53
|
+
return "- Z regions remain combined into the material parts." unless settings.separate_z_parts
|
|
54
|
+
|
|
55
|
+
"- Z regions use #{settings.color_region_parts + 1} plates: each CMYW color region includes " \
|
|
56
|
+
"a Base White carrier, followed by Top White; every plate starts at Z=0."
|
|
57
|
+
end
|
|
58
|
+
|
|
41
59
|
def profile_material(profile, material)
|
|
42
60
|
profile.materials.find { |candidate| candidate.channel == material }
|
|
43
61
|
end
|
|
@@ -63,7 +81,7 @@ module Lithop
|
|
|
63
81
|
"- Use a #{settings.nozzle_diameter} mm nozzle, #{settings.first_layer_height} mm base layers,\n " \
|
|
64
82
|
"#{settings.layer_height} mm color layers, and #{upper_height} mm upper White layers."
|
|
65
83
|
end
|
|
66
|
-
private_class_method :profile_material, :calibration_line, :thickness_method,
|
|
84
|
+
private_class_method :model_instruction, :z_part_line, :profile_material, :calibration_line, :thickness_method,
|
|
67
85
|
:base_layer_description, :layer_height_description
|
|
68
86
|
end
|
|
69
87
|
end
|
data/lib/lithop/export/result.rb
CHANGED
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Lithop
|
|
4
|
+
TileArtifact = Data.define(:tile, :name, :three_mf, :material_model)
|
|
5
|
+
|
|
4
6
|
ExportArtifacts = Data.define(
|
|
5
7
|
:stl_name,
|
|
6
|
-
:
|
|
8
|
+
:tile_artifacts,
|
|
7
9
|
:stl_triangle_count,
|
|
8
|
-
:
|
|
9
|
-
:
|
|
10
|
-
)
|
|
10
|
+
:material_model,
|
|
11
|
+
:tile_layout
|
|
12
|
+
) do
|
|
13
|
+
def three_mf_names = tile_artifacts.map(&:name)
|
|
14
|
+
|
|
15
|
+
def three_mf_name
|
|
16
|
+
three_mf_names.fetch(0) if three_mf_names.one?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def three_mf
|
|
20
|
+
results = tile_artifacts.map(&:three_mf)
|
|
21
|
+
counts = results.each_with_object(Hash.new(0)) do |result, totals|
|
|
22
|
+
result.triangle_counts_by_material.each { |material, count| totals[material] += count }
|
|
23
|
+
end
|
|
24
|
+
ThreeMF::Result.new(
|
|
25
|
+
vertex_count: results.sum(&:vertex_count),
|
|
26
|
+
triangle_count: results.sum(&:triangle_count),
|
|
27
|
+
material_count: counts.length,
|
|
28
|
+
object_count: results.sum(&:object_count),
|
|
29
|
+
triangle_counts_by_material: counts.freeze
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
11
33
|
|
|
12
34
|
ExportResult = Data.define(
|
|
13
35
|
:job_id,
|
|
@@ -4,14 +4,14 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Lithop
|
|
6
6
|
# Writes Bambu Studio material-assignment metadata into a 3MF package.
|
|
7
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
7
8
|
class ThreeMFBambuMetadata
|
|
8
9
|
PATH = "Metadata/model_settings.config"
|
|
9
|
-
FIRST_MODEL_OBJECT_INDEX = 1
|
|
10
10
|
|
|
11
|
-
def initialize(archive:,
|
|
11
|
+
def initialize(archive:, object_ids:, assemblies:, parts:, profile:, settings: nil)
|
|
12
12
|
@archive = archive
|
|
13
|
-
@assembly_id = assembly_id
|
|
14
13
|
@object_ids = object_ids
|
|
14
|
+
@assemblies = assemblies
|
|
15
15
|
@parts = parts
|
|
16
16
|
@profile = profile
|
|
17
17
|
@settings = settings
|
|
@@ -20,61 +20,148 @@ module Lithop
|
|
|
20
20
|
def write
|
|
21
21
|
@archive.put_next_entry(PATH)
|
|
22
22
|
@archive.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<config>\n")
|
|
23
|
-
@
|
|
24
|
-
@archive.write(" <metadata key=\"name\" value=\"Lithop CMYW lithophane\"/>\n")
|
|
23
|
+
@assemblies.each { |assembly| write_object(assembly) }
|
|
25
24
|
if bambu_project?
|
|
26
|
-
|
|
25
|
+
write_plates
|
|
26
|
+
write_assemble if separate_z_plates?
|
|
27
|
+
end
|
|
28
|
+
@archive.write("</config>\n")
|
|
29
|
+
write_project_entries if bambu_project?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def write_object(assembly)
|
|
35
|
+
@archive.write(" <object id=\"#{assembly.id}\">\n")
|
|
36
|
+
@archive.write(" <metadata key=\"name\" value=\"#{assembly.name}\"/>\n")
|
|
37
|
+
if bambu_project?
|
|
38
|
+
@archive.write(" <metadata key=\"extruder\" value=\"#{root_extruder(assembly)}\"/>\n")
|
|
27
39
|
@archive.write(" <metadata key=\"layer_height\" value=\"#{@settings.layer_height}\"/>\n")
|
|
28
40
|
@archive.write(" <metadata key=\"sparse_infill_density\" value=\"100%\"/>\n")
|
|
29
41
|
@archive.write(" <metadata key=\"sparse_infill_pattern\" value=\"zig-zag\"/>\n")
|
|
30
42
|
end
|
|
31
|
-
@object_ids.zip(@parts).
|
|
32
|
-
|
|
43
|
+
part_map = @object_ids.zip(@parts).to_h
|
|
44
|
+
assembly.object_ids.each_with_index do |object_id, volume_id|
|
|
45
|
+
part = part_map.fetch(object_id)
|
|
46
|
+
write_part(object_id, part, assembly, volume_id)
|
|
33
47
|
end
|
|
34
48
|
@archive.write(" </object>\n")
|
|
35
|
-
write_plate if bambu_project?
|
|
36
|
-
@archive.write("</config>\n")
|
|
37
|
-
write_project_entries if bambu_project?
|
|
38
49
|
end
|
|
39
50
|
|
|
40
|
-
|
|
51
|
+
def root_extruder(assembly)
|
|
52
|
+
return 1 if @assemblies.one?
|
|
53
|
+
|
|
54
|
+
part_map = @object_ids.zip(@parts).to_h
|
|
55
|
+
material = part_map.fetch(assembly.object_ids.first).material
|
|
56
|
+
@profile.materials.index { |candidate| candidate.channel == material } + 1
|
|
57
|
+
end
|
|
41
58
|
|
|
42
|
-
def write_part(object_id,
|
|
59
|
+
def write_part(object_id, part, assembly, volume_id)
|
|
60
|
+
material = part.material
|
|
43
61
|
profile_material = @profile.materials.find { |candidate| candidate.channel == material }
|
|
44
62
|
extruder = @profile.materials.index(profile_material) + 1
|
|
45
|
-
|
|
46
|
-
@archive.write("
|
|
63
|
+
uuid = separate_z_plates? ? %( uuid="#{part_uuid(object_id)}") : ""
|
|
64
|
+
@archive.write(" <part id=\"#{object_id}\" subtype=\"normal_part\"#{uuid}>\n")
|
|
65
|
+
@archive.write(" <metadata key=\"name\" value=\"#{part.name || profile_material.name}\"/>\n")
|
|
66
|
+
if separate_z_plates?
|
|
67
|
+
origin = assembly.component_origins.fetch(object_id)
|
|
68
|
+
@archive.write(" <metadata key=\"matrix\" value=\"#{matrix(origin)}\"/>\n")
|
|
69
|
+
@archive.write(" <metadata key=\"source_file\" value=\"lithop.3mf\"/>\n")
|
|
70
|
+
@archive.write(" <metadata key=\"source_object_id\" value=\"#{source_object_id(assembly)}\"/>\n")
|
|
71
|
+
@archive.write(" <metadata key=\"source_volume_id\" value=\"#{volume_id}\"/>\n")
|
|
72
|
+
@archive.write(" <metadata key=\"source_offset_x\" value=\"#{number(origin[0])}\"/>\n")
|
|
73
|
+
@archive.write(" <metadata key=\"source_offset_y\" value=\"#{number(origin[1])}\"/>\n")
|
|
74
|
+
source_z = number(origin[2] - part.build_z_offset)
|
|
75
|
+
@archive.write(" <metadata key=\"source_offset_z\" value=\"#{source_z}\"/>\n")
|
|
76
|
+
end
|
|
47
77
|
@archive.write(" <metadata key=\"extruder\" value=\"#{extruder}\"/>\n")
|
|
48
78
|
@archive.write(" </part>\n")
|
|
49
79
|
end
|
|
50
80
|
|
|
51
81
|
def bambu_project? = @settings&.bambu_project
|
|
52
82
|
|
|
53
|
-
def
|
|
54
|
-
|
|
83
|
+
def part_uuid(object_id)
|
|
84
|
+
format("%08x-7f79-4c88-9311-43ae0fc671e7", object_id)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def write_plates
|
|
88
|
+
@assemblies.each_with_index { |assembly, index| write_plate(assembly, index + 1) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def write_plate(assembly, plate_id)
|
|
92
|
+
name = @assemblies.one? ? "lithophane" : assembly.name
|
|
93
|
+
@archive.write(<<~XML.chomp)
|
|
55
94
|
<plate>
|
|
56
|
-
<metadata key="plater_id" value="
|
|
57
|
-
<metadata key="plater_name" value="
|
|
95
|
+
<metadata key="plater_id" value="#{plate_id}"/>
|
|
96
|
+
<metadata key="plater_name" value="#{name}"/>
|
|
58
97
|
<metadata key="locked" value="false"/>
|
|
59
98
|
<model_instance>
|
|
60
|
-
<metadata key="object_id" value="#{
|
|
99
|
+
<metadata key="object_id" value="#{assembly.id}"/>
|
|
61
100
|
<metadata key="instance_id" value="0"/>
|
|
62
|
-
</model_instance>
|
|
63
|
-
</plate>
|
|
64
101
|
XML
|
|
102
|
+
@archive.write("\n <metadata key=\"identify_id\" value=\"#{identify_id(plate_id)}\"/>") if separate_z_plates?
|
|
103
|
+
@archive.write("\n </model_instance>\n</plate>\n")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def write_assemble
|
|
107
|
+
@archive.write("<assemble>\n")
|
|
108
|
+
@assemblies.each do |assembly|
|
|
109
|
+
@archive.write(
|
|
110
|
+
" <assemble_item object_id=\"#{assembly.id}\" instance_id=\"0\" " \
|
|
111
|
+
"transform=\"1 0 0 0 1 0 0 0 1 0 0 0\" offset=\"0 0 0\"/>\n"
|
|
112
|
+
)
|
|
113
|
+
assembly.object_ids.each_with_index do |object_id, volume_id|
|
|
114
|
+
transform = volume_transform(assembly.component_origins.fetch(object_id))
|
|
115
|
+
@archive.write(
|
|
116
|
+
" <assemble_item object_id=\"#{assembly.id}\" volume_id=\"#{volume_id}\" " \
|
|
117
|
+
"transform=\"#{transform}\"/>\n"
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
@archive.write("</assemble>\n")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def separate_z_plates? = @settings&.separate_z_parts && @assemblies.length > 1
|
|
125
|
+
|
|
126
|
+
def identify_id(plate_id)
|
|
127
|
+
[218, 253, 276, 287].fetch(plate_id - 1, 287 + (plate_id * 32))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def volume_transform(origin)
|
|
131
|
+
"1 0 0 0 1 0 0 0 1 #{origin.map { |value| number(value) }.join(" ")}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def matrix(origin)
|
|
135
|
+
"1 0 0 #{number(origin[0])} 0 1 0 #{number(origin[1])} 0 0 1 #{number(origin[2])} 0 0 0 1"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def source_object_id(assembly)
|
|
139
|
+
return 3 if assembly.region == "top_white"
|
|
140
|
+
|
|
141
|
+
assembly.region.delete_prefix("color_region_").to_i - 1
|
|
65
142
|
end
|
|
66
143
|
|
|
144
|
+
def number(value) = format("%.9g", value)
|
|
145
|
+
|
|
67
146
|
def write_project_entries
|
|
68
147
|
write_entry("Metadata/layer_config_ranges.xml", layer_ranges_xml)
|
|
69
148
|
write_entry("Metadata/project_settings.config", "#{JSON.pretty_generate(project_settings)}\n")
|
|
70
149
|
end
|
|
71
150
|
|
|
72
151
|
def layer_ranges_xml
|
|
152
|
+
object_id = @assemblies.length
|
|
153
|
+
min_z = @settings.top_white_start
|
|
154
|
+
max_z = @settings.maximum_thickness
|
|
155
|
+
if @settings.separate_z_parts
|
|
156
|
+
color_end = @settings.layer_structure.color_region_end
|
|
157
|
+
min_z = (min_z - color_end).round(9)
|
|
158
|
+
max_z = (max_z - color_end).round(9)
|
|
159
|
+
end
|
|
73
160
|
<<~XML
|
|
74
161
|
<?xml version="1.0" encoding="utf-8"?>
|
|
75
162
|
<objects>
|
|
76
|
-
<object id="#{
|
|
77
|
-
<range min_z="#{
|
|
163
|
+
<object id="#{object_id}">
|
|
164
|
+
<range min_z="#{min_z}" max_z="#{max_z}">
|
|
78
165
|
<option opt_key="extruder">4</option>
|
|
79
166
|
<option opt_key="layer_height">#{@settings.top_layer_height}</option>
|
|
80
167
|
</range>
|