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,388 @@
|
|
|
1
|
+
use crate::mesh::{index_mesh_vertex, MeshBuilder, MeshData, VertexKey};
|
|
2
|
+
use crate::COMPONENTS;
|
|
3
|
+
use magnus::{Error, Ruby};
|
|
4
|
+
|
|
5
|
+
struct ColumnMeshInput<'a> {
|
|
6
|
+
columns: usize,
|
|
7
|
+
rows: usize,
|
|
8
|
+
x_boundaries: &'a [f64],
|
|
9
|
+
y_boundaries: &'a [f64],
|
|
10
|
+
z_boundaries: &'a [f64],
|
|
11
|
+
intervals: Vec<Option<(usize, usize)>>,
|
|
12
|
+
group_ids: &'a [usize],
|
|
13
|
+
group_count: usize,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl ColumnMeshInput<'_> {
|
|
17
|
+
fn interval_at(&self, row: isize, column: isize) -> Option<(usize, usize)> {
|
|
18
|
+
if row < 0 || column < 0 || row as usize >= self.rows || column as usize >= self.columns {
|
|
19
|
+
return None;
|
|
20
|
+
}
|
|
21
|
+
self.intervals[row as usize * self.columns + column as usize]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fn group_at(&self, row: isize, column: isize) -> usize {
|
|
25
|
+
if row < 0 || column < 0 || row as usize >= self.rows || column as usize >= self.columns {
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
self.group_ids[row as usize * self.columns + column as usize]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn grouped_interval(
|
|
32
|
+
&self,
|
|
33
|
+
row: usize,
|
|
34
|
+
column: usize,
|
|
35
|
+
neighbour_row: isize,
|
|
36
|
+
neighbour_column: isize,
|
|
37
|
+
) -> Option<(usize, usize)> {
|
|
38
|
+
if self.group_at(row as isize, column as isize)
|
|
39
|
+
!= self.group_at(neighbour_row, neighbour_column)
|
|
40
|
+
{
|
|
41
|
+
return None;
|
|
42
|
+
}
|
|
43
|
+
self.interval_at(neighbour_row, neighbour_column)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn occupied(&self, layer: isize, row: isize, column: isize, group: usize) -> bool {
|
|
47
|
+
if layer < 0 || self.group_at(row, column) != group {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
self.interval_at(row, column)
|
|
51
|
+
.is_some_and(|(bottom, top)| layer as usize >= bottom && (layer as usize) < top)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pub(crate) fn column_mesh(
|
|
56
|
+
ruby: &Ruby,
|
|
57
|
+
columns: usize,
|
|
58
|
+
rows: usize,
|
|
59
|
+
x_boundaries: Vec<f64>,
|
|
60
|
+
y_boundaries: Vec<f64>,
|
|
61
|
+
z_boundaries: Vec<f64>,
|
|
62
|
+
raw_intervals: Vec<i64>,
|
|
63
|
+
group_ids: Vec<usize>,
|
|
64
|
+
group_count: usize,
|
|
65
|
+
) -> Result<MeshBuilder, Error> {
|
|
66
|
+
let expected_cells = columns.saturating_mul(rows);
|
|
67
|
+
let valid = columns > 0
|
|
68
|
+
&& rows > 0
|
|
69
|
+
&& x_boundaries.len() == columns + 1
|
|
70
|
+
&& y_boundaries.len() == rows + 1
|
|
71
|
+
&& z_boundaries.len() >= 2
|
|
72
|
+
&& raw_intervals.len() == expected_cells * 2
|
|
73
|
+
&& group_ids.len() == expected_cells
|
|
74
|
+
&& group_count > 0
|
|
75
|
+
&& x_boundaries.iter().all(|value| value.is_finite())
|
|
76
|
+
&& y_boundaries.iter().all(|value| value.is_finite())
|
|
77
|
+
&& z_boundaries.iter().all(|value| value.is_finite());
|
|
78
|
+
if !valid {
|
|
79
|
+
return Err(Error::new(
|
|
80
|
+
ruby.exception_arg_error(),
|
|
81
|
+
"invalid native column mesh input",
|
|
82
|
+
));
|
|
83
|
+
}
|
|
84
|
+
let maximum_vertices_per_cell =
|
|
85
|
+
8_usize.saturating_add(16_usize.saturating_mul(z_boundaries.len().saturating_sub(1)));
|
|
86
|
+
if expected_cells.saturating_mul(maximum_vertices_per_cell) > u32::MAX as usize {
|
|
87
|
+
return Err(Error::new(
|
|
88
|
+
ruby.exception_arg_error(),
|
|
89
|
+
"native column mesh contains too many vertices",
|
|
90
|
+
));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let intervals = raw_intervals
|
|
94
|
+
.chunks_exact(2)
|
|
95
|
+
.map(|pair| match pair {
|
|
96
|
+
[-1, -1] => Ok(None),
|
|
97
|
+
[bottom, top] if *bottom >= 0 && *top > *bottom && *top < z_boundaries.len() as i64 => {
|
|
98
|
+
Ok(Some((*bottom as usize, *top as usize)))
|
|
99
|
+
}
|
|
100
|
+
_ => Err(Error::new(
|
|
101
|
+
ruby.exception_arg_error(),
|
|
102
|
+
"invalid native column interval",
|
|
103
|
+
)),
|
|
104
|
+
})
|
|
105
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
106
|
+
let input = ColumnMeshInput {
|
|
107
|
+
columns,
|
|
108
|
+
rows,
|
|
109
|
+
x_boundaries: &x_boundaries,
|
|
110
|
+
y_boundaries: &y_boundaries,
|
|
111
|
+
z_boundaries: &z_boundaries,
|
|
112
|
+
intervals,
|
|
113
|
+
group_ids: &group_ids,
|
|
114
|
+
group_count,
|
|
115
|
+
};
|
|
116
|
+
let mut data = MeshData::default();
|
|
117
|
+
data.triangles.reserve_exact(column_triangle_count(&input));
|
|
118
|
+
for row in 0..rows {
|
|
119
|
+
for column in 0..columns {
|
|
120
|
+
let Some(current) = input.interval_at(row as isize, column as isize) else {
|
|
121
|
+
continue;
|
|
122
|
+
};
|
|
123
|
+
emit_column_caps(&mut data, &input, row, column, current);
|
|
124
|
+
emit_column_sides(&mut data, &input, row, column, current);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
Ok(MeshBuilder::from_data(data))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
type ColumnOwner = (usize, usize, usize);
|
|
131
|
+
|
|
132
|
+
fn column_triangle_count(input: &ColumnMeshInput<'_>) -> usize {
|
|
133
|
+
let mut count = 0;
|
|
134
|
+
for row in 0..input.rows {
|
|
135
|
+
for column in 0..input.columns {
|
|
136
|
+
let Some(current) = input.interval_at(row as isize, column as isize) else {
|
|
137
|
+
continue;
|
|
138
|
+
};
|
|
139
|
+
count += 4;
|
|
140
|
+
count += 2 * [
|
|
141
|
+
(row as isize, column as isize - 1),
|
|
142
|
+
(row as isize, column as isize + 1),
|
|
143
|
+
(row as isize + 1, column as isize),
|
|
144
|
+
(row as isize - 1, column as isize),
|
|
145
|
+
]
|
|
146
|
+
.into_iter()
|
|
147
|
+
.map(|(neighbour_row, neighbour_column)| {
|
|
148
|
+
exposed_layer_count(
|
|
149
|
+
current,
|
|
150
|
+
input.grouped_interval(row, column, neighbour_row, neighbour_column),
|
|
151
|
+
)
|
|
152
|
+
})
|
|
153
|
+
.sum::<usize>();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
count
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fn emit_column_caps(
|
|
160
|
+
data: &mut MeshData,
|
|
161
|
+
input: &ColumnMeshInput<'_>,
|
|
162
|
+
row: usize,
|
|
163
|
+
column: usize,
|
|
164
|
+
current: (usize, usize),
|
|
165
|
+
) {
|
|
166
|
+
let (bottom, top) = current;
|
|
167
|
+
let bottom_owner = (bottom, row, column);
|
|
168
|
+
let top_owner = (top - 1, row, column);
|
|
169
|
+
emit_column_quad(
|
|
170
|
+
data,
|
|
171
|
+
[
|
|
172
|
+
column_vertex(input, column, row + 1, bottom, bottom_owner),
|
|
173
|
+
column_vertex(input, column, row, bottom, bottom_owner),
|
|
174
|
+
column_vertex(input, column + 1, row, bottom, bottom_owner),
|
|
175
|
+
column_vertex(input, column + 1, row + 1, bottom, bottom_owner),
|
|
176
|
+
],
|
|
177
|
+
);
|
|
178
|
+
emit_column_quad(
|
|
179
|
+
data,
|
|
180
|
+
[
|
|
181
|
+
column_vertex(input, column, row + 1, top, top_owner),
|
|
182
|
+
column_vertex(input, column + 1, row + 1, top, top_owner),
|
|
183
|
+
column_vertex(input, column + 1, row, top, top_owner),
|
|
184
|
+
column_vertex(input, column, row, top, top_owner),
|
|
185
|
+
],
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fn emit_column_sides(
|
|
190
|
+
data: &mut MeshData,
|
|
191
|
+
input: &ColumnMeshInput<'_>,
|
|
192
|
+
row: usize,
|
|
193
|
+
column: usize,
|
|
194
|
+
current: (usize, usize),
|
|
195
|
+
) {
|
|
196
|
+
for layer in exposed_layers(
|
|
197
|
+
current,
|
|
198
|
+
input.grouped_interval(row, column, row as isize, column as isize - 1),
|
|
199
|
+
) {
|
|
200
|
+
let owner = (layer, row, column);
|
|
201
|
+
emit_column_quad(
|
|
202
|
+
data,
|
|
203
|
+
[
|
|
204
|
+
column_vertex(input, column, row + 1, layer, owner),
|
|
205
|
+
column_vertex(input, column, row + 1, layer + 1, owner),
|
|
206
|
+
column_vertex(input, column, row, layer + 1, owner),
|
|
207
|
+
column_vertex(input, column, row, layer, owner),
|
|
208
|
+
],
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
for layer in exposed_layers(
|
|
212
|
+
current,
|
|
213
|
+
input.grouped_interval(row, column, row as isize, column as isize + 1),
|
|
214
|
+
) {
|
|
215
|
+
let owner = (layer, row, column);
|
|
216
|
+
emit_column_quad(
|
|
217
|
+
data,
|
|
218
|
+
[
|
|
219
|
+
column_vertex(input, column + 1, row + 1, layer, owner),
|
|
220
|
+
column_vertex(input, column + 1, row, layer, owner),
|
|
221
|
+
column_vertex(input, column + 1, row, layer + 1, owner),
|
|
222
|
+
column_vertex(input, column + 1, row + 1, layer + 1, owner),
|
|
223
|
+
],
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
for layer in exposed_layers(
|
|
227
|
+
current,
|
|
228
|
+
input.grouped_interval(row, column, row as isize + 1, column as isize),
|
|
229
|
+
) {
|
|
230
|
+
let owner = (layer, row, column);
|
|
231
|
+
emit_column_quad(
|
|
232
|
+
data,
|
|
233
|
+
[
|
|
234
|
+
column_vertex(input, column, row + 1, layer, owner),
|
|
235
|
+
column_vertex(input, column + 1, row + 1, layer, owner),
|
|
236
|
+
column_vertex(input, column + 1, row + 1, layer + 1, owner),
|
|
237
|
+
column_vertex(input, column, row + 1, layer + 1, owner),
|
|
238
|
+
],
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
for layer in exposed_layers(
|
|
242
|
+
current,
|
|
243
|
+
input.grouped_interval(row, column, row as isize - 1, column as isize),
|
|
244
|
+
) {
|
|
245
|
+
let owner = (layer, row, column);
|
|
246
|
+
emit_column_quad(
|
|
247
|
+
data,
|
|
248
|
+
[
|
|
249
|
+
column_vertex(input, column, row, layer, owner),
|
|
250
|
+
column_vertex(input, column, row, layer + 1, owner),
|
|
251
|
+
column_vertex(input, column + 1, row, layer + 1, owner),
|
|
252
|
+
column_vertex(input, column + 1, row, layer, owner),
|
|
253
|
+
],
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fn exposed_layers(current: (usize, usize), neighbour: Option<(usize, usize)>) -> Vec<usize> {
|
|
259
|
+
let (bottom, top) = current;
|
|
260
|
+
let Some((neighbour_bottom, neighbour_top)) = neighbour else {
|
|
261
|
+
return (bottom..top).collect();
|
|
262
|
+
};
|
|
263
|
+
if neighbour_top <= bottom || neighbour_bottom >= top {
|
|
264
|
+
return (bottom..top).collect();
|
|
265
|
+
}
|
|
266
|
+
let mut layers = Vec::new();
|
|
267
|
+
if neighbour_bottom > bottom {
|
|
268
|
+
layers.extend(bottom..top.min(neighbour_bottom));
|
|
269
|
+
}
|
|
270
|
+
if neighbour_top < top {
|
|
271
|
+
layers.extend(bottom.max(neighbour_top)..top);
|
|
272
|
+
}
|
|
273
|
+
layers
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
fn exposed_layer_count(current: (usize, usize), neighbour: Option<(usize, usize)>) -> usize {
|
|
277
|
+
let (bottom, top) = current;
|
|
278
|
+
let Some((neighbour_bottom, neighbour_top)) = neighbour else {
|
|
279
|
+
return top - bottom;
|
|
280
|
+
};
|
|
281
|
+
if neighbour_top <= bottom || neighbour_bottom >= top {
|
|
282
|
+
return top - bottom;
|
|
283
|
+
}
|
|
284
|
+
neighbour_bottom.saturating_sub(bottom).min(top - bottom)
|
|
285
|
+
+ top.saturating_sub(neighbour_top).min(top - bottom)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
fn column_vertex(
|
|
289
|
+
input: &ColumnMeshInput<'_>,
|
|
290
|
+
x_index: usize,
|
|
291
|
+
row_boundary: usize,
|
|
292
|
+
z_index: usize,
|
|
293
|
+
owner: ColumnOwner,
|
|
294
|
+
) -> ([f64; COMPONENTS], VertexKey) {
|
|
295
|
+
let (_, owner_row, owner_column) = owner;
|
|
296
|
+
let group = input.group_at(owner_row as isize, owner_column as isize);
|
|
297
|
+
let coordinate_key =
|
|
298
|
+
((x_index * (input.rows + 1) + row_boundary) * input.z_boundaries.len()) + z_index;
|
|
299
|
+
let cache_key = coordinate_key * input.group_count + group;
|
|
300
|
+
let mask = occupancy_mask(input, x_index, row_boundary, z_index, group);
|
|
301
|
+
let owner_bit = owner_bit(owner, x_index, row_boundary, z_index);
|
|
302
|
+
let component = component_root(mask, owner_bit);
|
|
303
|
+
let topology = cache_key * 8 + component;
|
|
304
|
+
let point = [
|
|
305
|
+
input.x_boundaries[x_index],
|
|
306
|
+
input.y_boundaries[input.y_boundaries.len() - 1] - input.y_boundaries[row_boundary],
|
|
307
|
+
input.z_boundaries[z_index],
|
|
308
|
+
];
|
|
309
|
+
(point, VertexKey::Topology(topology as i64))
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
fn occupancy_mask(
|
|
313
|
+
input: &ColumnMeshInput<'_>,
|
|
314
|
+
x_index: usize,
|
|
315
|
+
row_boundary: usize,
|
|
316
|
+
z_index: usize,
|
|
317
|
+
group: usize,
|
|
318
|
+
) -> u8 {
|
|
319
|
+
let mut mask = 0;
|
|
320
|
+
for layer_offset in 0..2 {
|
|
321
|
+
for row_offset in 0..2 {
|
|
322
|
+
for column_offset in 0..2 {
|
|
323
|
+
let bit = layer_offset * 4 + row_offset * 2 + column_offset;
|
|
324
|
+
if input.occupied(
|
|
325
|
+
z_index as isize - 1 + layer_offset as isize,
|
|
326
|
+
row_boundary as isize - 1 + row_offset as isize,
|
|
327
|
+
x_index as isize - 1 + column_offset as isize,
|
|
328
|
+
group,
|
|
329
|
+
) {
|
|
330
|
+
mask |= 1 << bit;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
mask
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
fn owner_bit(owner: ColumnOwner, x_index: usize, row_boundary: usize, z_index: usize) -> usize {
|
|
339
|
+
let (layer, row, column) = owner;
|
|
340
|
+
((layer as isize - z_index as isize + 1) * 4
|
|
341
|
+
+ (row as isize - row_boundary as isize + 1) * 2
|
|
342
|
+
+ column as isize
|
|
343
|
+
- x_index as isize
|
|
344
|
+
+ 1) as usize
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
fn component_root(mask: u8, start: usize) -> usize {
|
|
348
|
+
let mut visited = [false; 8];
|
|
349
|
+
let mut pending = [0; 8];
|
|
350
|
+
pending[0] = start;
|
|
351
|
+
let mut pending_count = 1;
|
|
352
|
+
visited[start] = true;
|
|
353
|
+
let mut root = start;
|
|
354
|
+
while pending_count > 0 {
|
|
355
|
+
pending_count -= 1;
|
|
356
|
+
let current = pending[pending_count];
|
|
357
|
+
root = root.min(current);
|
|
358
|
+
for candidate in 0..8 {
|
|
359
|
+
let difference = current ^ candidate;
|
|
360
|
+
if !visited[candidate]
|
|
361
|
+
&& mask & (1 << candidate) != 0
|
|
362
|
+
&& matches!(difference, 1 | 2 | 4)
|
|
363
|
+
{
|
|
364
|
+
visited[candidate] = true;
|
|
365
|
+
pending[pending_count] = candidate;
|
|
366
|
+
pending_count += 1;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
root
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
fn emit_column_quad(data: &mut MeshData, vertices: [([f64; COMPONENTS], VertexKey); 4]) {
|
|
374
|
+
let indices = vertices.map(|(point, key)| index_mesh_vertex(data, point, key));
|
|
375
|
+
data.triangles.push([indices[0], indices[1], indices[2]]);
|
|
376
|
+
data.triangles.push([indices[0], indices[2], indices[3]]);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#[cfg(test)]
|
|
380
|
+
mod tests {
|
|
381
|
+
use super::component_root;
|
|
382
|
+
|
|
383
|
+
#[test]
|
|
384
|
+
fn finds_face_connected_voxel_components_without_joining_diagonals() {
|
|
385
|
+
assert_eq!(component_root(0b0000_0011, 1), 0);
|
|
386
|
+
assert_eq!(component_root(0b1000_0001, 7), 7);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
mod column_mesh;
|
|
2
|
+
mod mesh;
|
|
3
|
+
mod palette;
|
|
4
|
+
mod xml;
|
|
5
|
+
|
|
6
|
+
use column_mesh::column_mesh;
|
|
7
|
+
use magnus::{function, method, prelude::*, Error, Ruby};
|
|
8
|
+
use mesh::MeshBuilder;
|
|
9
|
+
use palette::palette_indices;
|
|
10
|
+
use xml::{triangle_xml, vertex_xml};
|
|
11
|
+
|
|
12
|
+
pub(crate) const COMPONENTS: usize = 3;
|
|
13
|
+
|
|
14
|
+
fn extension_version() -> &'static str {
|
|
15
|
+
env!("CARGO_PKG_VERSION")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[magnus::init]
|
|
19
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
20
|
+
let lithop = ruby.define_module("Lithop")?;
|
|
21
|
+
let native = lithop.define_module("Native")?;
|
|
22
|
+
native.define_singleton_method("palette_indices", function!(palette_indices, 2))?;
|
|
23
|
+
native.define_singleton_method("vertex_xml", function!(vertex_xml, 2))?;
|
|
24
|
+
native.define_singleton_method("triangle_xml", function!(triangle_xml, 1))?;
|
|
25
|
+
native.define_singleton_method("extension_version", function!(extension_version, 0))?;
|
|
26
|
+
native.define_singleton_method("column_mesh", function!(column_mesh, 8))?;
|
|
27
|
+
let mesh_builder = native.define_class("MeshBuilder", ruby.class_object())?;
|
|
28
|
+
mesh_builder.define_singleton_method("new", function!(MeshBuilder::new, 0))?;
|
|
29
|
+
mesh_builder.define_method("add_batch", method!(MeshBuilder::add_batch, 2))?;
|
|
30
|
+
mesh_builder.define_method("vertex_count", method!(MeshBuilder::vertex_count, 0))?;
|
|
31
|
+
mesh_builder.define_method("triangle_count", method!(MeshBuilder::triangle_count, 0))?;
|
|
32
|
+
mesh_builder.define_method("center", method!(MeshBuilder::center, 0))?;
|
|
33
|
+
mesh_builder.define_method("vertex_xml", method!(MeshBuilder::vertex_xml, 3))?;
|
|
34
|
+
mesh_builder.define_method("triangle_xml", method!(MeshBuilder::triangle_xml, 2))?;
|
|
35
|
+
mesh_builder.define_method("memory_bytes", method!(MeshBuilder::memory_bytes, 0))?;
|
|
36
|
+
Ok(())
|
|
37
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
use crate::xml::{build_triangle_records, build_vertex_records};
|
|
2
|
+
use crate::COMPONENTS;
|
|
3
|
+
use magnus::{Error, Ruby};
|
|
4
|
+
use std::cell::RefCell;
|
|
5
|
+
use std::collections::HashMap;
|
|
6
|
+
|
|
7
|
+
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
8
|
+
pub(crate) enum VertexKey {
|
|
9
|
+
Coordinates([u64; COMPONENTS]),
|
|
10
|
+
Topology(i64),
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#[derive(Default)]
|
|
14
|
+
pub(crate) struct MeshData {
|
|
15
|
+
pub(crate) vertices: Vec<[f64; COMPONENTS]>,
|
|
16
|
+
pub(crate) triangles: Vec<[u32; COMPONENTS]>,
|
|
17
|
+
coordinate_indices: HashMap<[u64; COMPONENTS], u32>,
|
|
18
|
+
topology_indices: HashMap<i64, u32>,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[magnus::wrap(class = "Lithop::Native::MeshBuilder", free_immediately, size)]
|
|
22
|
+
pub(crate) struct MeshBuilder(RefCell<MeshData>);
|
|
23
|
+
|
|
24
|
+
impl MeshBuilder {
|
|
25
|
+
pub(crate) fn new() -> Self {
|
|
26
|
+
Self(RefCell::new(MeshData::default()))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub(crate) fn from_data(data: MeshData) -> Self {
|
|
30
|
+
Self(RefCell::new(data))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub(crate) fn add_batch(
|
|
34
|
+
ruby: &Ruby,
|
|
35
|
+
rb_self: &Self,
|
|
36
|
+
coordinates: Vec<f64>,
|
|
37
|
+
topology_keys: Vec<i64>,
|
|
38
|
+
) -> Result<usize, Error> {
|
|
39
|
+
if coordinates.len() % (COMPONENTS * COMPONENTS) != 0 {
|
|
40
|
+
return Err(Error::new(
|
|
41
|
+
ruby.exception_arg_error(),
|
|
42
|
+
"3MF mesh coordinates must contain complete triangles",
|
|
43
|
+
));
|
|
44
|
+
}
|
|
45
|
+
if topology_keys.len() * COMPONENTS != coordinates.len() {
|
|
46
|
+
return Err(Error::new(
|
|
47
|
+
ruby.exception_arg_error(),
|
|
48
|
+
"3MF mesh topology key count must match its vertices",
|
|
49
|
+
));
|
|
50
|
+
}
|
|
51
|
+
if coordinates.iter().any(|value| !value.is_finite()) {
|
|
52
|
+
return Err(Error::new(
|
|
53
|
+
ruby.exception_arg_error(),
|
|
54
|
+
"3MF coordinates must be finite",
|
|
55
|
+
));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let mut data = rb_self.0.borrow_mut();
|
|
59
|
+
let maximum_vertex_count = data.vertices.len().checked_add(topology_keys.len());
|
|
60
|
+
if maximum_vertex_count.is_none_or(|count| count > u32::MAX as usize) {
|
|
61
|
+
return Err(Error::new(
|
|
62
|
+
ruby.exception_arg_error(),
|
|
63
|
+
"3MF mesh contains too many vertices",
|
|
64
|
+
));
|
|
65
|
+
}
|
|
66
|
+
for (triangle_coordinates, triangle_keys) in coordinates
|
|
67
|
+
.chunks_exact(COMPONENTS * COMPONENTS)
|
|
68
|
+
.zip(topology_keys.chunks_exact(COMPONENTS))
|
|
69
|
+
{
|
|
70
|
+
let mut triangle = [0; COMPONENTS];
|
|
71
|
+
for vertex_offset in 0..COMPONENTS {
|
|
72
|
+
let start = vertex_offset * COMPONENTS;
|
|
73
|
+
let point = [
|
|
74
|
+
triangle_coordinates[start],
|
|
75
|
+
triangle_coordinates[start + 1],
|
|
76
|
+
triangle_coordinates[start + 2],
|
|
77
|
+
];
|
|
78
|
+
let key = vertex_key(point, triangle_keys[vertex_offset]);
|
|
79
|
+
triangle[vertex_offset] = index_mesh_vertex(&mut data, point, key);
|
|
80
|
+
}
|
|
81
|
+
data.triangles.push(triangle);
|
|
82
|
+
}
|
|
83
|
+
Ok(data.triangles.len())
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pub(crate) fn vertex_count(&self) -> usize {
|
|
87
|
+
self.0.borrow().vertices.len()
|
|
88
|
+
}
|
|
89
|
+
pub(crate) fn triangle_count(&self) -> usize {
|
|
90
|
+
self.0.borrow().triangles.len()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
pub(crate) fn center(&self) -> Vec<f64> {
|
|
94
|
+
let data = self.0.borrow();
|
|
95
|
+
let mut minimum = [f64::INFINITY; COMPONENTS];
|
|
96
|
+
let mut maximum = [f64::NEG_INFINITY; COMPONENTS];
|
|
97
|
+
for vertex in &data.vertices {
|
|
98
|
+
for axis in 0..COMPONENTS {
|
|
99
|
+
minimum[axis] = minimum[axis].min(vertex[axis]);
|
|
100
|
+
maximum[axis] = maximum[axis].max(vertex[axis]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
(0..COMPONENTS)
|
|
104
|
+
.map(|axis| (minimum[axis] + maximum[axis]) / 2.0)
|
|
105
|
+
.collect()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
pub(crate) fn vertex_xml(
|
|
109
|
+
ruby: &Ruby,
|
|
110
|
+
rb_self: &Self,
|
|
111
|
+
offset: usize,
|
|
112
|
+
count: usize,
|
|
113
|
+
origin: Vec<f64>,
|
|
114
|
+
) -> Result<String, Error> {
|
|
115
|
+
let data = rb_self.0.borrow();
|
|
116
|
+
let end = offset.saturating_add(count).min(data.vertices.len());
|
|
117
|
+
if offset > end {
|
|
118
|
+
return Err(Error::new(
|
|
119
|
+
ruby.exception_arg_error(),
|
|
120
|
+
"3MF vertex offset is out of range",
|
|
121
|
+
));
|
|
122
|
+
}
|
|
123
|
+
build_vertex_records(&data.vertices[offset..end], &origin)
|
|
124
|
+
.map_err(|message| Error::new(ruby.exception_arg_error(), message))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
pub(crate) fn triangle_xml(
|
|
128
|
+
ruby: &Ruby,
|
|
129
|
+
rb_self: &Self,
|
|
130
|
+
offset: usize,
|
|
131
|
+
count: usize,
|
|
132
|
+
) -> Result<String, Error> {
|
|
133
|
+
let data = rb_self.0.borrow();
|
|
134
|
+
let end = offset.saturating_add(count).min(data.triangles.len());
|
|
135
|
+
if offset > end {
|
|
136
|
+
return Err(Error::new(
|
|
137
|
+
ruby.exception_arg_error(),
|
|
138
|
+
"3MF triangle offset is out of range",
|
|
139
|
+
));
|
|
140
|
+
}
|
|
141
|
+
build_triangle_records(&data.triangles[offset..end])
|
|
142
|
+
.map_err(|message| Error::new(ruby.exception_arg_error(), message))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
pub(crate) fn memory_bytes(&self) -> usize {
|
|
146
|
+
let data = self.0.borrow();
|
|
147
|
+
data.vertices.capacity() * std::mem::size_of::<[f64; COMPONENTS]>()
|
|
148
|
+
+ data.triangles.capacity() * std::mem::size_of::<[u32; COMPONENTS]>()
|
|
149
|
+
+ data.coordinate_indices.capacity()
|
|
150
|
+
* (std::mem::size_of::<[u64; COMPONENTS]>() + std::mem::size_of::<u32>())
|
|
151
|
+
+ data.topology_indices.capacity()
|
|
152
|
+
* (std::mem::size_of::<i64>() + std::mem::size_of::<u32>())
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn vertex_key(point: [f64; COMPONENTS], topology_key: i64) -> VertexKey {
|
|
157
|
+
if topology_key >= 0 {
|
|
158
|
+
VertexKey::Topology(topology_key)
|
|
159
|
+
} else {
|
|
160
|
+
VertexKey::Coordinates(point.map(|value| if value == 0.0 { 0 } else { value.to_bits() }))
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
pub(crate) fn index_mesh_vertex(
|
|
165
|
+
data: &mut MeshData,
|
|
166
|
+
point: [f64; COMPONENTS],
|
|
167
|
+
key: VertexKey,
|
|
168
|
+
) -> u32 {
|
|
169
|
+
let existing = match key {
|
|
170
|
+
VertexKey::Coordinates(coordinates) => data.coordinate_indices.get(&coordinates),
|
|
171
|
+
VertexKey::Topology(topology) => data.topology_indices.get(&topology),
|
|
172
|
+
};
|
|
173
|
+
if let Some(index) = existing {
|
|
174
|
+
return *index;
|
|
175
|
+
}
|
|
176
|
+
let index = data.vertices.len() as u32;
|
|
177
|
+
data.vertices.push(point);
|
|
178
|
+
match key {
|
|
179
|
+
VertexKey::Coordinates(coordinates) => {
|
|
180
|
+
data.coordinate_indices.insert(coordinates, index);
|
|
181
|
+
}
|
|
182
|
+
VertexKey::Topology(topology) => {
|
|
183
|
+
data.topology_indices.insert(topology, index);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
index
|
|
187
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
use crate::COMPONENTS;
|
|
2
|
+
use magnus::{Error, Ruby};
|
|
3
|
+
|
|
4
|
+
fn select_palette_indices(cmys: &[f64], coverages: &[f64]) -> Result<Vec<usize>, &'static str> {
|
|
5
|
+
if cmys.len() % COMPONENTS != 0 {
|
|
6
|
+
return Err("CMY values must contain triples");
|
|
7
|
+
}
|
|
8
|
+
if coverages.is_empty() || coverages.len() % COMPONENTS != 0 {
|
|
9
|
+
return Err("recipe coverage must contain triples");
|
|
10
|
+
}
|
|
11
|
+
if cmys
|
|
12
|
+
.iter()
|
|
13
|
+
.any(|value| !value.is_finite() || !(0.0..=1.0).contains(value))
|
|
14
|
+
{
|
|
15
|
+
return Err("CMY values must be finite values between zero and one");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let recipes: Vec<&[f64]> = coverages.chunks_exact(COMPONENTS).collect();
|
|
19
|
+
let mut indices = Vec::with_capacity(cmys.len() / COMPONENTS);
|
|
20
|
+
for target in cmys.chunks_exact(COMPONENTS) {
|
|
21
|
+
let mut best_index = 0;
|
|
22
|
+
let mut best_distance = f64::INFINITY;
|
|
23
|
+
for (index, coverage) in recipes.iter().enumerate() {
|
|
24
|
+
let cyan = coverage[0] - target[0];
|
|
25
|
+
let magenta = coverage[1] - target[1];
|
|
26
|
+
let yellow = coverage[2] - target[2];
|
|
27
|
+
let distance = (cyan * cyan) + (magenta * magenta) + (yellow * yellow);
|
|
28
|
+
if distance < best_distance {
|
|
29
|
+
best_index = index;
|
|
30
|
+
best_distance = distance;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
indices.push(best_index);
|
|
34
|
+
}
|
|
35
|
+
Ok(indices)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub(crate) fn palette_indices(
|
|
39
|
+
ruby: &Ruby,
|
|
40
|
+
cmys: Vec<f64>,
|
|
41
|
+
coverages: Vec<f64>,
|
|
42
|
+
) -> Result<Vec<usize>, Error> {
|
|
43
|
+
select_palette_indices(&cmys, &coverages)
|
|
44
|
+
.map_err(|message| Error::new(ruby.exception_arg_error(), message))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[cfg(test)]
|
|
48
|
+
mod tests {
|
|
49
|
+
use super::select_palette_indices;
|
|
50
|
+
|
|
51
|
+
#[test]
|
|
52
|
+
fn selects_the_first_recipe_at_equal_distance() {
|
|
53
|
+
let indices =
|
|
54
|
+
select_palette_indices(&[0.5, 0.0, 0.0], &[0.0, 0.0, 0.0, 1.0, 0.0, 0.0]).unwrap();
|
|
55
|
+
assert_eq!(indices, vec![0]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[test]
|
|
59
|
+
fn selects_multiple_samples_in_one_call() {
|
|
60
|
+
let indices = select_palette_indices(
|
|
61
|
+
&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0],
|
|
62
|
+
&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0],
|
|
63
|
+
)
|
|
64
|
+
.unwrap();
|
|
65
|
+
assert_eq!(indices, vec![0, 1]);
|
|
66
|
+
}
|
|
67
|
+
}
|