Coverage for trimesh/iteration.py: 100%

24 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-24 04:40 +0000

1from math import log2 

2from typing import Any 

3 

4from .typed import Callable, Iterable, NDArray, Sequence 

5 

6 

7def reduce_cascade(operation: Callable, items: Sequence | NDArray): 

8 """ 

9 Call an operation function in a cascaded pairwise way against a 

10 flat list of items. 

11 

12 This should produce the same result as `functools.reduce` 

13 if `operation` is commutable like addition or multiplication. 

14 This may be faster for an `operation` that runs with a speed 

15 proportional to its largest input, which mesh booleans appear to. 

16 

17 The union of a large number of small meshes appears to be 

18 "much faster" using this method. 

19 

20 This only differs from `functools.reduce` for commutative `operation` 

21 in that it returns `None` on empty inputs rather than `functools.reduce` 

22 which raises a `TypeError`. 

23 

24 For example on `a b c d e f g` this function would run and return: 

25 a b 

26 c d 

27 e f 

28 ab cd 

29 ef g 

30 abcd efg 

31 -> abcdefg 

32 

33 Where `functools.reduce` would run and return: 

34 a b 

35 ab c 

36 abc d 

37 abcd e 

38 abcde f 

39 abcdef g 

40 -> abcdefg 

41 

42 Parameters 

43 ---------- 

44 operation 

45 The function to call on pairs of items. 

46 items 

47 The flat list of items to apply operation against. 

48 """ 

49 if len(items) == 0: 

50 return None 

51 elif len(items) == 1: 

52 # skip the loop overhead for a single item 

53 return items[0] 

54 elif len(items) == 2: 

55 # skip the loop overhead for a single pair 

56 return operation(items[0], items[1]) 

57 

58 for _ in range(int(1 + log2(len(items)))): 

59 results = [] 

60 

61 # loop over pairs of items. 

62 items_mod = len(items) % 2 

63 for i in range(0, len(items) - items_mod, 2): 

64 results.append(operation(items[i], items[i + 1])) 

65 

66 # if we had a non-even number of items it will have been 

67 # skipped by the loop so append it to our list 

68 if items_mod != 0: 

69 results.append(items[-1]) 

70 

71 items = results 

72 

73 # logic should have reduced to a single item 

74 assert len(results) == 1 

75 

76 return results[0] 

77 

78 

79def chain(*args: Iterable[Any] | Any | None) -> list[Any]: 

80 """ 

81 A less principled version of `list(itertools.chain(*args))` that 

82 accepts non-iterable values, filters `None`, and returns a list 

83 rather than yielding values. 

84 

85 If all passed values are iterables this will return identical 

86 results to `list(itertools.chain(*args))`. 

87 

88 

89 Examples 

90 ---------- 

91 

92 In [1]: list(itertools.chain([1,2], [3])) 

93 Out[1]: [1, 2, 3] 

94 

95 In [2]: trimesh.util.chain([1,2], [3]) 

96 Out[2]: [1, 2, 3] 

97 

98 In [3]: trimesh.util.chain([1,2], [3], 4) 

99 Out[3]: [1, 2, 3, 4] 

100 

101 In [4]: list(itertools.chain([1,2], [3], 4)) 

102 ----> 1 list(itertools.chain([1,2], [3], 4)) 

103 TypeError: 'int' object is not iterable 

104 

105 In [5]: trimesh.util.chain([1,2], None, 3, None, [4], [], [], 5, []) 

106 Out[5]: [1, 2, 3, 4, 5] 

107 

108 

109 Parameters 

110 ----------- 

111 args 

112 Will be individually checked to see if they're iterable 

113 before either being appended or extended to a flat list. 

114 

115 

116 Returns 

117 ---------- 

118 chained 

119 The values in a flat list. 

120 """ 

121 # collect values to a flat list 

122 chained = [] 

123 # extend if it's a sequence, otherwise append 

124 [ 

125 chained.extend(a) 

126 if (hasattr(a, "__iter__") and not isinstance(a, (str, bytes))) 

127 else chained.append(a) 

128 for a in args 

129 if a is not None 

130 ] 

131 return chained