simscope 0.1.1__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.
- simscope/__init__.py +6 -0
- simscope/__main__.py +8 -0
- simscope/_assets/simscope-app.css +2 -0
- simscope/_assets/simscope-app.js +4311 -0
- simscope/_assets/simscope-player.js +4325 -0
- simscope/_assets/simscope-web.LICENSES.txt +407 -0
- simscope/_icon.py +22 -0
- simscope/_mjviser.py +203 -0
- simscope/annotations.py +1132 -0
- simscope/cli.py +482 -0
- simscope/core.py +257 -0
- simscope/derived.py +697 -0
- simscope/export.py +799 -0
- simscope/highlights.py +947 -0
- simscope/importers.py +874 -0
- simscope/index.py +579 -0
- simscope/io/__init__.py +45 -0
- simscope/io/blockfile.py +938 -0
- simscope/io/cas.py +294 -0
- simscope/io/codecs.py +566 -0
- simscope/io/errors.py +9 -0
- simscope/io/manifest.py +358 -0
- simscope/io/pack.py +563 -0
- simscope/io/scene.py +239 -0
- simscope/isaaclab.py +1460 -0
- simscope/library.py +705 -0
- simscope/mujoco.py +578 -0
- simscope/py.typed +0 -0
- simscope/recorder.py +784 -0
- simscope/server/__init__.py +9 -0
- simscope/server/app.py +149 -0
- simscope/server/blocks.py +191 -0
- simscope/server/jobs.py +166 -0
- simscope/server/routes.py +707 -0
- simscope/server/security.py +218 -0
- simscope/server/state.py +751 -0
- simscope/server/static.py +84 -0
- simscope/transforms.py +147 -0
- simscope-0.1.1.dist-info/METADATA +132 -0
- simscope-0.1.1.dist-info/RECORD +45 -0
- simscope-0.1.1.dist-info/WHEEL +4 -0
- simscope-0.1.1.dist-info/entry_points.txt +3 -0
- simscope-0.1.1.dist-info/licenses/LICENSE.md +201 -0
- simscope-0.1.1.dist-info/licenses/THIRD_PARTY_NOTICES.md +267 -0
- simscope-0.1.1.dist-info/licenses/src/simscope/_assets/simscope-web.LICENSES.txt +407 -0
simscope/io/scene.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Scene descriptors: canonical JSON and content-addressed storage."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Callable, Iterable, Mapping, Sequence
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from simscope import core
|
|
10
|
+
from simscope.io import cas, codecs, errors
|
|
11
|
+
|
|
12
|
+
SCENE_FORMAT = "simscope-scene/1"
|
|
13
|
+
|
|
14
|
+
AssetGetter = Callable[[cas.Ref], bytes]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def canonical_bytes(obj: Any) -> bytes:
|
|
18
|
+
"""Serializes a JSON value canonically, so equal content hashes the same.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
obj: A JSON-compatible value. Floats must already be widened from f32.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Sorted-key, compact, UTF-8 bytes without a trailing newline.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ValueError: If a float is NaN or infinite.
|
|
28
|
+
"""
|
|
29
|
+
return json.dumps(
|
|
30
|
+
obj,
|
|
31
|
+
sort_keys=True,
|
|
32
|
+
separators=(",", ":"),
|
|
33
|
+
ensure_ascii=False,
|
|
34
|
+
allow_nan=False,
|
|
35
|
+
).encode("utf-8")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _f(x: float) -> float:
|
|
39
|
+
"""Widens a value through float32, so the JSON hash is stable."""
|
|
40
|
+
return float(np.float32(x))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _fs(values: Iterable[float]) -> list[float]:
|
|
44
|
+
"""Widens each value through float32."""
|
|
45
|
+
return [_f(v) for v in values]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _body_json(body: core.Body) -> dict[str, Any]:
|
|
49
|
+
"""Returns a body's descriptor; ``mass`` only when it is known (> 0).
|
|
50
|
+
|
|
51
|
+
A scene without masses thus keeps the bytes, and the hash, it had before
|
|
52
|
+
bodies had a mass.
|
|
53
|
+
"""
|
|
54
|
+
out: dict[str, Any] = {"name": body.name, "parent": body.parent}
|
|
55
|
+
if body.mass > 0:
|
|
56
|
+
out["mass"] = _f(body.mass)
|
|
57
|
+
return out
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def scene_to_json(
|
|
61
|
+
scene: core.Scene,
|
|
62
|
+
mesh_refs: Sequence[cas.Ref],
|
|
63
|
+
texture_refs: Sequence[cas.Ref],
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
"""Builds the JSON object of a scene descriptor.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
scene: The scene.
|
|
69
|
+
mesh_refs: References to the encoded mesh blobs, one per mesh.
|
|
70
|
+
texture_refs: References to the texture blobs, one per texture.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The descriptor as a JSON-compatible dict, with every float widened
|
|
74
|
+
through float32.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ValueError: If the reference counts do not match the scene.
|
|
78
|
+
"""
|
|
79
|
+
if len(mesh_refs) != len(scene.meshes):
|
|
80
|
+
raise ValueError("need one mesh ref per mesh")
|
|
81
|
+
if len(texture_refs) != len(scene.textures):
|
|
82
|
+
raise ValueError("need one texture ref per texture")
|
|
83
|
+
return {
|
|
84
|
+
"format": SCENE_FORMAT,
|
|
85
|
+
"bodies": [_body_json(b) for b in scene.bodies],
|
|
86
|
+
"geoms": [
|
|
87
|
+
{
|
|
88
|
+
"body": g.body,
|
|
89
|
+
"kind": g.kind,
|
|
90
|
+
"size": _fs(g.size),
|
|
91
|
+
"pos": _fs(g.pos),
|
|
92
|
+
"quat": _fs(g.quat),
|
|
93
|
+
"scale": _fs(g.scale),
|
|
94
|
+
"material": g.material,
|
|
95
|
+
"mesh": g.mesh,
|
|
96
|
+
"role": g.role,
|
|
97
|
+
"name": g.name,
|
|
98
|
+
}
|
|
99
|
+
for g in scene.geoms
|
|
100
|
+
],
|
|
101
|
+
"materials": [
|
|
102
|
+
{
|
|
103
|
+
"rgba": _fs(m.rgba),
|
|
104
|
+
"metallic": _f(m.metallic),
|
|
105
|
+
"roughness": _f(m.roughness),
|
|
106
|
+
"texture": m.texture,
|
|
107
|
+
"texrepeat": _fs(m.texrepeat),
|
|
108
|
+
}
|
|
109
|
+
for m in scene.materials
|
|
110
|
+
],
|
|
111
|
+
"meshes": [
|
|
112
|
+
{
|
|
113
|
+
**ref.to_json(),
|
|
114
|
+
"n_verts": len(mesh.vertices),
|
|
115
|
+
"n_faces": len(mesh.faces),
|
|
116
|
+
}
|
|
117
|
+
for mesh, ref in zip(scene.meshes, mesh_refs, strict=True)
|
|
118
|
+
],
|
|
119
|
+
"textures": [
|
|
120
|
+
{
|
|
121
|
+
**ref.to_json(),
|
|
122
|
+
"media_type": t.media_type,
|
|
123
|
+
"width": t.width,
|
|
124
|
+
"height": t.height,
|
|
125
|
+
}
|
|
126
|
+
for t, ref in zip(scene.textures, texture_refs, strict=True)
|
|
127
|
+
],
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def put_scene(store: cas.ContentStore, scene: core.Scene) -> cas.Ref:
|
|
132
|
+
"""Stores a scene: raw mesh blobs, texture blobs, then the descriptor.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
store: The content store of the library.
|
|
136
|
+
scene: The scene to store.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The reference of the canonical scene JSON under ``scenes/``. Equal
|
|
140
|
+
scenes give equal references.
|
|
141
|
+
"""
|
|
142
|
+
mesh_refs = [store.put(codecs.encode_mesh(m, "raw")) for m in scene.meshes]
|
|
143
|
+
tex_refs = [store.put(t.data) for t in scene.textures]
|
|
144
|
+
doc = scene_to_json(scene, mesh_refs, tex_refs)
|
|
145
|
+
return store.put(canonical_bytes(doc), "scene")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _check_format(doc: Mapping[str, Any]) -> None:
|
|
149
|
+
"""Rejects a descriptor with an unknown format string."""
|
|
150
|
+
fmt = doc.get("format")
|
|
151
|
+
if not isinstance(fmt, str) or not fmt.startswith("simscope-scene/"):
|
|
152
|
+
raise errors.FormatError(f"not a scene descriptor: format={fmt!r}")
|
|
153
|
+
if fmt.split("/", 1)[1].split(".")[0] != "1":
|
|
154
|
+
raise errors.FormatError(f"unknown scene major version: {fmt!r}")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def scene_from_json(
|
|
158
|
+
doc: Mapping[str, Any], get_asset: AssetGetter
|
|
159
|
+
) -> core.Scene:
|
|
160
|
+
"""Rebuilds a scene from its descriptor.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
doc: The parsed descriptor.
|
|
164
|
+
get_asset: Returns the bytes of a mesh or texture blob by reference.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
The scene. Meshes are decoded from their blobs (q16 meshes have no
|
|
168
|
+
normals).
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
errors.FormatError: If the descriptor or a blob is malformed.
|
|
172
|
+
"""
|
|
173
|
+
_check_format(doc)
|
|
174
|
+
try:
|
|
175
|
+
bodies = tuple(
|
|
176
|
+
core.Body(b["name"], int(b["parent"]), float(b.get("mass", 0.0)))
|
|
177
|
+
for b in doc["bodies"]
|
|
178
|
+
)
|
|
179
|
+
geoms = tuple(
|
|
180
|
+
core.Geom(
|
|
181
|
+
body=int(g["body"]),
|
|
182
|
+
kind=g["kind"],
|
|
183
|
+
size=tuple(g["size"]),
|
|
184
|
+
pos=tuple(g["pos"]),
|
|
185
|
+
quat=tuple(g["quat"]),
|
|
186
|
+
scale=tuple(g["scale"]),
|
|
187
|
+
material=int(g["material"]),
|
|
188
|
+
mesh=g["mesh"],
|
|
189
|
+
role=g["role"],
|
|
190
|
+
name=g.get("name", ""),
|
|
191
|
+
)
|
|
192
|
+
for g in doc["geoms"]
|
|
193
|
+
)
|
|
194
|
+
materials = tuple(
|
|
195
|
+
core.Material(
|
|
196
|
+
rgba=tuple(m["rgba"]),
|
|
197
|
+
metallic=m["metallic"],
|
|
198
|
+
roughness=m["roughness"],
|
|
199
|
+
texture=m["texture"],
|
|
200
|
+
texrepeat=tuple(m["texrepeat"]),
|
|
201
|
+
)
|
|
202
|
+
for m in doc["materials"]
|
|
203
|
+
)
|
|
204
|
+
meshes = tuple(
|
|
205
|
+
codecs.decode_mesh(get_asset(cas.Ref.from_json(m)))
|
|
206
|
+
for m in doc["meshes"]
|
|
207
|
+
)
|
|
208
|
+
textures = tuple(
|
|
209
|
+
core.Texture(
|
|
210
|
+
get_asset(cas.Ref.from_json(t)),
|
|
211
|
+
t["media_type"],
|
|
212
|
+
int(t["width"]),
|
|
213
|
+
int(t["height"]),
|
|
214
|
+
)
|
|
215
|
+
for t in doc["textures"]
|
|
216
|
+
)
|
|
217
|
+
return core.Scene(bodies, geoms, materials, meshes, textures)
|
|
218
|
+
except (KeyError, TypeError) as exc:
|
|
219
|
+
raise errors.FormatError(
|
|
220
|
+
f"malformed scene descriptor: {exc!r}"
|
|
221
|
+
) from exc
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def load_scene(store: cas.ContentStore, ref: cas.Ref) -> core.Scene:
|
|
225
|
+
"""Loads a scene and its assets from a content store.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
store: The content store of the library.
|
|
229
|
+
ref: Reference of the scene JSON.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
The scene.
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
errors.FormatError: If the scene or an asset is malformed.
|
|
236
|
+
FileNotFoundError: If a referenced file is missing.
|
|
237
|
+
"""
|
|
238
|
+
doc = json.loads(store.get(ref, "scene"))
|
|
239
|
+
return scene_from_json(doc, store.get)
|