mujoco-react 8.4.2 → 8.6.0
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.
- package/README.md +128 -14
- package/bin/mujoco-react-codegen.mjs +3 -0
- package/bin/mujoco-react.mjs +86 -0
- package/dist/index.d.ts +100 -3
- package/dist/index.js +548 -76
- package/dist/index.js.map +1 -1
- package/dist/vite.d.ts +47 -0
- package/dist/vite.js +162 -0
- package/dist/vite.js.map +1 -0
- package/package.json +13 -1
- package/src/components/InstancedGeomRenderer.tsx +158 -0
- package/src/components/SceneRenderer.tsx +51 -12
- package/src/core/MujocoCanvas.tsx +2 -0
- package/src/core/MujocoPhysics.tsx +2 -0
- package/src/core/MujocoSimProvider.tsx +138 -10
- package/src/core/ObservationBuilder.ts +120 -0
- package/src/core/SceneLoader.ts +190 -60
- package/src/hooks/useObservation.ts +38 -0
- package/src/index.ts +9 -0
- package/src/types.ts +64 -1
- package/src/vite.ts +223 -0
package/src/vite.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
type ViteConfig = { root: string };
|
|
10
|
+
type ViteServer = {
|
|
11
|
+
watcher: {
|
|
12
|
+
add(paths: string | string[]): void;
|
|
13
|
+
on(event: 'add' | 'change' | 'unlink', listener: (file: string) => void): void;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface MujocoReactPluginOptions {
|
|
18
|
+
/** Entry MJCF/URDF files to scan. */
|
|
19
|
+
models: string | readonly string[];
|
|
20
|
+
/** Generated declaration file. Defaults to `src/mujoco-register.gen.d.ts`. */
|
|
21
|
+
generatedRegister?: string;
|
|
22
|
+
/** Module name to augment. Defaults to `mujoco-react`. */
|
|
23
|
+
moduleName?: string;
|
|
24
|
+
/** Disable console output. */
|
|
25
|
+
disableLogging?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MujocoRegisterCodegenOptions {
|
|
29
|
+
models: readonly string[];
|
|
30
|
+
out: string;
|
|
31
|
+
moduleName?: string;
|
|
32
|
+
root?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MujocoRegisterCodegenResult {
|
|
36
|
+
out: string;
|
|
37
|
+
files: string[];
|
|
38
|
+
counts: Record<RegisterKey, number>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type RegisterKey = 'actuators' | 'sensors' | 'bodies' | 'joints' | 'sites' | 'geoms' | 'keyframes';
|
|
42
|
+
|
|
43
|
+
const REGISTER_KEYS: RegisterKey[] = ['actuators', 'sensors', 'bodies', 'joints', 'sites', 'geoms', 'keyframes'];
|
|
44
|
+
const MODEL_EXTENSIONS = new Set(['.xml', '.mjcf', '.urdf']);
|
|
45
|
+
|
|
46
|
+
function createEmptyNames(): Record<RegisterKey, Set<string>> {
|
|
47
|
+
return {
|
|
48
|
+
actuators: new Set(),
|
|
49
|
+
sensors: new Set(),
|
|
50
|
+
bodies: new Set(),
|
|
51
|
+
joints: new Set(),
|
|
52
|
+
sites: new Set(),
|
|
53
|
+
geoms: new Set(),
|
|
54
|
+
keyframes: new Set(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function mujocoReact(options: MujocoReactPluginOptions) {
|
|
59
|
+
const models = Array.isArray(options.models) ? options.models : [options.models];
|
|
60
|
+
let root = process.cwd();
|
|
61
|
+
let generatedRegister = options.generatedRegister ?? 'src/mujoco-register.gen.d.ts';
|
|
62
|
+
let watchedFiles: string[] = [];
|
|
63
|
+
|
|
64
|
+
async function generate() {
|
|
65
|
+
const result = await generateMujocoRegister({
|
|
66
|
+
models,
|
|
67
|
+
out: generatedRegister,
|
|
68
|
+
moduleName: options.moduleName,
|
|
69
|
+
root,
|
|
70
|
+
});
|
|
71
|
+
watchedFiles = result.files;
|
|
72
|
+
if (!options.disableLogging) {
|
|
73
|
+
const total = Object.values(result.counts).reduce((sum, count) => sum + count, 0);
|
|
74
|
+
console.log(`[mujoco-react] generated ${path.relative(root, result.out)} (${total} names)`);
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
name: 'mujoco-react',
|
|
81
|
+
enforce: 'pre' as const,
|
|
82
|
+
configResolved(config: ViteConfig) {
|
|
83
|
+
root = config.root;
|
|
84
|
+
generatedRegister = path.resolve(root, generatedRegister);
|
|
85
|
+
},
|
|
86
|
+
async buildStart(this: { addWatchFile?: (file: string) => void }) {
|
|
87
|
+
const result = await generate();
|
|
88
|
+
for (const file of result.files) this.addWatchFile?.(file);
|
|
89
|
+
},
|
|
90
|
+
configureServer(server: ViteServer) {
|
|
91
|
+
generate().then((result) => server.watcher.add(result.files)).catch((error: unknown) => {
|
|
92
|
+
console.error('[mujoco-react] register generation failed', error);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
server.watcher.on('add', regenerateIfModelFile);
|
|
96
|
+
server.watcher.on('change', regenerateIfModelFile);
|
|
97
|
+
server.watcher.on('unlink', regenerateIfModelFile);
|
|
98
|
+
|
|
99
|
+
function regenerateIfModelFile(file: string) {
|
|
100
|
+
if (!shouldRegenerate(file, watchedFiles, models, root)) return;
|
|
101
|
+
generate().then((result) => server.watcher.add(result.files)).catch((error: unknown) => {
|
|
102
|
+
console.error('[mujoco-react] register generation failed', error);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function generateMujocoRegister(
|
|
110
|
+
options: MujocoRegisterCodegenOptions
|
|
111
|
+
): Promise<MujocoRegisterCodegenResult> {
|
|
112
|
+
const root = path.resolve(options.root ?? process.cwd());
|
|
113
|
+
const out = path.resolve(root, options.out);
|
|
114
|
+
const moduleName = options.moduleName ?? 'mujoco-react';
|
|
115
|
+
const names = createEmptyNames();
|
|
116
|
+
const seen = new Set<string>();
|
|
117
|
+
|
|
118
|
+
for (const model of options.models) {
|
|
119
|
+
await scanModel(path.resolve(root, model), root, seen, names);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await mkdir(path.dirname(out), { recursive: true });
|
|
123
|
+
await writeFile(out, renderRegister(moduleName, names), 'utf8');
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
out,
|
|
127
|
+
files: [...seen].sort((a, b) => a.localeCompare(b)),
|
|
128
|
+
counts: Object.fromEntries(REGISTER_KEYS.map((key) => [key, names[key].size])) as Record<RegisterKey, number>,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function scanModel(
|
|
133
|
+
filePath: string,
|
|
134
|
+
root: string,
|
|
135
|
+
seen: Set<string>,
|
|
136
|
+
names: Record<RegisterKey, Set<string>>
|
|
137
|
+
) {
|
|
138
|
+
const normalized = path.normalize(filePath);
|
|
139
|
+
if (seen.has(normalized)) return;
|
|
140
|
+
seen.add(normalized);
|
|
141
|
+
|
|
142
|
+
const xml = await readFile(normalized, 'utf8');
|
|
143
|
+
collectSimpleTagNames(xml, 'body', names.bodies);
|
|
144
|
+
collectSimpleTagNames(xml, 'joint', names.joints);
|
|
145
|
+
collectSimpleTagNames(xml, 'site', names.sites);
|
|
146
|
+
collectSimpleTagNames(xml, 'geom', names.geoms);
|
|
147
|
+
collectSimpleTagNames(xml, 'key', names.keyframes);
|
|
148
|
+
collectSectionNames(xml, 'actuator', names.actuators);
|
|
149
|
+
collectSectionNames(xml, 'sensor', names.sensors);
|
|
150
|
+
|
|
151
|
+
for (const includePath of collectIncludePaths(xml)) {
|
|
152
|
+
const next = path.resolve(path.dirname(normalized), includePath);
|
|
153
|
+
if (next.startsWith(root)) await scanModel(next, root, seen, names);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function collectSimpleTagNames(xml: string, tag: string, target: Set<string>) {
|
|
158
|
+
const pattern = new RegExp(`<\\s*${tag}\\b([^>]*)>`, 'gi');
|
|
159
|
+
for (const match of xml.matchAll(pattern)) {
|
|
160
|
+
const name = readAttr(match[1], 'name');
|
|
161
|
+
if (name) target.add(name);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function collectSectionNames(xml: string, section: string, target: Set<string>) {
|
|
166
|
+
const sectionPattern = new RegExp(`<\\s*${section}\\b[^>]*>([\\s\\S]*?)<\\s*/\\s*${section}\\s*>`, 'gi');
|
|
167
|
+
for (const sectionMatch of xml.matchAll(sectionPattern)) {
|
|
168
|
+
const tagPattern = /<\s*[a-zA-Z0-9_:-]+\b([^>]*)>/g;
|
|
169
|
+
for (const tagMatch of sectionMatch[1].matchAll(tagPattern)) {
|
|
170
|
+
const name = readAttr(tagMatch[1], 'name');
|
|
171
|
+
if (name) target.add(name);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function collectIncludePaths(xml: string): string[] {
|
|
177
|
+
const result: string[] = [];
|
|
178
|
+
const pattern = /<\s*include\b([^>]*)>/gi;
|
|
179
|
+
for (const match of xml.matchAll(pattern)) {
|
|
180
|
+
const file = readAttr(match[1], 'file');
|
|
181
|
+
if (file && !file.includes('://') && !file.startsWith('/')) result.push(file);
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function readAttr(attrs: string, attr: string): string | undefined {
|
|
187
|
+
const pattern = new RegExp(`\\b${attr}\\s*=\\s*(['"])(.*?)\\1`, 'i');
|
|
188
|
+
return attrs.match(pattern)?.[2];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function renderRegister(moduleName: string, names: Record<RegisterKey, Set<string>>): string {
|
|
192
|
+
const fields = REGISTER_KEYS
|
|
193
|
+
.filter((key) => names[key].size > 0)
|
|
194
|
+
.map((key) => ` ${key}: ${renderUnion(names[key])};`);
|
|
195
|
+
|
|
196
|
+
return `// Auto-generated by mujoco-react. Do not edit.
|
|
197
|
+
// Regenerate by running Vite with the mujocoReact() plugin or \`mujoco-react codegen\`.
|
|
198
|
+
|
|
199
|
+
import 'mujoco-react';
|
|
200
|
+
|
|
201
|
+
declare module '${moduleName}' {
|
|
202
|
+
interface Register {
|
|
203
|
+
${fields.join('\n')}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function renderUnion(values: Set<string>): string {
|
|
210
|
+
return [...values].sort((a, b) => a.localeCompare(b)).map((value) => `'${escapeTs(value)}'`).join(' | ');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function escapeTs(value: string): string {
|
|
214
|
+
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function shouldRegenerate(file: string, watchedFiles: string[], models: readonly string[], root: string): boolean {
|
|
218
|
+
const absolute = path.resolve(file);
|
|
219
|
+
if (watchedFiles.includes(absolute)) return true;
|
|
220
|
+
if (!MODEL_EXTENSIONS.has(path.extname(absolute).toLowerCase())) return false;
|
|
221
|
+
const modelDirs = models.map((model) => path.dirname(path.resolve(root, model)));
|
|
222
|
+
return modelDirs.some((dir) => absolute.startsWith(dir));
|
|
223
|
+
}
|