spatial-reasoner 0.1.0__py3-none-any.whl
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.
- spatial_reasoner/__init__.py +94 -0
- spatial_reasoner/bbox_sector.py +351 -0
- spatial_reasoner/exporter.py +128 -0
- spatial_reasoner/reasoner.py +557 -0
- spatial_reasoner/spatial_basics.py +256 -0
- spatial_reasoner/spatial_inference.py +920 -0
- spatial_reasoner/spatial_object.py +2375 -0
- spatial_reasoner/spatial_predicate.py +753 -0
- spatial_reasoner/spatial_relation.py +120 -0
- spatial_reasoner/spatial_taxonomy.py +210 -0
- spatial_reasoner/vector2.py +176 -0
- spatial_reasoner/vector3.py +82 -0
- spatial_reasoner-0.1.0.dist-info/METADATA +145 -0
- spatial_reasoner-0.1.0.dist-info/RECORD +17 -0
- spatial_reasoner-0.1.0.dist-info/WHEEL +5 -0
- spatial_reasoner-0.1.0.dist-info/licenses/LICENSE +121 -0
- spatial_reasoner-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Public API for the Spatial Reasoner Python implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
|
|
7
|
+
from .bbox_sector import BBoxSector, BBoxSectorFlags
|
|
8
|
+
from .reasoner import SpatialReasoner
|
|
9
|
+
from .spatial_basics import (
|
|
10
|
+
MotionState,
|
|
11
|
+
NearbySchema,
|
|
12
|
+
ObjectCause,
|
|
13
|
+
ObjectConfidence,
|
|
14
|
+
ObjectHandling,
|
|
15
|
+
ObjectShape,
|
|
16
|
+
SectorSchema,
|
|
17
|
+
SpatialAdjustment,
|
|
18
|
+
SpatialAtribute,
|
|
19
|
+
SpatialExistence,
|
|
20
|
+
SpatialPredicateCategories,
|
|
21
|
+
defaultAdjustment,
|
|
22
|
+
)
|
|
23
|
+
from .spatial_inference import SpatialInference
|
|
24
|
+
from .spatial_object import SpatialObject
|
|
25
|
+
from .spatial_predicate import (
|
|
26
|
+
PredicateTerm,
|
|
27
|
+
SpatialPredicate,
|
|
28
|
+
SpatialTerms,
|
|
29
|
+
adjacency,
|
|
30
|
+
assembly,
|
|
31
|
+
comparability,
|
|
32
|
+
connectivity,
|
|
33
|
+
contacts,
|
|
34
|
+
directionality,
|
|
35
|
+
geography,
|
|
36
|
+
orientations,
|
|
37
|
+
proximity,
|
|
38
|
+
sectors,
|
|
39
|
+
similarity,
|
|
40
|
+
topology,
|
|
41
|
+
visibility,
|
|
42
|
+
)
|
|
43
|
+
from .spatial_relation import SpatialRelation
|
|
44
|
+
from .spatial_taxonomy import SpatialObjectConcept, SpatialTaxonomy, TaxonomyParser
|
|
45
|
+
from .vector2 import Vector2
|
|
46
|
+
from .vector3 import Vector3
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
__version__ = version("spatial-reasoner")
|
|
50
|
+
except PackageNotFoundError: # pragma: no cover - direct source-tree import
|
|
51
|
+
__version__ = "0.1.0"
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"BBoxSector",
|
|
55
|
+
"BBoxSectorFlags",
|
|
56
|
+
"MotionState",
|
|
57
|
+
"NearbySchema",
|
|
58
|
+
"ObjectCause",
|
|
59
|
+
"ObjectConfidence",
|
|
60
|
+
"ObjectHandling",
|
|
61
|
+
"ObjectShape",
|
|
62
|
+
"PredicateTerm",
|
|
63
|
+
"SectorSchema",
|
|
64
|
+
"SpatialAdjustment",
|
|
65
|
+
"SpatialAtribute",
|
|
66
|
+
"SpatialExistence",
|
|
67
|
+
"SpatialInference",
|
|
68
|
+
"SpatialObject",
|
|
69
|
+
"SpatialObjectConcept",
|
|
70
|
+
"SpatialPredicate",
|
|
71
|
+
"SpatialPredicateCategories",
|
|
72
|
+
"SpatialReasoner",
|
|
73
|
+
"SpatialRelation",
|
|
74
|
+
"SpatialTaxonomy",
|
|
75
|
+
"SpatialTerms",
|
|
76
|
+
"TaxonomyParser",
|
|
77
|
+
"Vector2",
|
|
78
|
+
"Vector3",
|
|
79
|
+
"__version__",
|
|
80
|
+
"adjacency",
|
|
81
|
+
"assembly",
|
|
82
|
+
"comparability",
|
|
83
|
+
"connectivity",
|
|
84
|
+
"contacts",
|
|
85
|
+
"defaultAdjustment",
|
|
86
|
+
"directionality",
|
|
87
|
+
"geography",
|
|
88
|
+
"orientations",
|
|
89
|
+
"proximity",
|
|
90
|
+
"sectors",
|
|
91
|
+
"similarity",
|
|
92
|
+
"topology",
|
|
93
|
+
"visibility",
|
|
94
|
+
]
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# src/BBoxSector.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import IntFlag
|
|
6
|
+
from typing import Any, ClassVar
|
|
7
|
+
|
|
8
|
+
from .spatial_predicate import SpatialPredicate
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BBoxSectorFlags(IntFlag):
|
|
12
|
+
none = 0 # no sector specified
|
|
13
|
+
i = 1 << 0 # i : inside, inner
|
|
14
|
+
a = 1 << 1 # a : ahead
|
|
15
|
+
b = 1 << 2 # b : behind
|
|
16
|
+
l = 1 << 3 # noqa: E741 # SRSwift-compatible short sector code for left
|
|
17
|
+
r = 1 << 4 # r : right
|
|
18
|
+
o = 1 << 5 # o : over
|
|
19
|
+
u = 1 << 6 # u : under
|
|
20
|
+
|
|
21
|
+
# Composite sectors
|
|
22
|
+
al = a | l
|
|
23
|
+
ar = a | r
|
|
24
|
+
bl = b | l
|
|
25
|
+
br = b | r
|
|
26
|
+
ao = a | o
|
|
27
|
+
au = a | u
|
|
28
|
+
bo = b | o
|
|
29
|
+
bu = b | u
|
|
30
|
+
lo = l | o
|
|
31
|
+
lu = l | u
|
|
32
|
+
ro = r | o
|
|
33
|
+
ru = r | u
|
|
34
|
+
alo = a | l | o
|
|
35
|
+
aro = a | r | o
|
|
36
|
+
blo = b | l | o
|
|
37
|
+
bro = b | r | o
|
|
38
|
+
alu = a | l | u
|
|
39
|
+
aru = a | r | u
|
|
40
|
+
blu = b | l | u
|
|
41
|
+
bru = b | r | u
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BBoxSector:
|
|
45
|
+
"""
|
|
46
|
+
A mutable class that represents spatial sectors using bitmask flags.
|
|
47
|
+
Mimics Swift's OptionSet behavior.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
# Predefined descriptions for composite and individual sectors
|
|
51
|
+
debug_descriptions: ClassVar[dict[BBoxSectorFlags, str]] = {
|
|
52
|
+
BBoxSectorFlags.i: "i",
|
|
53
|
+
BBoxSectorFlags.a: "a",
|
|
54
|
+
BBoxSectorFlags.b: "b",
|
|
55
|
+
BBoxSectorFlags.l: "l",
|
|
56
|
+
BBoxSectorFlags.r: "r",
|
|
57
|
+
BBoxSectorFlags.o: "o",
|
|
58
|
+
BBoxSectorFlags.u: "u",
|
|
59
|
+
BBoxSectorFlags.al: "al",
|
|
60
|
+
BBoxSectorFlags.ar: "ar",
|
|
61
|
+
BBoxSectorFlags.bl: "bl",
|
|
62
|
+
BBoxSectorFlags.br: "br",
|
|
63
|
+
BBoxSectorFlags.ao: "ao",
|
|
64
|
+
BBoxSectorFlags.au: "au",
|
|
65
|
+
BBoxSectorFlags.bo: "bo",
|
|
66
|
+
BBoxSectorFlags.bu: "bu",
|
|
67
|
+
BBoxSectorFlags.lo: "lo",
|
|
68
|
+
BBoxSectorFlags.lu: "lu",
|
|
69
|
+
BBoxSectorFlags.ro: "ro",
|
|
70
|
+
BBoxSectorFlags.ru: "ru",
|
|
71
|
+
BBoxSectorFlags.alo: "alo",
|
|
72
|
+
BBoxSectorFlags.aro: "aro",
|
|
73
|
+
BBoxSectorFlags.blo: "blo",
|
|
74
|
+
BBoxSectorFlags.bro: "bro",
|
|
75
|
+
BBoxSectorFlags.alu: "alu",
|
|
76
|
+
BBoxSectorFlags.aru: "aru",
|
|
77
|
+
BBoxSectorFlags.blu: "blu",
|
|
78
|
+
BBoxSectorFlags.bru: "bru",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Define base flags (individual flags only)
|
|
82
|
+
base_flags: ClassVar[set[BBoxSectorFlags]] = {
|
|
83
|
+
BBoxSectorFlags.i,
|
|
84
|
+
BBoxSectorFlags.a,
|
|
85
|
+
BBoxSectorFlags.b,
|
|
86
|
+
BBoxSectorFlags.l,
|
|
87
|
+
BBoxSectorFlags.r,
|
|
88
|
+
BBoxSectorFlags.o,
|
|
89
|
+
BBoxSectorFlags.u,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
def __init__(self, flags=BBoxSectorFlags.none):
|
|
93
|
+
"""
|
|
94
|
+
Initialize a BBoxSector instance.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
flags (BBoxSectorFlags, optional): Initial sector flags. Defaults to BBoxSectorFlags.none.
|
|
98
|
+
"""
|
|
99
|
+
self.flags = flags
|
|
100
|
+
|
|
101
|
+
def insert(self, flag: BBoxSectorFlags):
|
|
102
|
+
"""
|
|
103
|
+
Insert a sector flag.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
flag (BBoxSectorFlags): The flag to insert.
|
|
107
|
+
"""
|
|
108
|
+
self.flags |= flag
|
|
109
|
+
|
|
110
|
+
def remove(self, flag: BBoxSectorFlags):
|
|
111
|
+
"""
|
|
112
|
+
Remove a sector flag.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
flag (BBoxSectorFlags): The flag to remove.
|
|
116
|
+
"""
|
|
117
|
+
self.flags &= ~flag
|
|
118
|
+
|
|
119
|
+
def contains_flag(self, flag: BBoxSectorFlags) -> bool:
|
|
120
|
+
"""
|
|
121
|
+
Check if a sector flag is present.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
flag (BBoxSectorFlags): The flag to check.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
bool: True if the flag is present, False otherwise.
|
|
128
|
+
"""
|
|
129
|
+
return (self.flags & flag) == flag
|
|
130
|
+
|
|
131
|
+
def contains(self, flag: BBoxSectorFlags) -> bool:
|
|
132
|
+
"""
|
|
133
|
+
Alias for contains_flag to maintain backward compatibility.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
flag (BBoxSectorFlags): The flag to check.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
bool: True if the flag is present, False otherwise.
|
|
140
|
+
"""
|
|
141
|
+
return self.contains_flag(flag)
|
|
142
|
+
|
|
143
|
+
def divergencies(self) -> int:
|
|
144
|
+
"""
|
|
145
|
+
Calculate the amount of divergency from the inner zone in all 3 directions.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
int: 0 if the sector includes 'i' (inside), otherwise the number of set bits.
|
|
149
|
+
"""
|
|
150
|
+
if self.contains_flag(BBoxSectorFlags.i):
|
|
151
|
+
return 0
|
|
152
|
+
return bin(self.flags.value).count("1")
|
|
153
|
+
|
|
154
|
+
def list_base_flags(self):
|
|
155
|
+
"""
|
|
156
|
+
List only the base flags present in the sector.
|
|
157
|
+
"""
|
|
158
|
+
return [
|
|
159
|
+
name
|
|
160
|
+
for name, member in BBoxSectorFlags.__members__.items()
|
|
161
|
+
if member in self.flags and name != "none" and member in BBoxSector.base_flags
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
def __str__(self) -> str:
|
|
165
|
+
"""
|
|
166
|
+
Provide a string representation for the sector.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
str: The descriptive string of the sector.
|
|
170
|
+
"""
|
|
171
|
+
# Always list base flags to match test expectations
|
|
172
|
+
flags = self.list_base_flags()
|
|
173
|
+
if flags:
|
|
174
|
+
return "".join(flags)
|
|
175
|
+
elif self.flags == BBoxSectorFlags.none:
|
|
176
|
+
return "no sector"
|
|
177
|
+
else:
|
|
178
|
+
# If no base flags are set, but some composite flags are, list them
|
|
179
|
+
# This can happen if only composite flags are set without their base flags
|
|
180
|
+
composite_flags = [
|
|
181
|
+
name
|
|
182
|
+
for name, member in BBoxSectorFlags.__members__.items()
|
|
183
|
+
if member in self.flags and name != "none" and member not in BBoxSector.base_flags
|
|
184
|
+
]
|
|
185
|
+
if composite_flags:
|
|
186
|
+
return "".join(composite_flags)
|
|
187
|
+
return "no sector"
|
|
188
|
+
|
|
189
|
+
def __eq__(self, other: Any) -> bool:
|
|
190
|
+
"""
|
|
191
|
+
Check equality with another BBoxSector instance.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
other (Any): The object to compare.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
bool: True if equal, False otherwise.
|
|
198
|
+
"""
|
|
199
|
+
if isinstance(other, BBoxSector):
|
|
200
|
+
return self.flags == other.flags
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
def __repr__(self) -> str:
|
|
204
|
+
"""
|
|
205
|
+
Return the official string representation of the sector.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
str: The string representation.
|
|
209
|
+
"""
|
|
210
|
+
return f"BBoxSector(flags={self.flags})"
|
|
211
|
+
|
|
212
|
+
def __hash__(self) -> int:
|
|
213
|
+
"""
|
|
214
|
+
Make BBoxSector hashable (so you can use it in sets/dicts),
|
|
215
|
+
just like Swift’s Hashable conformance.
|
|
216
|
+
"""
|
|
217
|
+
return hash(self.flags)
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def named(cls, name: str) -> BBoxSector:
|
|
221
|
+
"""
|
|
222
|
+
Look up a sector by its name (e.g. "alo", "b", "ru").
|
|
223
|
+
Returns BBoxSector.none if no match found.
|
|
224
|
+
"""
|
|
225
|
+
for flag, desc in cls.debug_descriptions.items():
|
|
226
|
+
if desc == name:
|
|
227
|
+
return cls(flag)
|
|
228
|
+
return cls()
|
|
229
|
+
|
|
230
|
+
def __or__(self, other: Any) -> BBoxSector:
|
|
231
|
+
"""
|
|
232
|
+
Define the behavior of the | operator.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
other (BBoxSector or BBoxSectorFlags): The other sector or flag to combine.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
BBoxSector: A new BBoxSector instance with combined flags.
|
|
239
|
+
"""
|
|
240
|
+
if isinstance(other, BBoxSector):
|
|
241
|
+
return BBoxSector(self.flags | other.flags)
|
|
242
|
+
elif isinstance(other, BBoxSectorFlags):
|
|
243
|
+
return BBoxSector(self.flags | other)
|
|
244
|
+
else:
|
|
245
|
+
return NotImplemented
|
|
246
|
+
|
|
247
|
+
def __ior__(self, other: Any) -> BBoxSector:
|
|
248
|
+
"""
|
|
249
|
+
Define the behavior of the |= operator.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
other (BBoxSector or BBoxSectorFlags): The other sector or flag to combine.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
BBoxSector: The updated BBoxSector instance.
|
|
256
|
+
"""
|
|
257
|
+
if isinstance(other, BBoxSector):
|
|
258
|
+
self.flags |= other.flags
|
|
259
|
+
return self
|
|
260
|
+
elif isinstance(other, BBoxSectorFlags):
|
|
261
|
+
self.flags |= other
|
|
262
|
+
return self
|
|
263
|
+
else:
|
|
264
|
+
return NotImplemented
|
|
265
|
+
|
|
266
|
+
def __contains__(self, item: Any) -> bool:
|
|
267
|
+
"""
|
|
268
|
+
Enable the 'in' operator to check for SpatialPredicate or BBoxSectorFlags.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
item (Any): SpatialPredicate or BBoxSectorFlags to check.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
bool: True if the item is present, False otherwise.
|
|
275
|
+
"""
|
|
276
|
+
if isinstance(item, SpatialPredicate):
|
|
277
|
+
# Map SpatialPredicate to BBoxSectorFlags
|
|
278
|
+
flag_map = {
|
|
279
|
+
SpatialPredicate.l: BBoxSectorFlags.l,
|
|
280
|
+
SpatialPredicate.r: BBoxSectorFlags.r,
|
|
281
|
+
SpatialPredicate.a: BBoxSectorFlags.a,
|
|
282
|
+
SpatialPredicate.b: BBoxSectorFlags.b,
|
|
283
|
+
SpatialPredicate.o: BBoxSectorFlags.o,
|
|
284
|
+
SpatialPredicate.u: BBoxSectorFlags.u,
|
|
285
|
+
SpatialPredicate.i: BBoxSectorFlags.i,
|
|
286
|
+
SpatialPredicate.al: BBoxSectorFlags.al,
|
|
287
|
+
SpatialPredicate.ar: BBoxSectorFlags.ar,
|
|
288
|
+
SpatialPredicate.bl: BBoxSectorFlags.bl,
|
|
289
|
+
SpatialPredicate.br: BBoxSectorFlags.br,
|
|
290
|
+
SpatialPredicate.ao: BBoxSectorFlags.ao,
|
|
291
|
+
SpatialPredicate.au: BBoxSectorFlags.au,
|
|
292
|
+
SpatialPredicate.bo: BBoxSectorFlags.bo,
|
|
293
|
+
SpatialPredicate.bu: BBoxSectorFlags.bu,
|
|
294
|
+
SpatialPredicate.lo: BBoxSectorFlags.lo,
|
|
295
|
+
SpatialPredicate.lu: BBoxSectorFlags.lu,
|
|
296
|
+
SpatialPredicate.ro: BBoxSectorFlags.ro,
|
|
297
|
+
SpatialPredicate.ru: BBoxSectorFlags.ru,
|
|
298
|
+
SpatialPredicate.alo: BBoxSectorFlags.alo,
|
|
299
|
+
SpatialPredicate.aro: BBoxSectorFlags.aro,
|
|
300
|
+
SpatialPredicate.blo: BBoxSectorFlags.blo,
|
|
301
|
+
SpatialPredicate.bro: BBoxSectorFlags.bro,
|
|
302
|
+
SpatialPredicate.alu: BBoxSectorFlags.alu,
|
|
303
|
+
SpatialPredicate.aru: BBoxSectorFlags.aru,
|
|
304
|
+
SpatialPredicate.blu: BBoxSectorFlags.blu,
|
|
305
|
+
SpatialPredicate.bru: BBoxSectorFlags.bru,
|
|
306
|
+
# Add more mappings as needed
|
|
307
|
+
}
|
|
308
|
+
flag = flag_map.get(item, None)
|
|
309
|
+
if flag is not None:
|
|
310
|
+
return self.contains_flag(flag)
|
|
311
|
+
elif isinstance(item, BBoxSectorFlags):
|
|
312
|
+
return self.contains_flag(item)
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# Example Usage
|
|
317
|
+
|
|
318
|
+
if __name__ == "__main__":
|
|
319
|
+
# Initialize with a predefined composite sector
|
|
320
|
+
sector = BBoxSector(BBoxSectorFlags.alo)
|
|
321
|
+
print(f"Sector: {sector}") # Output: Sector: a+l+o
|
|
322
|
+
print(f"Divergencies: {sector.divergencies()}") # Output: Divergencies: 3
|
|
323
|
+
|
|
324
|
+
# Create an empty sector and insert flags
|
|
325
|
+
combined_sector = BBoxSector()
|
|
326
|
+
combined_sector.insert(BBoxSectorFlags.a)
|
|
327
|
+
combined_sector.insert(BBoxSectorFlags.l)
|
|
328
|
+
combined_sector.insert(BBoxSectorFlags.o)
|
|
329
|
+
print(f"Combined Sector: {combined_sector}") # Output: Combined Sector: a+l+o
|
|
330
|
+
print(f"Divergencies: {combined_sector.divergencies()}") # Output: Divergencies: 3
|
|
331
|
+
|
|
332
|
+
# Initialize with the 'inside' sector
|
|
333
|
+
inner_sector = BBoxSector(BBoxSectorFlags.i)
|
|
334
|
+
print(f"Inner Sector: {inner_sector}") # Output: Inner Sector: i
|
|
335
|
+
print(f"Divergencies: {inner_sector.divergencies()}") # Output: Divergencies: 0
|
|
336
|
+
|
|
337
|
+
# Initialize with no sector
|
|
338
|
+
no_sector = BBoxSector()
|
|
339
|
+
print(f"No Sector: {no_sector}") # Output: No Sector: no sector
|
|
340
|
+
print(f"Divergencies: {no_sector.divergencies()}") # Output: Divergencies: 0
|
|
341
|
+
|
|
342
|
+
# Undefined composite sector example
|
|
343
|
+
undefined_sector = BBoxSector()
|
|
344
|
+
undefined_sector.insert(BBoxSectorFlags.a)
|
|
345
|
+
undefined_sector.insert(BBoxSectorFlags.b)
|
|
346
|
+
undefined_sector.insert(BBoxSectorFlags.l)
|
|
347
|
+
undefined_sector.insert(BBoxSectorFlags.r)
|
|
348
|
+
print(
|
|
349
|
+
f"Undefined Combined Sector: {undefined_sector}"
|
|
350
|
+
) # Output: Undefined Combined Sector: a+b+l+r
|
|
351
|
+
print(f"Divergencies: {undefined_sector.divergencies()}") # Output: Divergencies: 4
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Optional OpenUSD scene export support.
|
|
2
|
+
|
|
3
|
+
Install ``spatial-reasoner[export]`` before importing this module.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
import random
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from pxr import Gf, Sdf, Usd, UsdGeom, UsdShade, UsdUtils
|
|
15
|
+
except ModuleNotFoundError as exc: # pragma: no cover - depends on installation extras
|
|
16
|
+
raise ModuleNotFoundError(
|
|
17
|
+
"Scene export requires the optional dependency group; "
|
|
18
|
+
"install it with `pip install spatial-reasoner[export]`."
|
|
19
|
+
) from exc
|
|
20
|
+
|
|
21
|
+
from .spatial_object import SpatialObject
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SceneExporter:
|
|
25
|
+
"""Export spatial-object bounding boxes as USDZ scenes."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, root_dir: str | Path):
|
|
28
|
+
"""Create an exporter writing into ``root_dir``."""
|
|
29
|
+
self.root_dir = Path(root_dir)
|
|
30
|
+
self.root_dir.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
self.usd_file_path: Path | None = None
|
|
32
|
+
self.temp_usd_path: Path | None = None
|
|
33
|
+
self.stage = None
|
|
34
|
+
|
|
35
|
+
def exportUSDZ(self, spatial_objects: list[SpatialObject], filename: str | Path) -> None:
|
|
36
|
+
"""Export ``spatial_objects`` to a USDZ file.
|
|
37
|
+
|
|
38
|
+
The method name intentionally follows SRSwift's public API.
|
|
39
|
+
"""
|
|
40
|
+
output_path = Path(filename)
|
|
41
|
+
if not output_path.is_absolute():
|
|
42
|
+
output_path = self.root_dir / output_path
|
|
43
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
output_path.unlink(missing_ok=True)
|
|
45
|
+
self.usd_file_path = output_path
|
|
46
|
+
|
|
47
|
+
with tempfile.NamedTemporaryFile(
|
|
48
|
+
prefix="spatial_reasoner_", suffix=".usd", dir=self.root_dir, delete=False
|
|
49
|
+
) as temporary_file:
|
|
50
|
+
self.temp_usd_path = Path(temporary_file.name)
|
|
51
|
+
self.temp_usd_path.unlink(missing_ok=True)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
self.stage = Usd.Stage.CreateNew(str(self.temp_usd_path))
|
|
55
|
+
for obj in spatial_objects:
|
|
56
|
+
prim_path = self._create_obj_cube(obj)
|
|
57
|
+
self._create_bbox_cube(obj, prim_path)
|
|
58
|
+
|
|
59
|
+
self.stage.GetRootLayer().Save()
|
|
60
|
+
self.stage = None
|
|
61
|
+
created = UsdUtils.CreateNewUsdzPackage(
|
|
62
|
+
Sdf.AssetPath(str(self.temp_usd_path)), str(output_path)
|
|
63
|
+
)
|
|
64
|
+
if not created or not output_path.exists():
|
|
65
|
+
raise RuntimeError(f"OpenUSD could not create USDZ package: {output_path}")
|
|
66
|
+
finally:
|
|
67
|
+
self.stage = None
|
|
68
|
+
self.temp_usd_path.unlink(missing_ok=True)
|
|
69
|
+
|
|
70
|
+
def _create_obj_cube(self, spatial_object: SpatialObject) -> str:
|
|
71
|
+
"""Add a spatial object and its material to the current USD stage."""
|
|
72
|
+
prim_path = f"/{spatial_object.id}"
|
|
73
|
+
xform = UsdGeom.Xform.Define(self.stage, prim_path)
|
|
74
|
+
xform.AddRotateYOp().Set(math.degrees(spatial_object.angle))
|
|
75
|
+
xform.AddTranslateOp().Set(
|
|
76
|
+
Gf.Vec3d(
|
|
77
|
+
spatial_object.position.x,
|
|
78
|
+
spatial_object.position.y,
|
|
79
|
+
spatial_object.position.z,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
xform.AddScaleOp().Set(
|
|
83
|
+
Gf.Vec3f(spatial_object.width, spatial_object.height, spatial_object.depth)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
cube = UsdGeom.Cube.Define(self.stage, f"{prim_path}/Cube")
|
|
87
|
+
material_path = f"{prim_path}/Material"
|
|
88
|
+
material = UsdShade.Material.Define(self.stage, material_path)
|
|
89
|
+
color = Gf.Vec3f(random.random(), random.random(), random.random())
|
|
90
|
+
|
|
91
|
+
shader = UsdShade.Shader.Define(self.stage, f"{material_path}/Shader")
|
|
92
|
+
shader.CreateIdAttr("UsdPreviewSurface")
|
|
93
|
+
shader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).Set(color)
|
|
94
|
+
shader.CreateInput("opacity", Sdf.ValueTypeNames.Float).Set(
|
|
95
|
+
1.0 - spatial_object.transparency
|
|
96
|
+
)
|
|
97
|
+
shader.CreateOutput("surface", Sdf.ValueTypeNames.Token)
|
|
98
|
+
material.CreateSurfaceOutput().ConnectToSource(shader.GetOutput("surface"))
|
|
99
|
+
UsdShade.MaterialBindingAPI(xform.GetPrim()).Bind(material)
|
|
100
|
+
|
|
101
|
+
prim = cube.GetPrim()
|
|
102
|
+
prim.CreateAttribute("user:transparency", Sdf.ValueTypeNames.Float).Set(
|
|
103
|
+
spatial_object.transparency
|
|
104
|
+
)
|
|
105
|
+
prim.CreateAttribute("user:color", Sdf.ValueTypeNames.Color3f).Set(color)
|
|
106
|
+
return prim_path
|
|
107
|
+
|
|
108
|
+
def _create_bbox_cube(
|
|
109
|
+
self,
|
|
110
|
+
obj: SpatialObject,
|
|
111
|
+
parent_path: str,
|
|
112
|
+
color: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Add the object's nearby-radius sphere below ``parent_path``."""
|
|
115
|
+
bbox_prim_path = f"{parent_path}/BBox"
|
|
116
|
+
UsdGeom.Xform.Define(self.stage, bbox_prim_path)
|
|
117
|
+
sphere_path = f"{bbox_prim_path}/NearbySphere"
|
|
118
|
+
sphere = UsdGeom.Sphere.Define(self.stage, sphere_path)
|
|
119
|
+
sphere.GetPrim().CreateAttribute("radius", Sdf.ValueTypeNames.Float).Set(obj.nearbyRadius())
|
|
120
|
+
|
|
121
|
+
material = UsdShade.Material.Define(self.stage, f"{sphere_path}/Material")
|
|
122
|
+
shader = UsdShade.Shader.Define(self.stage, f"{sphere_path}/Material/Shader")
|
|
123
|
+
shader.CreateIdAttr("UsdPreviewSurface")
|
|
124
|
+
shader.CreateInput("diffuseColor", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(*color))
|
|
125
|
+
shader.CreateInput("opacity", Sdf.ValueTypeNames.Float).Set(0.3)
|
|
126
|
+
shader.CreateOutput("surface", Sdf.ValueTypeNames.Token)
|
|
127
|
+
material.CreateSurfaceOutput().ConnectToSource(shader.GetOutput("surface"))
|
|
128
|
+
UsdShade.MaterialBindingAPI(sphere.GetPrim()).Bind(material)
|