bambu-printer-mcp 1.1.32 → 1.1.33
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 +2 -0
- 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.js +29 -9
- package/package.json +1 -1
- package/src/slicer/orca-profile-cache.ts +379 -0
- package/src/slicer/profile-flatten.ts +1 -1
- package/src/stl/stl-manipulator.ts +30 -9
package/CONTRIBUTORS.md
CHANGED
|
@@ -6,6 +6,8 @@ Thank you to everyone who builds, tests, reports problems, and shares real print
|
|
|
6
6
|
|
|
7
7
|
| Contributor | Contribution |
|
|
8
8
|
| --- | --- |
|
|
9
|
+
| [bwees](https://github.com/bwees) | Reports and fixes missing non-preset vendor data in [OrcaSlicer #16700](https://github.com/OrcaSlicer/OrcaSlicer/issues/16700) and [#16706](https://github.com/OrcaSlicer/OrcaSlicer/pull/16706), restoring the authoritative BBL CLI matrix required by cache-based MCP slicing. |
|
|
10
|
+
| [ExPikaPaka](https://github.com/ExPikaPaka), [raistlin7447](https://github.com/raistlin7447), [HanifKoh](https://github.com/HanifKoh), and [SoftFever](https://github.com/SoftFever) | Provide and maintain OrcaSlicer's published preset-cache format, source-profile inheritance and include handling used as the contract for this independent cache reader. Credit covers upstream source and format evidence; it does not claim they tested this MCP integration. |
|
|
9
11
|
| [mygrexit](https://github.com/mygrexit) | Contributes pinned x86-64 Linux X2D native setup, emergency routing and one-shot cleanup in [#56](https://github.com/DMontgomery40/bambu-printer-mcp/pull/56), checked Orca/time-lapse G-code forms in [#57](https://github.com/DMontgomery40/bambu-printer-mcp/pull/57), and model-specific hidden-HMS notices in [#58](https://github.com/DMontgomery40/bambu-printer-mcp/pull/58). Supplies regressions, review corrections, and contributor-reported Debian/X2D read-only, heater-off and print-start evidence in [#55](https://github.com/DMontgomery40/bambu-printer-mcp/issues/55); maintainer validation is offline and mocked. Also reports and fixes hanging Linux X2D stop/heater-off calls in [#61](https://github.com/DMontgomery40/bambu-printer-mcp/pull/61), with never-connecting transport and helper-exit regressions; the release adds protection against expired commands dispatching after reconnection. In [#64](https://github.com/DMontgomery40/bambu-printer-mcp/pull/64), contributes ready-card native job storage, fresh dispatch rechecks and tests, and reports Linux X2D firmware 01.01.01.00 printing to external storage with plug-in 02.08.02.54 and a Bambuddy thumbnail; maintainer verification uses source and mocked transports. |
|
|
10
12
|
| [maziggy](https://github.com/maziggy) | Proposes choosing ready external storage by default in [Bambu Studio #12146](https://github.com/bambulab/BambuStudio/pull/12146), a related upstream proposal supporting the storage choice in [#64](https://github.com/DMontgomery40/bambu-printer-mcp/pull/64). The upstream proposal remains unmerged; this credit does not claim it shipped in Bambu Studio. |
|
|
11
13
|
| [patrikmadoran-cyber](https://github.com/patrikmadoran-cyber) | Adds explicit Standard/High Flow nozzle selection across slicing and print auto-slicing in [#54](https://github.com/DMontgomery40/bambu-printer-mcp/pull/54), with unsupported-variant regressions and real Windows BambuStudio 02.06.00.51 H2S 0.6 slicing evidence. No physical print is claimed. |
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface CachedProfile {
|
|
2
|
+
subPath: string;
|
|
3
|
+
data: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
/** Read raw source diffs, not merged profiles: inheritance/includes still use the shared resolver. */
|
|
6
|
+
export declare function decodeOrcaBblCache(bytes: Buffer): CachedProfile[];
|
|
7
|
+
/** Expand only the selected installation's cache in the owning server's private temp directory. */
|
|
8
|
+
export declare function materializeOrcaProfilesRoot(profilesRoot: string, tempDir: string): Promise<string>;
|
|
9
|
+
interface CliProfiles {
|
|
10
|
+
settingsArg?: string;
|
|
11
|
+
filamentPaths: string[];
|
|
12
|
+
}
|
|
13
|
+
/** Orca v2 cache builds resolve User presets by loaded file identity, not just inherits. */
|
|
14
|
+
export declare function stageOrcaCliProfiles(profilesRoot: string, tempDir: string, bundle: CliProfiles): Promise<CliProfiles & {
|
|
15
|
+
extraArgs: string[];
|
|
16
|
+
dataDir: string;
|
|
17
|
+
}>;
|
|
18
|
+
export {};
|
|
@@ -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];
|
|
@@ -11,6 +11,7 @@ import * as crypto from 'crypto';
|
|
|
11
11
|
import { execFile } from 'child_process';
|
|
12
12
|
import { flattenForCli, detectProfilesRoot, resolveBblMachineProfile } from '../slicer/profile-flatten.js';
|
|
13
13
|
import { profileJsonFiles } from '../slicer/profile-files.js';
|
|
14
|
+
import { materializeOrcaProfilesRoot, stageOrcaCliProfiles } from '../slicer/orca-profile-cache.js';
|
|
14
15
|
import { sliceWithOrcaApi, validateRemoteSliceOptions } from '../slicer/remote-slicer.js';
|
|
15
16
|
const readFileAsync = promisify(fs.readFile);
|
|
16
17
|
const writeFileAsync = promisify(fs.writeFile);
|
|
@@ -184,7 +185,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
184
185
|
}
|
|
185
186
|
return value;
|
|
186
187
|
}
|
|
187
|
-
sanitizeProcessForOrca(processConfig, printerPreset) {
|
|
188
|
+
sanitizeProcessForOrca(processConfig, printerPreset, preserveExtrusionMode = false) {
|
|
188
189
|
const sanitized = { ...processConfig };
|
|
189
190
|
sanitized.type = 'process';
|
|
190
191
|
if (!sanitized.from || sanitized.from === 'project') {
|
|
@@ -200,7 +201,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
200
201
|
else if (sanitized.prime_tower_brim_width !== undefined) {
|
|
201
202
|
sanitized.prime_tower_brim_width = String(sanitized.prime_tower_brim_width);
|
|
202
203
|
}
|
|
203
|
-
return this.useAbsoluteExtrusionForOrca(sanitized);
|
|
204
|
+
return preserveExtrusionMode ? sanitized : this.useAbsoluteExtrusionForOrca(sanitized);
|
|
204
205
|
}
|
|
205
206
|
/** Orca slices with absolute E distances; per-layer `G92 E0` resets would break them. */
|
|
206
207
|
useAbsoluteExtrusionForOrca(config) {
|
|
@@ -218,7 +219,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
218
219
|
fs.writeFileSync(outPath, serialized);
|
|
219
220
|
return outPath;
|
|
220
221
|
}
|
|
221
|
-
async resolveBambuLikeSettingsBundle(outputBase, slicerType, slicerProfile, printerPreset, bambuOptions, activeProfilesRoot) {
|
|
222
|
+
async resolveBambuLikeSettingsBundle(outputBase, slicerType, slicerProfile, printerPreset, bambuOptions, activeProfilesRoot, preserveOrcaExtrusionMode = false) {
|
|
222
223
|
const roots = activeProfilesRoot
|
|
223
224
|
? [path.join(activeProfilesRoot, 'BBL')]
|
|
224
225
|
: this.getAvailableProfileRoots();
|
|
@@ -279,7 +280,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
279
280
|
type: 'process',
|
|
280
281
|
};
|
|
281
282
|
processPath = this.writeTempJson(outputBase, slicerType === 'orcaslicer' ? 'process_orca' : 'process', slicerType === 'orcaslicer'
|
|
282
|
-
? this.sanitizeProcessForOrca(mergedProcess, printerPreset)
|
|
283
|
+
? this.sanitizeProcessForOrca(mergedProcess, printerPreset, preserveOrcaExtrusionMode)
|
|
283
284
|
: mergedProcess);
|
|
284
285
|
const defaultFilamentProfiles = !bambuOptions?.loadFilaments && Array.isArray(parsedProfile.default_filament_profile)
|
|
285
286
|
? parsedProfile.default_filament_profile
|
|
@@ -317,7 +318,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
317
318
|
if (defaultProcessPath) {
|
|
318
319
|
processPath =
|
|
319
320
|
slicerType === 'orcaslicer'
|
|
320
|
-
? this.writeTempJson(outputBase, 'process_default_orca', this.sanitizeProcessForOrca(this.readJsonFile(defaultProcessPath), printerPreset))
|
|
321
|
+
? this.writeTempJson(outputBase, 'process_default_orca', this.sanitizeProcessForOrca(this.readJsonFile(defaultProcessPath), printerPreset, preserveOrcaExtrusionMode))
|
|
321
322
|
: defaultProcessPath;
|
|
322
323
|
}
|
|
323
324
|
}
|
|
@@ -1240,6 +1241,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1240
1241
|
const outputFileName = path.basename(stlFilePath, '.stl') + '.gcode';
|
|
1241
1242
|
let outputFilePath = path.join(this.tempDir, outputFileName);
|
|
1242
1243
|
let args = [];
|
|
1244
|
+
const orcaTemporaryDirectories = [];
|
|
1243
1245
|
try {
|
|
1244
1246
|
if (progressCallback)
|
|
1245
1247
|
progressCallback(0, `Starting slicing with ${slicerType}...`);
|
|
@@ -1291,8 +1293,13 @@ export class STLManipulator extends EventEmitter {
|
|
|
1291
1293
|
const profileSlicerType = remote ? 'orcaslicer' : slicerType;
|
|
1292
1294
|
const bambuOutputPath = path.join(this.tempDir, outputBase + (remote ? `_${operationId}` : '') + '_sliced.3mf');
|
|
1293
1295
|
const outputDir = path.dirname(bambuOutputPath);
|
|
1294
|
-
const
|
|
1295
|
-
const
|
|
1296
|
+
const selectedProfilesRoot = detectProfilesRoot(remote ? undefined : slicerPath, profileSlicerType);
|
|
1297
|
+
const profilesRoot = !remote && profileSlicerType === 'orcaslicer'
|
|
1298
|
+
? await materializeOrcaProfilesRoot(selectedProfilesRoot, this.tempDir)
|
|
1299
|
+
: selectedProfilesRoot;
|
|
1300
|
+
if (profilesRoot !== selectedProfilesRoot)
|
|
1301
|
+
orcaTemporaryDirectories.push(profilesRoot);
|
|
1302
|
+
const rawBundle = await this.resolveBambuLikeSettingsBundle(outputBase, profileSlicerType, slicerProfile, printerPreset, bambuOptions, profilesRoot, profileSlicerType === 'orcaslicer' && profilesRoot !== selectedProfilesRoot);
|
|
1296
1303
|
// Resolve every BBL dependency before launching the slicer.
|
|
1297
1304
|
const settingsBundle = await this.maybeFlattenBundle(await this.expandProjectFilaments(stlFilePath, rawBundle), bambuOptions, profilesRoot);
|
|
1298
1305
|
// The machine preset assumes the stock hotend; record the installed one so
|
|
@@ -1312,14 +1319,21 @@ export class STLManipulator extends EventEmitter {
|
|
|
1312
1319
|
// absolute-extrusion normalization after resolving those ancestors.
|
|
1313
1320
|
// The machine owns the extrusion mode and layer G-code once process
|
|
1314
1321
|
// files lose machine keys, so normalize it there as well.
|
|
1315
|
-
if (profileSlicerType === 'orcaslicer' && settingsBundle.settingsArg) {
|
|
1322
|
+
if (profileSlicerType === 'orcaslicer' && settingsBundle.settingsArg && profilesRoot === selectedProfilesRoot) {
|
|
1316
1323
|
const [machinePath, processPath] = settingsBundle.settingsArg.split(';');
|
|
1317
1324
|
settingsBundle.settingsArg = [
|
|
1318
1325
|
this.writeTempJson(outputBase, 'machine_orca_resolved', this.useAbsoluteExtrusionForOrca(this.readJsonFile(machinePath))),
|
|
1319
1326
|
this.writeTempJson(outputBase, 'process_orca_resolved', this.sanitizeProcessForOrca(this.readJsonFile(processPath), printerPreset)),
|
|
1320
1327
|
].join(';');
|
|
1321
1328
|
}
|
|
1322
|
-
|
|
1329
|
+
let cliBundle = this.isolateMachineSettings(settingsBundle);
|
|
1330
|
+
let orcaExtraArgs = [];
|
|
1331
|
+
if (!remote && profileSlicerType === 'orcaslicer' && profilesRoot !== selectedProfilesRoot) {
|
|
1332
|
+
const staged = await stageOrcaCliProfiles(profilesRoot, this.tempDir, cliBundle);
|
|
1333
|
+
cliBundle = { ...cliBundle, settingsArg: staged.settingsArg, filamentPaths: staged.filamentPaths };
|
|
1334
|
+
orcaExtraArgs = staged.extraArgs;
|
|
1335
|
+
orcaTemporaryDirectories.push(staged.dataDir);
|
|
1336
|
+
}
|
|
1323
1337
|
if (remote) {
|
|
1324
1338
|
const [machinePath, processPath] = cliBundle.settingsArg?.split(';') ?? [];
|
|
1325
1339
|
if (!machinePath || !processPath)
|
|
@@ -1332,6 +1346,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1332
1346
|
break;
|
|
1333
1347
|
}
|
|
1334
1348
|
args = [
|
|
1349
|
+
...orcaExtraArgs,
|
|
1335
1350
|
'--slice', String(bambuOptions?.slicePlate ?? 0),
|
|
1336
1351
|
'--outputdir', outputDir,
|
|
1337
1352
|
'--export-3mf', path.basename(bambuOutputPath),
|
|
@@ -1455,6 +1470,11 @@ export class STLManipulator extends EventEmitter {
|
|
|
1455
1470
|
}
|
|
1456
1471
|
finally {
|
|
1457
1472
|
this.activeOperations.delete(operationId);
|
|
1473
|
+
for (const directory of orcaTemporaryDirectories.reverse()) {
|
|
1474
|
+
await fs.promises.rm(directory, { recursive: true, force: true }).catch(error => {
|
|
1475
|
+
console.error('Could not remove temporary Orca profiles:', error);
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1458
1478
|
}
|
|
1459
1479
|
}
|
|
1460
1480
|
/**
|
package/package.json
CHANGED
|
@@ -0,0 +1,379 @@
|
|
|
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
|
+
|
|
6
|
+
// OrcaSlicer 14da40e3a7b82aa8fc88593c2cf880e66560028f:
|
|
7
|
+
// PresetCacheFormat.{hpp,cpp}, Config.hpp, Config.cpp::save_to_json,
|
|
8
|
+
// Preset.hpp::VendorProfile and Semver.hpp. Only the named v2 format is read;
|
|
9
|
+
// the old ordinal-based cache must never be interpreted as this format.
|
|
10
|
+
const MAX_CACHE_BYTES = 64 * 1024 * 1024;
|
|
11
|
+
const MAX_ITEMS = 100_000;
|
|
12
|
+
const VECTOR = 0x4000;
|
|
13
|
+
// Nullable declarations in the same pinned PrintConfig.cpp; unknown nil values fail closed.
|
|
14
|
+
const NULLABLE_KEYS = new Set([
|
|
15
|
+
"bridge_acceleration",
|
|
16
|
+
"bridge_speed",
|
|
17
|
+
"default_acceleration",
|
|
18
|
+
"default_jerk",
|
|
19
|
+
"default_junction_deviation",
|
|
20
|
+
"deretract_speed_extruder_change",
|
|
21
|
+
"enable_overhang_speed",
|
|
22
|
+
"extruder_max_nozzle_count",
|
|
23
|
+
"filament_adaptive_volumetric_speed",
|
|
24
|
+
"filament_cooling_before_tower",
|
|
25
|
+
"filament_deretraction_speed",
|
|
26
|
+
"filament_flow_ratio",
|
|
27
|
+
"filament_flush_temp",
|
|
28
|
+
"filament_flush_temp_fast",
|
|
29
|
+
"filament_flush_volumetric_speed",
|
|
30
|
+
"filament_ironing_flow",
|
|
31
|
+
"filament_ironing_inset",
|
|
32
|
+
"filament_ironing_spacing",
|
|
33
|
+
"filament_ironing_speed",
|
|
34
|
+
"filament_long_retractions_when_cut",
|
|
35
|
+
"filament_pre_cooling_temperature",
|
|
36
|
+
"filament_pre_cooling_temperature_nc",
|
|
37
|
+
"filament_preheat_temperature_delta",
|
|
38
|
+
"filament_ramming_travel_time",
|
|
39
|
+
"filament_ramming_travel_time_nc",
|
|
40
|
+
"filament_ramming_volumetric_speed",
|
|
41
|
+
"filament_ramming_volumetric_speed_nc",
|
|
42
|
+
"filament_retract_after_wipe",
|
|
43
|
+
"filament_retract_before_wipe",
|
|
44
|
+
"filament_retract_length_nc",
|
|
45
|
+
"filament_retract_length_toolchange",
|
|
46
|
+
"filament_retract_lift_above",
|
|
47
|
+
"filament_retract_lift_below",
|
|
48
|
+
"filament_retract_lift_enforce",
|
|
49
|
+
"filament_retract_restart_extra",
|
|
50
|
+
"filament_retract_restart_extra_toolchange",
|
|
51
|
+
"filament_retract_when_changing_layer",
|
|
52
|
+
"filament_retraction_distances_when_cut",
|
|
53
|
+
"filament_retraction_length",
|
|
54
|
+
"filament_retraction_minimum_travel",
|
|
55
|
+
"filament_retraction_speed",
|
|
56
|
+
"filament_wipe",
|
|
57
|
+
"filament_wipe_distance",
|
|
58
|
+
"filament_z_hop",
|
|
59
|
+
"filament_z_hop_types",
|
|
60
|
+
"gap_infill_speed",
|
|
61
|
+
"hotend_cooling_rate",
|
|
62
|
+
"hotend_heating_rate",
|
|
63
|
+
"infill_jerk",
|
|
64
|
+
"initial_layer_acceleration",
|
|
65
|
+
"initial_layer_infill_speed",
|
|
66
|
+
"initial_layer_jerk",
|
|
67
|
+
"initial_layer_speed",
|
|
68
|
+
"initial_layer_travel_acceleration",
|
|
69
|
+
"initial_layer_travel_jerk",
|
|
70
|
+
"initial_layer_travel_speed",
|
|
71
|
+
"inner_wall_acceleration",
|
|
72
|
+
"inner_wall_jerk",
|
|
73
|
+
"inner_wall_speed",
|
|
74
|
+
"internal_bridge_speed",
|
|
75
|
+
"internal_solid_infill_acceleration",
|
|
76
|
+
"internal_solid_infill_speed",
|
|
77
|
+
"long_retractions_when_ec",
|
|
78
|
+
"nozzle_flush_dataset",
|
|
79
|
+
"nozzle_type",
|
|
80
|
+
"nozzle_volume",
|
|
81
|
+
"outer_wall_acceleration",
|
|
82
|
+
"outer_wall_jerk",
|
|
83
|
+
"outer_wall_speed",
|
|
84
|
+
"overhang_1_4_speed",
|
|
85
|
+
"overhang_2_4_speed",
|
|
86
|
+
"overhang_3_4_speed",
|
|
87
|
+
"overhang_4_4_speed",
|
|
88
|
+
"retraction_distances_when_ec",
|
|
89
|
+
"slowdown_for_curled_perimeters",
|
|
90
|
+
"small_perimeter_speed",
|
|
91
|
+
"small_perimeter_threshold",
|
|
92
|
+
"sparse_infill_acceleration",
|
|
93
|
+
"sparse_infill_speed",
|
|
94
|
+
"support_interface_speed",
|
|
95
|
+
"support_speed",
|
|
96
|
+
"top_solid_infill_flow_ratio",
|
|
97
|
+
"top_surface_acceleration",
|
|
98
|
+
"top_surface_jerk",
|
|
99
|
+
"top_surface_speed",
|
|
100
|
+
"travel_acceleration",
|
|
101
|
+
"travel_jerk",
|
|
102
|
+
"travel_speed",
|
|
103
|
+
"travel_speed_z",
|
|
104
|
+
]);
|
|
105
|
+
const fail = (message: string): never => { throw new Error(`Orca profile cache: ${message}`); };
|
|
106
|
+
|
|
107
|
+
class Reader {
|
|
108
|
+
offset = 0;
|
|
109
|
+
constructor(readonly bytes: Buffer) {}
|
|
110
|
+
take(size: number): Buffer {
|
|
111
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > this.bytes.length - this.offset) fail('truncated or invalid length');
|
|
112
|
+
const result = this.bytes.subarray(this.offset, this.offset + size);
|
|
113
|
+
this.offset += size;
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
u8(): number { return this.take(1).readUInt8(); }
|
|
117
|
+
u16(): number { return this.take(2).readUInt16LE(); }
|
|
118
|
+
u32(): number { return this.take(4).readUInt32LE(); }
|
|
119
|
+
i32(): number { return this.take(4).readInt32LE(); }
|
|
120
|
+
u64(): number {
|
|
121
|
+
const value = this.take(8).readBigUInt64LE();
|
|
122
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) fail('length exceeds safe range');
|
|
123
|
+
return Number(value);
|
|
124
|
+
}
|
|
125
|
+
count(): number {
|
|
126
|
+
const n = this.u64();
|
|
127
|
+
if (n > MAX_ITEMS || n > this.bytes.length - this.offset) fail('collection length out of bounds');
|
|
128
|
+
return n;
|
|
129
|
+
}
|
|
130
|
+
string(): string {
|
|
131
|
+
const raw = this.take(this.u64());
|
|
132
|
+
try { return new TextDecoder('utf-8', { fatal: true }).decode(raw); }
|
|
133
|
+
catch { return fail('invalid UTF-8'); }
|
|
134
|
+
}
|
|
135
|
+
strings(): string[] { return this.array(() => this.string()); }
|
|
136
|
+
array<T>(read: () => T): T[] { return Array.from({ length: this.count() }, read); }
|
|
137
|
+
double(): number { return this.take(8).readDoubleLE(); }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function crc32(bytes: Buffer): number {
|
|
141
|
+
let crc = 0xffffffff;
|
|
142
|
+
for (const byte of bytes) {
|
|
143
|
+
crc ^= byte;
|
|
144
|
+
for (let bit = 0; bit < 8; bit++) crc = (crc >>> 1) ^ ((crc & 1) ? 0xedb88320 : 0);
|
|
145
|
+
}
|
|
146
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface CachedProfile { subPath: string; data: Record<string, unknown>; }
|
|
150
|
+
|
|
151
|
+
/** Read raw source diffs, not merged profiles: inheritance/includes still use the shared resolver. */
|
|
152
|
+
export function decodeOrcaBblCache(bytes: Buffer): CachedProfile[] {
|
|
153
|
+
if (bytes.length < 20 || bytes.length > MAX_CACHE_BYTES) fail('file size out of bounds');
|
|
154
|
+
const header = new Reader(bytes);
|
|
155
|
+
if (header.u32() !== 0x4f52435a) fail('unknown magic');
|
|
156
|
+
if (header.u32() !== 2) fail('unsupported format version');
|
|
157
|
+
if (header.u64() !== bytes.length - 20) fail('body size does not match file');
|
|
158
|
+
const expectedCrc = header.u32();
|
|
159
|
+
const body = bytes.subarray(20);
|
|
160
|
+
if (crc32(body) !== expectedCrc) fail('CRC checksum mismatch');
|
|
161
|
+
const r = new Reader(body);
|
|
162
|
+
if (r.u32() !== 2) fail('unsupported body version');
|
|
163
|
+
if (r.string() !== 'BBL') fail('expected BBL vendor');
|
|
164
|
+
const version = r.string();
|
|
165
|
+
if (!/^\d+(?:\.\d+){2,3}(?:[-+][\w.-]+)?$/.test(version)) fail('invalid vendor version');
|
|
166
|
+
const keys = r.strings();
|
|
167
|
+
const types = r.array(() => r.u16());
|
|
168
|
+
const enums = r.strings();
|
|
169
|
+
if (keys.length !== types.length || keys.length > 65535 || enums.length > 65535 || enums[0] !== '') fail('invalid option dictionary');
|
|
170
|
+
if (new Set(keys).size !== keys.length || keys.some(key => !key || ['__proto__', 'constructor', 'prototype'].includes(key))) fail('invalid option name');
|
|
171
|
+
|
|
172
|
+
// VendorMap is std::map<string, VendorProfile>, serialized by cereal in declaration order.
|
|
173
|
+
const modelsMetadata: CachedProfile[] = [];
|
|
174
|
+
const vendors = r.count();
|
|
175
|
+
let ownVendor = false;
|
|
176
|
+
let vendorProfileVersion = "";
|
|
177
|
+
for (let i = 0; i < vendors; i++) {
|
|
178
|
+
const mapKey = r.string();
|
|
179
|
+
r.string(); // display name
|
|
180
|
+
const id = r.string();
|
|
181
|
+
const profileVersion = r.string();
|
|
182
|
+
if (mapKey === 'BBL' && id === 'BBL' && profileVersion) { ownVendor = true; vendorProfileVersion = profileVersion; }
|
|
183
|
+
r.string(); r.string(); // update/changelog URLs
|
|
184
|
+
const models = r.count();
|
|
185
|
+
for (let j = 0; j < models; j++) {
|
|
186
|
+
const modelId = r.string(); const modelName = r.string();
|
|
187
|
+
r.string(); r.u8(); r.string();
|
|
188
|
+
r.strings(); r.strings(); const unsupportedBeds = r.strings();
|
|
189
|
+
r.string(); r.string(); const imageBedType = r.string();
|
|
190
|
+
for (let k = 0; k < 6; k++) r.string();
|
|
191
|
+
if (mapKey === 'BBL' && modelName) {
|
|
192
|
+
modelsMetadata.push({ subPath: `machine/__models__/${encodeURIComponent(modelId)}.json`,
|
|
193
|
+
data: { name: modelName, type: 'machine_model',
|
|
194
|
+
...(imageBedType ? { image_bed_type: imageBedType } : {}),
|
|
195
|
+
...(unsupportedBeds.length ? { not_support_bed_type: unsupportedBeds.join(';') } : {}) } });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
r.strings(); r.strings(); // default filament/material sets
|
|
199
|
+
}
|
|
200
|
+
if (!ownVendor) fail('cache does not carry its BBL vendor profile');
|
|
201
|
+
|
|
202
|
+
const number = (value: number, nullable = false): string => {
|
|
203
|
+
if (nullable && Number.isNaN(value)) return 'nil';
|
|
204
|
+
if (!Number.isFinite(value)) return fail('non-finite numeric option');
|
|
205
|
+
return String(value);
|
|
206
|
+
};
|
|
207
|
+
const point = (dimension: number, separator: string): string =>
|
|
208
|
+
Array.from({ length: dimension }, () => number(r.double())).join(separator);
|
|
209
|
+
const scalar = (type: number, vector: boolean): string => {
|
|
210
|
+
switch (type) {
|
|
211
|
+
case 1: return number(r.double(), vector);
|
|
212
|
+
case 2: { const n = r.i32(); return vector && n === 2147483647 ? 'nil' : String(n); }
|
|
213
|
+
case 3: return r.string();
|
|
214
|
+
case 4: { const n = r.double(); return Number.isNaN(n) && vector ? 'nil' : `${number(n)}%`; }
|
|
215
|
+
case 5: {
|
|
216
|
+
const n = r.double(); const percent = r.u8();
|
|
217
|
+
if (percent > 1) return fail('invalid percent flag');
|
|
218
|
+
return Number.isNaN(n) && vector ? 'nil' : number(n) + (percent ? '%' : '');
|
|
219
|
+
}
|
|
220
|
+
case 6: return point(2, vector ? 'x' : ',');
|
|
221
|
+
case 7: return point(3, ',');
|
|
222
|
+
case 8: {
|
|
223
|
+
const n = r.u8();
|
|
224
|
+
if (vector && n === 255) return 'nil';
|
|
225
|
+
if (n > 1) return fail('invalid boolean option');
|
|
226
|
+
return String(n);
|
|
227
|
+
}
|
|
228
|
+
default: return fail(`unsupported option type ${type}`);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const option = (type: number): string | string[] => {
|
|
232
|
+
if (type === 9 || type === VECTOR + 9) {
|
|
233
|
+
const count = r.u32();
|
|
234
|
+
if (count > MAX_ITEMS || (type === 9 && count !== 1)) fail('invalid enum count');
|
|
235
|
+
const values = Array.from({ length: count }, () => {
|
|
236
|
+
const index = r.u16();
|
|
237
|
+
if (index >= enums.length) return fail('enum index out of bounds');
|
|
238
|
+
if (index !== 0) return enums[index];
|
|
239
|
+
const value = r.i32();
|
|
240
|
+
if (type === VECTOR + 9 && value === 2147483647) return 'nil';
|
|
241
|
+
return fail('unnamed enum cannot be exported safely');
|
|
242
|
+
});
|
|
243
|
+
return type === 9 ? values[0] : values;
|
|
244
|
+
}
|
|
245
|
+
if (type === VECTOR + 10) return r.array(() => r.array(() => point(2, 'x')).join(','));
|
|
246
|
+
if (type === VECTOR + 11) return r.array(() => r.array(() => String(r.i32())).join(','));
|
|
247
|
+
if (type & VECTOR) return r.array(() => scalar(type - VECTOR, true));
|
|
248
|
+
return scalar(type, false);
|
|
249
|
+
};
|
|
250
|
+
const profiles: CachedProfile[] = [...modelsMetadata];
|
|
251
|
+
const names = new Set(profiles.map(p => String(p.data.name))); const paths = new Set(profiles.map(p => p.subPath.toLowerCase()));
|
|
252
|
+
for (const kind of ['process', 'filament', 'machine']) {
|
|
253
|
+
const count = r.u32();
|
|
254
|
+
if (count > MAX_ITEMS) fail('preset count out of bounds');
|
|
255
|
+
for (let i = 0; i < count; i++) {
|
|
256
|
+
const name = r.string(); const subPath = r.string();
|
|
257
|
+
if (!name || names.has(name)) fail('duplicate/empty preset name');
|
|
258
|
+
const normalized = subPath.replace(/\\/g, '/');
|
|
259
|
+
if (!normalized.startsWith(`${kind}/`) || !normalized.endsWith('.json') || normalized.split('/').some(p => !p || p === '.' || p === '..') || normalized.includes(':') || paths.has(normalized.toLowerCase())) fail('invalid or colliding preset path');
|
|
260
|
+
names.add(name); paths.add(normalized.toLowerCase());
|
|
261
|
+
const data: Record<string, unknown> = Object.create(null);
|
|
262
|
+
const configCount = r.u32(); const seen = new Set<number>();
|
|
263
|
+
if (configCount > keys.length) fail('option count out of bounds');
|
|
264
|
+
for (let j = 0; j < configCount; j++) {
|
|
265
|
+
const key = r.u16();
|
|
266
|
+
if (key >= keys.length || seen.has(key)) fail('invalid/duplicate option index');
|
|
267
|
+
seen.add(key);
|
|
268
|
+
const value = option(types[key]);
|
|
269
|
+
if (types[key] !== VECTOR + 3 && Array.isArray(value) && value.includes('nil') && !NULLABLE_KEYS.has(keys[key])) fail(`unexpected nil for ${keys[key]}`);
|
|
270
|
+
data[keys[key]] = value;
|
|
271
|
+
}
|
|
272
|
+
const inherits = r.string(); const includes = r.strings();
|
|
273
|
+
const description = r.string(); const instantiation = r.string();
|
|
274
|
+
const settingId = r.string(); const filamentId = r.string(); const renamed = r.strings();
|
|
275
|
+
Object.assign(data, { name, type: kind, from: 'system', version: vendorProfileVersion, instantiation, inherits });
|
|
276
|
+
if (includes.length) data.include = includes;
|
|
277
|
+
if (description) data.description = description;
|
|
278
|
+
if (settingId) data.setting_id = settingId;
|
|
279
|
+
if (filamentId) data.filament_id = filamentId;
|
|
280
|
+
if (renamed.length) data.renamed_from = renamed.map(value => JSON.stringify(value)).join(';');
|
|
281
|
+
profiles.push({ subPath: normalized, data });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (r.u64() !== 0) fail('vendor cache records profile parse errors');
|
|
285
|
+
if (r.offset !== body.length) fail('unexpected trailing cache data');
|
|
286
|
+
return profiles;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Expand only the selected installation's cache in the owning server's private temp directory. */
|
|
290
|
+
export async function materializeOrcaProfilesRoot(profilesRoot: string, tempDir: string): Promise<string> {
|
|
291
|
+
if (profileJsonFiles(path.join(profilesRoot, 'BBL', 'machine')).length) return profilesRoot;
|
|
292
|
+
const cachePath = path.join(profilesRoot, 'BBL.opc');
|
|
293
|
+
let stat;
|
|
294
|
+
try { stat = await fs.stat(cachePath); }
|
|
295
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return profilesRoot; throw error; }
|
|
296
|
+
if (stat.size > MAX_CACHE_BYTES) fail('file size out of bounds');
|
|
297
|
+
let cli: Buffer;
|
|
298
|
+
try { cli = await fs.readFile(path.join(profilesRoot, 'BBL', 'cli_config.json')); }
|
|
299
|
+
catch { return fail('selected installation is missing BBL/cli_config.json; update Orca or supply its exact matching profile tree'); }
|
|
300
|
+
const parsed: unknown = JSON.parse(cli.toString('utf8'));
|
|
301
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) fail('invalid BBL/cli_config.json');
|
|
302
|
+
const cache = await fs.readFile(cachePath);
|
|
303
|
+
const profiles = decodeOrcaBblCache(cache);
|
|
304
|
+
const digest = createHash('sha256').update(cache).update(cli).digest('hex');
|
|
305
|
+
const output = await fs.mkdtemp(path.join(tempDir, `orca-profiles-${digest.slice(0, 16)}-`));
|
|
306
|
+
try {
|
|
307
|
+
await fs.mkdir(path.join(output, 'BBL'), { recursive: true });
|
|
308
|
+
await fs.writeFile(path.join(output, 'BBL', 'cli_config.json'), cli, { flag: 'wx' });
|
|
309
|
+
await fs.writeFile(path.join(output, 'BBL.opc'), cache, { flag: 'wx' });
|
|
310
|
+
for (const profile of profiles) {
|
|
311
|
+
const file = path.join(output, 'BBL', profile.subPath);
|
|
312
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
313
|
+
await fs.writeFile(file, JSON.stringify(profile.data), { flag: 'wx' });
|
|
314
|
+
}
|
|
315
|
+
return output;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
await fs.rm(output, { recursive: true, force: true });
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
interface CliProfiles { settingsArg?: string; filamentPaths: string[]; }
|
|
323
|
+
|
|
324
|
+
/** Orca v2 cache builds resolve User presets by loaded file identity, not just inherits. */
|
|
325
|
+
export async function stageOrcaCliProfiles(
|
|
326
|
+
profilesRoot: string, tempDir: string, bundle: CliProfiles
|
|
327
|
+
): Promise<CliProfiles & { extraArgs: string[]; dataDir: string }> {
|
|
328
|
+
const settings = bundle.settingsArg?.split(';') ?? [];
|
|
329
|
+
if (settings.length !== 2) fail('complete machine and process settings are required');
|
|
330
|
+
const root = await fs.mkdtemp(path.join(tempDir, 'orca-cli-'));
|
|
331
|
+
try {
|
|
332
|
+
await fs.mkdir(path.join(root, 'system'));
|
|
333
|
+
await fs.copyFile(path.join(profilesRoot, 'BBL.opc'), path.join(root, 'system', 'BBL.opc'));
|
|
334
|
+
const profiles = decodeOrcaBblCache(await fs.readFile(path.join(profilesRoot, 'BBL.opc')));
|
|
335
|
+
const parents = new Set(profiles.map(p => `${p.data.type}:${p.data.name}`));
|
|
336
|
+
const version = profiles.find(p => p.data.type === 'machine')?.data.version;
|
|
337
|
+
if (typeof version !== 'string') fail('missing vendor profile version');
|
|
338
|
+
const output: string[] = [];
|
|
339
|
+
const colours: string[] = [];
|
|
340
|
+
const extras: string[] = [];
|
|
341
|
+
for (const [index, file] of [...settings, ...bundle.filamentPaths].entries()) {
|
|
342
|
+
const kind = index === 0 ? 'machine' : index === 1 ? 'process' : 'filament';
|
|
343
|
+
const data = JSON.parse(await fs.readFile(file, 'utf8')) as Record<string, unknown>;
|
|
344
|
+
if (kind === 'filament') {
|
|
345
|
+
const colour = Array.isArray(data.filament_colour) ? data.filament_colour[0] : undefined;
|
|
346
|
+
if (typeof colour !== 'string' || !/^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/i.test(colour)) fail('missing positional filament colour');
|
|
347
|
+
colours.push(colour);
|
|
348
|
+
}
|
|
349
|
+
if (kind === 'process') {
|
|
350
|
+
if (typeof data.curr_bed_type === 'string') extras.push('--curr-bed-type', data.curr_bed_type);
|
|
351
|
+
for (const key of ['wipe_tower_x', 'wipe_tower_y']) {
|
|
352
|
+
if (data[key] === undefined) continue;
|
|
353
|
+
const values = Array.isArray(data[key]) ? data[key] as unknown[] : [data[key]];
|
|
354
|
+
if (!values.length || values.some(v => !Number.isFinite(Number(v)))) fail('invalid saved tower position');
|
|
355
|
+
extras.push(`--${key.replace(/_/g, '-')}`, values.join(','));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// The selected machine's system identity remains the validated cache leaf.
|
|
359
|
+
// A fully resolved custom process/filament may be a standalone User root.
|
|
360
|
+
if (!parents.has(`${kind}:${data.inherits}`)) {
|
|
361
|
+
if (kind === 'machine') fail('selected machine parent is absent from the exact cache');
|
|
362
|
+
delete data.inherits;
|
|
363
|
+
}
|
|
364
|
+
const digest = createHash('sha256').update(JSON.stringify(data)).digest('hex').slice(0, 16);
|
|
365
|
+
const name = `mcp_${kind}_${index}_${digest}`;
|
|
366
|
+
Object.assign(data, { name, from: 'User', version });
|
|
367
|
+
const directory = path.join(root, 'user', 'default', kind);
|
|
368
|
+
await fs.mkdir(directory, { recursive: true });
|
|
369
|
+
const staged = path.join(directory, `${name}.json`);
|
|
370
|
+
await fs.writeFile(staged, JSON.stringify(data), { flag: 'wx' });
|
|
371
|
+
output.push(staged);
|
|
372
|
+
}
|
|
373
|
+
return { settingsArg: output.slice(0, 2).join(';'), filamentPaths: output.slice(2),
|
|
374
|
+
extraArgs: ['--datadir', root, ...extras, '--filament-colour', colours.join(';')], dataDir: root };
|
|
375
|
+
} catch (error) {
|
|
376
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -836,7 +836,7 @@ export function detectProfilesRoot(slicerPath?: string, slicerType = 'bambustudi
|
|
|
836
836
|
path.resolve(bin, '..', 'share', name, 'resources', 'profiles'),
|
|
837
837
|
]),
|
|
838
838
|
];
|
|
839
|
-
const found = candidates.find(root => existsSync(path.join(root, 'BBL', 'machine')));
|
|
839
|
+
const found = candidates.find(root => (existsSync(path.join(root, 'BBL', 'machine')) || existsSync(path.join(root, 'BBL.opc'))));
|
|
840
840
|
// Do not select another installation if this executable has no profile tree.
|
|
841
841
|
// flattenForCli reports the missing tree and asks for BAMBU_PROFILES_ROOT.
|
|
842
842
|
return found ?? candidates[0];
|
|
@@ -11,6 +11,7 @@ import * as crypto from 'crypto';
|
|
|
11
11
|
import { execFile } from 'child_process';
|
|
12
12
|
import { flattenForCli, detectProfilesRoot, resolveBblMachineProfile } from '../slicer/profile-flatten.js';
|
|
13
13
|
import { profileJsonFiles } from '../slicer/profile-files.js';
|
|
14
|
+
import { materializeOrcaProfilesRoot, stageOrcaCliProfiles } from '../slicer/orca-profile-cache.js';
|
|
14
15
|
import { sliceWithOrcaApi, validateRemoteSliceOptions } from '../slicer/remote-slicer.js';
|
|
15
16
|
|
|
16
17
|
const readFileAsync = promisify(fs.readFile);
|
|
@@ -291,7 +292,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
291
292
|
return value;
|
|
292
293
|
}
|
|
293
294
|
|
|
294
|
-
private sanitizeProcessForOrca(processConfig: any, printerPreset?: string): any {
|
|
295
|
+
private sanitizeProcessForOrca(processConfig: any, printerPreset?: string, preserveExtrusionMode = false): any {
|
|
295
296
|
const sanitized = { ...processConfig };
|
|
296
297
|
|
|
297
298
|
sanitized.type = 'process';
|
|
@@ -312,7 +313,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
312
313
|
sanitized.prime_tower_brim_width = String(sanitized.prime_tower_brim_width);
|
|
313
314
|
}
|
|
314
315
|
|
|
315
|
-
return this.useAbsoluteExtrusionForOrca(sanitized);
|
|
316
|
+
return preserveExtrusionMode ? sanitized : this.useAbsoluteExtrusionForOrca(sanitized);
|
|
316
317
|
}
|
|
317
318
|
|
|
318
319
|
/** Orca slices with absolute E distances; per-layer `G92 E0` resets would break them. */
|
|
@@ -344,7 +345,8 @@ export class STLManipulator extends EventEmitter {
|
|
|
344
345
|
slicerProfile?: string,
|
|
345
346
|
printerPreset?: string,
|
|
346
347
|
bambuOptions?: BambuSliceOptions,
|
|
347
|
-
activeProfilesRoot?: string
|
|
348
|
+
activeProfilesRoot?: string,
|
|
349
|
+
preserveOrcaExtrusionMode = false
|
|
348
350
|
): Promise<BambuSettingsBundle> {
|
|
349
351
|
const roots = activeProfilesRoot
|
|
350
352
|
? [path.join(activeProfilesRoot, 'BBL')]
|
|
@@ -417,7 +419,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
417
419
|
outputBase,
|
|
418
420
|
slicerType === 'orcaslicer' ? 'process_orca' : 'process',
|
|
419
421
|
slicerType === 'orcaslicer'
|
|
420
|
-
? this.sanitizeProcessForOrca(mergedProcess, printerPreset)
|
|
422
|
+
? this.sanitizeProcessForOrca(mergedProcess, printerPreset, preserveOrcaExtrusionMode)
|
|
421
423
|
: mergedProcess
|
|
422
424
|
);
|
|
423
425
|
|
|
@@ -460,7 +462,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
460
462
|
? this.writeTempJson(
|
|
461
463
|
outputBase,
|
|
462
464
|
'process_default_orca',
|
|
463
|
-
this.sanitizeProcessForOrca(this.readJsonFile(defaultProcessPath), printerPreset)
|
|
465
|
+
this.sanitizeProcessForOrca(this.readJsonFile(defaultProcessPath), printerPreset, preserveOrcaExtrusionMode)
|
|
464
466
|
)
|
|
465
467
|
: defaultProcessPath;
|
|
466
468
|
}
|
|
@@ -1608,6 +1610,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1608
1610
|
let outputFilePath = path.join(this.tempDir, outputFileName);
|
|
1609
1611
|
|
|
1610
1612
|
let args: string[] = [];
|
|
1613
|
+
const orcaTemporaryDirectories: string[] = [];
|
|
1611
1614
|
|
|
1612
1615
|
try {
|
|
1613
1616
|
if (progressCallback) progressCallback(0, `Starting slicing with ${slicerType}...`);
|
|
@@ -1660,14 +1663,19 @@ export class STLManipulator extends EventEmitter {
|
|
|
1660
1663
|
const profileSlicerType = remote ? 'orcaslicer' : slicerType;
|
|
1661
1664
|
const bambuOutputPath = path.join(this.tempDir, outputBase + (remote ? `_${operationId}` : '') + '_sliced.3mf');
|
|
1662
1665
|
const outputDir = path.dirname(bambuOutputPath);
|
|
1663
|
-
const
|
|
1666
|
+
const selectedProfilesRoot = detectProfilesRoot(remote ? undefined : slicerPath, profileSlicerType);
|
|
1667
|
+
const profilesRoot = !remote && profileSlicerType === 'orcaslicer'
|
|
1668
|
+
? await materializeOrcaProfilesRoot(selectedProfilesRoot, this.tempDir)
|
|
1669
|
+
: selectedProfilesRoot;
|
|
1670
|
+
if (profilesRoot !== selectedProfilesRoot) orcaTemporaryDirectories.push(profilesRoot);
|
|
1664
1671
|
const rawBundle = await this.resolveBambuLikeSettingsBundle(
|
|
1665
1672
|
outputBase,
|
|
1666
1673
|
profileSlicerType,
|
|
1667
1674
|
slicerProfile,
|
|
1668
1675
|
printerPreset,
|
|
1669
1676
|
bambuOptions,
|
|
1670
|
-
profilesRoot
|
|
1677
|
+
profilesRoot,
|
|
1678
|
+
profileSlicerType === 'orcaslicer' && profilesRoot !== selectedProfilesRoot
|
|
1671
1679
|
);
|
|
1672
1680
|
// Resolve every BBL dependency before launching the slicer.
|
|
1673
1681
|
const settingsBundle = await this.maybeFlattenBundle(
|
|
@@ -1689,7 +1697,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1689
1697
|
// absolute-extrusion normalization after resolving those ancestors.
|
|
1690
1698
|
// The machine owns the extrusion mode and layer G-code once process
|
|
1691
1699
|
// files lose machine keys, so normalize it there as well.
|
|
1692
|
-
if (profileSlicerType === 'orcaslicer' && settingsBundle.settingsArg) {
|
|
1700
|
+
if (profileSlicerType === 'orcaslicer' && settingsBundle.settingsArg && profilesRoot === selectedProfilesRoot) {
|
|
1693
1701
|
const [machinePath, processPath] = settingsBundle.settingsArg.split(';');
|
|
1694
1702
|
settingsBundle.settingsArg = [
|
|
1695
1703
|
this.writeTempJson(outputBase, 'machine_orca_resolved',
|
|
@@ -1698,7 +1706,14 @@ export class STLManipulator extends EventEmitter {
|
|
|
1698
1706
|
this.sanitizeProcessForOrca(this.readJsonFile(processPath), printerPreset)),
|
|
1699
1707
|
].join(';');
|
|
1700
1708
|
}
|
|
1701
|
-
|
|
1709
|
+
let cliBundle = this.isolateMachineSettings(settingsBundle);
|
|
1710
|
+
let orcaExtraArgs: string[] = [];
|
|
1711
|
+
if (!remote && profileSlicerType === 'orcaslicer' && profilesRoot !== selectedProfilesRoot) {
|
|
1712
|
+
const staged = await stageOrcaCliProfiles(profilesRoot, this.tempDir, cliBundle);
|
|
1713
|
+
cliBundle = { ...cliBundle, settingsArg: staged.settingsArg, filamentPaths: staged.filamentPaths };
|
|
1714
|
+
orcaExtraArgs = staged.extraArgs;
|
|
1715
|
+
orcaTemporaryDirectories.push(staged.dataDir);
|
|
1716
|
+
}
|
|
1702
1717
|
if (remote) {
|
|
1703
1718
|
const [machinePath, processPath] = cliBundle.settingsArg?.split(';') ?? [];
|
|
1704
1719
|
if (!machinePath || !processPath) throw new Error('Remote slicing requires complete prepared machine and process profiles.');
|
|
@@ -1710,6 +1725,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1710
1725
|
break;
|
|
1711
1726
|
}
|
|
1712
1727
|
args = [
|
|
1728
|
+
...orcaExtraArgs,
|
|
1713
1729
|
'--slice', String(bambuOptions?.slicePlate ?? 0),
|
|
1714
1730
|
'--outputdir', outputDir,
|
|
1715
1731
|
'--export-3mf', path.basename(bambuOutputPath),
|
|
@@ -1825,6 +1841,11 @@ export class STLManipulator extends EventEmitter {
|
|
|
1825
1841
|
throw error;
|
|
1826
1842
|
} finally {
|
|
1827
1843
|
this.activeOperations.delete(operationId);
|
|
1844
|
+
for (const directory of orcaTemporaryDirectories.reverse()) {
|
|
1845
|
+
await fs.promises.rm(directory, { recursive: true, force: true }).catch(error => {
|
|
1846
|
+
console.error('Could not remove temporary Orca profiles:', error);
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1828
1849
|
}
|
|
1829
1850
|
}
|
|
1830
1851
|
|