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,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "puma"
|
|
4
|
+
require_relative "app"
|
|
5
|
+
|
|
6
|
+
module Lithop
|
|
7
|
+
module Web
|
|
8
|
+
# Starts the local UI with Puma on the loopback interface.
|
|
9
|
+
module Server
|
|
10
|
+
HOST = "127.0.0.1"
|
|
11
|
+
DEFAULT_PORT = 4567
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def run(project_dir:, port: DEFAULT_PORT)
|
|
16
|
+
application = App.configured(project_dir:)
|
|
17
|
+
application.run!(bind: HOST, port:, server: :puma)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lithop
|
|
4
|
+
module Web
|
|
5
|
+
# Resolves browser form values through the same Settings validation used by the CLI.
|
|
6
|
+
class SettingsForm
|
|
7
|
+
FLOAT_FIELDS = %i[
|
|
8
|
+
width height margin_top margin_right margin_bottom margin_left crop_position_x crop_position_y
|
|
9
|
+
crop_scale brightness contrast saturation yellow_mix_percent minimum_thickness maximum_thickness pixel_size
|
|
10
|
+
color_pixel_size nozzle_diameter
|
|
11
|
+
].freeze
|
|
12
|
+
INTEGER_FIELDS = %i[tile_count tile_rows tile_columns color_region_parts].freeze
|
|
13
|
+
STRING_FIELDS = %i[image_fit tile_layout yellow_mix_color pigment_order].freeze
|
|
14
|
+
BOOLEAN_FIELDS = %i[suppress_pale_colors separate_z_parts].freeze
|
|
15
|
+
FIELD_NAMES = (FLOAT_FIELDS + INTEGER_FIELDS + STRING_FIELDS + BOOLEAN_FIELDS).map(&:to_s).freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :defaults
|
|
18
|
+
|
|
19
|
+
def initialize(project_dir:)
|
|
20
|
+
@project_dir = File.expand_path(project_dir)
|
|
21
|
+
@file_settings = SettingsFile.load(File.join(@project_dir, SettingsFile::DEFAULT_PATH)).values
|
|
22
|
+
@defaults = Settings.new(**@file_settings)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def resolve(params)
|
|
26
|
+
overrides = parse_fields(params)
|
|
27
|
+
Settings.new(**@file_settings, **overrides)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def values(settings = defaults)
|
|
31
|
+
result = settings.to_h.slice(*Settings::DEFAULTS.keys).transform_keys(&:to_s)
|
|
32
|
+
result["pigment_order"] = settings.pigment_order.join(",")
|
|
33
|
+
result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def parse_fields(params)
|
|
39
|
+
values = {}
|
|
40
|
+
FLOAT_FIELDS.each { |key| assign_number(values, params, key, Float) }
|
|
41
|
+
INTEGER_FIELDS.each { |key| assign_number(values, params, key, Integer) }
|
|
42
|
+
STRING_FIELDS.each { |key| values[key] = params[key.to_s] if present?(params[key.to_s]) }
|
|
43
|
+
BOOLEAN_FIELDS.each { |key| values[key] = params[key.to_s] == "1" }
|
|
44
|
+
clear_unused_grid(values)
|
|
45
|
+
values
|
|
46
|
+
rescue ArgumentError
|
|
47
|
+
raise ConfigurationError, "settings must contain valid numbers"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def assign_number(values, params, key, converter)
|
|
51
|
+
value = params[key.to_s]
|
|
52
|
+
return unless present?(value)
|
|
53
|
+
|
|
54
|
+
values[key] = converter == Integer ? Integer(value, 10) : Float(value)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def clear_unused_grid(values)
|
|
58
|
+
return if values.fetch(:tile_layout, defaults.tile_layout) == "grid"
|
|
59
|
+
|
|
60
|
+
values[:tile_rows] = nil
|
|
61
|
+
values[:tile_columns] = nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def present?(value) = !value.nil? && !value.to_s.strip.empty?
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module Lithop
|
|
8
|
+
module Web
|
|
9
|
+
# Persists reusable browser settings without changing config/settings.json.
|
|
10
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
11
|
+
class SettingsLibrary
|
|
12
|
+
SCHEMA_VERSION = 1
|
|
13
|
+
PATH = File.join("config", "ui-settings.json")
|
|
14
|
+
MAX_NAME_LENGTH = 80
|
|
15
|
+
|
|
16
|
+
def initialize(project_dir:, clock: -> { Time.now.utc })
|
|
17
|
+
@project_dir = File.expand_path(project_dir)
|
|
18
|
+
@path = File.join(@project_dir, PATH)
|
|
19
|
+
@clock = clock
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def read
|
|
23
|
+
document = load_document
|
|
24
|
+
{
|
|
25
|
+
"schema_version" => SCHEMA_VERSION,
|
|
26
|
+
"presets" => document.fetch("presets").sort.to_h,
|
|
27
|
+
"draft" => document["draft"]
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def save_preset(name, values)
|
|
32
|
+
update do |document|
|
|
33
|
+
document.fetch("presets")[validate_name!(name)] = entry(values)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def rename_preset(name, new_name)
|
|
38
|
+
update do |document|
|
|
39
|
+
presets = document.fetch("presets")
|
|
40
|
+
source = presets.delete(validate_name!(name))
|
|
41
|
+
raise InputError, "preset not found" unless source
|
|
42
|
+
|
|
43
|
+
target = validate_name!(new_name)
|
|
44
|
+
raise InputError, "preset already exists" if presets.key?(target)
|
|
45
|
+
|
|
46
|
+
presets[target] = source.merge("updated_at" => timestamp)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def delete_preset(name)
|
|
51
|
+
update do |document|
|
|
52
|
+
removed = document.fetch("presets").delete(validate_name!(name))
|
|
53
|
+
raise InputError, "preset not found" unless removed
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def save_draft(values)
|
|
58
|
+
update { |document| document["draft"] = entry(values) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def delete_draft
|
|
62
|
+
update { |document| document.delete("draft") }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def entry(values)
|
|
68
|
+
{ "settings" => normalize(values), "updated_at" => timestamp }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def normalize(values)
|
|
72
|
+
supplied = values.to_h.slice(*SettingsForm::FIELD_NAMES).transform_values do |value|
|
|
73
|
+
if value == true
|
|
74
|
+
"1"
|
|
75
|
+
elsif value == false
|
|
76
|
+
""
|
|
77
|
+
elsif value.nil?
|
|
78
|
+
nil
|
|
79
|
+
else
|
|
80
|
+
value.to_s
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
resolved = SettingsForm.new(project_dir: @project_dir).resolve(supplied)
|
|
84
|
+
SettingsForm.new(project_dir: @project_dir).values(resolved).slice(*SettingsForm::FIELD_NAMES)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate_name!(name)
|
|
88
|
+
value = name.to_s.strip
|
|
89
|
+
raise InputError, "preset name is required" if value.empty?
|
|
90
|
+
raise InputError, "preset name is too long" if value.length > MAX_NAME_LENGTH
|
|
91
|
+
raise InputError, "preset name contains unsupported characters" if value.match?(/[\x00-\x1f\x7f]/)
|
|
92
|
+
|
|
93
|
+
value
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def update
|
|
97
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
98
|
+
lock_directory = File.join(@project_dir, "build")
|
|
99
|
+
FileUtils.mkdir_p(lock_directory)
|
|
100
|
+
lock_path = File.join(lock_directory, ".ui-settings.lock")
|
|
101
|
+
File.open(lock_path, File::RDWR | File::CREAT, 0o600) do |lock|
|
|
102
|
+
lock.flock(File::LOCK_EX)
|
|
103
|
+
document = load_document
|
|
104
|
+
yield document
|
|
105
|
+
write_document(document)
|
|
106
|
+
read
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def load_document
|
|
111
|
+
return empty_document unless File.file?(@path)
|
|
112
|
+
raise InputError, "UI settings file must not be a symbolic link" if File.symlink?(@path)
|
|
113
|
+
|
|
114
|
+
document = JSON.parse(File.read(@path))
|
|
115
|
+
raise InputError, "unsupported UI settings schema" unless document["schema_version"] == SCHEMA_VERSION
|
|
116
|
+
raise InputError, "invalid UI settings file" unless document["presets"].is_a?(Hash)
|
|
117
|
+
|
|
118
|
+
validate_entries!(document)
|
|
119
|
+
document
|
|
120
|
+
rescue JSON::ParserError
|
|
121
|
+
raise InputError, "invalid UI settings JSON"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def validate_entries!(document)
|
|
125
|
+
document.fetch("presets").each_value { |value| validate_entry!(value) }
|
|
126
|
+
validate_entry!(document["draft"]) if document["draft"]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def validate_entry!(entry)
|
|
130
|
+
raise InputError, "invalid saved UI settings" unless entry.is_a?(Hash) && entry["settings"].is_a?(Hash)
|
|
131
|
+
|
|
132
|
+
unknown = entry.fetch("settings").keys - SettingsForm::FIELD_NAMES
|
|
133
|
+
raise InputError, "unsupported saved settings: #{unknown.join(", ")}" unless unknown.empty?
|
|
134
|
+
|
|
135
|
+
entry["settings"] = normalize(entry.fetch("settings"))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def write_document(document)
|
|
139
|
+
temporary = "#{@path}.tmp-#{Process.pid}"
|
|
140
|
+
File.open(temporary, "w", 0o600) do |file|
|
|
141
|
+
file.write(JSON.pretty_generate(document) << "\n")
|
|
142
|
+
file.flush
|
|
143
|
+
file.fsync
|
|
144
|
+
end
|
|
145
|
+
File.rename(temporary, @path)
|
|
146
|
+
ensure
|
|
147
|
+
FileUtils.rm_f(temporary) if temporary
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def empty_document = { "schema_version" => SCHEMA_VERSION, "presets" => {} }
|
|
151
|
+
def timestamp = @clock.call.iso8601(6)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|