bambu-printer-mcp 1.1.32 → 1.1.34
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.
- package/CONTRIBUTORS.md +5 -1
- package/README.md +4 -2
- package/dist/bambu-network-bridge.d.ts +78 -4
- package/dist/bambu-network-bridge.js +292 -47
- package/dist/bambu-network-certs.d.ts +13 -0
- package/dist/bambu-network-certs.js +152 -0
- package/dist/index.js +28 -11
- package/dist/printers/bambu.d.ts +12 -2
- package/dist/printers/bambu.js +74 -41
- package/dist/safety/printer-state.d.ts +2 -0
- package/dist/safety/printer-state.js +42 -1
- package/dist/slicer/orca-profile-cache.d.ts +18 -0
- package/dist/slicer/orca-profile-cache.js +460 -0
- package/dist/slicer/profile-flatten.js +1 -1
- package/dist/stl/stl-manipulator.d.ts +8 -0
- package/dist/stl/stl-manipulator.js +148 -21
- package/package.json +1 -1
- package/src/bambu-network-bridge.ts +324 -55
- package/src/bambu-network-certs.ts +154 -0
- package/src/index.ts +43 -25
- package/src/printers/bambu.ts +74 -58
- package/src/safety/printer-state.ts +42 -1
- package/src/slicer/orca-profile-cache.ts +379 -0
- package/src/slicer/profile-flatten.ts +1 -1
- package/src/stl/stl-manipulator.ts +156 -16
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { profileJsonFiles } from './profile-files.js';
|
|
5
|
+
// OrcaSlicer 14da40e3a7b82aa8fc88593c2cf880e66560028f:
|
|
6
|
+
// PresetCacheFormat.{hpp,cpp}, Config.hpp, Config.cpp::save_to_json,
|
|
7
|
+
// Preset.hpp::VendorProfile and Semver.hpp. Only the named v2 format is read;
|
|
8
|
+
// the old ordinal-based cache must never be interpreted as this format.
|
|
9
|
+
const MAX_CACHE_BYTES = 64 * 1024 * 1024;
|
|
10
|
+
const MAX_ITEMS = 100000;
|
|
11
|
+
const VECTOR = 0x4000;
|
|
12
|
+
// Nullable declarations in the same pinned PrintConfig.cpp; unknown nil values fail closed.
|
|
13
|
+
const NULLABLE_KEYS = new Set([
|
|
14
|
+
"bridge_acceleration",
|
|
15
|
+
"bridge_speed",
|
|
16
|
+
"default_acceleration",
|
|
17
|
+
"default_jerk",
|
|
18
|
+
"default_junction_deviation",
|
|
19
|
+
"deretract_speed_extruder_change",
|
|
20
|
+
"enable_overhang_speed",
|
|
21
|
+
"extruder_max_nozzle_count",
|
|
22
|
+
"filament_adaptive_volumetric_speed",
|
|
23
|
+
"filament_cooling_before_tower",
|
|
24
|
+
"filament_deretraction_speed",
|
|
25
|
+
"filament_flow_ratio",
|
|
26
|
+
"filament_flush_temp",
|
|
27
|
+
"filament_flush_temp_fast",
|
|
28
|
+
"filament_flush_volumetric_speed",
|
|
29
|
+
"filament_ironing_flow",
|
|
30
|
+
"filament_ironing_inset",
|
|
31
|
+
"filament_ironing_spacing",
|
|
32
|
+
"filament_ironing_speed",
|
|
33
|
+
"filament_long_retractions_when_cut",
|
|
34
|
+
"filament_pre_cooling_temperature",
|
|
35
|
+
"filament_pre_cooling_temperature_nc",
|
|
36
|
+
"filament_preheat_temperature_delta",
|
|
37
|
+
"filament_ramming_travel_time",
|
|
38
|
+
"filament_ramming_travel_time_nc",
|
|
39
|
+
"filament_ramming_volumetric_speed",
|
|
40
|
+
"filament_ramming_volumetric_speed_nc",
|
|
41
|
+
"filament_retract_after_wipe",
|
|
42
|
+
"filament_retract_before_wipe",
|
|
43
|
+
"filament_retract_length_nc",
|
|
44
|
+
"filament_retract_length_toolchange",
|
|
45
|
+
"filament_retract_lift_above",
|
|
46
|
+
"filament_retract_lift_below",
|
|
47
|
+
"filament_retract_lift_enforce",
|
|
48
|
+
"filament_retract_restart_extra",
|
|
49
|
+
"filament_retract_restart_extra_toolchange",
|
|
50
|
+
"filament_retract_when_changing_layer",
|
|
51
|
+
"filament_retraction_distances_when_cut",
|
|
52
|
+
"filament_retraction_length",
|
|
53
|
+
"filament_retraction_minimum_travel",
|
|
54
|
+
"filament_retraction_speed",
|
|
55
|
+
"filament_wipe",
|
|
56
|
+
"filament_wipe_distance",
|
|
57
|
+
"filament_z_hop",
|
|
58
|
+
"filament_z_hop_types",
|
|
59
|
+
"gap_infill_speed",
|
|
60
|
+
"hotend_cooling_rate",
|
|
61
|
+
"hotend_heating_rate",
|
|
62
|
+
"infill_jerk",
|
|
63
|
+
"initial_layer_acceleration",
|
|
64
|
+
"initial_layer_infill_speed",
|
|
65
|
+
"initial_layer_jerk",
|
|
66
|
+
"initial_layer_speed",
|
|
67
|
+
"initial_layer_travel_acceleration",
|
|
68
|
+
"initial_layer_travel_jerk",
|
|
69
|
+
"initial_layer_travel_speed",
|
|
70
|
+
"inner_wall_acceleration",
|
|
71
|
+
"inner_wall_jerk",
|
|
72
|
+
"inner_wall_speed",
|
|
73
|
+
"internal_bridge_speed",
|
|
74
|
+
"internal_solid_infill_acceleration",
|
|
75
|
+
"internal_solid_infill_speed",
|
|
76
|
+
"long_retractions_when_ec",
|
|
77
|
+
"nozzle_flush_dataset",
|
|
78
|
+
"nozzle_type",
|
|
79
|
+
"nozzle_volume",
|
|
80
|
+
"outer_wall_acceleration",
|
|
81
|
+
"outer_wall_jerk",
|
|
82
|
+
"outer_wall_speed",
|
|
83
|
+
"overhang_1_4_speed",
|
|
84
|
+
"overhang_2_4_speed",
|
|
85
|
+
"overhang_3_4_speed",
|
|
86
|
+
"overhang_4_4_speed",
|
|
87
|
+
"retraction_distances_when_ec",
|
|
88
|
+
"slowdown_for_curled_perimeters",
|
|
89
|
+
"small_perimeter_speed",
|
|
90
|
+
"small_perimeter_threshold",
|
|
91
|
+
"sparse_infill_acceleration",
|
|
92
|
+
"sparse_infill_speed",
|
|
93
|
+
"support_interface_speed",
|
|
94
|
+
"support_speed",
|
|
95
|
+
"top_solid_infill_flow_ratio",
|
|
96
|
+
"top_surface_acceleration",
|
|
97
|
+
"top_surface_jerk",
|
|
98
|
+
"top_surface_speed",
|
|
99
|
+
"travel_acceleration",
|
|
100
|
+
"travel_jerk",
|
|
101
|
+
"travel_speed",
|
|
102
|
+
"travel_speed_z",
|
|
103
|
+
]);
|
|
104
|
+
const fail = (message) => { throw new Error(`Orca profile cache: ${message}`); };
|
|
105
|
+
class Reader {
|
|
106
|
+
constructor(bytes) {
|
|
107
|
+
this.bytes = bytes;
|
|
108
|
+
this.offset = 0;
|
|
109
|
+
}
|
|
110
|
+
take(size) {
|
|
111
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > this.bytes.length - this.offset)
|
|
112
|
+
fail('truncated or invalid length');
|
|
113
|
+
const result = this.bytes.subarray(this.offset, this.offset + size);
|
|
114
|
+
this.offset += size;
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
u8() { return this.take(1).readUInt8(); }
|
|
118
|
+
u16() { return this.take(2).readUInt16LE(); }
|
|
119
|
+
u32() { return this.take(4).readUInt32LE(); }
|
|
120
|
+
i32() { return this.take(4).readInt32LE(); }
|
|
121
|
+
u64() {
|
|
122
|
+
const value = this.take(8).readBigUInt64LE();
|
|
123
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER))
|
|
124
|
+
fail('length exceeds safe range');
|
|
125
|
+
return Number(value);
|
|
126
|
+
}
|
|
127
|
+
count() {
|
|
128
|
+
const n = this.u64();
|
|
129
|
+
if (n > MAX_ITEMS || n > this.bytes.length - this.offset)
|
|
130
|
+
fail('collection length out of bounds');
|
|
131
|
+
return n;
|
|
132
|
+
}
|
|
133
|
+
string() {
|
|
134
|
+
const raw = this.take(this.u64());
|
|
135
|
+
try {
|
|
136
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(raw);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return fail('invalid UTF-8');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
strings() { return this.array(() => this.string()); }
|
|
143
|
+
array(read) { return Array.from({ length: this.count() }, read); }
|
|
144
|
+
double() { return this.take(8).readDoubleLE(); }
|
|
145
|
+
}
|
|
146
|
+
function crc32(bytes) {
|
|
147
|
+
let crc = 0xffffffff;
|
|
148
|
+
for (const byte of bytes) {
|
|
149
|
+
crc ^= byte;
|
|
150
|
+
for (let bit = 0; bit < 8; bit++)
|
|
151
|
+
crc = (crc >>> 1) ^ ((crc & 1) ? 0xedb88320 : 0);
|
|
152
|
+
}
|
|
153
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
154
|
+
}
|
|
155
|
+
/** Read raw source diffs, not merged profiles: inheritance/includes still use the shared resolver. */
|
|
156
|
+
export function decodeOrcaBblCache(bytes) {
|
|
157
|
+
if (bytes.length < 20 || bytes.length > MAX_CACHE_BYTES)
|
|
158
|
+
fail('file size out of bounds');
|
|
159
|
+
const header = new Reader(bytes);
|
|
160
|
+
if (header.u32() !== 0x4f52435a)
|
|
161
|
+
fail('unknown magic');
|
|
162
|
+
if (header.u32() !== 2)
|
|
163
|
+
fail('unsupported format version');
|
|
164
|
+
if (header.u64() !== bytes.length - 20)
|
|
165
|
+
fail('body size does not match file');
|
|
166
|
+
const expectedCrc = header.u32();
|
|
167
|
+
const body = bytes.subarray(20);
|
|
168
|
+
if (crc32(body) !== expectedCrc)
|
|
169
|
+
fail('CRC checksum mismatch');
|
|
170
|
+
const r = new Reader(body);
|
|
171
|
+
if (r.u32() !== 2)
|
|
172
|
+
fail('unsupported body version');
|
|
173
|
+
if (r.string() !== 'BBL')
|
|
174
|
+
fail('expected BBL vendor');
|
|
175
|
+
const version = r.string();
|
|
176
|
+
if (!/^\d+(?:\.\d+){2,3}(?:[-+][\w.-]+)?$/.test(version))
|
|
177
|
+
fail('invalid vendor version');
|
|
178
|
+
const keys = r.strings();
|
|
179
|
+
const types = r.array(() => r.u16());
|
|
180
|
+
const enums = r.strings();
|
|
181
|
+
if (keys.length !== types.length || keys.length > 65535 || enums.length > 65535 || enums[0] !== '')
|
|
182
|
+
fail('invalid option dictionary');
|
|
183
|
+
if (new Set(keys).size !== keys.length || keys.some(key => !key || ['__proto__', 'constructor', 'prototype'].includes(key)))
|
|
184
|
+
fail('invalid option name');
|
|
185
|
+
// VendorMap is std::map<string, VendorProfile>, serialized by cereal in declaration order.
|
|
186
|
+
const modelsMetadata = [];
|
|
187
|
+
const vendors = r.count();
|
|
188
|
+
let ownVendor = false;
|
|
189
|
+
let vendorProfileVersion = "";
|
|
190
|
+
for (let i = 0; i < vendors; i++) {
|
|
191
|
+
const mapKey = r.string();
|
|
192
|
+
r.string(); // display name
|
|
193
|
+
const id = r.string();
|
|
194
|
+
const profileVersion = r.string();
|
|
195
|
+
if (mapKey === 'BBL' && id === 'BBL' && profileVersion) {
|
|
196
|
+
ownVendor = true;
|
|
197
|
+
vendorProfileVersion = profileVersion;
|
|
198
|
+
}
|
|
199
|
+
r.string();
|
|
200
|
+
r.string(); // update/changelog URLs
|
|
201
|
+
const models = r.count();
|
|
202
|
+
for (let j = 0; j < models; j++) {
|
|
203
|
+
const modelId = r.string();
|
|
204
|
+
const modelName = r.string();
|
|
205
|
+
r.string();
|
|
206
|
+
r.u8();
|
|
207
|
+
r.string();
|
|
208
|
+
r.strings();
|
|
209
|
+
r.strings();
|
|
210
|
+
const unsupportedBeds = r.strings();
|
|
211
|
+
r.string();
|
|
212
|
+
r.string();
|
|
213
|
+
const imageBedType = r.string();
|
|
214
|
+
for (let k = 0; k < 6; k++)
|
|
215
|
+
r.string();
|
|
216
|
+
if (mapKey === 'BBL' && modelName) {
|
|
217
|
+
modelsMetadata.push({ subPath: `machine/__models__/${encodeURIComponent(modelId)}.json`,
|
|
218
|
+
data: { name: modelName, type: 'machine_model',
|
|
219
|
+
...(imageBedType ? { image_bed_type: imageBedType } : {}),
|
|
220
|
+
...(unsupportedBeds.length ? { not_support_bed_type: unsupportedBeds.join(';') } : {}) } });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
r.strings();
|
|
224
|
+
r.strings(); // default filament/material sets
|
|
225
|
+
}
|
|
226
|
+
if (!ownVendor)
|
|
227
|
+
fail('cache does not carry its BBL vendor profile');
|
|
228
|
+
const number = (value, nullable = false) => {
|
|
229
|
+
if (nullable && Number.isNaN(value))
|
|
230
|
+
return 'nil';
|
|
231
|
+
if (!Number.isFinite(value))
|
|
232
|
+
return fail('non-finite numeric option');
|
|
233
|
+
return String(value);
|
|
234
|
+
};
|
|
235
|
+
const point = (dimension, separator) => Array.from({ length: dimension }, () => number(r.double())).join(separator);
|
|
236
|
+
const scalar = (type, vector) => {
|
|
237
|
+
switch (type) {
|
|
238
|
+
case 1: return number(r.double(), vector);
|
|
239
|
+
case 2: {
|
|
240
|
+
const n = r.i32();
|
|
241
|
+
return vector && n === 2147483647 ? 'nil' : String(n);
|
|
242
|
+
}
|
|
243
|
+
case 3: return r.string();
|
|
244
|
+
case 4: {
|
|
245
|
+
const n = r.double();
|
|
246
|
+
return Number.isNaN(n) && vector ? 'nil' : `${number(n)}%`;
|
|
247
|
+
}
|
|
248
|
+
case 5: {
|
|
249
|
+
const n = r.double();
|
|
250
|
+
const percent = r.u8();
|
|
251
|
+
if (percent > 1)
|
|
252
|
+
return fail('invalid percent flag');
|
|
253
|
+
return Number.isNaN(n) && vector ? 'nil' : number(n) + (percent ? '%' : '');
|
|
254
|
+
}
|
|
255
|
+
case 6: return point(2, vector ? 'x' : ',');
|
|
256
|
+
case 7: return point(3, ',');
|
|
257
|
+
case 8: {
|
|
258
|
+
const n = r.u8();
|
|
259
|
+
if (vector && n === 255)
|
|
260
|
+
return 'nil';
|
|
261
|
+
if (n > 1)
|
|
262
|
+
return fail('invalid boolean option');
|
|
263
|
+
return String(n);
|
|
264
|
+
}
|
|
265
|
+
default: return fail(`unsupported option type ${type}`);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
const option = (type) => {
|
|
269
|
+
if (type === 9 || type === VECTOR + 9) {
|
|
270
|
+
const count = r.u32();
|
|
271
|
+
if (count > MAX_ITEMS || (type === 9 && count !== 1))
|
|
272
|
+
fail('invalid enum count');
|
|
273
|
+
const values = Array.from({ length: count }, () => {
|
|
274
|
+
const index = r.u16();
|
|
275
|
+
if (index >= enums.length)
|
|
276
|
+
return fail('enum index out of bounds');
|
|
277
|
+
if (index !== 0)
|
|
278
|
+
return enums[index];
|
|
279
|
+
const value = r.i32();
|
|
280
|
+
if (type === VECTOR + 9 && value === 2147483647)
|
|
281
|
+
return 'nil';
|
|
282
|
+
return fail('unnamed enum cannot be exported safely');
|
|
283
|
+
});
|
|
284
|
+
return type === 9 ? values[0] : values;
|
|
285
|
+
}
|
|
286
|
+
if (type === VECTOR + 10)
|
|
287
|
+
return r.array(() => r.array(() => point(2, 'x')).join(','));
|
|
288
|
+
if (type === VECTOR + 11)
|
|
289
|
+
return r.array(() => r.array(() => String(r.i32())).join(','));
|
|
290
|
+
if (type & VECTOR)
|
|
291
|
+
return r.array(() => scalar(type - VECTOR, true));
|
|
292
|
+
return scalar(type, false);
|
|
293
|
+
};
|
|
294
|
+
const profiles = [...modelsMetadata];
|
|
295
|
+
const names = new Set(profiles.map(p => String(p.data.name)));
|
|
296
|
+
const paths = new Set(profiles.map(p => p.subPath.toLowerCase()));
|
|
297
|
+
for (const kind of ['process', 'filament', 'machine']) {
|
|
298
|
+
const count = r.u32();
|
|
299
|
+
if (count > MAX_ITEMS)
|
|
300
|
+
fail('preset count out of bounds');
|
|
301
|
+
for (let i = 0; i < count; i++) {
|
|
302
|
+
const name = r.string();
|
|
303
|
+
const subPath = r.string();
|
|
304
|
+
if (!name || names.has(name))
|
|
305
|
+
fail('duplicate/empty preset name');
|
|
306
|
+
const normalized = subPath.replace(/\\/g, '/');
|
|
307
|
+
if (!normalized.startsWith(`${kind}/`) || !normalized.endsWith('.json') || normalized.split('/').some(p => !p || p === '.' || p === '..') || normalized.includes(':') || paths.has(normalized.toLowerCase()))
|
|
308
|
+
fail('invalid or colliding preset path');
|
|
309
|
+
names.add(name);
|
|
310
|
+
paths.add(normalized.toLowerCase());
|
|
311
|
+
const data = Object.create(null);
|
|
312
|
+
const configCount = r.u32();
|
|
313
|
+
const seen = new Set();
|
|
314
|
+
if (configCount > keys.length)
|
|
315
|
+
fail('option count out of bounds');
|
|
316
|
+
for (let j = 0; j < configCount; j++) {
|
|
317
|
+
const key = r.u16();
|
|
318
|
+
if (key >= keys.length || seen.has(key))
|
|
319
|
+
fail('invalid/duplicate option index');
|
|
320
|
+
seen.add(key);
|
|
321
|
+
const value = option(types[key]);
|
|
322
|
+
if (types[key] !== VECTOR + 3 && Array.isArray(value) && value.includes('nil') && !NULLABLE_KEYS.has(keys[key]))
|
|
323
|
+
fail(`unexpected nil for ${keys[key]}`);
|
|
324
|
+
data[keys[key]] = value;
|
|
325
|
+
}
|
|
326
|
+
const inherits = r.string();
|
|
327
|
+
const includes = r.strings();
|
|
328
|
+
const description = r.string();
|
|
329
|
+
const instantiation = r.string();
|
|
330
|
+
const settingId = r.string();
|
|
331
|
+
const filamentId = r.string();
|
|
332
|
+
const renamed = r.strings();
|
|
333
|
+
Object.assign(data, { name, type: kind, from: 'system', version: vendorProfileVersion, instantiation, inherits });
|
|
334
|
+
if (includes.length)
|
|
335
|
+
data.include = includes;
|
|
336
|
+
if (description)
|
|
337
|
+
data.description = description;
|
|
338
|
+
if (settingId)
|
|
339
|
+
data.setting_id = settingId;
|
|
340
|
+
if (filamentId)
|
|
341
|
+
data.filament_id = filamentId;
|
|
342
|
+
if (renamed.length)
|
|
343
|
+
data.renamed_from = renamed.map(value => JSON.stringify(value)).join(';');
|
|
344
|
+
profiles.push({ subPath: normalized, data });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (r.u64() !== 0)
|
|
348
|
+
fail('vendor cache records profile parse errors');
|
|
349
|
+
if (r.offset !== body.length)
|
|
350
|
+
fail('unexpected trailing cache data');
|
|
351
|
+
return profiles;
|
|
352
|
+
}
|
|
353
|
+
/** Expand only the selected installation's cache in the owning server's private temp directory. */
|
|
354
|
+
export async function materializeOrcaProfilesRoot(profilesRoot, tempDir) {
|
|
355
|
+
if (profileJsonFiles(path.join(profilesRoot, 'BBL', 'machine')).length)
|
|
356
|
+
return profilesRoot;
|
|
357
|
+
const cachePath = path.join(profilesRoot, 'BBL.opc');
|
|
358
|
+
let stat;
|
|
359
|
+
try {
|
|
360
|
+
stat = await fs.stat(cachePath);
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
if (error.code === 'ENOENT')
|
|
364
|
+
return profilesRoot;
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
if (stat.size > MAX_CACHE_BYTES)
|
|
368
|
+
fail('file size out of bounds');
|
|
369
|
+
let cli;
|
|
370
|
+
try {
|
|
371
|
+
cli = await fs.readFile(path.join(profilesRoot, 'BBL', 'cli_config.json'));
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return fail('selected installation is missing BBL/cli_config.json; update Orca or supply its exact matching profile tree');
|
|
375
|
+
}
|
|
376
|
+
const parsed = JSON.parse(cli.toString('utf8'));
|
|
377
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
378
|
+
fail('invalid BBL/cli_config.json');
|
|
379
|
+
const cache = await fs.readFile(cachePath);
|
|
380
|
+
const profiles = decodeOrcaBblCache(cache);
|
|
381
|
+
const digest = createHash('sha256').update(cache).update(cli).digest('hex');
|
|
382
|
+
const output = await fs.mkdtemp(path.join(tempDir, `orca-profiles-${digest.slice(0, 16)}-`));
|
|
383
|
+
try {
|
|
384
|
+
await fs.mkdir(path.join(output, 'BBL'), { recursive: true });
|
|
385
|
+
await fs.writeFile(path.join(output, 'BBL', 'cli_config.json'), cli, { flag: 'wx' });
|
|
386
|
+
await fs.writeFile(path.join(output, 'BBL.opc'), cache, { flag: 'wx' });
|
|
387
|
+
for (const profile of profiles) {
|
|
388
|
+
const file = path.join(output, 'BBL', profile.subPath);
|
|
389
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
390
|
+
await fs.writeFile(file, JSON.stringify(profile.data), { flag: 'wx' });
|
|
391
|
+
}
|
|
392
|
+
return output;
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
await fs.rm(output, { recursive: true, force: true });
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/** Orca v2 cache builds resolve User presets by loaded file identity, not just inherits. */
|
|
400
|
+
export async function stageOrcaCliProfiles(profilesRoot, tempDir, bundle) {
|
|
401
|
+
const settings = bundle.settingsArg?.split(';') ?? [];
|
|
402
|
+
if (settings.length !== 2)
|
|
403
|
+
fail('complete machine and process settings are required');
|
|
404
|
+
const root = await fs.mkdtemp(path.join(tempDir, 'orca-cli-'));
|
|
405
|
+
try {
|
|
406
|
+
await fs.mkdir(path.join(root, 'system'));
|
|
407
|
+
await fs.copyFile(path.join(profilesRoot, 'BBL.opc'), path.join(root, 'system', 'BBL.opc'));
|
|
408
|
+
const profiles = decodeOrcaBblCache(await fs.readFile(path.join(profilesRoot, 'BBL.opc')));
|
|
409
|
+
const parents = new Set(profiles.map(p => `${p.data.type}:${p.data.name}`));
|
|
410
|
+
const version = profiles.find(p => p.data.type === 'machine')?.data.version;
|
|
411
|
+
if (typeof version !== 'string')
|
|
412
|
+
fail('missing vendor profile version');
|
|
413
|
+
const output = [];
|
|
414
|
+
const colours = [];
|
|
415
|
+
const extras = [];
|
|
416
|
+
for (const [index, file] of [...settings, ...bundle.filamentPaths].entries()) {
|
|
417
|
+
const kind = index === 0 ? 'machine' : index === 1 ? 'process' : 'filament';
|
|
418
|
+
const data = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
419
|
+
if (kind === 'filament') {
|
|
420
|
+
const colour = Array.isArray(data.filament_colour) ? data.filament_colour[0] : undefined;
|
|
421
|
+
if (typeof colour !== 'string' || !/^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/i.test(colour))
|
|
422
|
+
fail('missing positional filament colour');
|
|
423
|
+
colours.push(colour);
|
|
424
|
+
}
|
|
425
|
+
if (kind === 'process') {
|
|
426
|
+
if (typeof data.curr_bed_type === 'string')
|
|
427
|
+
extras.push('--curr-bed-type', data.curr_bed_type);
|
|
428
|
+
for (const key of ['wipe_tower_x', 'wipe_tower_y']) {
|
|
429
|
+
if (data[key] === undefined)
|
|
430
|
+
continue;
|
|
431
|
+
const values = Array.isArray(data[key]) ? data[key] : [data[key]];
|
|
432
|
+
if (!values.length || values.some(v => !Number.isFinite(Number(v))))
|
|
433
|
+
fail('invalid saved tower position');
|
|
434
|
+
extras.push(`--${key.replace(/_/g, '-')}`, values.join(','));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// The selected machine's system identity remains the validated cache leaf.
|
|
438
|
+
// A fully resolved custom process/filament may be a standalone User root.
|
|
439
|
+
if (!parents.has(`${kind}:${data.inherits}`)) {
|
|
440
|
+
if (kind === 'machine')
|
|
441
|
+
fail('selected machine parent is absent from the exact cache');
|
|
442
|
+
delete data.inherits;
|
|
443
|
+
}
|
|
444
|
+
const digest = createHash('sha256').update(JSON.stringify(data)).digest('hex').slice(0, 16);
|
|
445
|
+
const name = `mcp_${kind}_${index}_${digest}`;
|
|
446
|
+
Object.assign(data, { name, from: 'User', version });
|
|
447
|
+
const directory = path.join(root, 'user', 'default', kind);
|
|
448
|
+
await fs.mkdir(directory, { recursive: true });
|
|
449
|
+
const staged = path.join(directory, `${name}.json`);
|
|
450
|
+
await fs.writeFile(staged, JSON.stringify(data), { flag: 'wx' });
|
|
451
|
+
output.push(staged);
|
|
452
|
+
}
|
|
453
|
+
return { settingsArg: output.slice(0, 2).join(';'), filamentPaths: output.slice(2),
|
|
454
|
+
extraArgs: ['--datadir', root, ...extras, '--filament-colour', colours.join(';')], dataDir: root };
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
@@ -674,7 +674,7 @@ export function detectProfilesRoot(slicerPath, slicerType = 'bambustudio') {
|
|
|
674
674
|
path.resolve(bin, '..', 'share', name, 'resources', 'profiles'),
|
|
675
675
|
]),
|
|
676
676
|
];
|
|
677
|
-
const found = candidates.find(root => existsSync(path.join(root, 'BBL', 'machine')));
|
|
677
|
+
const found = candidates.find(root => (existsSync(path.join(root, 'BBL', 'machine')) || existsSync(path.join(root, 'BBL.opc'))));
|
|
678
678
|
// Do not select another installation if this executable has no profile tree.
|
|
679
679
|
// flattenForCli reports the missing tree and asks for BAMBU_PROFILES_ROOT.
|
|
680
680
|
return found ?? candidates[0];
|
|
@@ -114,6 +114,14 @@ export declare class STLManipulator extends EventEmitter {
|
|
|
114
114
|
* Scale an STL model uniformly or along specific axes
|
|
115
115
|
*/
|
|
116
116
|
scaleSTL(stlFilePath: string, scaleFactors: number | [number, number, number], progressCallback?: ProgressCallback): Promise<string>;
|
|
117
|
+
/**
|
|
118
|
+
* Apply slice_stl's scale and rotations to a copy of the mesh as OrcaSlicer's
|
|
119
|
+
* CLI would, about the model's centre. Orca adds each --rotate* to the
|
|
120
|
+
* object's Euler angles (ModelVolume::rotate), so the result rotates about X,
|
|
121
|
+
* then Y, then Z whatever the argument order, after scaling. The slicer then
|
|
122
|
+
* places the result on the plate.
|
|
123
|
+
*/
|
|
124
|
+
private transformMeshForCli;
|
|
117
125
|
/**
|
|
118
126
|
* Rotate an STL model around specific axes
|
|
119
127
|
*/
|