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
@@ -1,13 +1,33 @@
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.
4
+ # Shared mesh counting and conversion helpers for export plans.
5
+ module MaterialMeshPlanningHelpers
6
+ private
7
+
8
+ def triangle_count(geometry)
9
+ geometry.respond_to?(:triangle_count) ? geometry.triangle_count : geometry.each_triangle.count
10
+ end
11
+
12
+ def triangle_source(geometry)
13
+ geometry.is_a?(ColumnGeometry) ? geometry : geometry.each_triangle
14
+ end
15
+
16
+ def counts_by_material(entries, entry_counts)
17
+ entries.zip(entry_counts).each_with_object(Hash.new(0)) do |(entry, count), totals|
18
+ totals[entry.material] += count
19
+ end.freeze
20
+ end
21
+ end
22
+
23
+ # Slices the CMYW model into contiguous plates over strength-adjusted White supports.
5
24
  class ZSeparatedMaterialMeshPlan
25
+ include MaterialMeshPlanningHelpers
26
+
6
27
  Entry = Data.define(:material, :geometry, :name, :region, :build_z_offset)
7
28
 
8
- def initialize(model:, base_geometry:, top_geometry:, settings:, maximum_triangles:, limit_message:)
29
+ def initialize(model:, top_geometry:, settings:, maximum_triangles:, limit_message:)
9
30
  @model = model
10
- @base_geometry = base_geometry
11
31
  @top_geometry = top_geometry
12
32
  @settings = settings
13
33
  @maximum_triangles = maximum_triangles
@@ -36,42 +56,73 @@ module Lithop
36
56
 
37
57
  def build_entries
38
58
  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
- )
59
+ plate_ranges.each_with_index do |plate_range, index|
60
+ region = "z_part_#{index + 1}"
61
+ source_z_min = plate_range.begin
62
+ build_z_offset = @settings.z_part_base_white_thickness(index) - source_z_min
63
+ append_base_white_entry(entries, index:, region:)
64
+ append_color_entries(entries, index:, region:, build_z_offset:)
65
+ append_top_white_entry(entries, index:, plate_range:, region:, build_z_offset:)
66
+ end
67
+ entries
68
+ end
69
+
70
+ def append_base_white_entry(entries, index:, region:)
71
+ range = 0.0...@settings.z_part_base_white_thickness(index)
72
+ entries << Entry.new(
73
+ material: :white, geometry: box_geometry(range),
74
+ name: "Z plate #{index + 1} Base White", region:, build_z_offset: 0.0
75
+ )
76
+ end
77
+
78
+ def append_color_entries(entries, index:, region:, build_z_offset:)
79
+ range = color_ranges.fetch(index)
80
+ return unless range.begin < range.end
81
+
82
+ region_model = ZRegionModel.new(model: @model, z_range: range)
83
+ entries << Entry.new(
84
+ material: :white, geometry: color_region_white_geometry(region_model),
85
+ name: "Z plate #{index + 1} White", region:, build_z_offset:
86
+ )
87
+ Palette::PIGMENTS.each do |material|
88
+ next unless region_model.materials.include?(material)
89
+
47
90
  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:
91
+ material:, geometry: ColumnGeometry.new(model: region_model, material:),
92
+ name: "Z plate #{index + 1} #{material.to_s.capitalize}", region:, build_z_offset:
53
93
  )
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
94
  end
95
+ end
96
+
97
+ def append_top_white_entry(entries, index:, plate_range:, region:, build_z_offset:)
98
+ top_start = @settings.layer_structure.color_region_end
99
+ range = intersection(plate_range, top_start...@settings.z_separated_source_end)
100
+ return unless range
101
+
102
+ geometry = index == plate_ranges.length - 1 ? @top_geometry : box_geometry(range)
66
103
  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
104
+ material: :white, geometry:, name: "Z plate #{index + 1} Top White",
105
+ region:, build_z_offset:
69
106
  )
70
- entries
71
107
  end
72
108
 
73
109
  def color_ranges
74
- @settings.layer_structure.color_region_layer_ranges(@settings.color_region_parts)
110
+ @settings.resolved_z_color_ranges
111
+ end
112
+
113
+ def plate_ranges = @settings.resolved_z_part_ranges
114
+
115
+ def intersection(left, right)
116
+ start = [left.begin, right.begin].max
117
+ finish = [left.end, right.end].min
118
+ start < finish ? start...finish : nil
119
+ end
120
+
121
+ def box_geometry(range)
122
+ BoxGeometry.new(
123
+ width: @model.x_boundaries.last, height: @model.y_boundaries.last,
124
+ z_min: range.begin, z_max: range.end
125
+ )
75
126
  end
76
127
 
77
128
  def color_region_white_geometry(model)
@@ -82,20 +133,6 @@ module Lithop
82
133
  z_max: model.z_boundaries.last
83
134
  )
84
135
  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
136
  end
100
137
 
101
138
  # Builds the reference-style overlapping White height field and contiguous CMY columns.
@@ -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|
@@ -38,7 +38,7 @@ module Lithop
38
38
  - White thickness uses #{settings.pixel_size} mm XY cells; CMYW recipes use
39
39
  #{settings.effective_color_pixel_size} mm color cells.
40
40
  - Use 100% rectilinear infill and verify every layer in the slicer before printing.
41
- - The effective thickness range is #{settings.effective_minimum_thickness}–#{settings.effective_maximum_thickness} mm.
41
+ - The effective thickness range is #{output_minimum_thickness(settings)}–#{output_maximum_thickness(settings)} mm.
42
42
  MARKDOWN
43
43
  end
44
44
 
@@ -52,8 +52,23 @@ module Lithop
52
52
  def z_part_line(settings)
53
53
  return "- Z regions remain combined into the material parts." unless settings.separate_z_parts
54
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."
55
+ "- The separated model uses #{settings.resolved_z_part_count} contiguous Z plates with Base White " \
56
+ "supports of #{settings.z_part_base_white_layers_by_plate.join("/")} layers " \
57
+ "(#{settings.z_part_base_white_thicknesses.join("/")} mm); every plate is at least " \
58
+ "#{settings.z_part_minimum_build_thickness} mm thick. The assembled thickness range is " \
59
+ "#{settings.z_separated_output_minimum_thickness}–#{settings.z_separated_output_maximum_thickness} mm."
60
+ end
61
+
62
+ def output_minimum_thickness(settings)
63
+ return settings.effective_minimum_thickness unless settings.separate_z_parts
64
+
65
+ settings.z_separated_output_minimum_thickness
66
+ end
67
+
68
+ def output_maximum_thickness(settings)
69
+ return settings.effective_maximum_thickness unless settings.separate_z_parts
70
+
71
+ settings.z_separated_output_maximum_thickness
57
72
  end
58
73
 
59
74
  def profile_material(profile, material)
@@ -81,7 +96,8 @@ module Lithop
81
96
  "- Use a #{settings.nozzle_diameter} mm nozzle, #{settings.first_layer_height} mm base layers,\n " \
82
97
  "#{settings.layer_height} mm color layers, and #{upper_height} mm upper White layers."
83
98
  end
84
- private_class_method :model_instruction, :z_part_line, :profile_material, :calibration_line, :thickness_method,
99
+ private_class_method :model_instruction, :z_part_line, :output_minimum_thickness, :profile_material,
100
+ :calibration_line, :thickness_method,
85
101
  :base_layer_description, :layer_height_description
86
102
  end
87
103
  end
@@ -136,9 +136,7 @@ module Lithop
136
136
  end
137
137
 
138
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
139
+ assembly.region.delete_prefix("z_part_").to_i - 1
142
140
  end
143
141
 
144
142
  def number(value) = format("%.9g", value)
@@ -150,12 +148,13 @@ module Lithop
150
148
 
151
149
  def layer_ranges_xml
152
150
  object_id = @assemblies.length
153
- min_z = @settings.top_white_start
154
- max_z = @settings.maximum_thickness
151
+ min_z = @settings.layer_structure.top_white_start
152
+ max_z = @settings.effective_maximum_thickness
155
153
  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)
154
+ source_z_min = last_plate_source_z_min
155
+ base_white = @settings.z_part_base_white_thickness(@settings.resolved_z_part_count - 1)
156
+ min_z = (min_z - source_z_min + base_white).round(9)
157
+ max_z = (max_z - source_z_min + base_white).round(9)
159
158
  end
160
159
  <<~XML
161
160
  <?xml version="1.0" encoding="utf-8"?>
@@ -170,6 +169,10 @@ module Lithop
170
169
  XML
171
170
  end
172
171
 
172
+ def last_plate_source_z_min
173
+ @settings.resolved_z_part_ranges.last.begin
174
+ end
175
+
173
176
  def project_settings
174
177
  {
175
178
  "filament_colour" => %w[#0086D6 #EC008C #FCE300 #FFFFFF],
@@ -3,11 +3,15 @@
3
3
  module Lithop
4
4
  # Indexes vertices and triangles for a 3MF mesh resource.
5
5
  class ThreeMFMesh
6
- attr_reader :vertices, :vertex_indices, :triangle_count
6
+ NATIVE_BATCH_SIZE = 8_192
7
+ MAXIMUM_NATIVE_TOPOLOGY_KEY = (2**63) - 1
8
+ class UnsupportedNativeTopology < StandardError; end
9
+
10
+ attr_reader :vertices, :vertex_indices, :triangle_count, :native_mesh
7
11
 
8
12
  def initialize(triangles:)
9
13
  @triangles = triangles
10
- index_resources
14
+ Native.enabled? ? index_native_resources : index_ruby_resources
11
15
  end
12
16
 
13
17
  def each_indexed_triangle
@@ -18,9 +22,74 @@ module Lithop
18
22
  end
19
23
  end
20
24
 
25
+ def native_indexed? = !native_mesh.nil?
26
+
27
+ def vertex_count = native_indexed? ? native_mesh.vertex_count : vertices.length
28
+
29
+ def center
30
+ return native_mesh.center if native_indexed?
31
+
32
+ axes = vertices.transpose
33
+ axes.map { |values| (values.min + values.max) / 2.0 }
34
+ end
35
+
21
36
  private
22
37
 
23
- def index_resources
38
+ def index_native_resources
39
+ if @triangles.respond_to?(:native_indexed_mesh)
40
+ @native_mesh = @triangles.native_indexed_mesh
41
+ raise InputError, "3MF mesh contains no triangles" if native_mesh.vertex_count.zero?
42
+
43
+ @triangle_count = native_mesh.triangle_count
44
+ @vertices = nil
45
+ @vertex_indices = nil
46
+ return
47
+ end
48
+
49
+ @native_mesh = Native::MeshBuilder.new
50
+ coordinates = []
51
+ topology_keys = []
52
+ count = 0
53
+ @triangles.each do |triangle|
54
+ validate_triangle!(triangle)
55
+ triangle.each do |vertex|
56
+ coordinates.concat(coordinates(vertex))
57
+ topology_keys << native_topology_key(vertex)
58
+ end
59
+ count += 1
60
+ flush_native_batch(coordinates, topology_keys) if (count % NATIVE_BATCH_SIZE).zero?
61
+ end
62
+ flush_native_batch(coordinates, topology_keys)
63
+ raise InputError, "3MF mesh contains no triangles" if native_mesh.vertex_count.zero?
64
+
65
+ @triangle_count = native_mesh.triangle_count
66
+ @vertices = nil
67
+ @vertex_indices = nil
68
+ rescue UnsupportedNativeTopology
69
+ @native_mesh = nil
70
+ index_ruby_resources
71
+ end
72
+
73
+ def flush_native_batch(coordinates, topology_keys)
74
+ return if coordinates.empty?
75
+
76
+ native_mesh.add_batch(coordinates, topology_keys)
77
+ coordinates.clear
78
+ topology_keys.clear
79
+ end
80
+
81
+ def native_topology_key(vertex)
82
+ return -1 unless vertex.respond_to?(:topology)
83
+
84
+ topology = vertex.topology
85
+ unless topology.is_a?(Integer) && topology.between?(0, MAXIMUM_NATIVE_TOPOLOGY_KEY)
86
+ raise UnsupportedNativeTopology
87
+ end
88
+
89
+ topology
90
+ end
91
+
92
+ def index_ruby_resources
24
93
  @vertices = []
25
94
  @vertex_indices = {}
26
95
  @triangle_count = 0
@@ -78,7 +78,7 @@ module Lithop
78
78
  ThreeMFPartWriter.new(archive: @archive, profile: @profile).write(object_id, part, mesh)
79
79
  end
80
80
  part.triangles.release_cache! if part.triangles.respond_to?(:release_cache!)
81
- vertex_count += mesh.vertices.length
81
+ vertex_count += mesh.vertex_count
82
82
  triangle_counts[part.material] = triangle_counts.fetch(part.material, 0) + mesh.triangle_count
83
83
  end
84
84
  Result.new(
@@ -96,7 +96,7 @@ module Lithop
96
96
  result
97
97
  end
98
98
 
99
- # rubocop:disable-next Metrics/AbcSize
99
+ # rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength
100
100
  def write_external_parts(object_ids, assemblies)
101
101
  parts_by_object_id = object_ids.zip(@parts).to_h
102
102
  vertex_count = 0
@@ -106,22 +106,26 @@ module Lithop
106
106
  measure_xml do
107
107
  @archive.write(submodel_header)
108
108
  write_materials
109
- assembly.object_ids.each do |object_id|
110
- part = parts_by_object_id.fetch(object_id)
111
- mesh = @instrumentation.measure(:three_mf_indexing) do
112
- ThreeMFMesh.new(triangles: part.triangles)
113
- end
114
- center = mesh_center(mesh)
115
- assembly.component_origins[object_id] = component_origin(
116
- center, assembly.component_origins.fetch(object_id)
117
- )
109
+ end
110
+ assembly.object_ids.each do |object_id|
111
+ part = parts_by_object_id.fetch(object_id)
112
+ mesh = @instrumentation.measure(:three_mf_indexing) do
113
+ ThreeMFMesh.new(triangles: part.triangles)
114
+ end
115
+ center = mesh_center(mesh)
116
+ assembly.component_origins[object_id] = component_origin(
117
+ center, assembly.component_origins.fetch(object_id)
118
+ )
119
+ measure_xml do
118
120
  ThreeMFPartWriter.new(
119
121
  archive: @archive, profile: @profile, production: true, origin: center
120
122
  ).write(object_id, part, mesh)
121
- part.triangles.release_cache! if part.triangles.respond_to?(:release_cache!)
122
- vertex_count += mesh.vertices.length
123
- triangle_counts[part.material] = triangle_counts.fetch(part.material, 0) + mesh.triangle_count
124
123
  end
124
+ part.triangles.release_cache! if part.triangles.respond_to?(:release_cache!)
125
+ vertex_count += mesh.vertex_count
126
+ triangle_counts[part.material] = triangle_counts.fetch(part.material, 0) + mesh.triangle_count
127
+ end
128
+ measure_xml do
125
129
  @archive.write(" </resources>\n <build/>\n</model>\n")
126
130
  end
127
131
  end
@@ -194,9 +198,8 @@ module Lithop
194
198
 
195
199
  def assembly_name(region)
196
200
  return "Lithop CMYW lithophane" unless region
197
- return "Top White" if region == "top_white"
198
201
 
199
- region.sub("color_region_", "Color region ")
202
+ region.sub("z_part_", "Z plate ")
200
203
  end
201
204
 
202
205
  def write_assembly(assembly, submodel_index = nil)
@@ -252,8 +255,7 @@ module Lithop
252
255
  end
253
256
 
254
257
  def mesh_center(mesh)
255
- axes = mesh.vertices.transpose
256
- axes.map { |values| (values.min + values.max) / 2.0 }
258
+ mesh.center
257
259
  end
258
260
 
259
261
  def component_origin(center, base_origin)
@@ -261,9 +263,7 @@ module Lithop
261
263
  end
262
264
 
263
265
  def plate_index(assembly)
264
- return 3 if assembly.region == "top_white"
265
-
266
- assembly.region.delete_prefix("color_region_").to_i - 1
266
+ assembly.region.delete_prefix("z_part_").to_i - 1
267
267
  end
268
268
 
269
269
  def model_header
@@ -3,6 +3,8 @@
3
3
  module Lithop
4
4
  # Serializes one indexed material mesh into a 3MF model document.
5
5
  class ThreeMFPartWriter
6
+ XML_BATCH_SIZE = 8_192
7
+
6
8
  def initialize(archive:, profile:, production: false, origin: nil)
7
9
  @archive = archive
8
10
  @profile = profile
@@ -31,6 +33,9 @@ module Lithop
31
33
  private
32
34
 
33
35
  def write_vertices(mesh)
36
+ return write_indexed_native_vertices(mesh) if mesh.native_indexed?
37
+ return write_native_vertices(mesh) if Native.enabled?
38
+
34
39
  mesh.vertices.each do |x, y, z|
35
40
  @archive.write(
36
41
  " <vertex x=\"#{number(x - @origin[0])}\" y=\"#{number(y - @origin[1])}\" " \
@@ -40,6 +45,9 @@ module Lithop
40
45
  end
41
46
 
42
47
  def write_triangles(mesh)
48
+ return write_indexed_native_triangles(mesh) if mesh.native_indexed?
49
+ return write_native_triangles(mesh) if Native.enabled?
50
+
43
51
  count = 0
44
52
  mesh.each_indexed_triangle do |vertices|
45
53
  @archive.write(
@@ -52,6 +60,35 @@ module Lithop
52
60
  raise InputError, "3MF triangle enumerables must be repeatable"
53
61
  end
54
62
 
63
+ def write_native_vertices(mesh)
64
+ mesh.vertices.each_slice(XML_BATCH_SIZE) do |vertices|
65
+ @archive.write(Native.vertex_xml(vertices.flatten, @origin))
66
+ end
67
+ end
68
+
69
+ def write_indexed_native_vertices(mesh)
70
+ 0.step(mesh.vertex_count - 1, XML_BATCH_SIZE) do |offset|
71
+ @archive.write(mesh.native_mesh.vertex_xml(offset, XML_BATCH_SIZE, @origin))
72
+ end
73
+ end
74
+
75
+ def write_native_triangles(mesh)
76
+ count = 0
77
+ mesh.each_indexed_triangle.each_slice(XML_BATCH_SIZE) do |triangles|
78
+ @archive.write(Native.triangle_xml(triangles.flatten))
79
+ count += triangles.length
80
+ end
81
+ return if count == mesh.triangle_count
82
+
83
+ raise InputError, "3MF triangle enumerables must be repeatable"
84
+ end
85
+
86
+ def write_indexed_native_triangles(mesh)
87
+ 0.step(mesh.triangle_count - 1, XML_BATCH_SIZE) do |offset|
88
+ @archive.write(mesh.native_mesh.triangle_xml(offset, XML_BATCH_SIZE))
89
+ end
90
+ end
91
+
55
92
  def number(value) = format("%.9g", value)
56
93
  end
57
94
  end
@@ -18,7 +18,7 @@ module Lithop
18
18
  MODEL_RELATIONSHIP = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"
19
19
  MODEL_CONTENT_TYPE = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml"
20
20
  ZIP_MUTEX = Mutex.new
21
- # Triangles are indexed and then serialized, so their enumerable must be replayable.
21
+ # The Rust indexer consumes triangles once; the Ruby comparison path requires replayable input.
22
22
  Part = Data.define(:material, :triangles, :name, :region, :build_z_offset) do
23
23
  def initialize(material:, triangles:, name: nil, region: nil, build_z_offset: 0.0)
24
24
  super