@wonderlandengine/mcp-plugin 1.0.3

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.
@@ -0,0 +1,149 @@
1
+ import { z } from "zod";
2
+ const SafeRecord = z.record(z.string(), z.any());
3
+ export const ResourceTypes = [
4
+ "objects",
5
+ "textures",
6
+ "meshes",
7
+ "materials",
8
+ "animations",
9
+ "skins",
10
+ "images",
11
+ "shaders",
12
+ "pipelines",
13
+ "fonts",
14
+ "morphTargets",
15
+ "particleEffects",
16
+ ];
17
+ export const queryResourcesSchema = {
18
+ resourceType: z.enum(ResourceTypes).describe("Type of resource to query."),
19
+ ids: z
20
+ .string()
21
+ .array()
22
+ .describe("List of ids to query, leave out to list all resources of given type.")
23
+ .optional(),
24
+ includeFilter: SafeRecord.describe("Optional properties to match. Name will be matched with contains substring comparison rather than full comparison. Leave out to disable this filtering.").optional(),
25
+ excludeFilter: SafeRecord.describe("Optional properties, which will exclude items from the list if match. Name will be matched with contains substring comparison rather than full comparison. Leave out to disable this filtering.").optional(),
26
+ paths: z
27
+ .string()
28
+ .array()
29
+ .optional()
30
+ .describe('List of paths to filter each object by to reduce output (similar to GraphQL APIs), e.g. "name" will return only the object\'s name for each object.'),
31
+ };
32
+ export const querySettingsSchema = {
33
+ paths: z
34
+ .string()
35
+ .array()
36
+ .optional()
37
+ .describe("List of settings paths to filter by, e.g. physics.enabled"),
38
+ };
39
+ export const modifySettingsSchema = {
40
+ changeSettings: SafeRecord.optional().describe('Nested partial settings object to merge. Example: {"physx": {"enable": true}, "editor": {"importPhysicalAsPhongMaterials": true}}.'),
41
+ resetPaths: z
42
+ .string()
43
+ .array()
44
+ .optional()
45
+ .describe("Settings paths to reset, separated by '.', e.g. `editor.importPhysicalAsPhongMaterials`"),
46
+ };
47
+ export const queryComponentTypesSchema = {
48
+ names: z
49
+ .string()
50
+ .array()
51
+ .optional()
52
+ .describe("List of component names to filter by.")
53
+ };
54
+ export const deleteObjectsSchema = {
55
+ objectIds: z.string().array(),
56
+ };
57
+ export const modifyObjectsSchema = {
58
+ modifications: z
59
+ .object({
60
+ name: z.string().describe("Name for the object").optional(),
61
+ id: z
62
+ .string()
63
+ .describe("ID of the object. Creates a new object, if not set.")
64
+ .optional(),
65
+ parentId: z
66
+ .string()
67
+ .nullable()
68
+ .describe("ID of the parent to parent to.")
69
+ .optional(),
70
+ position: z
71
+ .number()
72
+ .array()
73
+ .length(3)
74
+ .describe("Array of three numbers for position")
75
+ .optional(),
76
+ rotation: z
77
+ .number()
78
+ .array()
79
+ .length(4)
80
+ .describe("Array of four numbers for rotation quaternion")
81
+ .optional(),
82
+ scaling: z
83
+ .number()
84
+ .array()
85
+ .length(3)
86
+ .describe("Array of three numbers for scaling")
87
+ .optional(),
88
+ addComponents: z
89
+ .object({
90
+ type: z.string(),
91
+ properties: z.record(z.string(), z.any()).optional(),
92
+ })
93
+ .array()
94
+ .describe("Components to add, specify the type and the properties to set.")
95
+ .optional(),
96
+ modifyComponents: z
97
+ .object({
98
+ index: z.number(),
99
+ properties: z.record(z.string(), z.any()).optional(),
100
+ })
101
+ .array()
102
+ .describe("Components to modify on the object.")
103
+ .optional(),
104
+ removeComponents: z
105
+ .number()
106
+ .array()
107
+ .describe("Indices of components to remove from the object")
108
+ .optional(),
109
+ })
110
+ .array(),
111
+ };
112
+ export const modifyResourcesSchema = {
113
+ modifications: z
114
+ .object({
115
+ resourceType: z.enum(ResourceTypes).describe("Type of resource."),
116
+ id: z
117
+ .string()
118
+ .describe("ID of the resource to modify. If omitted, a new resource will be created (where supported).")
119
+ .optional(),
120
+ changeProperties: SafeRecord.optional().describe("Properties to change."),
121
+ resetProperties: z
122
+ .string()
123
+ .array()
124
+ .describe("Property names to reset to their defaults.")
125
+ .optional(),
126
+ })
127
+ .array(),
128
+ };
129
+ export const importSceneFilesSchema = {
130
+ imports: z
131
+ .object({
132
+ path: z
133
+ .string()
134
+ .describe("Path of the scene file relative to the project file. Formats are preferred in this order: GLB, FBX, GLTF, OBJ, PLY"),
135
+ })
136
+ .array(),
137
+ };
138
+ export const importFilesSchema = {
139
+ imports: z
140
+ .string()
141
+ .describe("Path of the scene file relative to the project file. Formats are preferred in this order: GLB, FBX, GLTF, OBJ, PLY")
142
+ .array(),
143
+ };
144
+ export const computeMeshBoundsSchema = {
145
+ meshIds: z.string().array(),
146
+ };
147
+ export const computeObjectBoundsSchema = {
148
+ objectIds: z.string().array(),
149
+ };
@@ -0,0 +1,6 @@
1
+ import { WorkQueue } from "./utils/work-queue.js";
2
+ export declare function main(params: {
3
+ port: number;
4
+ queue: WorkQueue;
5
+ }): Promise<void>;
6
+ export declare function shutdown(): Promise<void>;
@@ -0,0 +1,111 @@
1
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
3
+ import express from "express";
4
+ import { server, setQueue } from "./mcp-server.js";
5
+ const app = express();
6
+ app.use(express.json());
7
+ app.use((req, _res, next) => {
8
+ console.log(req.method, req.path);
9
+ console.dir(req.body, { depth: null });
10
+ next();
11
+ });
12
+ let transport = null;
13
+ async function connectTransport() {
14
+ const newTransport = new StreamableHTTPServerTransport({
15
+ // A session id is still internally generated by the transport, but
16
+ // we do not expose or require it anymore.
17
+ sessionIdGenerator: () => "single-session",
18
+ onsessioninitialized: () => {
19
+ console.log("[MCP] Single session initialized");
20
+ },
21
+ });
22
+ newTransport.onclose = () => {
23
+ console.log("[MCP] Transport closed");
24
+ transport = null;
25
+ };
26
+ console.log("[MCP] Connecting transport");
27
+ await server.connect(newTransport);
28
+ transport = newTransport;
29
+ }
30
+ async function ensureTransportInitialized(body) {
31
+ const wantsInitialize = isInitializeRequest(body);
32
+ if (!transport) {
33
+ if (!wantsInitialize) {
34
+ return false;
35
+ }
36
+ await connectTransport();
37
+ return true;
38
+ }
39
+ if (wantsInitialize) {
40
+ console.log("[MCP] Reinitializing transport after new initialize request");
41
+ const oldTransport = transport;
42
+ transport = null;
43
+ try {
44
+ if (oldTransport && typeof oldTransport.close === "function") {
45
+ await Promise.resolve(oldTransport.close());
46
+ }
47
+ }
48
+ catch (err) {
49
+ console.warn("[MCP] Error while closing existing transport", err);
50
+ }
51
+ try {
52
+ await server.close();
53
+ }
54
+ catch (err) {
55
+ console.warn("[MCP] Error while closing MCP server before reinitialization", err);
56
+ }
57
+ await connectTransport();
58
+ }
59
+ return true;
60
+ }
61
+ app.post("/mcp", async (req, res) => {
62
+ try {
63
+ const ready = await ensureTransportInitialized(req.body);
64
+ if (!ready || !transport) {
65
+ res.status(400).json({
66
+ jsonrpc: "2.0",
67
+ error: { code: -32000, message: "Must call initialize first" },
68
+ id: req.body?.id ?? null,
69
+ });
70
+ return;
71
+ }
72
+ await transport.handleRequest(req, res, req.body);
73
+ }
74
+ catch (e) {
75
+ console.error("[MCP] POST /mcp error", e);
76
+ if (!res.headersSent) {
77
+ res.status(500).json({
78
+ jsonrpc: "2.0",
79
+ error: { code: -32001, message: "Internal server error" },
80
+ id: req.body?.id ?? null,
81
+ });
82
+ }
83
+ }
84
+ });
85
+ async function handleRequest(req, res) {
86
+ if (!transport) {
87
+ res.status(400).send("Not initialized");
88
+ return;
89
+ }
90
+ await transport.handleRequest(req, res);
91
+ }
92
+ app.get("/mcp", handleRequest);
93
+ app.delete("/mcp", handleRequest);
94
+ export async function main(params) {
95
+ setQueue(params.queue);
96
+ return new Promise((res, rej) => {
97
+ try {
98
+ app.listen(params.port, (error) => {
99
+ if (error)
100
+ rej(error);
101
+ });
102
+ res();
103
+ }
104
+ catch (e) {
105
+ rej(e);
106
+ }
107
+ });
108
+ }
109
+ export async function shutdown() {
110
+ server.close();
111
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Copy bounds from src into out. Bounds layout: [minX, minY, minZ, maxX, maxY, maxZ]
3
+ */
4
+ export declare function copyBounds(out: Float32Array, src: Float32Array): Float32Array;
5
+ /**
6
+ * Join two bounds a and b into out using component-wise min/max.
7
+ * Returns out, or null if both inputs are null/undefined.
8
+ * Safe to pass out === a or out === b.
9
+ */
10
+ export declare function joinBounds(out: Float32Array, a?: Float32Array | null, b?: Float32Array | null): Float32Array | null;
11
+ /**
12
+ * Scale bounds src by scale vector [sx,sy,sz] into out.
13
+ * Handles negative scales by swapping min/max per axis as needed.
14
+ */
15
+ export declare function scaleBounds(out: Float32Array, src: Float32Array, scale: number[] | Float32Array): Float32Array;
16
+ export declare const quatToMat3RM: (q: number[] | Float32Array | undefined) => Float32Array;
17
+ export declare const mulMat3RM: (a: Float32Array, b: Float32Array) => Float32Array;
18
+ export declare const mulMat3Vec3RM: (m: Float32Array, v: number[] | Float32Array) => Float32Array;
19
+ export declare const buildLocalLinearRM: (rotation: number[] | Float32Array | undefined, scaling: number[] | Float32Array | undefined) => Float32Array;
20
+ export declare const transformAabbCornersRM: (out: Float32Array, src: Float32Array, linearRM: Float32Array, translation: number[] | Float32Array) => Float32Array;
21
+ export declare const mat3IdentityRM: () => Float32Array;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Copy bounds from src into out. Bounds layout: [minX, minY, minZ, maxX, maxY, maxZ]
3
+ */
4
+ export function copyBounds(out, src) {
5
+ out.set(src);
6
+ return out;
7
+ }
8
+ /**
9
+ * Join two bounds a and b into out using component-wise min/max.
10
+ * Returns out, or null if both inputs are null/undefined.
11
+ * Safe to pass out === a or out === b.
12
+ */
13
+ export function joinBounds(out, a, b) {
14
+ if (!a && !b)
15
+ return null;
16
+ if (!a)
17
+ return copyBounds(out, b);
18
+ if (!b)
19
+ return copyBounds(out, a);
20
+ const aminx = a[0], aminy = a[1], aminz = a[2], amaxx = a[3], amaxy = a[4], amaxz = a[5];
21
+ const bminx = b[0], bminy = b[1], bminz = b[2], bmaxx = b[3], bmaxy = b[4], bmaxz = b[5];
22
+ out[0] = Math.min(aminx, bminx);
23
+ out[1] = Math.min(aminy, bminy);
24
+ out[2] = Math.min(aminz, bminz);
25
+ out[3] = Math.max(amaxx, bmaxx);
26
+ out[4] = Math.max(amaxy, bmaxy);
27
+ out[5] = Math.max(amaxz, bmaxz);
28
+ return out;
29
+ }
30
+ /**
31
+ * Scale bounds src by scale vector [sx,sy,sz] into out.
32
+ * Handles negative scales by swapping min/max per axis as needed.
33
+ */
34
+ export function scaleBounds(out, src, scale) {
35
+ const sx = scale?.[0] ?? 1;
36
+ const sy = scale?.[1] ?? 1;
37
+ const sz = scale?.[2] ?? 1;
38
+ const x0 = src[0] * sx, x1 = src[3] * sx;
39
+ const y0 = src[1] * sy, y1 = src[4] * sy;
40
+ const z0 = src[2] * sz, z1 = src[5] * sz;
41
+ out[0] = Math.min(x0, x1);
42
+ out[1] = Math.min(y0, y1);
43
+ out[2] = Math.min(z0, z1);
44
+ out[3] = Math.max(x0, x1);
45
+ out[4] = Math.max(y0, y1);
46
+ out[5] = Math.max(z0, z1);
47
+ return out;
48
+ }
49
+ /* /!\ Warning: AI generated slop utils */
50
+ export const quatToMat3RM = (q) => {
51
+ // Read real part only if dual quaternion passed
52
+ const x = q?.[0] ?? 0;
53
+ const y = q?.[1] ?? 0;
54
+ const z = q?.[2] ?? 0;
55
+ const w = q?.[3] ?? 1;
56
+ const x2 = x + x, y2 = y + y, z2 = z + z;
57
+ const xx = x * x2, xy = x * y2, xz = x * z2;
58
+ const yy = y * y2, yz = y * z2, zz = z * z2;
59
+ const wx = w * x2, wy = w * y2, wz = w * z2;
60
+ // Row-major layout
61
+ return new Float32Array([
62
+ 1 - (yy + zz),
63
+ xy + wz,
64
+ xz - wy,
65
+ xy - wz,
66
+ 1 - (xx + zz),
67
+ yz + wx,
68
+ xz + wy,
69
+ yz - wx,
70
+ 1 - (xx + yy),
71
+ ]);
72
+ };
73
+ export const mulMat3RM = (a, b) => {
74
+ const out = new Float32Array(9);
75
+ // out = a * b (row-major)
76
+ out[0] = a[0] * b[0] + a[1] * b[3] + a[2] * b[6];
77
+ out[1] = a[0] * b[1] + a[1] * b[4] + a[2] * b[7];
78
+ out[2] = a[0] * b[2] + a[1] * b[5] + a[2] * b[8];
79
+ out[3] = a[3] * b[0] + a[4] * b[3] + a[5] * b[6];
80
+ out[4] = a[3] * b[1] + a[4] * b[4] + a[5] * b[7];
81
+ out[5] = a[3] * b[2] + a[4] * b[5] + a[5] * b[8];
82
+ out[6] = a[6] * b[0] + a[7] * b[3] + a[8] * b[6];
83
+ out[7] = a[6] * b[1] + a[7] * b[4] + a[8] * b[7];
84
+ out[8] = a[6] * b[2] + a[7] * b[5] + a[8] * b[8];
85
+ return out;
86
+ };
87
+ export const mulMat3Vec3RM = (m, v) => {
88
+ const x = v?.[0] ?? 0, y = v?.[1] ?? 0, z = v?.[2] ?? 0;
89
+ return new Float32Array([
90
+ m[0] * x + m[1] * y + m[2] * z,
91
+ m[3] * x + m[4] * y + m[5] * z,
92
+ m[6] * x + m[7] * y + m[8] * z,
93
+ ]);
94
+ };
95
+ export const buildLocalLinearRM = (rotation, scaling) => {
96
+ const r = quatToMat3RM(rotation);
97
+ const sx = scaling?.[0] ?? 1;
98
+ const sy = scaling?.[1] ?? 1;
99
+ const sz = scaling?.[2] ?? 1;
100
+ // R * S (row-major): multiply by diagonal scale
101
+ const s = new Float32Array([sx, 0, 0, 0, sy, 0, 0, 0, sz]);
102
+ return mulMat3RM(r, s);
103
+ };
104
+ export const transformAabbCornersRM = (out, src, linearRM, translation) => {
105
+ const minx = src[0], miny = src[1], minz = src[2], maxx = src[3], maxy = src[4], maxz = src[5];
106
+ const t = translation;
107
+ const tx = t?.[0] ?? 0, ty = t?.[1] ?? 0, tz = t?.[2] ?? 0;
108
+ // Iterate 8 corners
109
+ let wminx = Infinity, wminy = Infinity, wminz = Infinity, wmaxx = -Infinity, wmaxy = -Infinity, wmaxz = -Infinity;
110
+ const corners = [
111
+ [minx, miny, minz],
112
+ [minx, miny, maxz],
113
+ [minx, maxy, minz],
114
+ [minx, maxy, maxz],
115
+ [maxx, miny, minz],
116
+ [maxx, miny, maxz],
117
+ [maxx, maxy, minz],
118
+ [maxx, maxy, maxz],
119
+ ];
120
+ for (const c of corners) {
121
+ const p = mulMat3Vec3RM(linearRM, c);
122
+ const px = p[0] + tx, py = p[1] + ty, pz = p[2] + tz;
123
+ if (px < wminx)
124
+ wminx = px;
125
+ if (py < wminy)
126
+ wminy = py;
127
+ if (pz < wminz)
128
+ wminz = pz;
129
+ if (px > wmaxx)
130
+ wmaxx = px;
131
+ if (py > wmaxy)
132
+ wmaxy = py;
133
+ if (pz > wmaxz)
134
+ wmaxz = pz;
135
+ }
136
+ out[0] = wminx;
137
+ out[1] = wminy;
138
+ out[2] = wminz;
139
+ out[3] = wmaxx;
140
+ out[4] = wmaxy;
141
+ out[5] = wmaxz;
142
+ return out;
143
+ };
144
+ export const mat3IdentityRM = () => new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
@@ -0,0 +1,14 @@
1
+ export interface TreeBoundsResult {
2
+ bounds: Float32Array | null;
3
+ /** Number of descendants (subtree size excluding the root). */
4
+ count: number;
5
+ }
6
+ /**
7
+ * Recursively computes the axis-aligned bounds for an object subtree and counts descendants.
8
+ * - Mesh bounds are merged per object and transformed by the object's full SRT (scale, rotation, translation).
9
+ * - Child subtree bounds are computed in world space via hierarchical SRT and merged into the parent accumulator.
10
+ * - Handles negative scales and arbitrary rotations via center/extent AABB transform.
11
+ */
12
+ export declare function computeTreeBounds(objectId: string, objects: Record<string, any>): TreeBoundsResult;
13
+ /** Stringify while including defaults on Access objects. */
14
+ export declare function stringifyWithDefaults(input: any, space?: number | string): string;
@@ -0,0 +1,102 @@
1
+ import { tools } from "@wonderlandengine/editor-api";
2
+ import { buildLocalLinearRM, copyBounds, joinBounds, mat3IdentityRM, mulMat3RM, mulMat3Vec3RM, transformAabbCornersRM, } from "./bounds.js";
3
+ /**
4
+ * Recursively computes the axis-aligned bounds for an object subtree and counts descendants.
5
+ * - Mesh bounds are merged per object and transformed by the object's full SRT (scale, rotation, translation).
6
+ * - Child subtree bounds are computed in world space via hierarchical SRT and merged into the parent accumulator.
7
+ * - Handles negative scales and arbitrary rotations via center/extent AABB transform.
8
+ */
9
+ export function computeTreeBounds(objectId, objects) {
10
+ // Recursive computation carrying world linear matrix (3x3) and world translation
11
+ const computeRecursive = (oid, parentLinearRM, parentTranslation) => {
12
+ const obj = objects[oid];
13
+ if (!obj)
14
+ return { bounds: null, count: 0 };
15
+ const localLinearRM = buildLocalLinearRM(obj.rotation, obj.scaling);
16
+ const worldLinearRM = mulMat3RM(parentLinearRM, localLinearRM);
17
+ const localT = obj.translation ?? [0, 0, 0];
18
+ const worldT = mulMat3Vec3RM(parentLinearRM, localT);
19
+ worldT[0] += parentTranslation?.[0] ?? 0;
20
+ worldT[1] += parentTranslation?.[1] ?? 0;
21
+ worldT[2] += parentTranslation?.[2] ?? 0;
22
+ let count = 0;
23
+ const acc = new Float32Array(6);
24
+ const tmp = new Float32Array(6);
25
+ let has = false;
26
+ // Self mesh components (components is an object keyed by index)
27
+ const comps = Object.values(obj.components ?? {});
28
+ for (const comp of comps) {
29
+ if (!comp || comp.type !== "mesh")
30
+ continue;
31
+ const meshId = comp?.mesh?.mesh;
32
+ if (!meshId)
33
+ continue;
34
+ try {
35
+ const mb = new Float32Array(6);
36
+ tools.computeMeshBounds(meshId, mb);
37
+ transformAabbCornersRM(tmp, mb, worldLinearRM, worldT);
38
+ if (!has) {
39
+ copyBounds(acc, tmp);
40
+ has = true;
41
+ }
42
+ else {
43
+ joinBounds(acc, acc, tmp);
44
+ }
45
+ }
46
+ catch {
47
+ // Ignore compute errors per component
48
+ }
49
+ }
50
+ // Children
51
+ for (const [cid, child] of Object.entries(objects)) {
52
+ if (!child)
53
+ continue;
54
+ if (String(child.parent) !== String(oid))
55
+ continue;
56
+ const { bounds: cb, count: cc } = computeRecursive(cid, worldLinearRM, worldT);
57
+ count += 1 + cc; // include the child and its descendants
58
+ if (cb) {
59
+ if (!has) {
60
+ copyBounds(acc, cb);
61
+ has = true;
62
+ }
63
+ else {
64
+ joinBounds(acc, acc, cb);
65
+ }
66
+ }
67
+ }
68
+ return { bounds: has ? acc : null, count };
69
+ };
70
+ return computeRecursive(objectId, mat3IdentityRM(), [0, 0, 0]);
71
+ }
72
+ /** Stringify while including defaults on Access objects. */
73
+ export function stringifyWithDefaults(input, space) {
74
+ const seen = new WeakSet();
75
+ const materialize = (v) => {
76
+ if (v === null || typeof v !== "object")
77
+ return v;
78
+ if (seen.has(v))
79
+ return "[Circular]";
80
+ seen.add(v);
81
+ if (Array.isArray(v)) {
82
+ // Build dense array via inherited/enum indices
83
+ let max = -1;
84
+ for (const k in v) {
85
+ const i = +k;
86
+ if (Number.isInteger(i) && i > max)
87
+ max = i;
88
+ }
89
+ const len = Math.max(v.length, max + 1);
90
+ const out = new Array(len);
91
+ for (let i = 0; i < len; i++)
92
+ out[i] = materialize(v[i]);
93
+ return out;
94
+ }
95
+ // Copy own + inherited enumerable keys (triggers interceptors/getters)
96
+ const out = {};
97
+ for (const k in v)
98
+ out[k] = materialize(v[k]);
99
+ return out;
100
+ };
101
+ return JSON.stringify(materialize(input), null, space);
102
+ }
@@ -0,0 +1,18 @@
1
+ export declare function isObject(item: any): item is Record<string, any>;
2
+ export declare function assignNested(target: Record<string, any>, source: Record<string, any>): void;
3
+ /**
4
+ * Assigns properties to a component, ensuring type and length consistency.
5
+ *
6
+ * @param typeName Name of the component type
7
+ * @param comp Component properties
8
+ * @param properties Properties to assign to the component
9
+ * @param o Object containing the component
10
+ */
11
+ export declare function assignComponentProperties(typeName: string, comp: any, properties: Record<string, any>, o: {
12
+ name: string;
13
+ }, index?: number): void;
14
+ /**
15
+ * Filters the properties of an object based on the provided filters.
16
+ * @param filters List of property paths to filter by, e.g. "physx.enabled"
17
+ */
18
+ export declare function filterObjectProperties(filters: string[] | undefined, o: any): any;
@@ -0,0 +1,87 @@
1
+ export function isObject(item) {
2
+ return item && typeof item === "object" && !Array.isArray(item);
3
+ }
4
+ export function assignNested(target, source) {
5
+ for (const key in source) {
6
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
7
+ if (isObject(source[key]) && isObject(target[key])) {
8
+ assignNested(target[key], source[key]);
9
+ }
10
+ else {
11
+ target[key] = source[key];
12
+ }
13
+ }
14
+ }
15
+ }
16
+ /**
17
+ * Assigns properties to a component, ensuring type and length consistency.
18
+ *
19
+ * @param typeName Name of the component type
20
+ * @param comp Component properties
21
+ * @param properties Properties to assign to the component
22
+ * @param o Object containing the component
23
+ */
24
+ export function assignComponentProperties(typeName, comp, properties, o, index = 0) {
25
+ for (const key of Object.keys(comp)) {
26
+ if (key in properties) {
27
+ const inType = typeof properties[key];
28
+ /* Resource references may be null, but expect string. We special case this. */
29
+ if (typeof comp[key] !== inType &&
30
+ !(comp[key] == null && inType === "string")) {
31
+ throw new Error(`Type mismatch for property '${key}' in '${typeName}' component at index ${index} of object ${o.name}. Expected ${typeof comp[key]}, got ${inType}`);
32
+ }
33
+ if (Array.isArray(comp[key]) &&
34
+ properties[key].length !== comp[key].length) {
35
+ throw new Error(`Length mismatch for property '${key}' in '${typeName}' component with index ${index} of object "${o.name}". Expected length ${comp[key].length}, got ${properties[key].length}`);
36
+ }
37
+ if (isObject(properties[key])) {
38
+ assignNested(comp[key], properties[key]);
39
+ continue;
40
+ }
41
+ comp[key] = properties[key];
42
+ }
43
+ }
44
+ }
45
+ /**
46
+ * Filters the properties of an object based on the provided filters.
47
+ * @param filters List of property paths to filter by, e.g. "physx.enabled"
48
+ */
49
+ export function filterObjectProperties(filters, o) {
50
+ if (!filters || filters.length === 0) {
51
+ return o;
52
+ }
53
+ const result = {};
54
+ for (const filter of filters) {
55
+ const keys = filter.split(".");
56
+ // First, validate that the entire path exists on the source object
57
+ let src = o;
58
+ let valid = true;
59
+ for (let i = 0; i < keys.length; i++) {
60
+ const key = keys[i];
61
+ if (src == null || !(key in src)) {
62
+ valid = false;
63
+ break;
64
+ }
65
+ if (i < keys.length - 1)
66
+ src = src[key];
67
+ }
68
+ if (!valid)
69
+ continue; // Skip non-existent paths entirely
70
+ // Re-traverse to build the result path and assign the leaf value
71
+ let dst = result;
72
+ src = o;
73
+ for (let i = 0; i < keys.length; i++) {
74
+ const key = keys[i];
75
+ if (i === keys.length - 1) {
76
+ dst[key] = src[key];
77
+ }
78
+ else {
79
+ if (!(key in dst))
80
+ dst[key] = {};
81
+ dst = dst[key];
82
+ src = src[key];
83
+ }
84
+ }
85
+ }
86
+ return result;
87
+ }
@@ -0,0 +1,17 @@
1
+ export declare class WorkQueue {
2
+ private _queue;
3
+ /**
4
+ * Push a new work item into the queue
5
+ */
6
+ push(func: () => Promise<void>): Promise<void>;
7
+ /**
8
+ * Run the top most command in the queue
9
+ *
10
+ * @returns `true`, if a command was run, `false` if the queue was empty.
11
+ */
12
+ pop(): boolean;
13
+ /**
14
+ * Whether the work queue is empty
15
+ */
16
+ get empty(): boolean;
17
+ }