1use crate::error::Result;
13use crate::model::stats::{
14 AssemblyItem, BambuMeshStat, BambuObjectMetadata, BambuPartMetadata, BambuProfileConfig,
15 BambuProjectSettings, PartSubtype, PlateInfo, PlateModelInstance, SlicerWarning,
16};
17use crate::parser::xml_parser::{XmlParser, get_attribute};
18use quick_xml::events::Event;
19use serde_json::Value;
20use std::io::Cursor;
21
22#[derive(Debug, Clone, Default)]
26pub struct SliceInfoData {
27 pub client_type: Option<String>,
29 pub client_version: Option<String>,
31 pub plates: Vec<SlicePlateInfo>,
33}
34
35#[derive(Debug, Clone, Default)]
37pub struct SlicePlateInfo {
38 pub id: u32,
39 pub prediction: Option<u32>, pub weight: Option<f32>, pub filaments: Vec<SliceFilamentUsage>,
42 pub warnings: Vec<SlicerWarning>,
43 pub objects: Vec<SliceObjectInfo>,
44}
45
46#[derive(Debug, Clone, Default)]
48pub struct SliceFilamentUsage {
49 pub id: u32,
50 pub tray_info_idx: Option<String>,
51 pub type_: Option<String>,
52 pub color: Option<String>,
53 pub used_m: Option<f32>,
54 pub used_g: Option<f32>,
55}
56
57#[derive(Debug, Clone, Default)]
59pub struct SliceObjectInfo {
60 pub id: u32,
61 pub name: Option<String>,
62}
63
64#[derive(Debug, Clone, Default)]
68pub struct ModelSettingsData {
69 pub plates: Vec<PlateInfo>,
71 pub objects: Vec<BambuObjectMetadata>,
73 pub assembly: Vec<AssemblyItem>,
75}
76
77pub fn parse_slice_info(content: &[u8]) -> Result<SliceInfoData> {
84 if content.is_empty() {
85 return Ok(SliceInfoData::default());
86 }
87
88 let mut parser = XmlParser::new(Cursor::new(content));
89 let mut data = SliceInfoData::default();
90
91 loop {
92 match parser.read_next_event()? {
93 Event::Start(e) | Event::Empty(e) => {
94 match e.name().as_ref() {
95 b"header_item" => {
96 let key = get_attribute(&e, b"key");
98 let value = get_attribute(&e, b"value");
99 if let (Some(k), Some(v)) = (key, value) {
100 match k.as_ref() {
101 "X-BBL-Client-Type" => {
102 data.client_type = Some(v.into_owned());
103 }
104 "X-BBL-Client-Version" => {
105 data.client_version = Some(v.into_owned());
106 }
107 _ => {}
108 }
109 }
110 }
111 b"plate" => {
112 let plate = parse_slice_plate(&mut parser)?;
114 data.plates.push(plate);
115 }
116 _ => {}
117 }
118 }
119 Event::Eof => break,
120 _ => {}
121 }
122 }
123
124 Ok(data)
125}
126
127fn parse_slice_plate(parser: &mut XmlParser<Cursor<&[u8]>>) -> Result<SlicePlateInfo> {
128 let mut plate = SlicePlateInfo::default();
129
130 loop {
131 match parser.read_next_event()? {
132 Event::Empty(e) | Event::Start(e) => match e.name().as_ref() {
133 b"metadata" => {
134 let key = get_attribute(&e, b"key");
135 let value = get_attribute(&e, b"value");
136 if let (Some(k), Some(v)) = (key, value) {
137 match k.as_ref() {
138 "index" => {
139 if let Ok(id) = v.parse::<u32>() {
140 plate.id = id;
141 }
142 }
143 "prediction" => {
144 plate.prediction = v.parse::<u32>().ok();
145 }
146 "weight" => {
147 plate.weight = v.parse::<f32>().ok();
148 }
149 _ => {}
150 }
151 }
152 }
153 b"filament" => {
154 let id = get_attribute(&e, b"id")
155 .and_then(|v| v.parse::<u32>().ok())
156 .unwrap_or(0);
157 let tray_info_idx = get_attribute(&e, b"tray_info_idx").map(|v| v.into_owned());
158 let type_ = get_attribute(&e, b"type").map(|v| v.into_owned());
159 let color = get_attribute(&e, b"color").map(|v| v.into_owned());
160 let used_m = get_attribute(&e, b"used_m").and_then(|v| v.parse::<f32>().ok());
161 let used_g = get_attribute(&e, b"used_g").and_then(|v| v.parse::<f32>().ok());
162 plate.filaments.push(SliceFilamentUsage {
163 id,
164 tray_info_idx,
165 type_,
166 color,
167 used_m,
168 used_g,
169 });
170 }
171 b"warning" => {
172 let msg = get_attribute(&e, b"msg")
173 .map(|v| v.into_owned())
174 .unwrap_or_default();
175 let level = get_attribute(&e, b"level").map(|v| v.into_owned());
176 let error_code = get_attribute(&e, b"error_code").map(|v| v.into_owned());
177 plate.warnings.push(SlicerWarning {
178 msg,
179 level,
180 error_code,
181 });
182 }
183 b"object" => {
184 let id = get_attribute(&e, b"identify_id")
185 .and_then(|v| v.parse::<u32>().ok())
186 .unwrap_or(0);
187 let name = get_attribute(&e, b"name").map(|v| v.into_owned());
188 plate.objects.push(SliceObjectInfo { id, name });
189 }
190 _ => {}
191 },
192 Event::End(end) if end.name().as_ref() == b"plate" => break,
193 Event::Eof => break,
194 _ => {}
195 }
196 }
197
198 Ok(plate)
199}
200
201pub fn parse_model_settings(content: &[u8]) -> Result<ModelSettingsData> {
208 if content.is_empty() {
209 return Ok(ModelSettingsData::default());
210 }
211
212 let mut parser = XmlParser::new(Cursor::new(content));
213 let mut data = ModelSettingsData::default();
214
215 loop {
216 match parser.read_next_event()? {
217 Event::Start(e) => match e.name().as_ref() {
218 b"object" => {
219 let id = get_attribute(&e, b"id")
220 .and_then(|v| v.parse::<u32>().ok())
221 .unwrap_or(0);
222 let obj = parse_model_object(&mut parser, id)?;
223 data.objects.push(obj);
224 }
225 b"plate" => {
226 let plate = parse_model_plate(&mut parser)?;
227 data.plates.push(plate);
228 }
229 b"assemble" => {
230 let items = parse_assemble(&mut parser)?;
231 data.assembly.extend(items);
232 }
233 _ => {}
234 },
235 Event::Eof => break,
236 _ => {}
237 }
238 }
239
240 Ok(data)
241}
242
243fn parse_model_object(
244 parser: &mut XmlParser<Cursor<&[u8]>>,
245 id: u32,
246) -> Result<BambuObjectMetadata> {
247 let mut obj = BambuObjectMetadata {
248 id,
249 ..Default::default()
250 };
251
252 loop {
253 match parser.read_next_event()? {
254 Event::Empty(e) | Event::Start(e) => {
255 match e.name().as_ref() {
256 b"metadata" => {
257 let key = get_attribute(&e, b"key");
261 let value = get_attribute(&e, b"value");
262 if let Some(k) = key {
263 if let Some(v) = value {
264 match k.as_ref() {
265 "name" => obj.name = Some(v.into_owned()),
266 "extruder" => {
267 obj.extruder = v.parse::<u32>().ok();
268 }
269 _ => {}
270 }
271 }
272 } else {
273 if let Some(fc) = get_attribute(&e, b"face_count") {
275 obj.face_count = fc.parse::<u64>().ok();
276 }
277 }
278 }
279 b"part" => {
280 let part_id = get_attribute(&e, b"id")
281 .and_then(|v| v.parse::<u32>().ok())
282 .unwrap_or(0);
283 let subtype = get_attribute(&e, b"subtype")
284 .map(|v| PartSubtype::parse(&v))
285 .unwrap_or_default();
286 let part = parse_part(parser, part_id, subtype)?;
288 obj.parts.push(part);
289 }
290 _ => {}
291 }
292 }
293 Event::End(end) if end.name().as_ref() == b"object" => break,
294 Event::Eof => break,
295 _ => {}
296 }
297 }
298
299 Ok(obj)
300}
301
302fn parse_part(
303 parser: &mut XmlParser<Cursor<&[u8]>>,
304 id: u32,
305 subtype: PartSubtype,
306) -> Result<BambuPartMetadata> {
307 let mut part = BambuPartMetadata {
308 id,
309 subtype,
310 ..Default::default()
311 };
312
313 const KNOWN_KEYS: &[&str] = &[
315 "name",
316 "matrix",
317 "source_object_id",
318 "source_volume_id",
319 "source_offset_x",
320 "source_offset_y",
321 "source_offset_z",
322 "source_in_inches",
323 ];
324
325 loop {
326 match parser.read_next_event()? {
327 Event::Empty(e) | Event::Start(e) => match e.name().as_ref() {
328 b"metadata" => {
329 let key = get_attribute(&e, b"key");
330 let value = get_attribute(&e, b"value");
331 if let (Some(k), Some(v)) = (key, value) {
332 let k_str = k.as_ref();
333 match k_str {
334 "name" => part.name = Some(v.into_owned()),
335 "matrix" => part.matrix = Some(v.into_owned()),
336 "source_volume_id" => {
337 let src = part.source.get_or_insert_with(Default::default);
338 src.volume_id = v.parse::<u32>().ok();
339 }
340 "source_offset_x" => {
341 let src = part.source.get_or_insert_with(Default::default);
342 src.offset_x = v.parse::<f64>().ok();
343 }
344 "source_offset_y" => {
345 let src = part.source.get_or_insert_with(Default::default);
346 src.offset_y = v.parse::<f64>().ok();
347 }
348 "source_offset_z" => {
349 let src = part.source.get_or_insert_with(Default::default);
350 src.offset_z = v.parse::<f64>().ok();
351 }
352 other => {
353 if !KNOWN_KEYS.contains(&other) {
354 part.print_overrides
355 .insert(other.to_string(), v.into_owned());
356 }
357 }
358 }
359 }
360 }
361 b"mesh_stat" => {
362 let edges_fixed =
363 get_attribute(&e, b"edges_fixed").and_then(|v| v.parse::<u32>().ok());
364 let degenerate_facets =
365 get_attribute(&e, b"degenerate_facets").and_then(|v| v.parse::<u32>().ok());
366 let facets_removed =
367 get_attribute(&e, b"facets_removed").and_then(|v| v.parse::<u32>().ok());
368 let facets_reversed =
369 get_attribute(&e, b"facets_reversed").and_then(|v| v.parse::<u32>().ok());
370 let backwards_edges =
371 get_attribute(&e, b"backwards_edges").and_then(|v| v.parse::<u32>().ok());
372 part.mesh_stat = Some(BambuMeshStat {
373 edges_fixed,
374 degenerate_facets,
375 facets_removed,
376 facets_reversed,
377 backwards_edges,
378 });
379 }
380 _ => {}
381 },
382 Event::End(end) if end.name().as_ref() == b"part" => break,
383 Event::Eof => break,
384 _ => {}
385 }
386 }
387
388 Ok(part)
389}
390
391fn parse_model_plate(parser: &mut XmlParser<Cursor<&[u8]>>) -> Result<PlateInfo> {
392 let mut plate = PlateInfo::default();
393
394 loop {
395 match parser.read_next_event()? {
396 Event::Empty(e) | Event::Start(e) => match e.name().as_ref() {
397 b"metadata" => {
398 let key = get_attribute(&e, b"key");
399 let value = get_attribute(&e, b"value");
400 if let (Some(k), Some(v)) = (key, value) {
401 match k.as_ref() {
402 "plater_id" => {
403 if let Ok(id) = v.parse::<u32>() {
404 plate.id = id;
405 }
406 }
407 "plater_name" => {
408 if !v.is_empty() {
409 plate.name = Some(v.into_owned());
410 }
411 }
412 "locked" => {
413 plate.locked = v == "true";
414 }
415 "gcode_file" => {
416 plate.gcode_file = Some(v.into_owned());
417 }
418 "thumbnail_file" => {
419 plate.thumbnail_file = Some(v.into_owned());
420 }
421 _ => {}
422 }
423 }
424 }
425 b"model_instance" => {
426 let instance = parse_model_instance(parser)?;
427 plate.items.push(instance);
428 }
429 _ => {}
430 },
431 Event::End(end) if end.name().as_ref() == b"plate" => break,
432 Event::Eof => break,
433 _ => {}
434 }
435 }
436
437 Ok(plate)
438}
439
440fn parse_model_instance(parser: &mut XmlParser<Cursor<&[u8]>>) -> Result<PlateModelInstance> {
441 let mut instance = PlateModelInstance::default();
442
443 loop {
444 match parser.read_next_event()? {
445 Event::Empty(e) | Event::Start(e) => {
446 if e.name().as_ref() == b"metadata" {
447 let key = get_attribute(&e, b"key");
448 let value = get_attribute(&e, b"value");
449 if let (Some(k), Some(v)) = (key, value) {
450 match k.as_ref() {
451 "object_id" => {
452 instance.object_id = v.parse::<u32>().unwrap_or(0);
453 }
454 "instance_id" => {
455 instance.instance_id = v.parse::<u32>().unwrap_or(0);
456 }
457 "identify_id" => {
458 instance.identify_id = v.parse::<u32>().ok();
459 }
460 _ => {}
461 }
462 }
463 }
464 }
465 Event::End(end) if end.name().as_ref() == b"model_instance" => break,
466 Event::Eof => break,
467 _ => {}
468 }
469 }
470
471 Ok(instance)
472}
473
474fn parse_assemble(parser: &mut XmlParser<Cursor<&[u8]>>) -> Result<Vec<AssemblyItem>> {
475 let mut items = Vec::new();
476
477 loop {
478 match parser.read_next_event()? {
479 Event::Empty(e) | Event::Start(e) => {
480 if e.name().as_ref() == b"assemble_item" {
481 let object_id = get_attribute(&e, b"object_id")
482 .and_then(|v| v.parse::<u32>().ok())
483 .unwrap_or(0);
484 let instance_count = get_attribute(&e, b"instance_count")
485 .and_then(|v| v.parse::<u32>().ok())
486 .unwrap_or(1);
487 let transform = get_attribute(&e, b"transform").map(|v| v.into_owned());
488 let offset = get_attribute(&e, b"offset").map(|v| v.into_owned());
489 items.push(AssemblyItem {
490 object_id,
491 instance_count,
492 transform,
493 offset,
494 });
495 }
496 }
497 Event::End(end) if end.name().as_ref() == b"assemble" => break,
498 Event::Eof => break,
499 _ => {}
500 }
501 }
502
503 Ok(items)
504}
505
506const GCODE_BLOB_KEYS: &[&str] = &[
510 "change_filament_gcode",
511 "machine_end_gcode",
512 "machine_start_gcode",
513 "time_lapse_gcode",
514 "before_layer_change_gcode",
515 "layer_change_gcode",
516 "printer_start_gcode",
517 "printer_end_gcode",
518 "toolchange_gcode",
519 "gcode_end",
520 "gcode_start",
521];
522
523pub fn parse_project_settings(content: &[u8]) -> Result<BambuProjectSettings> {
530 if content.is_empty() {
531 return Ok(BambuProjectSettings::default());
532 }
533
534 let Ok(text) = std::str::from_utf8(content) else {
535 return Ok(BambuProjectSettings::default());
536 };
537
538 let Ok(json): std::result::Result<serde_json::Map<String, Value>, _> =
539 serde_json::from_str(text)
540 else {
541 return Ok(BambuProjectSettings::default());
542 };
543
544 let mut settings = BambuProjectSettings::default();
545
546 for (key, value) in &json {
547 if GCODE_BLOB_KEYS.contains(&key.as_str()) {
549 continue;
550 }
551
552 match key.as_str() {
553 "printer_model" => {
554 settings.printer_model = value.as_str().map(|s| s.to_string());
555 }
556 "inherits" => {
557 settings.printer_inherits = value.as_str().map(|s| s.to_string());
558 }
559 "bed_type" | "curr_bed_type" => {
560 if settings.bed_type.is_none() {
562 settings.bed_type = json_to_string_opt(value);
563 }
564 }
565 "layer_height" => {
566 settings.layer_height = json_to_f32(value);
567 }
568 "first_layer_height" => {
569 settings.first_layer_height = json_to_f32(value);
570 }
571 "filament_type" => {
572 settings.filament_type = json_to_string_vec(value);
573 }
574 "filament_colour" => {
575 settings.filament_colour = json_to_string_vec(value);
576 }
577 "nozzle_diameter" => {
578 settings.nozzle_diameter = json_to_f32_vec(value);
579 }
580 "print_sequence" => {
581 settings.print_sequence = value.as_str().map(|s| s.to_string());
582 }
583 "wall_loops" => {
584 settings.wall_loops = json_to_u32(value);
585 }
586 "sparse_infill_density" => {
587 settings.infill_density = value.as_str().map(|s| s.to_string());
588 }
589 "support_type" => {
590 settings.support_type = value.as_str().map(|s| s.to_string());
591 }
592 _ => {
593 settings.extras.insert(key.clone(), value.clone());
595 }
596 }
597 }
598
599 if let Some(curr) = json.get("curr_bed_type").and_then(|v| v.as_str()) {
601 settings.bed_type = Some(curr.to_string());
602 }
603
604 Ok(settings)
605}
606
607pub fn parse_profile_config(
617 content: &[u8],
618 config_type: &str,
619 index: u32,
620) -> Result<BambuProfileConfig> {
621 if content.is_empty() {
622 return Ok(BambuProfileConfig {
623 config_type: config_type.to_string(),
624 index,
625 ..Default::default()
626 });
627 }
628
629 let Ok(text) = std::str::from_utf8(content) else {
630 return Ok(BambuProfileConfig {
631 config_type: config_type.to_string(),
632 index,
633 ..Default::default()
634 });
635 };
636
637 let Ok(json): std::result::Result<serde_json::Map<String, Value>, _> =
638 serde_json::from_str(text)
639 else {
640 return Ok(BambuProfileConfig {
641 config_type: config_type.to_string(),
642 index,
643 ..Default::default()
644 });
645 };
646
647 let mut profile = BambuProfileConfig {
648 config_type: config_type.to_string(),
649 index,
650 ..Default::default()
651 };
652
653 for (key, value) in &json {
654 if GCODE_BLOB_KEYS.contains(&key.as_str()) {
655 continue;
656 }
657 match key.as_str() {
658 "inherits" => {
659 profile.inherits = value.as_str().map(|s| s.to_string());
660 }
661 "name" => {
662 profile.name = value.as_str().map(|s| s.to_string());
663 }
664 _ => {
665 profile.extras.insert(key.clone(), value.clone());
666 }
667 }
668 }
669
670 Ok(profile)
671}
672
673fn json_to_string_opt(v: &Value) -> Option<String> {
676 match v {
677 Value::String(s) => Some(s.clone()),
678 Value::Array(arr) => arr.first().and_then(|x| x.as_str()).map(|s| s.to_string()),
679 _ => None,
680 }
681}
682
683fn json_to_f32(v: &Value) -> Option<f32> {
684 match v {
685 Value::Number(n) => n.as_f64().map(|x| x as f32),
686 Value::String(s) => s.trim().parse::<f32>().ok(),
687 _ => None,
688 }
689}
690
691fn json_to_u32(v: &Value) -> Option<u32> {
692 match v {
693 Value::Number(n) => n.as_u64().map(|x| x as u32),
694 Value::String(s) => s.trim().parse::<u32>().ok(),
695 _ => None,
696 }
697}
698
699fn json_to_string_vec(v: &Value) -> Vec<String> {
700 match v {
701 Value::Array(arr) => arr
702 .iter()
703 .filter_map(|x| x.as_str().map(|s| s.to_string()))
704 .collect(),
705 Value::String(s) => vec![s.clone()],
706 _ => vec![],
707 }
708}
709
710fn json_to_f32_vec(v: &Value) -> Vec<f32> {
711 match v {
712 Value::Array(arr) => arr
713 .iter()
714 .filter_map(|x| match x {
715 Value::Number(n) => n.as_f64().map(|f| f as f32),
716 Value::String(s) => s.trim().parse::<f32>().ok(),
717 _ => None,
718 })
719 .collect(),
720 Value::Number(n) => n.as_f64().map(|f| vec![f as f32]).unwrap_or_default(),
721 Value::String(s) => s.trim().parse::<f32>().map(|f| vec![f]).unwrap_or_default(),
722 _ => vec![],
723 }
724}
725
726#[cfg(test)]
729mod tests {
730 use super::*;
731
732 #[test]
735 fn test_parse_slice_info_empty() {
736 let result = parse_slice_info(b"").unwrap();
737 assert!(result.client_type.is_none());
738 assert!(result.plates.is_empty());
739 }
740
741 #[test]
742 fn test_parse_slice_info_invalid_xml() {
743 let _ = parse_slice_info(b"not xml <<< garbage >>>");
745 }
746
747 #[test]
748 fn test_parse_slice_info_header_only() {
749 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
750<config>
751 <header>
752 <header_item key="X-BBL-Client-Type" value="slicer"/>
753 <header_item key="X-BBL-Client-Version" value="02.02.01.60"/>
754 </header>
755</config>"#;
756 let result = parse_slice_info(xml).unwrap();
757 assert_eq!(result.client_type.as_deref(), Some("slicer"));
758 assert_eq!(result.client_version.as_deref(), Some("02.02.01.60"));
759 assert!(result.plates.is_empty(), "SimplePyramid has no plates");
760 }
761
762 #[test]
763 fn test_parse_slice_info_with_plate() {
764 let xml = br##"<?xml version="1.0" encoding="UTF-8"?>
765<config>
766 <header>
767 <header_item key="X-BBL-Client-Type" value="slicer"/>
768 <header_item key="X-BBL-Client-Version" value="01.10.02.73"/>
769 </header>
770 <plate>
771 <metadata key="index" value="1"/>
772 <metadata key="prediction" value="1895"/>
773 <metadata key="weight" value="11.57"/>
774 <filament id="1" tray_info_idx="GFA00" type="PLA" color="#FFFFFF" used_m="3.82" used_g="11.57" />
775 <warning msg="bed_temp_too_high" level="1" error_code="1000C001" />
776 <object identify_id="145" name="3DBenchy.stl" skipped="false" />
777 </plate>
778</config>"##;
779 let result = parse_slice_info(xml).unwrap();
780 assert_eq!(result.client_type.as_deref(), Some("slicer"));
781 assert_eq!(result.plates.len(), 1);
782
783 let plate = &result.plates[0];
784 assert_eq!(plate.id, 1);
785 assert_eq!(plate.prediction, Some(1895));
786 assert!((plate.weight.unwrap() - 11.57_f32).abs() < 0.01);
787
788 assert_eq!(plate.filaments.len(), 1);
789 assert_eq!(plate.filaments[0].id, 1);
790 assert_eq!(plate.filaments[0].tray_info_idx.as_deref(), Some("GFA00"));
791 assert_eq!(plate.filaments[0].type_.as_deref(), Some("PLA"));
792 assert!((plate.filaments[0].used_g.unwrap() - 11.57_f32).abs() < 0.01);
793
794 assert_eq!(plate.warnings.len(), 1);
795 assert_eq!(plate.warnings[0].msg, "bed_temp_too_high");
796
797 assert_eq!(plate.objects.len(), 1);
798 assert_eq!(plate.objects[0].id, 145);
799 assert_eq!(plate.objects[0].name.as_deref(), Some("3DBenchy.stl"));
800 }
801
802 #[test]
805 fn test_parse_model_settings_empty() {
806 let result = parse_model_settings(b"").unwrap();
807 assert!(result.plates.is_empty());
808 assert!(result.objects.is_empty());
809 assert!(result.assembly.is_empty());
810 }
811
812 #[test]
813 fn test_parse_model_settings_invalid_xml() {
814 let _ = parse_model_settings(b"<bad xml");
815 }
816
817 #[test]
818 fn test_parse_model_settings_with_object() {
819 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
820<config>
821 <object id="8">
822 <metadata key="name" value="3DBenchy.stl"/>
823 <metadata key="extruder" value="1"/>
824 <metadata face_count="225154"/>
825 <part id="1" subtype="normal_part">
826 <metadata key="name" value="3DBenchy.stl"/>
827 <metadata key="matrix" value="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1"/>
828 <metadata key="source_volume_id" value="0"/>
829 <metadata key="inner_wall_speed" value="50"/>
830 <mesh_stat face_count="225154" edges_fixed="0" degenerate_facets="0" facets_removed="0" facets_reversed="0" backwards_edges="0"/>
831 </part>
832 </object>
833 <plate>
834 <metadata key="plater_id" value="1"/>
835 <metadata key="plater_name" value=""/>
836 <metadata key="locked" value="false"/>
837 <metadata key="gcode_file" value="Metadata/plate_1.gcode"/>
838 <metadata key="thumbnail_file" value="Metadata/plate_1.png"/>
839 <model_instance>
840 <metadata key="object_id" value="8"/>
841 <metadata key="instance_id" value="0"/>
842 <metadata key="identify_id" value="145"/>
843 </model_instance>
844 </plate>
845 <assemble>
846 <assemble_item object_id="8" instance_id="0" transform="1 0 0 0 1 0 0 0 1 0 0 0" offset="0 0 0" />
847 </assemble>
848</config>"#;
849 let result = parse_model_settings(xml).unwrap();
850
851 assert_eq!(result.objects.len(), 1);
853 let obj = &result.objects[0];
854 assert_eq!(obj.id, 8);
855 assert_eq!(obj.name.as_deref(), Some("3DBenchy.stl"));
856 assert_eq!(obj.extruder, Some(1));
857 assert_eq!(obj.face_count, Some(225154));
858 assert_eq!(obj.parts.len(), 1);
859
860 let part = &obj.parts[0];
861 assert_eq!(part.id, 1);
862 assert_eq!(part.subtype, PartSubtype::NormalPart);
863 assert_eq!(part.name.as_deref(), Some("3DBenchy.stl"));
864 assert!(part.matrix.is_some());
865 assert!(part.source.is_some());
866 assert!(part.mesh_stat.is_some());
867 assert!(part.print_overrides.contains_key("inner_wall_speed"));
869
870 assert_eq!(result.plates.len(), 1);
872 let plate = &result.plates[0];
873 assert_eq!(plate.id, 1);
874 assert!(!plate.locked);
875 assert_eq!(plate.gcode_file.as_deref(), Some("Metadata/plate_1.gcode"));
876 assert_eq!(
877 plate.thumbnail_file.as_deref(),
878 Some("Metadata/plate_1.png")
879 );
880 assert_eq!(plate.items.len(), 1);
881 assert_eq!(plate.items[0].object_id, 8);
882 assert_eq!(plate.items[0].identify_id, Some(145));
883
884 assert_eq!(result.assembly.len(), 1);
886 assert_eq!(result.assembly[0].object_id, 8);
887 assert!(result.assembly[0].transform.is_some());
888 }
889
890 #[test]
893 fn test_parse_project_settings_empty() {
894 let result = parse_project_settings(b"").unwrap();
895 assert!(result.printer_model.is_none());
896 assert!(result.filament_type.is_empty());
897 }
898
899 #[test]
900 fn test_parse_project_settings_invalid_json() {
901 let result = parse_project_settings(b"not json {{{").unwrap();
902 assert!(result.printer_model.is_none());
903 }
904
905 #[test]
906 fn test_parse_project_settings_empty_object() {
907 let result = parse_project_settings(b"{}").unwrap();
908 assert!(result.printer_model.is_none());
909 assert!(result.extras.is_empty());
910 }
911
912 #[test]
913 fn test_parse_project_settings_basic() {
914 let json = serde_json::json!({
915 "printer_model": "Bambu Lab A1",
916 "curr_bed_type": "Textured PEI Plate",
917 "layer_height": 0.2,
918 "filament_type": ["PLA"],
919 "filament_colour": ["#FFFFFF"],
920 "nozzle_diameter": ["0.4"],
921 "wall_loops": "3",
922 "sparse_infill_density": "15%",
923 "support_type": "normal(auto)",
924 "change_filament_gcode": "this is a big gcode blob that should be skipped",
925 "some_extra_key": "some_value"
926 })
927 .to_string();
928 let result = parse_project_settings(json.as_bytes()).unwrap();
929
930 assert_eq!(result.printer_model.as_deref(), Some("Bambu Lab A1"));
931 assert_eq!(result.bed_type.as_deref(), Some("Textured PEI Plate"));
932 assert!((result.layer_height.unwrap() - 0.2_f32).abs() < 0.001);
933 assert_eq!(result.filament_type, vec!["PLA"]);
934 assert_eq!(result.filament_colour, vec!["#FFFFFF"]);
935 assert_eq!(result.nozzle_diameter, vec![0.4_f32]);
936 assert_eq!(result.wall_loops, Some(3));
937 assert_eq!(result.infill_density.as_deref(), Some("15%"));
938 assert_eq!(result.support_type.as_deref(), Some("normal(auto)"));
939 assert!(!result.extras.contains_key("change_filament_gcode"));
941 assert!(result.extras.contains_key("some_extra_key"));
943 }
944
945 #[test]
948 fn test_parse_profile_config_empty() {
949 let result = parse_profile_config(b"", "filament", 1).unwrap();
950 assert_eq!(result.config_type, "filament");
951 assert_eq!(result.index, 1);
952 assert!(result.inherits.is_none());
953 assert!(result.name.is_none());
954 }
955
956 #[test]
957 fn test_parse_profile_config_invalid_json() {
958 let result = parse_profile_config(b"not json", "machine", 2).unwrap();
959 assert_eq!(result.config_type, "machine");
960 assert_eq!(result.index, 2);
961 }
962
963 #[test]
964 fn test_parse_profile_config_basic() {
965 let json = serde_json::json!({
966 "inherits": "Bambu PLA Basic @BBL P1P",
967 "name": "Bambu PLA Basic @BBL P1P(project.3mf)",
968 "filament_type": ["PLA"],
969 "change_filament_gcode": "gcode blob to skip"
970 })
971 .to_string();
972 let result = parse_profile_config(json.as_bytes(), "filament", 1).unwrap();
973
974 assert_eq!(result.config_type, "filament");
975 assert_eq!(result.index, 1);
976 assert_eq!(result.inherits.as_deref(), Some("Bambu PLA Basic @BBL P1P"));
977 assert_eq!(
978 result.name.as_deref(),
979 Some("Bambu PLA Basic @BBL P1P(project.3mf)")
980 );
981 assert!(!result.extras.contains_key("change_filament_gcode"));
983 assert!(result.extras.contains_key("filament_type"));
985 }
986}