bambu-printer-mcp 1.0.8 → 1.1.2

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.
@@ -9,9 +9,20 @@ import { promisify } from 'util';
9
9
  import { EventEmitter } from 'events';
10
10
  import * as crypto from 'crypto';
11
11
  import { execFile } from 'child_process';
12
+ import { flattenForCli, detectProfilesRoot } from '../slicer/profile-flatten.js';
12
13
 
13
14
  const readFileAsync = promisify(fs.readFile);
14
15
  const writeFileAsync = promisify(fs.writeFile);
16
+ const BAMBU_PROFILE_ROOTS = [
17
+ '/Applications/BambuStudio.app/Contents/Resources/profiles/BBL',
18
+ '/Applications/OrcaSlicer.app/Contents/Resources/profiles/BBL',
19
+ ];
20
+ const BAMBU_CLI_BED_TYPES: Record<string, string> = {
21
+ textured_plate: 'Textured PEI Plate',
22
+ cool_plate: 'Cool Plate',
23
+ engineering_plate: 'Engineering Plate',
24
+ hot_plate: 'High Temp Plate',
25
+ };
15
26
 
16
27
  // Define types for progress tracking
17
28
  export type ProgressCallback = (progress: number, message?: string) => void;
@@ -97,8 +108,15 @@ export function isBambuCompatibleSlicer(slicerType: SlicerType): boolean {
97
108
  );
98
109
  }
99
110
 
100
- function isPathLikeExecutable(executable: string): boolean {
101
- return path.isAbsolute(executable) || /[\\/]/.test(executable);
111
+ function isPathLikeExecutable(slicerPath: string): boolean {
112
+ return (
113
+ path.isAbsolute(slicerPath) ||
114
+ slicerPath.startsWith("./") ||
115
+ slicerPath.startsWith("../") ||
116
+ slicerPath.includes("/") ||
117
+ slicerPath.includes("\\") ||
118
+ /^[A-Za-z]:/.test(slicerPath)
119
+ );
102
120
  }
103
121
 
104
122
  function configuredBambuProfileDirs(): string[] {
@@ -129,35 +147,10 @@ function configuredBambuProfileDirs(): string[] {
129
147
  ];
130
148
 
131
149
  return roots.flatMap((root) =>
132
- appNames.map((appName) => path.join(root, appName, 'system', 'BBL', 'machine'))
150
+ appNames.map((appName) => path.join(root, appName, 'system', 'BBL'))
133
151
  );
134
152
  }
135
153
 
136
- function resolveBambuPrinterProfile(printerPreset: string): string | undefined {
137
- if (printerPreset.includes('/') || printerPreset.includes('\\')) {
138
- return undefined;
139
- }
140
-
141
- for (const profileDir of configuredBambuProfileDirs()) {
142
- const profilePath = path.join(profileDir, `${printerPreset}.json`);
143
- if (fs.existsSync(profilePath)) {
144
- return profilePath;
145
- }
146
- }
147
-
148
- return undefined;
149
- }
150
-
151
- function generatedBambuPrinterSettings(printerPreset: string): Record<string, string> {
152
- return {
153
- type: 'machine',
154
- from: 'system',
155
- name: printerPreset,
156
- inherits: printerPreset,
157
- printer_settings_id: printerPreset,
158
- };
159
- }
160
-
161
154
  // Bambu-compatible slicer CLI flags exposed via the slice_stl tool
162
155
  export interface BambuSliceOptions {
163
156
  uptodate?: boolean; // --uptodate: update 3MF configs to latest presets
@@ -169,6 +162,7 @@ export interface BambuSliceOptions {
169
162
  skipObjects?: string; // --skip-objects "3,5,10": skip specific objects
170
163
  loadFilaments?: string; // --load-filaments "f1.json;f2.json": filament profiles
171
164
  loadFilamentIds?: string; // --load-filament-ids "1,2,3,1": filament-to-object mapping
165
+ bedType?: string; // Bambu bed type, e.g. textured_plate or cool_plate
172
166
  enableTimelapse?: boolean; // --enable-timelapse: timelapse-aware slicing
173
167
  allowMixTemp?: boolean; // --allow-mix-temp: allow mixed-temp filaments
174
168
  scale?: number; // --scale factor: uniform scale
@@ -200,6 +194,340 @@ export class STLManipulator extends EventEmitter {
200
194
  return crypto.randomUUID();
201
195
  }
202
196
 
197
+ private getAvailableProfileRoots(): string[] {
198
+ return [...BAMBU_PROFILE_ROOTS, ...configuredBambuProfileDirs()].filter((root) => fs.existsSync(root));
199
+ }
200
+
201
+ private findProfileFile(
202
+ category: 'machine' | 'process' | 'filament',
203
+ profileName?: string
204
+ ): string | undefined {
205
+ if (!profileName) {
206
+ return undefined;
207
+ }
208
+
209
+ for (const root of this.getAvailableProfileRoots()) {
210
+ const candidate = path.join(root, category, `${profileName}.json`);
211
+ if (fs.existsSync(candidate)) {
212
+ return candidate;
213
+ }
214
+ }
215
+
216
+ return undefined;
217
+ }
218
+
219
+ private buildFilamentIdIndex(): Map<string, string> {
220
+ const index = new Map<string, string>();
221
+
222
+ for (const root of this.getAvailableProfileRoots()) {
223
+ const filamentDir = path.join(root, 'filament');
224
+ if (!fs.existsSync(filamentDir)) {
225
+ continue;
226
+ }
227
+
228
+ for (const entry of fs.readdirSync(filamentDir)) {
229
+ if (!entry.endsWith('.json')) {
230
+ continue;
231
+ }
232
+
233
+ const filePath = path.join(filamentDir, entry);
234
+ try {
235
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
236
+ const filamentId = typeof parsed?.filament_id === 'string' ? parsed.filament_id.trim() : '';
237
+ if (filamentId && !index.has(filamentId)) {
238
+ index.set(filamentId, filePath);
239
+ }
240
+ } catch {
241
+ // Ignore malformed JSON.
242
+ }
243
+ }
244
+ }
245
+
246
+ return index;
247
+ }
248
+
249
+ private readJsonFile(filePath: string): any {
250
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
251
+ }
252
+
253
+ private stripAbsoluteExtruderResets(value: unknown): unknown {
254
+ if (typeof value === 'string') {
255
+ return value
256
+ .split(/\r?\n/)
257
+ .filter((line) => line.trim().toUpperCase() !== 'G92 E0')
258
+ .join('\n');
259
+ }
260
+
261
+ if (Array.isArray(value)) {
262
+ return value.filter((line) => String(line).trim().toUpperCase() !== 'G92 E0');
263
+ }
264
+
265
+ return value;
266
+ }
267
+
268
+ private sanitizeProcessForOrca(processConfig: any, printerPreset?: string): any {
269
+ const sanitized = { ...processConfig };
270
+
271
+ sanitized.type = 'process';
272
+ if (!sanitized.from || sanitized.from === 'project') {
273
+ sanitized.from = 'User';
274
+ }
275
+
276
+ if (printerPreset) {
277
+ sanitized.compatible_printers = [printerPreset];
278
+ }
279
+
280
+ if (
281
+ sanitized.prime_tower_brim_width !== undefined &&
282
+ Number(sanitized.prime_tower_brim_width) < 0
283
+ ) {
284
+ sanitized.prime_tower_brim_width = '0';
285
+ } else if (sanitized.prime_tower_brim_width !== undefined) {
286
+ sanitized.prime_tower_brim_width = String(sanitized.prime_tower_brim_width);
287
+ }
288
+
289
+ sanitized.use_relative_e_distances = '0';
290
+ sanitized.before_layer_change_gcode = this.stripAbsoluteExtruderResets(
291
+ sanitized.before_layer_change_gcode
292
+ );
293
+ sanitized.layer_gcode = this.stripAbsoluteExtruderResets(sanitized.layer_gcode);
294
+ sanitized.layer_change_gcode = this.stripAbsoluteExtruderResets(
295
+ sanitized.layer_change_gcode
296
+ );
297
+
298
+ return sanitized;
299
+ }
300
+
301
+ private writeTempJson(outputBase: string, suffix: string, value: any): string {
302
+ const outPath = path.join(this.tempDir, `${outputBase}_${suffix}.json`);
303
+ fs.writeFileSync(outPath, JSON.stringify(value, null, 2));
304
+ return outPath;
305
+ }
306
+
307
+ private resolveBambuLikeSettingsBundle(
308
+ outputBase: string,
309
+ slicerType: 'orcaslicer' | 'orcaslicer-bambulab' | 'bambustudio',
310
+ slicerProfile?: string,
311
+ printerPreset?: string,
312
+ bambuOptions?: BambuSliceOptions
313
+ ): { settingsArg?: string; filamentPaths: string[] } {
314
+ const machinePath = this.findProfileFile('machine', printerPreset);
315
+ const machineConfig = machinePath ? this.readJsonFile(machinePath) : null;
316
+ const hasSlicerProfile = !!slicerProfile && fs.existsSync(slicerProfile);
317
+ const filamentPaths: string[] = [];
318
+
319
+ let processPath: string | undefined;
320
+ let parsedProfile: any = null;
321
+
322
+ if (hasSlicerProfile) {
323
+ try {
324
+ parsedProfile = this.readJsonFile(slicerProfile!);
325
+ } catch {
326
+ parsedProfile = null;
327
+ }
328
+ }
329
+
330
+ if (parsedProfile && typeof parsedProfile === 'object') {
331
+ const inheritedProcessName =
332
+ (typeof parsedProfile.inherits === 'string' && parsedProfile.inherits) ||
333
+ (typeof parsedProfile.print_settings_id === 'string' &&
334
+ parsedProfile.print_settings_id !== parsedProfile.name
335
+ ? parsedProfile.print_settings_id
336
+ : undefined) ||
337
+ (typeof parsedProfile.default_print_profile === 'string'
338
+ ? parsedProfile.default_print_profile
339
+ : undefined) ||
340
+ (typeof machineConfig?.default_print_profile === 'string'
341
+ ? machineConfig.default_print_profile
342
+ : undefined);
343
+
344
+ const inheritedProcessPath = this.findProfileFile('process', inheritedProcessName);
345
+ const inheritedProcess =
346
+ inheritedProcessPath && fs.existsSync(inheritedProcessPath)
347
+ ? this.readJsonFile(inheritedProcessPath)
348
+ : {};
349
+
350
+ const mergedProcess = {
351
+ ...inheritedProcess,
352
+ ...parsedProfile,
353
+ type: 'process',
354
+ };
355
+
356
+ processPath = this.writeTempJson(
357
+ outputBase,
358
+ slicerType === 'orcaslicer' ? 'process_orca' : 'process',
359
+ slicerType === 'orcaslicer'
360
+ ? this.sanitizeProcessForOrca(mergedProcess, printerPreset)
361
+ : mergedProcess
362
+ );
363
+
364
+ const defaultFilamentProfiles = Array.isArray(parsedProfile.default_filament_profile)
365
+ ? parsedProfile.default_filament_profile
366
+ : [];
367
+ for (const profileName of defaultFilamentProfiles) {
368
+ const filamentPath = this.findProfileFile('filament', String(profileName));
369
+ if (filamentPath) {
370
+ filamentPaths.push(filamentPath);
371
+ }
372
+ }
373
+
374
+ if (filamentPaths.length === 0 && Array.isArray(parsedProfile.filament_ids)) {
375
+ const filamentIdIndex = this.buildFilamentIdIndex();
376
+ for (const filamentId of parsedProfile.filament_ids) {
377
+ const filamentPath = filamentIdIndex.get(String(filamentId));
378
+ if (filamentPath) {
379
+ filamentPaths.push(filamentPath);
380
+ }
381
+ }
382
+ }
383
+ } else if (hasSlicerProfile) {
384
+ processPath = slicerProfile;
385
+ }
386
+
387
+ if (!processPath) {
388
+ const defaultProcessName =
389
+ typeof machineConfig?.default_print_profile === 'string'
390
+ ? machineConfig.default_print_profile
391
+ : undefined;
392
+ const defaultProcessPath = this.findProfileFile('process', defaultProcessName);
393
+ if (defaultProcessPath) {
394
+ processPath =
395
+ slicerType === 'orcaslicer'
396
+ ? this.writeTempJson(
397
+ outputBase,
398
+ 'process_default_orca',
399
+ this.sanitizeProcessForOrca(this.readJsonFile(defaultProcessPath), printerPreset)
400
+ )
401
+ : defaultProcessPath;
402
+ }
403
+ }
404
+
405
+ if (
406
+ !bambuOptions?.loadFilaments &&
407
+ filamentPaths.length === 0 &&
408
+ Array.isArray(machineConfig?.default_filament_profile)
409
+ ) {
410
+ for (const profileName of machineConfig.default_filament_profile) {
411
+ const filamentPath = this.findProfileFile('filament', String(profileName));
412
+ if (filamentPath) {
413
+ filamentPaths.push(filamentPath);
414
+ }
415
+ }
416
+ }
417
+
418
+ if (bambuOptions?.loadFilaments) {
419
+ filamentPaths.length = 0;
420
+ for (const filamentPath of bambuOptions.loadFilaments.split(';')) {
421
+ const trimmed = filamentPath.trim();
422
+ if (trimmed) {
423
+ filamentPaths.push(trimmed);
424
+ }
425
+ }
426
+ }
427
+
428
+ const settingsParts = [machinePath, processPath].filter(
429
+ (entry): entry is string => Boolean(entry)
430
+ );
431
+
432
+ return {
433
+ settingsArg: settingsParts.length > 0 ? settingsParts.join(';') : undefined,
434
+ filamentPaths: Array.from(new Set(filamentPaths)),
435
+ };
436
+ }
437
+
438
+ /**
439
+ * Optionally rewrite a Bambu-like settings bundle so the paths point at
440
+ * fully-flattened temp configs instead of the BBL-shipped leaf JSONs.
441
+ *
442
+ * BambuStudio's CLI does not resolve the `inherits` chain when loading
443
+ * profiles via --load-settings / --load-filaments, which causes a
444
+ * cluster of upstream bugs (see https://github.com/bambulab/BambuStudio/issues/9636
445
+ * and #9968). Our flattener (src/slicer/profile-flatten.ts) reproduces
446
+ * what the GUI does at slice time so the CLI accepts the configs.
447
+ *
448
+ * Opt-in via `BAMBU_CLI_FLATTEN=true`. When the env var is unset or
449
+ * not "true"/"1", returns the bundle unchanged so behavior is
450
+ * backward-compatible. When enabled, only BBL-shipped leaves get
451
+ * flattened; user-provided custom configs pass through untouched.
452
+ */
453
+ private async maybeFlattenBundle(
454
+ bundle: { settingsArg?: string; filamentPaths: string[] },
455
+ bambuOptions?: BambuSliceOptions,
456
+ activeSlicerPath?: string
457
+ ): Promise<{ settingsArg?: string; filamentPaths: string[] }> {
458
+ const flag = process.env.BAMBU_CLI_FLATTEN;
459
+ if (flag !== 'true' && flag !== '1') return bundle;
460
+ if (!bundle.settingsArg) return bundle;
461
+
462
+ // settingsArg is "machine.json;process.json".
463
+ const parts = bundle.settingsArg.split(';').filter(Boolean);
464
+ if (parts.length < 2) return bundle;
465
+ const [machinePath, processPath] = parts;
466
+
467
+ // Pull leaf names from each JSON's `name` field. If any is missing or
468
+ // looks non-BBL (e.g. a user-saved custom config), bail out and
469
+ // return the original bundle untouched.
470
+ const machineLeaf = this.readLeafName(machinePath);
471
+ const processLeaf = this.readLeafName(processPath);
472
+ const filamentLeaves = bundle.filamentPaths
473
+ .map((p) => this.readLeafName(p))
474
+ .filter((n): n is string => Boolean(n));
475
+
476
+ if (!machineLeaf || !processLeaf || filamentLeaves.length === 0) {
477
+ console.log(
478
+ '[cli-flatten] skipping: could not derive BBL leaf names from bundle paths'
479
+ );
480
+ return bundle;
481
+ }
482
+
483
+ const bedType = this.resolveBambuStudioBedType(bambuOptions?.bedType);
484
+
485
+ try {
486
+ const profilesRoot = detectProfilesRoot(activeSlicerPath || process.env.SLICER_PATH);
487
+ const flat = await flattenForCli({
488
+ machineLeaf,
489
+ processLeaf,
490
+ filamentLeaves,
491
+ profilesRoot,
492
+ tempDir: this.tempDir,
493
+ bedType,
494
+ });
495
+ console.log(
496
+ `[cli-flatten] applied for ${machineLeaf} (cliOverlay=${flat.meta.cliOverlayApplied})`
497
+ );
498
+ return {
499
+ settingsArg: `${flat.machinePath};${flat.processPath}`,
500
+ filamentPaths: flat.filamentPaths,
501
+ };
502
+ } catch (err: any) {
503
+ console.error(
504
+ `[cli-flatten] failed, falling back to unflattened bundle: ${err?.message ?? err}`
505
+ );
506
+ return bundle;
507
+ }
508
+ }
509
+
510
+ private resolveBambuStudioBedType(bedType?: string): string | undefined {
511
+ if (!bedType) return undefined;
512
+ const normalized = bedType.trim().toLowerCase();
513
+ if (normalized === 'supertack_plate') {
514
+ throw new Error(
515
+ 'BambuStudio CLI SuperTack bed type is not verified; use a pre-sliced 3MF or choose textured_plate, cool_plate, engineering_plate, or hot_plate.'
516
+ );
517
+ }
518
+ return BAMBU_CLI_BED_TYPES[normalized] || bedType;
519
+ }
520
+
521
+ /** Read a profile JSON's top-level `name` field, or null if unreadable. */
522
+ private readLeafName(filePath: string): string | null {
523
+ try {
524
+ const data = this.readJsonFile(filePath);
525
+ return typeof data?.name === 'string' && data.name.length > 0 ? data.name : null;
526
+ } catch {
527
+ return null;
528
+ }
529
+ }
530
+
203
531
  /**
204
532
  * Load STL file and return geometry and bounding box
205
533
  */
@@ -1101,12 +1429,12 @@ export class STLManipulator extends EventEmitter {
1101
1429
  /**
1102
1430
  * Slice an STL or 3MF file using the specified slicer
1103
1431
  * @param stlFilePath Path to the input STL or 3MF file
1104
- * @param slicerType Type of slicer (prusaslicer, cura, slic3r, orcaslicer, orcaslicer-bambulab, bambustudio)
1432
+ * @param slicerType Type of slicer (prusaslicer, cura, slic3r, orcaslicer, bambustudio)
1105
1433
  * @param slicerPath Path to the slicer executable
1106
1434
  * @param slicerProfile Optional path to the slicer profile/config file
1107
1435
  * @param progressCallback Optional callback for progress updates
1108
- * @param printerPreset Optional Bambu-compatible printer preset name (e.g., "Bambu Lab P1S 0.4 nozzle")
1109
- * @param bambuOptions Optional Bambu-compatible CLI flags
1436
+ * @param printerPreset Optional BambuStudio printer preset name (e.g., "Bambu Lab P1S 0.4 nozzle")
1437
+ * @param bambuOptions Optional BambuStudio-specific CLI flags
1110
1438
  * @returns Path to the generated G-code or sliced 3MF file
1111
1439
  */
1112
1440
  async sliceSTL(
@@ -1169,41 +1497,39 @@ export class STLManipulator extends EventEmitter {
1169
1497
  case 'orcaslicer':
1170
1498
  case 'orcaslicer-bambulab':
1171
1499
  // Bambu-compatible CLI: slice and export as 3MF with embedded gcode.
1172
- // FULU's OrcaSlicer-bambulab fork keeps OrcaSlicer's Bambu-style
1173
- // --slice/--export-3mf path, so it must not use the generic Prusa
1174
- // --output G-code flow.
1500
+ // FULU's OrcaSlicer-bambulab fork keeps this Bambu-style path.
1175
1501
  // For 3MF input: slice in place and export sliced 3MF
1176
1502
  // For STL input: slice and export as 3MF
1177
1503
  {
1178
1504
  const is3mf = stlFilePath.toLowerCase().endsWith('.3mf');
1179
1505
  const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
1180
1506
  const bambuOutputPath = path.join(this.tempDir, outputBase + '_sliced.3mf');
1507
+ const outputDir = path.dirname(bambuOutputPath);
1508
+ const rawBundle = this.resolveBambuLikeSettingsBundle(
1509
+ outputBase,
1510
+ slicerType,
1511
+ slicerProfile,
1512
+ printerPreset,
1513
+ bambuOptions
1514
+ );
1515
+ // Opt-in flatten step: walks the BBL `inherits` chain so the
1516
+ // CLI accepts what we hand it. See maybeFlattenBundle docstring.
1517
+ const settingsBundle =
1518
+ slicerType === 'bambustudio'
1519
+ ? await this.maybeFlattenBundle(rawBundle, bambuOptions, slicerPath)
1520
+ : rawBundle;
1181
1521
  args = [
1182
1522
  '--slice', String(bambuOptions?.slicePlate ?? 0),
1183
- '--export-3mf', bambuOutputPath,
1523
+ '--outputdir', outputDir,
1524
+ '--export-3mf', path.basename(bambuOutputPath),
1184
1525
  ];
1185
- if (slicerProfile) {
1186
- args.push('--load-settings', slicerProfile);
1526
+ if (settingsBundle.settingsArg) {
1527
+ args.push('--load-settings', settingsBundle.settingsArg);
1187
1528
  }
1188
1529
  // Always allow newer-version 3MF files (the CLI rejects them by default)
1189
1530
  args.push('--allow-newer-file');
1190
1531
 
1191
- // If a printer preset name is given AND no explicit profile was provided,
1192
- // prefer the slicer's installed machine profile. Real BambuStudio rejects
1193
- // skeletal JSON without profile metadata, so only fall back to generated
1194
- // settings when no installed profile can be found.
1195
- if (printerPreset && !slicerProfile) {
1196
- const installedProfile = resolveBambuPrinterProfile(printerPreset);
1197
- if (installedProfile) {
1198
- args.push('--load-settings', installedProfile);
1199
- } else {
1200
- const presetJson = path.join(this.tempDir, '_printer_preset.json');
1201
- fs.writeFileSync(presetJson, JSON.stringify(generatedBambuPrinterSettings(printerPreset), null, 2));
1202
- args.push('--load-settings', presetJson);
1203
- }
1204
- }
1205
-
1206
- // Bambu-compatible slicer flags
1532
+ // BambuSliceOptions flags
1207
1533
  if (bambuOptions?.uptodate) args.push('--uptodate');
1208
1534
  if (bambuOptions?.ensureOnBed) args.push('--ensure-on-bed');
1209
1535
  if (bambuOptions?.enableTimelapse) args.push('--enable-timelapse');
@@ -1219,11 +1545,14 @@ export class STLManipulator extends EventEmitter {
1219
1545
  if (bambuOptions?.rotateY !== undefined) args.push('--rotate-y', String(bambuOptions.rotateY));
1220
1546
  if (bambuOptions?.cloneObjects) args.push('--clone-objects', bambuOptions.cloneObjects);
1221
1547
  if (bambuOptions?.skipObjects) args.push('--skip-objects', bambuOptions.skipObjects);
1222
- if (bambuOptions?.loadFilaments) args.push('--load-filaments', bambuOptions.loadFilaments);
1548
+ if (settingsBundle.filamentPaths.length > 0) {
1549
+ args.push('--load-filaments', settingsBundle.filamentPaths.join(';'));
1550
+ args.push('--load-defaultfila');
1551
+ }
1223
1552
  if (bambuOptions?.loadFilamentIds) args.push('--load-filament-ids', bambuOptions.loadFilamentIds);
1224
1553
 
1225
1554
  args.push(stlFilePath);
1226
- // Override outputFilePath because Bambu-compatible slicers produce 3MF, not G-code.
1555
+ // Override outputFilePath for bambustudio since it produces 3MF, not gcode
1227
1556
  outputFilePath = bambuOutputPath;
1228
1557
  }
1229
1558
  break;
package/src/types.ts CHANGED
@@ -56,3 +56,44 @@ export interface ThreeMFData {
56
56
  build: { items: ThreeMFBuildItem[] };
57
57
  slicerConfig?: Partial<BambuSlicerConfig>;
58
58
  }
59
+
60
+ export interface CollarCharmRole {
61
+ role: 'inner' | 'outer';
62
+ objectIndex: number;
63
+ name: string;
64
+ area: number;
65
+ filamentPosition: number;
66
+ }
67
+
68
+ export interface CollarCharmAnalysis {
69
+ plateIndex: number;
70
+ usedFilamentPositions: number[];
71
+ amsSlots: number[];
72
+ roles: CollarCharmRole[];
73
+ }
74
+
75
+ export interface ThreeMFFilamentRequirement {
76
+ filamentPosition: number;
77
+ filamentId: number;
78
+ tray_info_idx: string | null;
79
+ type: string | null;
80
+ color: string | null;
81
+ }
82
+
83
+ export interface ThreeMFAmsRequirements {
84
+ plateIndex: number;
85
+ usedFilamentPositions: number[];
86
+ filaments: ThreeMFFilamentRequirement[];
87
+ }
88
+
89
+ export interface ThreeMFPlateObject {
90
+ id: number;
91
+ name: string;
92
+ area: number | null;
93
+ bbox: unknown;
94
+ }
95
+
96
+ export interface ThreeMFPlateObjects {
97
+ plateIndex: number;
98
+ objects: ThreeMFPlateObject[];
99
+ }