bambu-printer-mcp 1.0.8 → 1.1.4
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/README.md +517 -477
- package/dist/3mf_parser.d.ts +15 -1
- package/dist/3mf_parser.js +258 -1
- package/dist/ams-mapping.d.ts +3 -0
- package/dist/ams-mapping.js +45 -0
- package/dist/index.js +1536 -80
- package/dist/printers/bambu.d.ts +134 -0
- package/dist/printers/bambu.js +1013 -53
- package/dist/slicer/profile-flatten.d.ts +75 -0
- package/dist/slicer/profile-flatten.js +446 -0
- package/dist/stl/stl-manipulator.d.ts +31 -3
- package/dist/stl/stl-manipulator.js +300 -52
- package/dist/types.d.ts +35 -0
- package/package.json +8 -3
- package/patches/bambu-node+3.22.21.patch +16 -0
- package/src/3mf_parser.ts +301 -3
- package/src/ams-mapping.ts +47 -0
- package/src/index.ts +1982 -81
- package/src/printers/bambu.ts +1228 -56
- package/src/slicer/profile-flatten.ts +562 -0
- package/src/stl/stl-manipulator.ts +384 -55
- package/src/types.ts +41 -0
package/dist/3mf_parser.d.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
-
import { ThreeMFData } from './types.js';
|
|
1
|
+
import { ThreeMFData, CollarCharmAnalysis, ThreeMFAmsRequirements, ThreeMFPlateObjects } from './types.js';
|
|
2
2
|
export { ThreeMFData };
|
|
3
3
|
export declare function parse3MF(filePath: string): Promise<ThreeMFData>;
|
|
4
|
+
export declare function extractBambuTemplateSettings(filePath: string, outputDir: string): Promise<string>;
|
|
5
|
+
export declare function analyzeCollarCharm3MF(filePath: string, plateIndex?: number): Promise<CollarCharmAnalysis>;
|
|
6
|
+
export declare function getCollarCharmRolePolicy(): {
|
|
7
|
+
colors: {
|
|
8
|
+
readonly inner: "black";
|
|
9
|
+
readonly outer: "white";
|
|
10
|
+
};
|
|
11
|
+
amsSlots: {
|
|
12
|
+
readonly inner: 0;
|
|
13
|
+
readonly outer: 4;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export declare function analyze3MFAmsRequirements(filePath: string, plateIndex?: number): Promise<ThreeMFAmsRequirements>;
|
|
17
|
+
export declare function analyze3MFPlateObjects(filePath: string, plateIndex?: number): Promise<ThreeMFPlateObjects>;
|
package/dist/3mf_parser.js
CHANGED
|
@@ -77,7 +77,12 @@ function parseBambuJSONConfig(jsonData) {
|
|
|
77
77
|
}
|
|
78
78
|
async function parseBambuConfig(zip) {
|
|
79
79
|
// Look for common Bambu config file names
|
|
80
|
-
const potentialFiles = [
|
|
80
|
+
const potentialFiles = [
|
|
81
|
+
'Metadata/project_settings.config',
|
|
82
|
+
'Metadata/Slic3r_PE.config',
|
|
83
|
+
'Metadata/model_settings.config',
|
|
84
|
+
'Metadata/slice_info.config'
|
|
85
|
+
];
|
|
81
86
|
let configFile = null;
|
|
82
87
|
let configContent = '';
|
|
83
88
|
for (const name of potentialFiles) {
|
|
@@ -214,3 +219,255 @@ export async function parse3MF(filePath) {
|
|
|
214
219
|
throw new Error(`Failed to parse 3MF file: ${error.message}`);
|
|
215
220
|
}
|
|
216
221
|
}
|
|
222
|
+
export async function extractBambuTemplateSettings(filePath, outputDir) {
|
|
223
|
+
const data = await fs.readFile(filePath);
|
|
224
|
+
const zip = await JSZip.loadAsync(data);
|
|
225
|
+
const potentialFiles = [
|
|
226
|
+
'Metadata/project_settings.config',
|
|
227
|
+
'Metadata/Slic3r_PE.config',
|
|
228
|
+
'Metadata/model_settings.config',
|
|
229
|
+
'Metadata/slice_info.config'
|
|
230
|
+
];
|
|
231
|
+
for (const name of potentialFiles) {
|
|
232
|
+
const file = zip.file(name);
|
|
233
|
+
if (!file)
|
|
234
|
+
continue;
|
|
235
|
+
const content = await file.async('string');
|
|
236
|
+
const outputPath = `${outputDir}/${name.replace(/\//g, '_')}`;
|
|
237
|
+
await fs.writeFile(outputPath, content, 'utf8');
|
|
238
|
+
return outputPath;
|
|
239
|
+
}
|
|
240
|
+
throw new Error(`No slicer settings config found in template 3MF: ${filePath}`);
|
|
241
|
+
}
|
|
242
|
+
const COLLAR_CHARM_ROLE_COLORS = {
|
|
243
|
+
inner: 'black',
|
|
244
|
+
outer: 'white',
|
|
245
|
+
};
|
|
246
|
+
const COLLAR_CHARM_ROLE_AMS_SLOTS = {
|
|
247
|
+
// User-facing convention:
|
|
248
|
+
// - inner/smaller object -> AMS 1 slot 1 black
|
|
249
|
+
// - outer/larger object -> AMS 2 slot 1 white
|
|
250
|
+
// Internal absolute tray indices are 0-based:
|
|
251
|
+
// AMS 1 slot 1 -> absolute tray 0
|
|
252
|
+
// AMS 2 slot 1 -> absolute tray 4
|
|
253
|
+
inner: 0,
|
|
254
|
+
outer: 4,
|
|
255
|
+
};
|
|
256
|
+
export async function analyzeCollarCharm3MF(filePath, plateIndex = 0) {
|
|
257
|
+
const data = await fs.readFile(filePath);
|
|
258
|
+
const zip = await JSZip.loadAsync(data);
|
|
259
|
+
const plateName = `Metadata/plate_${plateIndex + 1}.json`;
|
|
260
|
+
const plateFile = zip.file(plateName);
|
|
261
|
+
if (!plateFile) {
|
|
262
|
+
throw new Error(`Prepared collar charm 3MF is missing ${plateName}. Slice the project first or export a printable 3MF.`);
|
|
263
|
+
}
|
|
264
|
+
const plateJson = JSON.parse(await plateFile.async('string'));
|
|
265
|
+
const bboxObjects = Array.isArray(plateJson?.bbox_objects) ? plateJson.bbox_objects : [];
|
|
266
|
+
const usedFilamentPositions = Array.isArray(plateJson?.filament_ids)
|
|
267
|
+
? plateJson.filament_ids.filter((value) => Number.isInteger(value)).map((value) => value)
|
|
268
|
+
: [];
|
|
269
|
+
if (bboxObjects.length !== 2) {
|
|
270
|
+
throw new Error(`Collar charm wrapper requires exactly 2 plate objects; found ${bboxObjects.length} in ${plateName}.`);
|
|
271
|
+
}
|
|
272
|
+
if (usedFilamentPositions.length !== 2) {
|
|
273
|
+
throw new Error(`Collar charm wrapper requires exactly 2 used filament positions; found ${usedFilamentPositions.length} in ${plateName}.`);
|
|
274
|
+
}
|
|
275
|
+
if (bboxObjects.length !== usedFilamentPositions.length) {
|
|
276
|
+
throw new Error(`Collar charm wrapper requires object count and used filament count to match; found ${bboxObjects.length} objects and ${usedFilamentPositions.length} filament positions in ${plateName}.`);
|
|
277
|
+
}
|
|
278
|
+
const normalizedObjects = bboxObjects.map((object, objectIndex) => {
|
|
279
|
+
const area = Number(object?.area);
|
|
280
|
+
if (!Number.isFinite(area) || area <= 0) {
|
|
281
|
+
throw new Error(`Collar charm object ${objectIndex} in ${plateName} has invalid area metadata.`);
|
|
282
|
+
}
|
|
283
|
+
const name = typeof object?.name === 'string' && object.name.trim().length > 0
|
|
284
|
+
? object.name.trim()
|
|
285
|
+
: `object_${objectIndex + 1}`;
|
|
286
|
+
return {
|
|
287
|
+
objectIndex,
|
|
288
|
+
name,
|
|
289
|
+
area,
|
|
290
|
+
filamentPosition: usedFilamentPositions[objectIndex],
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
const sortedByArea = [...normalizedObjects].sort((a, b) => a.area - b.area);
|
|
294
|
+
if (sortedByArea[0].area === sortedByArea[1].area) {
|
|
295
|
+
throw new Error(`Collar charm wrapper could not distinguish inner vs outer object because both plate objects have the same reported area in ${plateName}.`);
|
|
296
|
+
}
|
|
297
|
+
const roles = [
|
|
298
|
+
{
|
|
299
|
+
role: 'inner',
|
|
300
|
+
objectIndex: sortedByArea[0].objectIndex,
|
|
301
|
+
name: sortedByArea[0].name,
|
|
302
|
+
area: sortedByArea[0].area,
|
|
303
|
+
filamentPosition: sortedByArea[0].filamentPosition,
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
role: 'outer',
|
|
307
|
+
objectIndex: sortedByArea[1].objectIndex,
|
|
308
|
+
name: sortedByArea[1].name,
|
|
309
|
+
area: sortedByArea[1].area,
|
|
310
|
+
filamentPosition: sortedByArea[1].filamentPosition,
|
|
311
|
+
},
|
|
312
|
+
];
|
|
313
|
+
const trayByFilamentPosition = new Map();
|
|
314
|
+
for (const role of roles) {
|
|
315
|
+
trayByFilamentPosition.set(role.filamentPosition, COLLAR_CHARM_ROLE_AMS_SLOTS[role.role]);
|
|
316
|
+
}
|
|
317
|
+
const amsSlots = usedFilamentPositions.map((position) => {
|
|
318
|
+
const tray = trayByFilamentPosition.get(position);
|
|
319
|
+
if (tray === undefined) {
|
|
320
|
+
throw new Error(`Collar charm wrapper could not assign a tray to project filament position ${position}.`);
|
|
321
|
+
}
|
|
322
|
+
return tray;
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
plateIndex,
|
|
326
|
+
usedFilamentPositions,
|
|
327
|
+
amsSlots,
|
|
328
|
+
roles,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
export function getCollarCharmRolePolicy() {
|
|
332
|
+
return {
|
|
333
|
+
colors: COLLAR_CHARM_ROLE_COLORS,
|
|
334
|
+
amsSlots: COLLAR_CHARM_ROLE_AMS_SLOTS,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
export async function analyze3MFAmsRequirements(filePath, plateIndex = 0) {
|
|
338
|
+
const data = await fs.readFile(filePath);
|
|
339
|
+
const zip = await JSZip.loadAsync(data);
|
|
340
|
+
const plateNumber = plateIndex + 1;
|
|
341
|
+
const plateName = `Metadata/plate_${plateNumber}.json`;
|
|
342
|
+
const plateFile = zip.file(plateName);
|
|
343
|
+
if (!plateFile) {
|
|
344
|
+
throw new Error(`3MF is missing ${plateName}; cannot derive AMS filament order.`);
|
|
345
|
+
}
|
|
346
|
+
const plateJson = JSON.parse(await plateFile.async('string'));
|
|
347
|
+
const usedFilamentPositions = Array.isArray(plateJson?.filament_ids)
|
|
348
|
+
? plateJson.filament_ids.filter((value) => Number.isInteger(value)).map((value) => value)
|
|
349
|
+
: [];
|
|
350
|
+
if (usedFilamentPositions.length === 0) {
|
|
351
|
+
throw new Error(`${plateName} does not declare filament_ids; cannot derive AMS filament order.`);
|
|
352
|
+
}
|
|
353
|
+
const sliceInfo = zip.file('Metadata/slice_info.config');
|
|
354
|
+
const filamentById = new Map();
|
|
355
|
+
if (sliceInfo) {
|
|
356
|
+
try {
|
|
357
|
+
const parsed = await parseStringPromise(await sliceInfo.async('string'), {
|
|
358
|
+
explicitArray: false,
|
|
359
|
+
mergeAttrs: true,
|
|
360
|
+
});
|
|
361
|
+
const plateNodes = parsed?.config?.plate
|
|
362
|
+
? Array.isArray(parsed.config.plate) ? parsed.config.plate : [parsed.config.plate]
|
|
363
|
+
: [];
|
|
364
|
+
const selectedPlate = plateNodes.find((plate) => {
|
|
365
|
+
const metadata = Array.isArray(plate?.metadata)
|
|
366
|
+
? plate.metadata
|
|
367
|
+
: plate?.metadata
|
|
368
|
+
? [plate.metadata]
|
|
369
|
+
: [];
|
|
370
|
+
return metadata.some((item) => item?.key === 'index' && Number(item?.value) === plateNumber);
|
|
371
|
+
}) || plateNodes[plateIndex] || plateNodes[0];
|
|
372
|
+
const filamentNodes = selectedPlate?.filament
|
|
373
|
+
? Array.isArray(selectedPlate.filament) ? selectedPlate.filament : [selectedPlate.filament]
|
|
374
|
+
: [];
|
|
375
|
+
for (const filament of filamentNodes) {
|
|
376
|
+
const id = Number.parseInt(String(filament?.id ?? ''), 10);
|
|
377
|
+
if (!Number.isFinite(id))
|
|
378
|
+
continue;
|
|
379
|
+
filamentById.set(id, {
|
|
380
|
+
tray_info_idx: typeof filament?.tray_info_idx === 'string' ? filament.tray_info_idx : null,
|
|
381
|
+
type: typeof filament?.type === 'string' ? filament.type : null,
|
|
382
|
+
color: typeof filament?.color === 'string' ? filament.color : null,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// plate_N.json already gives the project filament order. Bad
|
|
388
|
+
// slice_info should degrade metadata only, not block AMS planning.
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
plateIndex,
|
|
393
|
+
usedFilamentPositions,
|
|
394
|
+
filaments: usedFilamentPositions.map((position, order) => {
|
|
395
|
+
const filamentId = position + 1;
|
|
396
|
+
const byOneBasedId = filamentById.get(filamentId);
|
|
397
|
+
const byOrder = Array.from(filamentById.values())[order];
|
|
398
|
+
const filament = byOneBasedId || byOrder || null;
|
|
399
|
+
return {
|
|
400
|
+
filamentPosition: position,
|
|
401
|
+
filamentId,
|
|
402
|
+
tray_info_idx: filament?.tray_info_idx ?? null,
|
|
403
|
+
type: filament?.type ?? null,
|
|
404
|
+
color: filament?.color ?? null,
|
|
405
|
+
};
|
|
406
|
+
}),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
export async function analyze3MFPlateObjects(filePath, plateIndex = 0) {
|
|
410
|
+
const data = await fs.readFile(filePath);
|
|
411
|
+
const zip = await JSZip.loadAsync(data);
|
|
412
|
+
const plateName = `Metadata/plate_${plateIndex + 1}.json`;
|
|
413
|
+
const plateFile = zip.file(plateName);
|
|
414
|
+
if (!plateFile) {
|
|
415
|
+
throw new Error(`3MF is missing ${plateName}; cannot list plate objects.`);
|
|
416
|
+
}
|
|
417
|
+
const plateJson = JSON.parse(await plateFile.async('string'));
|
|
418
|
+
const bboxObjects = Array.isArray(plateJson?.bbox_objects) ? plateJson.bbox_objects : [];
|
|
419
|
+
const sliceInfoObjects = [];
|
|
420
|
+
const sliceInfo = zip.file('Metadata/slice_info.config');
|
|
421
|
+
if (sliceInfo) {
|
|
422
|
+
try {
|
|
423
|
+
const parsed = await parseStringPromise(await sliceInfo.async('string'), {
|
|
424
|
+
explicitArray: false,
|
|
425
|
+
mergeAttrs: true,
|
|
426
|
+
});
|
|
427
|
+
const plateNodes = parsed?.config?.plate
|
|
428
|
+
? Array.isArray(parsed.config.plate) ? parsed.config.plate : [parsed.config.plate]
|
|
429
|
+
: [];
|
|
430
|
+
const plateNumber = plateIndex + 1;
|
|
431
|
+
const selectedPlate = plateNodes.find((plate) => {
|
|
432
|
+
const metadata = Array.isArray(plate?.metadata)
|
|
433
|
+
? plate.metadata
|
|
434
|
+
: plate?.metadata
|
|
435
|
+
? [plate.metadata]
|
|
436
|
+
: [];
|
|
437
|
+
return metadata.some((item) => item?.key === 'index' && Number(item?.value) === plateNumber);
|
|
438
|
+
}) || plateNodes[plateIndex] || plateNodes[0];
|
|
439
|
+
const objectNodes = selectedPlate?.object
|
|
440
|
+
? Array.isArray(selectedPlate.object) ? selectedPlate.object : [selectedPlate.object]
|
|
441
|
+
: [];
|
|
442
|
+
for (const object of objectNodes) {
|
|
443
|
+
const id = Number.parseInt(String(object?.identify_id ?? ''), 10);
|
|
444
|
+
if (!Number.isInteger(id))
|
|
445
|
+
continue;
|
|
446
|
+
sliceInfoObjects.push({
|
|
447
|
+
id,
|
|
448
|
+
name: typeof object?.name === 'string' ? object.name : null,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
// Fall back to bbox IDs for older or malformed 3MFs.
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
plateIndex,
|
|
458
|
+
objects: bboxObjects
|
|
459
|
+
.map((object, index) => {
|
|
460
|
+
const sliceObject = sliceInfoObjects[index];
|
|
461
|
+
const id = sliceObject?.id ?? Number(object?.id);
|
|
462
|
+
if (!Number.isInteger(id))
|
|
463
|
+
return null;
|
|
464
|
+
return {
|
|
465
|
+
id,
|
|
466
|
+
name: sliceObject?.name || (typeof object?.name === 'string' ? object.name : `object_${id}`),
|
|
467
|
+
area: Number.isFinite(Number(object?.area)) ? Number(object.area) : null,
|
|
468
|
+
bbox: object?.bbox ?? null,
|
|
469
|
+
};
|
|
470
|
+
})
|
|
471
|
+
.filter((object) => object !== null),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function normalizeBridgeAmsTrayValue(value, label) {
|
|
2
|
+
let numeric;
|
|
3
|
+
if (typeof value === "number") {
|
|
4
|
+
numeric = value;
|
|
5
|
+
}
|
|
6
|
+
else if (typeof value === "string" && value.trim() !== "") {
|
|
7
|
+
numeric = Number(value);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
throw new Error(`${label} must be an integer tray index; got ${JSON.stringify(value)}`);
|
|
11
|
+
}
|
|
12
|
+
if (!Number.isInteger(numeric) ||
|
|
13
|
+
numeric < -1 ||
|
|
14
|
+
(numeric > 15 && numeric < 128) ||
|
|
15
|
+
numeric > 254) {
|
|
16
|
+
throw new Error(`${label} must be an integer in [-1, 15] (absolute tray) or 128-254 (HT/external); got ${JSON.stringify(value)}`);
|
|
17
|
+
}
|
|
18
|
+
return numeric;
|
|
19
|
+
}
|
|
20
|
+
function isNumericMappingKey(key) {
|
|
21
|
+
const numeric = Number(key);
|
|
22
|
+
return Number.isInteger(numeric) && numeric >= 0;
|
|
23
|
+
}
|
|
24
|
+
export function normalizeAmsMappingObject(mapping, label = "ams_mapping") {
|
|
25
|
+
return Object.entries(mapping)
|
|
26
|
+
.sort(([leftKey], [rightKey]) => {
|
|
27
|
+
const leftNumeric = isNumericMappingKey(leftKey);
|
|
28
|
+
const rightNumeric = isNumericMappingKey(rightKey);
|
|
29
|
+
if (leftNumeric && rightNumeric)
|
|
30
|
+
return Number(leftKey) - Number(rightKey);
|
|
31
|
+
if (leftNumeric)
|
|
32
|
+
return -1;
|
|
33
|
+
if (rightNumeric)
|
|
34
|
+
return 1;
|
|
35
|
+
return 0;
|
|
36
|
+
})
|
|
37
|
+
.map(([key, value]) => normalizeBridgeAmsTrayValue(value, `${label}[${key}]`));
|
|
38
|
+
}
|
|
39
|
+
export function hasAmsMappingInput(value) {
|
|
40
|
+
if (Array.isArray(value))
|
|
41
|
+
return value.length > 0;
|
|
42
|
+
if (value && typeof value === "object")
|
|
43
|
+
return Object.keys(value).length > 0;
|
|
44
|
+
return value !== undefined && value !== null;
|
|
45
|
+
}
|