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,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