1#![deny(
41 clippy::unwrap_used,
42 clippy::expect_used,
43 clippy::indexing_slicing,
44 clippy::panic,
45 clippy::unreachable
46)]
47
48pub const STRUCTURE_STREAM: &str = "/Root/FastLoad/Structure";
50
51const MAX_INSTANCES: usize = 100_000;
53const MAX_OBJECTS: usize = 1 << 20;
55const BASE: usize = 0x0e;
57
58#[derive(Clone, Debug, PartialEq)]
60pub struct Component {
61 pub name: String,
63 pub rotation: [[f64; 3]; 3],
65 pub translation: [f64; 3],
67 pub layer: Option<u32>,
69 pub occurrence: Option<crate::invplace::Occurrence>,
72 pub document: Option<String>,
74}
75
76impl Component {
77 pub fn place(&self, p: [f64; 3]) -> [f64; 3] {
79 let r = &self.rotation;
80 let t = &self.translation;
81 [
82 p[0] * r[0][0] + p[1] * r[1][0] + p[2] * r[2][0] + t[0],
83 p[0] * r[0][1] + p[1] * r[1][1] + p[2] * r[2][1] + t[1],
84 p[0] * r[0][2] + p[1] * r[1][2] + p[2] * r[2][2] + t[2],
85 ]
86 }
87
88 pub fn within(&self, parent: &Component) -> Component {
90 let mut rotation = [[0.0; 3]; 3];
91 for (row, out) in self.rotation.iter().zip(rotation.iter_mut()) {
92 for (k, o) in out.iter_mut().enumerate() {
93 *o = parent
94 .rotation
95 .iter()
96 .zip(row)
97 .map(|(prow, &a)| a * prow.get(k).copied().unwrap_or(0.0))
98 .sum();
99 }
100 }
101 Component {
102 name: self.name.clone(),
103 rotation,
104 translation: parent.place(self.translation),
105 layer: self.layer,
106 occurrence: self.occurrence.clone(),
107 document: self.document.clone(),
108 }
109 }
110}
111
112pub fn identity() -> Component {
114 Component {
115 name: String::new(),
116 rotation: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
117 translation: [0.0; 3],
118 layer: None,
119 occurrence: None,
120 document: None,
121 }
122}
123
124pub fn components(bytes: &[u8]) -> Option<Vec<Component>> {
127 let streams = crate::nxpart::streams(bytes);
128 let stream = streams.iter().find(|s| s.name == STRUCTURE_STREAM)?;
129 structure(crate::nxpart::data(bytes, stream)?)
130}
131
132#[derive(Clone, Debug, PartialEq)]
135pub enum Kind {
136 Part(Vec<Component>),
139 Assembly(Vec<Component>),
141 Other,
143}
144
145const MAX_STREAM: u64 = 256 * 1024 * 1024;
147
148pub fn kind_of(path: &std::path::Path) -> std::io::Result<Kind> {
151 let file = std::io::BufReader::new(std::fs::File::open(path)?);
152 let Some(mut part) = crate::nxpart::NxPart::open(file)? else {
153 return Ok(Kind::Other);
154 };
155 let has_jt = part
156 .streams()
157 .iter()
158 .any(|s| s.name == crate::nxpart::JT_STREAM);
159 let mut components = match part.read(STRUCTURE_STREAM, MAX_STREAM)? {
160 Some(stream) => structure(&stream).unwrap_or_default(),
161 None => Vec::new(),
162 };
163 if components.iter().any(|c| c.layer.is_some()) {
164 let layers = part
165 .read(UG_PART_STREAM, MAX_STREAM)?
166 .and_then(|u| visible_layers(&u));
167 if let Some(layers) = layers {
168 components.retain(|c| c.layer.is_none_or(|l| layers.is_visible(l)));
169 }
170 }
171 Ok(match (has_jt, components.is_empty()) {
172 (true, _) => Kind::Part(components),
173 (false, false) => Kind::Assembly(components),
174 (false, true) => Kind::Other,
175 })
176}
177
178pub fn structure(s: &[u8]) -> Option<Vec<Component>> {
180 if s.get(0x0c..0x0e)? != b"OM" {
181 return None;
182 }
183 let objects = object_table(s)?;
184 let (start, end) = objects
185 .iter()
186 .copied()
187 .max_by_key(|&(a, b)| b.saturating_sub(a))?;
188 let records = find_records(s, start, end);
189 let names_from = records.as_ref().map_or(start, |r| r.1);
190 let (names, index) = find_names(s, names_from, end)?;
191 let records = records.map(|r| r.0).unwrap_or_default();
192 let n = index.len();
193 let map = if records.is_empty() {
194 None
195 } else {
196 Some(record_map(s, &objects, n, records.len())?)
197 };
198 let layers = layer_map(s, &objects, n);
199 let mut out = Vec::with_capacity(n);
200 for (i, &name_index) in index.iter().enumerate() {
201 let name = names.get(name_index.checked_sub(1)?)?.clone();
202 let record = map
203 .as_ref()
204 .and_then(|m| m.get(i).copied().flatten())
205 .and_then(|r| records.get(r));
206 let mut c = identity();
207 c.name = name;
208 if let Some(&(rotation, translation)) = record {
209 c.rotation = rotation;
210 c.translation = translation;
211 }
212 c.layer = layers.get(&i).copied();
213 out.push(c);
214 }
215 Some(out)
216}
217
218fn layer_map(
226 s: &[u8],
227 objects: &[(usize, usize)],
228 instances: usize,
229) -> std::collections::HashMap<usize, u32> {
230 let maps: Vec<Vec<(usize, Option<usize>)>> = objects
231 .iter()
232 .filter_map(|&(a, b)| int_map(s, a, b))
233 .filter(|m| !m.is_empty() && m.len() < instances)
234 .collect();
235 let keys = |m: &Vec<(usize, Option<usize>)>| {
236 let mut k: Vec<usize> = m.iter().map(|e| e.0).collect();
237 k.sort_unstable();
238 k
239 };
240 let candidates: Vec<&Vec<(usize, Option<usize>)>> = maps
241 .iter()
242 .filter(|m| {
243 m.iter()
244 .all(|&(k, v)| k < instances && v.is_some_and(|v| (1..=256).contains(&v)))
245 })
246 .filter(|m| maps.iter().filter(|o| keys(o) == keys(m)).count() == 1)
247 .collect();
248 match candidates.as_slice() {
249 [only] => only
250 .iter()
251 .filter_map(|&(k, v)| Some((k, u32::try_from(v?).ok()?)))
252 .collect(),
253 _ => std::collections::HashMap::new(),
254 }
255}
256
257pub const UG_PART_STREAM: &str = "/Root/UG_PART/UG_PART";
259
260#[derive(Clone, Debug, PartialEq, Eq)]
262pub struct Layers {
263 visible: [u32; 9],
264 pub work: u32,
266}
267
268impl Layers {
269 pub fn is_visible(&self, layer: u32) -> bool {
270 layer == self.work
271 || self
272 .visible
273 .get((layer / 32) as usize)
274 .is_some_and(|w| w >> (layer % 32) & 1 == 1)
275 }
276}
277
278fn sint(s: &[u8], at: usize) -> Option<(i64, usize)> {
281 let b = *s.get(at)?;
282 let byte = |k: usize| s.get(at + k).map(|&v| i64::from(v));
283 let (v, n, bits) = match b {
284 0x00..=0x7f => return Some((i64::from(b) - 1, 1)),
285 0x80..=0x9f => ((i64::from(b & 0x1f) << 8) | byte(1)?, 2, 13),
286 0xa0..=0xbf => (
287 (i64::from(b & 0x1f) << 16) | (byte(1)? << 8) | byte(2)?,
288 3,
289 21,
290 ),
291 0xc0..=0xdf => (
292 (i64::from(b & 0x1f) << 24) | (byte(1)? << 16) | (byte(2)? << 8) | byte(3)?,
293 4,
294 29,
295 ),
296 0xe0 | 0xff => (
297 i64::from(u32::from_be_bytes(s.get(at + 1..at + 5)?.try_into().ok()?)),
298 5,
299 32,
300 ),
301 _ => return None,
302 };
303 Some((
304 if v >> (bits - 1) != 0 {
305 v - (1 << bits)
306 } else {
307 v
308 },
309 n,
310 ))
311}
312
313pub fn visible_layers(ug_part: &[u8]) -> Option<Layers> {
320 const SIGNATURE: [u8; 5] = [0x00, 0x5f, 0x01, 0x04, 0x01];
321 let mut found = None;
322 let mut count = 0;
323 let mut from = 0;
324 while let Some(rel) = ug_part
325 .get(from..)
326 .and_then(|rest| rest.windows(SIGNATURE.len()).position(|w| w == SIGNATURE))
327 {
328 let at = from + rel;
329 from = at + 1;
330 if let Some(layers) = layer_parameter(ug_part, at + SIGNATURE.len()) {
331 count += 1;
332 found = Some(layers);
333 }
334 }
335 if count == 1 {
336 found
337 } else {
338 None
339 }
340}
341
342fn layer_parameter(s: &[u8], mut at: usize) -> Option<Layers> {
344 let mut masks = [[0u32; 9]; 4];
345 for mask in &mut masks {
346 for word in 0..17 {
347 let (v, n) = sint(s, at)?;
348 at += n;
349 if let Some(slot) = mask.get_mut(word) {
350 *slot = v as u32;
351 }
352 }
353 }
354 let (work, n) = sint(s, at)?;
355 at += n;
356 let (one, n) = sint(s, at)?;
357 at += n;
358 let (another, _) = sint(s, at)?;
359 let [selectable, visible, _, _] = masks;
360 let contained = selectable.iter().zip(&visible).all(|(a, b)| a & !b == 0);
361 if !(1..=256).contains(&work) || one != 1 || another != 1 || !contained {
362 return None;
363 }
364 Some(Layers {
365 visible,
366 work: work as u32,
367 })
368}
369
370fn u32_at(s: &[u8], at: usize) -> Option<usize> {
371 let b = s.get(at..at.checked_add(4)?)?;
372 usize::try_from(u32::from_le_bytes(b.try_into().ok()?)).ok()
373}
374
375fn object_table(s: &[u8]) -> Option<Vec<(usize, usize)>> {
377 let mut p: usize = 0x1f;
379 for _ in 0..2 {
380 p = p.checked_add(4)?.checked_add(u32_at(s, p)?)?;
381 }
382 let n = u32_at(s, p)?;
383 if n == 0 || n > MAX_OBJECTS || n.checked_mul(8)? >= s.len() {
384 return None;
385 }
386 let table_bytes = 4 * n;
387 let last = s.len().checked_sub(table_bytes * 2 + 4)?;
388 for at in p + 4..=last {
389 if u32_at(s, at + table_bytes) != Some(n) {
390 continue;
391 }
392 let offsets: Option<Vec<usize>> = (0..n).map(|k| u32_at(s, at + 4 * k)).collect();
393 let Some(offsets) = offsets else { continue };
394 let increasing = offsets.windows(2).all(|w| matches!(w, [a, b] if a < b));
395 let (Some(&first), Some(&end)) = (offsets.first(), offsets.last()) else {
396 continue;
397 };
398 if increasing && first + BASE > at && end + BASE < s.len() {
399 let starts: Vec<usize> = offsets.iter().map(|o| o + BASE).collect();
400 let mut out = Vec::with_capacity(n);
401 for (k, &a) in starts.iter().enumerate() {
402 out.push((a, starts.get(k + 1).copied().unwrap_or(s.len())));
403 }
404 return Some(out);
405 }
406 }
407 None
408}
409
410fn varint(s: &[u8], at: usize) -> Option<(usize, usize)> {
412 let b = *s.get(at)?;
413 let byte = |k: usize| s.get(at + k).map(|&v| usize::from(v));
414 Some(match b {
415 0x00..=0x7f => (usize::from(b), 1),
416 0x80..=0x9f => ((usize::from(b & 0x1f) << 8) | byte(1)?, 2),
417 0xa0..=0xbf => (
418 (usize::from(b & 0x1f) << 16) | (byte(1)? << 8) | byte(2)?,
419 3,
420 ),
421 0xe0 | 0xff => {
422 let v = u32::from_be_bytes(s.get(at + 1..at + 5)?.try_into().ok()?);
423 (usize::try_from(v).ok()?, 5)
424 }
425 _ => return None,
426 })
427}
428
429fn number(s: &[u8], at: usize) -> Option<(f64, usize)> {
431 let t = *s.get(at)?;
432 let double = |first: u8, rest: &[u8]| {
434 let mut b = [0u8; 8];
435 for (d, &v) in b.iter_mut().zip(std::iter::once(&first).chain(rest)) {
436 *d = v;
437 }
438 f64::from_be_bytes(b) / 4.0
439 };
440 let v = match t & 0x70 {
441 0x20 | 0x30 => (double(t.wrapping_add(0x10), s.get(at + 1..at + 8)?), 8),
442 0x40 | 0x50 => (double(t.wrapping_sub(0x10), s.get(at + 1..at + 4)?), 4),
443 _ => match t {
444 0x00 | 0x80 => (0.0, 1),
445 0x01 => (1.0, 1),
446 0x81 => (-1.0, 1),
447 _ => return None,
448 },
449 };
450 (v.0.is_finite()).then_some((if v.0.abs() < 1e-9 { 0.0 } else { v.0 }, v.1))
451}
452
453type Placement = ([[f64; 3]; 3], [f64; 3]);
454
455fn record(s: &[u8], at: usize) -> Option<(Placement, usize)> {
457 let mut v = [0.0f64; 9];
458 let mut p = at;
459 for slot in &mut v {
460 let (x, n) = number(s, p)?;
461 *slot = x;
462 p += n;
463 }
464 let [a0, a1, a2, b0, b1, b2, x, y, z] = v;
465 let (r0, r1) = ([a0, a1, a2], [b0, b1, b2]);
466 let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
467 let unit = |a| (dot(a, a) - 1.0).abs() < 1e-3;
468 if !(unit(r0)
469 && unit(r1)
470 && dot(r0, r1).abs() < 1e-3
471 && [x, y, z].iter().all(|c| c.abs() < 1e9))
472 {
473 return None;
474 }
475 let r2 = [a1 * b2 - a2 * b1, a2 * b0 - a0 * b2, a0 * b1 - a1 * b0];
476 Some((([r0, r1, r2], [x, y, z]), p))
477}
478
479fn find_records(s: &[u8], start: usize, end: usize) -> Option<(Vec<Placement>, usize)> {
482 for p in start..end {
483 if s.get(p) != Some(&0x01) {
484 continue;
485 }
486 let Some((count, n)) = varint(s, p + 1) else {
487 continue;
488 };
489 for numbers in [count, count.wrapping_sub(1)] {
491 if numbers == 0 || numbers % 9 != 0 || numbers / 9 > MAX_INSTANCES {
492 continue;
493 }
494 let mut out = Vec::new();
495 let mut q = p + 1 + n;
496 while out.len() < numbers / 9 {
497 let Some((rec, next)) = record(s, q) else {
498 break;
499 };
500 out.push(rec);
501 q = next;
502 }
503 if out.len() == numbers / 9 && q <= end {
504 return Some((out, q));
505 }
506 }
507 }
508 None
509}
510
511fn find_names(s: &[u8], start: usize, end: usize) -> Option<(Vec<String>, Vec<usize>)> {
513 'candidates: for p in start..end {
514 if s.get(p) != Some(&0x01) {
515 continue;
516 }
517 let Some((m, n)) = varint(s, p + 1) else {
518 continue;
519 };
520 let m = m.wrapping_sub(1);
521 if m == 0 || m > MAX_INSTANCES || s.get(p + 1 + n) != Some(&0x04) {
522 continue;
523 }
524 let mut q = p + 1 + n;
525 let mut names = Vec::with_capacity(m.min(1024));
526 for _ in 0..m {
527 if s.get(q) != Some(&0x04) {
528 continue 'candidates;
529 }
530 let Some((len, k)) = varint(s, q + 1) else {
531 continue 'candidates;
532 };
533 let Some(len) = len.checked_sub(2) else {
534 continue 'candidates;
535 };
536 let text_at = q + 1 + k;
537 let (Some(text), Some(&0)) = (s.get(text_at..text_at + len), s.get(text_at + len))
538 else {
539 continue 'candidates;
540 };
541 let Ok(text) = std::str::from_utf8(text) else {
542 continue 'candidates;
543 };
544 if text.is_empty() || text.chars().any(char::is_control) {
545 continue 'candidates;
546 }
547 names.push(text.to_string());
548 q = text_at + len + 1;
549 }
550 if s.get(q) != Some(&0x01) {
551 continue;
552 }
553 let Some((count, k)) = varint(s, q + 1) else {
554 continue;
555 };
556 let count = count.wrapping_sub(1);
557 if count == 0 || count > MAX_INSTANCES {
558 continue;
559 }
560 let mut r = q + 1 + k;
561 let mut index = Vec::with_capacity(count);
562 for _ in 0..count {
563 let Some((v, k)) = varint(s, r) else {
564 continue 'candidates;
565 };
566 if v == 0 || v > m {
567 continue 'candidates;
568 }
569 index.push(v);
570 r += k;
571 }
572 return Some((names, index));
573 }
574 None
575}
576
577fn record_map(
580 s: &[u8],
581 objects: &[(usize, usize)],
582 instances: usize,
583 records: usize,
584) -> Option<Vec<Option<usize>>> {
585 for &(start, end) in objects {
589 let Some(map) = int_map(s, start, end) else {
590 continue;
591 };
592 if map.len() > instances || map.len() < records {
593 continue;
594 }
595 let mut out = vec![None; instances];
596 let mut seen = vec![false; records];
597 let mut ok = true;
598 for (key, value) in map {
599 let Some(slot) = out.get_mut(key) else {
600 ok = false;
601 break;
602 };
603 let Some(value) = value else { continue };
604 match seen.get_mut(value) {
605 Some(used) => {
606 *used = true;
607 *slot = Some(value);
608 }
609 None => {
610 ok = false;
611 break;
612 }
613 }
614 }
615 if ok && seen.iter().all(|&used| used) {
616 return Some(out);
617 }
618 }
619 None
620}
621
622fn int_map(s: &[u8], start: usize, end: usize) -> Option<Vec<(usize, Option<usize>)>> {
625 let mut p = start;
626 let next = |p: &mut usize| -> Option<usize> {
627 let (v, n) = varint(s, *p)?;
628 *p += n;
629 (*p <= end).then_some(v)
630 };
631 let n = next(&mut p)?.checked_sub(1)?;
632 if s.get(p) != Some(&0x01) {
633 return None;
634 }
635 p += 1;
636 let cap = next(&mut p)?.checked_sub(1)?;
637 if !cap.is_power_of_two() || cap > 1 << 20 || n > cap {
638 return None;
639 }
640 let mut keys = Vec::with_capacity(cap);
641 for _ in 0..cap {
642 keys.push(next(&mut p)?);
643 }
644 if s.get(p) != Some(&0x01) {
645 return None;
646 }
647 p += 1;
648 let words = next(&mut p)?.checked_sub(1)?;
649 if words != cap.div_ceil(16) {
650 return None;
651 }
652 let mut state = Vec::with_capacity(words);
653 for _ in 0..words {
654 state.push(next(&mut p)?);
655 }
656 if s.get(p) != Some(&0x01) {
657 return None;
658 }
659 p += 1;
660 if next(&mut p)?.checked_sub(1)? != cap {
661 return None;
662 }
663 let mut out = Vec::with_capacity(n);
664 for (slot, key) in keys.iter().enumerate() {
665 let value = next(&mut p)?;
666 let word = state.get(slot / 16)?;
667 if (word >> (2 * (slot % 16))) & 3 == 2 {
668 out.push((key.checked_sub(1)?, value.checked_sub(1)));
669 }
670 }
671 (out.len() == n).then_some(out)
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677
678 fn fixture(name: &str) -> Vec<u8> {
679 std::fs::read(
680 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
681 .join("tests/fixtures/cad/nist/mtc-assembly/nx")
682 .join(name),
683 )
684 .unwrap()
685 }
686
687 #[test]
690 fn the_nist_assembly_places_its_36_components() {
691 let c = components(&fixture("NIST mtc crada assembly.prt")).unwrap();
692 assert_eq!(c.len(), 36);
693 let count = |n: &str| c.iter().filter(|x| x.name.ends_with(n)).count();
694 assert_eq!(count("HEX NUT"), 10);
695 assert_eq!(count("91274A118 SHCS"), 14);
696 assert_eq!(count("91274A141 SHCS"), 4);
697 assert_eq!(count("FHSCS"), 4);
698 for single in ["BOX", "COVER", "PLATE", "POLYCARB"] {
699 assert_eq!(count(single), 1, "{single}");
700 }
701 let nut = c.first().unwrap();
703 assert!(nut.name.ends_with("HEX NUT"));
704 let close = |a: f64, b: f64| (a - b).abs() < 1e-4;
705 assert!(close(nut.translation[0], 57.350284) && close(nut.translation[1], 75.387787));
706 assert!(close(nut.translation[2], 88.875));
707 assert_eq!(
708 nut.rotation[2],
709 [0.0, 0.0, -1.0],
710 "third row = row 0 × row 1"
711 );
712 let bx = c.iter().find(|x| x.name.ends_with("BOX")).unwrap();
714 assert_eq!(bx.translation, [0.0; 3]);
715 for x in &c {
717 for row in x.rotation {
718 assert!(close(row.iter().map(|v| v * v).sum(), 1.0), "{x:?}");
719 }
720 }
721 }
722
723 #[test]
724 fn parts_and_garbage_have_no_structure() {
725 assert!(components(&fixture("90591A141 HEX NUT.prt"))
726 .is_none_or(|c| c.is_empty() || c.len() < 2));
727 assert!(components(b"SPLMSSTR").is_none());
728 assert!(structure(&[0; 64]).is_none());
729 let mut s = vec![0u8; 0x0c];
730 s.extend(b"OM");
731 s.resize(0x30, 0);
732 assert!(structure(&s).is_none());
733 }
734
735 #[test]
736 fn damaged_structures_never_panic() {
737 let data = fixture("NIST mtc crada assembly.prt");
738 let streams = crate::nxpart::streams(&data);
739 let st = streams.iter().find(|s| s.name == STRUCTURE_STREAM).unwrap();
740 let s = crate::nxpart::data(&data, st).unwrap().to_vec();
741 for at in (0..s.len()).step_by(7) {
742 let mut copy = s.clone();
743 if let Some(b) = copy.get_mut(at) {
744 *b ^= 0xA5;
745 }
746 let _ = structure(©);
747 let _ = structure(s.get(..at).unwrap_or_default());
748 }
749 }
750
751 fn layer_parameter_bytes(selectable: u8, visible: u8, work: u8) -> Vec<u8> {
754 let mut b = vec![0x41, 0x00, 0x5f, 0x01, 0x04, 0x01];
755 for first in [selectable, visible, selectable, visible] {
756 b.push(first + 1);
757 b.extend([0x01; 16]);
758 }
759 b.extend([work + 1, 0x02, 0x02, 0x1b]);
760 b
761 }
762
763 #[test]
764 fn layer_settings() {
765 let mut ug = vec![0xAB; 40];
767 ug.extend(layer_parameter_bytes(0b010, 0b110, 1));
768 ug.extend([0xCD; 40]);
769 let layers = visible_layers(&ug).unwrap();
770 assert_eq!(layers.work, 1);
771 assert!(layers.is_visible(1) && layers.is_visible(2));
772 assert!(!layers.is_visible(3) && !layers.is_visible(9) && !layers.is_visible(256));
773 let twice = [ug.clone(), ug.clone()].concat();
775 assert_eq!(visible_layers(&twice), None);
776 assert_eq!(
777 visible_layers(&layer_parameter_bytes(0b1000, 0b0110, 1)),
778 None
779 );
780 assert_eq!(
781 visible_layers(&layer_parameter_bytes(0b10, 0b10, 0)),
782 None,
783 "work layer 0"
784 );
785 assert_eq!(sint(&[0x00], 0), Some((-1, 1)));
787 assert_eq!(
788 sint(&[0x9f, 0xfe], 0),
789 Some((-2, 2)),
790 "13-bit −2: all bits but bit 0"
791 );
792 assert_eq!(
793 sint(&[0xe0, 0x80, 0, 0, 0], 0),
794 Some((i64::from(i32::MIN), 5))
795 );
796 }
797
798 #[test]
799 fn assemblies_without_layer_changes_keep_every_component() {
800 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
801 .join("tests/fixtures/cad/nist/mtc-assembly/nx/NIST mtc crada assembly.prt");
802 let Kind::Assembly(c) = kind_of(&path).unwrap() else {
803 panic!("an assembly");
804 };
805 assert_eq!(c.len(), 36);
806 assert!(c.iter().all(|x| x.layer.is_none()));
807 let part = path.with_file_name("90591A141 HEX NUT.prt");
808 assert!(matches!(kind_of(&part).unwrap(), Kind::Part(_)));
809 }
810
811 #[test]
812 fn numbers_and_placements() {
813 assert_eq!(
814 number(&[0x30, 0x77, 0xc3, 0x33, 0x33, 0x33, 0x33, 0x34], 0).map(|v| v.1),
815 Some(8)
816 );
817 let (v, _) = number(&[0x30, 0x77, 0xc3, 0x33, 0x33, 0x33, 0x33, 0x34], 0).unwrap();
818 assert!((v - 95.05).abs() < 1e-9, "380.2 / 4");
819 assert_eq!(number(&[0x81], 0), Some((-1.0, 1)));
820 assert_eq!(varint(&[0x87, 0xf0], 0), Some((0x7f0, 2)));
821 let quarter = Component {
822 name: "A".into(),
823 rotation: [[0.0, 1.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
824 translation: [10.0, 0.0, 0.0],
825 layer: Some(9),
826 occurrence: None,
827 document: None,
828 };
829 assert_eq!(quarter.within(&identity()).layer, Some(9));
830 assert_eq!(quarter.place([1.0, 0.0, 0.0]), [10.0, 1.0, 0.0]);
831 let nested = quarter.within(&quarter);
832 assert_eq!(
833 nested.place([1.0, 0.0, 0.0]),
834 quarter.place(quarter.place([1.0, 0.0, 0.0]))
835 );
836 }
837}