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