@kite3d/engine 0.15.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.
Files changed (74) hide show
  1. package/dist/authoring.d.ts +54 -0
  2. package/dist/authoring.js +165 -0
  3. package/dist/authoring.js.map +1 -0
  4. package/dist/authoringValidation.d.ts +110 -0
  5. package/dist/authoringValidation.js +488 -0
  6. package/dist/authoringValidation.js.map +1 -0
  7. package/dist/defaults.d.ts +2 -0
  8. package/dist/fileTypes.d.ts +5 -0
  9. package/dist/fileTypes.js +51 -0
  10. package/dist/fileTypes.js.map +1 -0
  11. package/dist/importMap.d.ts +15 -0
  12. package/dist/importMap.js +62 -0
  13. package/dist/importMap.js.map +1 -0
  14. package/dist/index.d.ts +15 -0
  15. package/dist/index.js +2756 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/migrations.js +5 -0
  18. package/dist/migrations.js.map +1 -0
  19. package/dist/paths.d.ts +9 -0
  20. package/dist/paths.js +13 -0
  21. package/dist/paths.js.map +1 -0
  22. package/dist/plugins/GeneratorComponent.d.ts +40 -0
  23. package/dist/plugins/HtmlUiComponent.d.ts +52 -0
  24. package/dist/plugins/HtmlUiComponent.example.d.ts +0 -0
  25. package/dist/plugins/cannon/Cannon3DBodyComponent.d.ts +27 -0
  26. package/dist/plugins/cannon/Cannon3DShapeComponent.d.ts +57 -0
  27. package/dist/plugins/cannon/CannonPhysicsPlugin.d.ts +63 -0
  28. package/dist/plugins/cannon/CannonRagdollComponent.d.ts +148 -0
  29. package/dist/plugins/cannon/helper.d.ts +24 -0
  30. package/dist/plugins/cannon/threeToCannon.d.ts +63 -0
  31. package/dist/plugins/cannon/utils.d.ts +9 -0
  32. package/dist/projectFormat.js +108 -0
  33. package/dist/projectFormat.js.map +1 -0
  34. package/dist/runtime/createGame.d.ts +29 -0
  35. package/dist/runtime/index.d.ts +18 -0
  36. package/dist/runtime/migrations.d.ts +5 -0
  37. package/dist/runtime/nestedAssets.d.ts +26 -0
  38. package/dist/runtime/projectFormat.d.ts +83 -0
  39. package/dist/runtime/version.d.ts +1 -0
  40. package/dist/runtime.js +72835 -0
  41. package/dist/runtime.js.map +1 -0
  42. package/dist/sceneSerialization.d.ts +17 -0
  43. package/dist/sceneSerialization.js +174 -0
  44. package/dist/sceneSerialization.js.map +1 -0
  45. package/dist/scripts.d.ts +17 -0
  46. package/dist/version.js +7 -0
  47. package/dist/version.js.map +1 -0
  48. package/package.json +86 -0
  49. package/src/authoring.ts +293 -0
  50. package/src/authoringValidation.ts +815 -0
  51. package/src/defaults.ts +277 -0
  52. package/src/fileTypes.ts +53 -0
  53. package/src/importMap.ts +105 -0
  54. package/src/index.ts +15 -0
  55. package/src/paths.ts +9 -0
  56. package/src/plugins/GeneratorComponent.ts +275 -0
  57. package/src/plugins/HtmlUiComponent.example.ts +202 -0
  58. package/src/plugins/HtmlUiComponent.md +219 -0
  59. package/src/plugins/HtmlUiComponent.ts +320 -0
  60. package/src/plugins/cannon/Cannon3DBodyComponent.ts +227 -0
  61. package/src/plugins/cannon/Cannon3DShapeComponent.ts +258 -0
  62. package/src/plugins/cannon/CannonPhysicsPlugin.ts +409 -0
  63. package/src/plugins/cannon/CannonRagdollComponent.ts +1170 -0
  64. package/src/plugins/cannon/helper.ts +255 -0
  65. package/src/plugins/cannon/threeToCannon.ts +441 -0
  66. package/src/plugins/cannon/utils.ts +185 -0
  67. package/src/runtime/createGame.ts +337 -0
  68. package/src/runtime/index.ts +19 -0
  69. package/src/runtime/migrations.ts +6 -0
  70. package/src/runtime/nestedAssets.ts +226 -0
  71. package/src/runtime/projectFormat.ts +233 -0
  72. package/src/runtime/version.ts +3 -0
  73. package/src/sceneSerialization.ts +289 -0
  74. package/src/scripts.ts +52 -0
@@ -0,0 +1,17 @@
1
+ import { ThreeViewer } from 'threepipe';
2
+ export interface SerializeSceneGltfOptions {
3
+ scenePath?: string;
4
+ }
5
+ export interface SerializedSceneFile {
6
+ path: string;
7
+ bytes: Uint8Array;
8
+ }
9
+ export interface SerializedSceneGltf {
10
+ document: Record<string, unknown>;
11
+ gltf: Uint8Array;
12
+ files: SerializedSceneFile[];
13
+ }
14
+ /** Export a viewer scene as deterministic text glTF plus external resources. */
15
+ export declare function serializeSceneGltf(viewer: Pick<ThreeViewer, 'exportScene'>, options?: SerializeSceneGltfOptions): Promise<SerializedSceneGltf>;
16
+ /** Canonicalize JSON glTF and extract every embedded resource. */
17
+ export declare function serializeSceneGltfDocument(input: unknown, options?: SerializeSceneGltfOptions): Promise<SerializedSceneGltf>;
@@ -0,0 +1,174 @@
1
+ const l = new TextEncoder();
2
+ async function N(e, t = {}) {
3
+ const n = await e.exportScene({
4
+ exportExt: "gltf",
5
+ preserveUUIDs: !0,
6
+ viewerConfig: !0,
7
+ embedUrlImages: !1,
8
+ onlyVisible: !0,
9
+ jsonSpaces: 2
10
+ }, !1);
11
+ if (!n) throw new Error("The scene exporter returned no glTF data");
12
+ return x(JSON.parse(await n.text()), t);
13
+ }
14
+ async function x(e, t = {}) {
15
+ if (!a(e) || !a(e.asset))
16
+ throw new Error("The scene is not a JSON glTF document");
17
+ const n = U(e);
18
+ u(n), j(n);
19
+ const r = T(t.scenePath || "assets/main.scene.gltf"), s = z(r), f = [];
20
+ E(n, s, P(r), f), await A(n, s, f, 16);
21
+ const i = y(n);
22
+ return {
23
+ document: i,
24
+ gltf: l.encode(`${JSON.stringify(i, null, 2)}
25
+ `),
26
+ files: f.sort((d, m) => d.path.localeCompare(m.path))
27
+ };
28
+ }
29
+ function u(e) {
30
+ if (Array.isArray(e)) {
31
+ e.forEach(u);
32
+ return;
33
+ }
34
+ if (a(e)) {
35
+ if (e.rootSceneModelRoot === !0 && delete e.gltfUUID, a(e.WEBGI_viewer)) {
36
+ h(e.WEBGI_viewer);
37
+ const t = e.WEBGI_viewer.scene, n = a(t) ? t.defaultCamera : void 0, r = a(n) ? n.object : void 0;
38
+ a(r) && delete r.uuid;
39
+ }
40
+ Object.values(e).forEach(u);
41
+ }
42
+ }
43
+ function j(e) {
44
+ const t = /* @__PURE__ */ new Map();
45
+ p(e, (n, r) => {
46
+ t.set(n, (t.get(n) || 0) + 1), typeof r == "string" && t.set(r, (t.get(r) || 0) + 1);
47
+ }), p(e, (n, r, s) => {
48
+ n === "uuid" && typeof r == "string" && t.get(r) === 1 && delete s[n];
49
+ });
50
+ }
51
+ function p(e, t) {
52
+ if (Array.isArray(e)) {
53
+ e.forEach((n) => p(n, t));
54
+ return;
55
+ }
56
+ if (a(e))
57
+ for (const [n, r] of Object.entries(e))
58
+ t(n, r, e), p(r, t);
59
+ }
60
+ function h(e) {
61
+ if (Array.isArray(e)) {
62
+ e.forEach(h);
63
+ return;
64
+ }
65
+ a(e) && (e.isEuler === !0 && (typeof e.order != "string" && (e.order = "XYZ"), typeof e.x != "number" && (e.x = 0), typeof e.y != "number" && (e.y = 0), typeof e.z != "number" && (e.z = 0)), e.autoAspect === !0 && (delete e.aspect, a(e.object) && delete e.object.aspect), Object.values(e).forEach(h));
66
+ }
67
+ function E(e, t, n, r) {
68
+ const s = e.buffers;
69
+ if (!(s != null && s.length)) return;
70
+ const f = s.map((o) => {
71
+ var c;
72
+ return (c = o.uri) != null && c.startsWith("data:") ? w(o.uri).bytes : void 0;
73
+ });
74
+ if (f.every((o) => o === void 0)) return;
75
+ if (f.some((o, c) => {
76
+ var b;
77
+ return o === void 0 && ((b = s[c]) == null ? void 0 : b.uri);
78
+ }))
79
+ throw new Error("A scene cannot combine embedded and external buffers during serialization");
80
+ let i = 0;
81
+ const d = f.map((o) => {
82
+ const c = J(i);
83
+ return i = c + ((o == null ? void 0 : o.byteLength) || 0), c;
84
+ }), m = new Uint8Array(i);
85
+ f.forEach((o, c) => {
86
+ o && m.set(o, d[c]);
87
+ });
88
+ for (const o of e.bufferViews || []) {
89
+ const c = d[o.buffer];
90
+ if (c === void 0) throw new Error(`Invalid glTF buffer index: ${o.buffer}`);
91
+ o.byteOffset = (o.byteOffset || 0) + c, o.buffer = 0;
92
+ }
93
+ const g = `${n}.bin`;
94
+ e.buffers = [{ byteLength: m.byteLength, uri: g }], r.push({ path: $(t, g), bytes: m });
95
+ }
96
+ async function A(e, t, n, r) {
97
+ var f;
98
+ const s = /* @__PURE__ */ new Map();
99
+ for (const i of e.images || []) {
100
+ if (!((f = i.uri) != null && f.startsWith("data:"))) continue;
101
+ const d = w(i.uri), m = d.mimeType || i.mimeType || "application/octet-stream", o = `assets/textures/${(await I(d.bytes)).slice(0, r)}.${S(m)}`;
102
+ s.set(o, d.bytes), i.uri = C(t, o), i.mimeType = m;
103
+ }
104
+ for (const [i, d] of s) n.push({ path: i, bytes: d });
105
+ }
106
+ function w(e) {
107
+ const t = /^data:([^;,]*)(;base64)?,(.*)$/s.exec(e);
108
+ if (!t) throw new Error("Invalid data URL in glTF");
109
+ return {
110
+ bytes: t[2] ? O(t[3]) : l.encode(decodeURIComponent(t[3])),
111
+ mimeType: t[1] || void 0
112
+ };
113
+ }
114
+ function O(e) {
115
+ const t = atob(e);
116
+ return Uint8Array.from(t, (n) => n.charCodeAt(0));
117
+ }
118
+ async function I(e) {
119
+ const t = await crypto.subtle.digest("SHA-256", e);
120
+ return Array.from(new Uint8Array(t), (n) => n.toString(16).padStart(2, "0")).join("");
121
+ }
122
+ function S(e) {
123
+ var r;
124
+ const t = e.toLowerCase().split(";", 1)[0];
125
+ return {
126
+ "image/avif": "avif",
127
+ "image/gif": "gif",
128
+ "image/jpeg": "jpg",
129
+ "image/ktx2": "ktx2",
130
+ "image/png": "png",
131
+ "image/svg+xml": "svg",
132
+ "image/webp": "webp"
133
+ }[t] || ((r = t.split("/")[1]) == null ? void 0 : r.replace(/[^a-z0-9.+-]/g, "")) || "bin";
134
+ }
135
+ function y(e) {
136
+ return Array.isArray(e) ? e.map(y) : a(e) ? Object.fromEntries(Object.keys(e).sort().map((t) => [t, y(e[t])])) : e;
137
+ }
138
+ function U(e) {
139
+ return JSON.parse(JSON.stringify(e));
140
+ }
141
+ function a(e) {
142
+ return !!e && typeof e == "object" && !Array.isArray(e);
143
+ }
144
+ function T(e) {
145
+ const t = e.replace(/\\/g, "/").replace(/^\.\//, "");
146
+ if (!t || t.startsWith("/") || t.split("/").includes(".."))
147
+ throw new Error(`Invalid project path: ${e}`);
148
+ return t;
149
+ }
150
+ function z(e) {
151
+ const t = e.lastIndexOf("/");
152
+ return t < 0 ? "" : e.slice(0, t);
153
+ }
154
+ function P(e) {
155
+ const t = e.slice(e.lastIndexOf("/") + 1), n = t.lastIndexOf(".");
156
+ return n < 0 ? t : t.slice(0, n);
157
+ }
158
+ function $(e, t) {
159
+ return e ? `${e}/${t}` : t;
160
+ }
161
+ function C(e, t) {
162
+ const n = e ? e.split("/") : [], r = t.split("/");
163
+ for (; n.length && r.length && n[0] === r[0]; )
164
+ n.shift(), r.shift();
165
+ return [...n.map(() => ".."), ...r].join("/") || ".";
166
+ }
167
+ function J(e) {
168
+ return e + 3 & -4;
169
+ }
170
+ export {
171
+ N as serializeSceneGltf,
172
+ x as serializeSceneGltfDocument
173
+ };
174
+ //# sourceMappingURL=sceneSerialization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sceneSerialization.js","sources":["../src/sceneSerialization.ts"],"sourcesContent":["import type {ThreeViewer} from 'threepipe'\n\nexport interface SerializeSceneGltfOptions {\n scenePath?: string\n}\n\nexport interface SerializedSceneFile {\n path: string\n bytes: Uint8Array\n}\n\nexport interface SerializedSceneGltf {\n document: Record<string, unknown>\n gltf: Uint8Array\n files: SerializedSceneFile[]\n}\n\ninterface GltfBuffer {\n byteLength?: number\n uri?: string\n [key: string]: unknown\n}\n\ninterface GltfBufferView {\n buffer: number\n byteOffset?: number\n [key: string]: unknown\n}\n\ninterface GltfImage {\n uri?: string\n mimeType?: string\n [key: string]: unknown\n}\n\ninterface GltfDocument extends Record<string, unknown> {\n buffers?: GltfBuffer[]\n bufferViews?: GltfBufferView[]\n images?: GltfImage[]\n}\n\nconst encoder = new TextEncoder()\n\n/** Export a viewer scene as deterministic text glTF plus external resources. */\nexport async function serializeSceneGltf(\n viewer: Pick<ThreeViewer, 'exportScene'>,\n options: SerializeSceneGltfOptions = {},\n): Promise<SerializedSceneGltf> {\n const blob = await viewer.exportScene({\n exportExt: 'gltf',\n preserveUUIDs: true,\n viewerConfig: true,\n embedUrlImages: false,\n onlyVisible: true,\n jsonSpaces: 2,\n }, false)\n if (!blob) throw new Error('The scene exporter returned no glTF data')\n return serializeSceneGltfDocument(JSON.parse(await blob.text()), options)\n}\n\n/** Canonicalize JSON glTF and extract every embedded resource. */\nexport async function serializeSceneGltfDocument(\n input: unknown,\n options: SerializeSceneGltfOptions = {},\n): Promise<SerializedSceneGltf> {\n if (!isRecord(input) || !isRecord(input.asset)) {\n throw new Error('The scene is not a JSON glTF document')\n }\n const document = cloneJson(input) as GltfDocument\n removeVolatileViewerIds(document)\n removeUnreferencedUuids(document)\n const scenePath = normalizeProjectPath(options.scenePath || 'assets/main.scene.gltf')\n const sceneDirectory = directoryName(scenePath)\n const files: SerializedSceneFile[] = []\n\n extractBuffers(document, sceneDirectory, fileStem(scenePath), files)\n await extractImages(document, sceneDirectory, files, 16)\n\n const sorted = sortObjectKeys(document) as GltfDocument\n return {\n document: sorted,\n gltf: encoder.encode(`${JSON.stringify(sorted, null, 2)}\\n`),\n files: files.sort((left, right) => left.path.localeCompare(right.path)),\n }\n}\n\nfunction removeVolatileViewerIds(value: unknown): void {\n if (Array.isArray(value)) {\n value.forEach(removeVolatileViewerIds)\n return\n }\n if (!isRecord(value)) return\n if (value.rootSceneModelRoot === true) delete value.gltfUUID\n if (isRecord(value.WEBGI_viewer)) {\n canonicalizeViewerConfig(value.WEBGI_viewer)\n const scene = value.WEBGI_viewer.scene\n const camera = isRecord(scene) ? scene.defaultCamera : undefined\n const object = isRecord(camera) ? camera.object : undefined\n if (isRecord(object)) delete object.uuid\n }\n Object.values(value).forEach(removeVolatileViewerIds)\n}\n\nfunction removeUnreferencedUuids(document: GltfDocument): void {\n const uses = new Map<string, number>()\n visitJson(document, (key, value) => {\n uses.set(key, (uses.get(key) || 0) + 1)\n if (typeof value === 'string') uses.set(value, (uses.get(value) || 0) + 1)\n })\n visitJson(document, (key, value, owner) => {\n if (key === 'uuid' && typeof value === 'string' && uses.get(value) === 1) delete owner[key]\n })\n}\n\nfunction visitJson(\n value: unknown,\n visit: (key: string, value: unknown, owner: Record<string, unknown>) => void,\n): void {\n if (Array.isArray(value)) {\n value.forEach((child) => visitJson(child, visit))\n return\n }\n if (!isRecord(value)) return\n for (const [key, child] of Object.entries(value)) {\n visit(key, child, value)\n visitJson(child, visit)\n }\n}\n\nfunction canonicalizeViewerConfig(value: unknown): void {\n if (Array.isArray(value)) {\n value.forEach(canonicalizeViewerConfig)\n return\n }\n if (!isRecord(value)) return\n if (value.isEuler === true) {\n if (typeof value.order !== 'string') value.order = 'XYZ'\n if (typeof value.x !== 'number') value.x = 0\n if (typeof value.y !== 'number') value.y = 0\n if (typeof value.z !== 'number') value.z = 0\n }\n if (value.autoAspect === true) {\n delete value.aspect\n if (isRecord(value.object)) delete value.object.aspect\n }\n Object.values(value).forEach(canonicalizeViewerConfig)\n}\n\nfunction extractBuffers(\n document: GltfDocument,\n sceneDirectory: string,\n baseName: string,\n files: SerializedSceneFile[],\n): void {\n const buffers = document.buffers\n if (!buffers?.length) return\n\n const embedded = buffers.map((buffer) => buffer.uri?.startsWith('data:') ? decodeDataUrl(buffer.uri).bytes : undefined)\n if (embedded.every((value) => value === undefined)) return\n if (embedded.some((value, index) => value === undefined && buffers[index]?.uri)) {\n throw new Error('A scene cannot combine embedded and external buffers during serialization')\n }\n\n let byteLength = 0\n const offsets = embedded.map((bytes) => {\n const offset = align4(byteLength)\n byteLength = offset + (bytes?.byteLength || 0)\n return offset\n })\n const combined = new Uint8Array(byteLength)\n embedded.forEach((bytes, index) => {\n if (bytes) combined.set(bytes, offsets[index])\n })\n\n for (const view of document.bufferViews || []) {\n const offset = offsets[view.buffer]\n if (offset === undefined) throw new Error(`Invalid glTF buffer index: ${view.buffer}`)\n view.byteOffset = (view.byteOffset || 0) + offset\n view.buffer = 0\n }\n\n const binName = `${baseName}.bin`\n document.buffers = [{byteLength: combined.byteLength, uri: binName}]\n files.push({path: joinProjectPath(sceneDirectory, binName), bytes: combined})\n}\n\nasync function extractImages(\n document: GltfDocument,\n sceneDirectory: string,\n files: SerializedSceneFile[],\n hashLength: number,\n): Promise<void> {\n const byPath = new Map<string, Uint8Array>()\n for (const image of document.images || []) {\n if (!image.uri?.startsWith('data:')) continue\n const decoded = decodeDataUrl(image.uri)\n const mimeType = decoded.mimeType || image.mimeType || 'application/octet-stream'\n const hash = await sha256(decoded.bytes)\n const path = `assets/textures/${hash.slice(0, hashLength)}.${imageExtension(mimeType)}`\n byPath.set(path, decoded.bytes)\n image.uri = relativeProjectPath(sceneDirectory, path)\n image.mimeType = mimeType\n }\n for (const [path, bytes] of byPath) files.push({path, bytes})\n}\n\nfunction decodeDataUrl(uri: string): {bytes: Uint8Array, mimeType?: string} {\n const match = /^data:([^;,]*)(;base64)?,(.*)$/s.exec(uri)\n if (!match) throw new Error('Invalid data URL in glTF')\n return {\n bytes: match[2] ? decodeBase64(match[3]) : encoder.encode(decodeURIComponent(match[3])),\n mimeType: match[1] || undefined,\n }\n}\n\nfunction decodeBase64(value: string): Uint8Array {\n const decoded = atob(value)\n return Uint8Array.from(decoded, (character) => character.charCodeAt(0))\n}\n\nasync function sha256(bytes: Uint8Array): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', bytes as BufferSource)\n return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, '0')).join('')\n}\n\nfunction imageExtension(mimeType: string): string {\n const normalized = mimeType.toLowerCase().split(';', 1)[0]\n const extensions: Record<string, string> = {\n 'image/avif': 'avif',\n 'image/gif': 'gif',\n 'image/jpeg': 'jpg',\n 'image/ktx2': 'ktx2',\n 'image/png': 'png',\n 'image/svg+xml': 'svg',\n 'image/webp': 'webp',\n }\n return extensions[normalized] || normalized.split('/')[1]?.replace(/[^a-z0-9.+-]/g, '') || 'bin'\n}\n\nfunction sortObjectKeys(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortObjectKeys)\n if (!isRecord(value)) return value\n return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortObjectKeys(value[key])]))\n}\n\nfunction cloneJson<T>(value: T): T {\n return JSON.parse(JSON.stringify(value)) as T\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction normalizeProjectPath(path: string): string {\n const normalized = path.replace(/\\\\/g, '/').replace(/^\\.\\//, '')\n if (!normalized || normalized.startsWith('/') || normalized.split('/').includes('..')) {\n throw new Error(`Invalid project path: ${path}`)\n }\n return normalized\n}\n\nfunction directoryName(path: string): string {\n const end = path.lastIndexOf('/')\n return end < 0 ? '' : path.slice(0, end)\n}\n\nfunction fileStem(path: string): string {\n const name = path.slice(path.lastIndexOf('/') + 1)\n const end = name.lastIndexOf('.')\n return end < 0 ? name : name.slice(0, end)\n}\n\nfunction joinProjectPath(directory: string, name: string): string {\n return directory ? `${directory}/${name}` : name\n}\n\nfunction relativeProjectPath(fromDirectory: string, target: string): string {\n const from = fromDirectory ? fromDirectory.split('/') : []\n const to = target.split('/')\n while (from.length && to.length && from[0] === to[0]) {\n from.shift()\n to.shift()\n }\n return [...from.map(() => '..'), ...to].join('/') || '.'\n}\n\nfunction align4(value: number): number {\n return (value + 3) & ~3\n}\n"],"names":["encoder","serializeSceneGltf","viewer","options","blob","serializeSceneGltfDocument","input","isRecord","document","cloneJson","removeVolatileViewerIds","removeUnreferencedUuids","scenePath","normalizeProjectPath","sceneDirectory","directoryName","files","extractBuffers","fileStem","extractImages","sorted","sortObjectKeys","left","right","value","canonicalizeViewerConfig","scene","camera","object","uses","visitJson","key","owner","visit","child","baseName","buffers","embedded","buffer","_a","decodeDataUrl","index","byteLength","offsets","bytes","offset","align4","combined","view","binName","joinProjectPath","hashLength","byPath","image","decoded","mimeType","path","sha256","imageExtension","relativeProjectPath","uri","match","decodeBase64","character","digest","normalized","end","name","directory","fromDirectory","target","from","to"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { Class, IViewerPlugin, ThreeViewer, TObject3DComponent } from 'threepipe';
2
+ export type ScriptModule = Record<string, unknown>;
3
+ export interface WalkedScriptExports {
4
+ plugins: Array<{
5
+ name: string;
6
+ value: Class<IViewerPlugin>;
7
+ }>;
8
+ components: Array<{
9
+ name: string;
10
+ value: TObject3DComponent;
11
+ }>;
12
+ }
13
+ export declare function walkScriptExports(module: ScriptModule): WalkedScriptExports;
14
+ /** Register every plugin and Object3D component exported by project modules. */
15
+ export declare function registerScripts(viewer: ThreeViewer, modules: Iterable<ScriptModule>): Promise<WalkedScriptExports>;
16
+ export declare function isPluginType(value: unknown): value is Class<IViewerPlugin>;
17
+ export declare function isComponentType(value: unknown): value is TObject3DComponent;
@@ -0,0 +1,7 @@
1
+ const n = "0.15.0", e = {
2
+ version: n
3
+ }, o = e.version;
4
+ export {
5
+ o as RUNTIME_VERSION
6
+ };
7
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sources":["../src/runtime/version.ts"],"sourcesContent":["import enginePackage from '../../package.json'\n\nexport const RUNTIME_VERSION = enginePackage.version\n"],"names":["RUNTIME_VERSION","enginePackage"],"mappings":";;GAEaA,IAAkBC,EAAc;"}
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@kite3d/engine",
3
+ "version": "0.15.0",
4
+ "description": "The open Kite3D game runtime, project format, scripting helpers, and game plugins.",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./fileTypes": {
16
+ "types": "./dist/fileTypes.d.ts",
17
+ "import": "./dist/fileTypes.js"
18
+ },
19
+ "./paths": {
20
+ "types": "./dist/paths.d.ts",
21
+ "import": "./dist/paths.js"
22
+ },
23
+ "./sceneSerialization": {
24
+ "types": "./dist/sceneSerialization.d.ts",
25
+ "import": "./dist/sceneSerialization.js"
26
+ },
27
+ "./authoring": {
28
+ "types": "./dist/authoring.d.ts",
29
+ "import": "./dist/authoring.js"
30
+ },
31
+ "./authoringValidation": {
32
+ "types": "./dist/authoringValidation.d.ts",
33
+ "import": "./dist/authoringValidation.js"
34
+ },
35
+ "./version": {
36
+ "types": "./dist/runtime/version.d.ts",
37
+ "import": "./dist/version.js"
38
+ },
39
+ "./projectFormat": {
40
+ "types": "./dist/runtime/projectFormat.d.ts",
41
+ "import": "./dist/projectFormat.js"
42
+ },
43
+ "./migrations": {
44
+ "types": "./dist/runtime/migrations.d.ts",
45
+ "import": "./dist/migrations.js"
46
+ },
47
+ "./importMap": {
48
+ "types": "./dist/importMap.d.ts",
49
+ "import": "./dist/importMap.js"
50
+ },
51
+ "./runtime.js": "./dist/runtime.js",
52
+ "./package.json": "./package.json",
53
+ "./src/*": "./src/*"
54
+ },
55
+ "files": [
56
+ "dist",
57
+ "src"
58
+ ],
59
+ "scripts": {
60
+ "build": "vite build && vite build --config vite.runtime.config.ts",
61
+ "typecheck": "tsc --noEmit",
62
+ "lint": "eslint src --ext ts --report-unused-disable-directives --max-warnings 0",
63
+ "test": "vitest run test/unit",
64
+ "test:gates": "npm run build && playwright test persistence-gate.spec.ts startup-failures.spec.ts release-gates.spec.ts",
65
+ "test:runtime": "npm run build && vitest run test/unit && playwright test"
66
+ },
67
+ "dependencies": {
68
+ "cannon-es": "^0.20.0",
69
+ "jsonc-parser": "^3.3.1",
70
+ "meshoptimizer": "^0.20.0",
71
+ "threepipe": "0.5.1",
72
+ "ts-browser-helpers": "0.19.4",
73
+ "uiconfig.js": "^0.3.1"
74
+ },
75
+ "devDependencies": {
76
+ "@playwright/test": "^1.57.0",
77
+ "@types/node": "^22.1.0",
78
+ "@typescript-eslint/eslint-plugin": "^6.14.0",
79
+ "@typescript-eslint/parser": "^6.14.0",
80
+ "eslint": "^8.55.0",
81
+ "typescript": "^5.7.2",
82
+ "vite": "^6.3.5",
83
+ "vite-plugin-dts": "^4.5.4",
84
+ "vitest": "^3.2.4"
85
+ }
86
+ }
@@ -0,0 +1,293 @@
1
+ import type {IObject3D} from 'threepipe'
2
+
3
+ export const KITE3D_AUTHORING_METADATA_KEY = 'kite3dAuthoring'
4
+ export const KITE3D_RUNTIME_METADATA_KEY = 'kite3dRuntime'
5
+
6
+ export type AuthoringRole = 'direct' | 'template' | 'generator'
7
+
8
+ export interface AuthoringMetadata {
9
+ role: AuthoringRole
10
+ id: string
11
+ sourceId?: string
12
+ allowCameraInside?: boolean
13
+ }
14
+
15
+ export interface RuntimeObjectMetadata {
16
+ ownerId: string
17
+ kind: 'clone' | 'effect'
18
+ sourceId: string
19
+ overrides?: RuntimeMutableProperty[]
20
+ }
21
+
22
+ export type RuntimeMutableProperty = 'position' | 'rotation' | 'scale' | 'visible' | 'material'
23
+
24
+ export interface RuntimeCloneOptions {
25
+ name?: string
26
+ visible?: boolean
27
+ position?: [number, number, number]
28
+ rotation?: [number, number, number]
29
+ scale?: [number, number, number]
30
+ runtimeMutable?: RuntimeMutableProperty[]
31
+ }
32
+
33
+ export interface RuntimeScene extends IObject3D {
34
+ modelRoot: IObject3D
35
+ }
36
+
37
+ interface TrackedRuntimeObject {
38
+ object: IObject3D
39
+ owner: IObject3D
40
+ scene?: RuntimeScene
41
+ }
42
+
43
+ const authoringRoles = new Set<AuthoringRole>(['direct', 'template', 'generator'])
44
+ const trackedRuntimeObjects = new Map<IObject3D, TrackedRuntimeObject>()
45
+
46
+ /** Store a validated, serializable authored identity on an object. */
47
+ export function setAuthoringMetadata(object: IObject3D, metadata: AuthoringMetadata): AuthoringMetadata {
48
+ const value = normalizeAuthoringMetadata(metadata)
49
+ object.userData[KITE3D_AUTHORING_METADATA_KEY] = value
50
+ object.setDirty?.({source: 'Kite3D authoring', change: `userData.${KITE3D_AUTHORING_METADATA_KEY}`})
51
+ return value
52
+ }
53
+
54
+ /** Read authoring metadata without trusting arbitrary userData. */
55
+ export function getAuthoringMetadata(object: IObject3D): AuthoringMetadata | undefined {
56
+ const value = object.userData?.[KITE3D_AUTHORING_METADATA_KEY]
57
+ if (!isRecord(value) || !authoringRoles.has(value.role as AuthoringRole)
58
+ || typeof value.id !== 'string' || !value.id.trim()
59
+ || (value.sourceId !== undefined && (typeof value.sourceId !== 'string' || !value.sourceId.trim()))
60
+ || (value.allowCameraInside !== undefined && typeof value.allowCameraInside !== 'boolean')) return undefined
61
+ return value as unknown as AuthoringMetadata
62
+ }
63
+
64
+ export function getRuntimeObjectMetadata(object: IObject3D): RuntimeObjectMetadata | undefined {
65
+ const value = object.userData?.[KITE3D_RUNTIME_METADATA_KEY]
66
+ if (!isRecord(value) || typeof value.ownerId !== 'string' || !value.ownerId
67
+ || !['clone', 'effect'].includes(String(value.kind))
68
+ || typeof value.sourceId !== 'string' || !value.sourceId) return undefined
69
+ return value as unknown as RuntimeObjectMetadata
70
+ }
71
+
72
+ export function isRuntimeObject(object: IObject3D): boolean {
73
+ return getRuntimeObjectMetadata(object) !== undefined
74
+ }
75
+
76
+ /** Return the live objects tracked against one viewer scene. */
77
+ export function getTrackedRuntimeObjects(scene: IObject3D): readonly IObject3D[] {
78
+ return [...trackedRuntimeObjects.values()]
79
+ .filter((record) => record.scene === scene)
80
+ .map(({object}) => object)
81
+ }
82
+
83
+ /**
84
+ * Owns Play-only roots and clones. A single owner should be created in start()
85
+ * and cleaned in stop(); cleanup is idempotent.
86
+ */
87
+ export class RuntimeObjectOwner {
88
+ private readonly objects = new Set<IObject3D>()
89
+ private readonly protectedResources = new Set<object>()
90
+ private runtimeParent?: IObject3D
91
+ private nextObject = 0
92
+
93
+ constructor(readonly ownerId: string) {
94
+ if (!ownerId?.trim()) throw new Error('RuntimeObjectOwner requires a stable owner id')
95
+ }
96
+
97
+ attachRuntimeRoot<T extends IObject3D>(root: T, scene: RuntimeScene, authoredOwner: IObject3D): T {
98
+ if ((root as IObject3D) === (scene as IObject3D) || root === scene.modelRoot
99
+ || root === authoredOwner || isDescendantOf(authoredOwner, root)) {
100
+ throw new Error('A runtime root cannot be its authored owner or an ancestor of that owner')
101
+ }
102
+ if (!isDescendantOf(authoredOwner, scene.modelRoot)) {
103
+ throw new Error('A runtime root requires an authored owner beneath modelRoot')
104
+ }
105
+ this.track(root, authoredOwner, 'effect', scene)
106
+ scene.add(root)
107
+ this.runtimeParent = root
108
+ return root
109
+ }
110
+
111
+ trackEffect<T extends IObject3D>(root: T, owner: IObject3D): T {
112
+ if (root === owner || isDescendantOf(owner, root) || isRuntimeObject(owner)) {
113
+ throw new Error('An effect root requires a persistent authored owner outside that root')
114
+ }
115
+ const scene = findTrackedScene(root) || findRuntimeScene(owner)
116
+ if (scene && isDescendantOf(root, scene.modelRoot)) {
117
+ throw new Error('Runtime effects must live outside modelRoot')
118
+ }
119
+ this.track(root, owner, 'effect', scene)
120
+ return root
121
+ }
122
+
123
+ cloneFrom<T extends IObject3D>(source: T, parent: IObject3D | undefined = this.runtimeParent, options: RuntimeCloneOptions = {}): T {
124
+ const sourceMetadata = getAuthoringMetadata(source)
125
+ if (!sourceMetadata || sourceMetadata.sourceId) {
126
+ throw new Error('Runtime clones require a direct, template, or generator source with a stable authored id')
127
+ }
128
+ if (!parent) throw new Error('Runtime clones require a parent or an attached runtime root')
129
+ const scene = findTrackedScene(parent) || findRuntimeScene(source) || findRuntimeScene(parent)
130
+ if (scene && isDescendantOf(parent, scene.modelRoot)) {
131
+ throw new Error('Runtime clones must live outside modelRoot')
132
+ }
133
+
134
+ collectResources(source, this.protectedResources)
135
+ const clone = source.clone(true) as T
136
+ clone.traverse((child) => {
137
+ if (child !== clone) {
138
+ delete child.userData?.[KITE3D_AUTHORING_METADATA_KEY]
139
+ delete child.userData?.[KITE3D_RUNTIME_METADATA_KEY]
140
+ }
141
+ cloneMaterials(child)
142
+ })
143
+ clone.name = options.name || `${source.name || sourceMetadata.id} (Runtime)`
144
+ clone.visible = options.visible ?? true
145
+ if (options.position) clone.position.set(...options.position)
146
+ if (options.rotation) clone.rotation.set(...options.rotation)
147
+ if (options.scale) clone.scale.set(...options.scale)
148
+
149
+ const overrides = new Set<RuntimeMutableProperty>(options.runtimeMutable || ['position', 'rotation'])
150
+ if (options.position) overrides.add('position')
151
+ if (options.rotation) overrides.add('rotation')
152
+ if (options.scale) overrides.add('scale')
153
+ if (options.visible !== undefined || clone.visible !== source.visible) overrides.add('visible')
154
+ this.markRuntimeObject(clone, sourceMetadata, 'clone', [...overrides])
155
+ parent.add(clone)
156
+ this.objects.add(clone)
157
+ trackedRuntimeObjects.set(clone, {object: clone, owner: source, scene})
158
+ return clone
159
+ }
160
+
161
+ cleanup(): void {
162
+ const disposed = new Set<object>()
163
+ for (const object of this.objects) {
164
+ object.removeFromParent()
165
+ disposeObjectResources(object, this.protectedResources, disposed)
166
+ trackedRuntimeObjects.delete(object)
167
+ }
168
+ this.objects.clear()
169
+ this.protectedResources.clear()
170
+ this.runtimeParent = undefined
171
+ }
172
+
173
+ private track<T extends IObject3D>(
174
+ object: T,
175
+ owner: IObject3D,
176
+ kind: RuntimeObjectMetadata['kind'],
177
+ scene?: RuntimeScene,
178
+ ): void {
179
+ const ownerMetadata = getAuthoringMetadata(owner)
180
+ const sourceId = ownerMetadata?.id || owner.uuid
181
+ collectResources(owner, this.protectedResources)
182
+ this.markRuntimeObject(object, ownerMetadata || {role: 'direct', id: sourceId}, kind)
183
+ this.objects.add(object)
184
+ trackedRuntimeObjects.set(object, {object, owner, scene})
185
+ }
186
+
187
+ private markRuntimeObject(
188
+ object: IObject3D,
189
+ source: AuthoringMetadata,
190
+ kind: RuntimeObjectMetadata['kind'],
191
+ overrides?: RuntimeMutableProperty[],
192
+ ): void {
193
+ const sourceId = source.id
194
+ object.userData[KITE3D_AUTHORING_METADATA_KEY] = {
195
+ role: source.role,
196
+ id: `${this.ownerId}:${++this.nextObject}`,
197
+ sourceId,
198
+ } satisfies AuthoringMetadata
199
+ object.userData[KITE3D_RUNTIME_METADATA_KEY] = {
200
+ ownerId: this.ownerId,
201
+ kind,
202
+ sourceId,
203
+ ...(overrides?.length ? {overrides} : {}),
204
+ } satisfies RuntimeObjectMetadata
205
+ }
206
+ }
207
+
208
+ function normalizeAuthoringMetadata(metadata: AuthoringMetadata): AuthoringMetadata {
209
+ if (!metadata || !authoringRoles.has(metadata.role) || typeof metadata.id !== 'string' || !metadata.id.trim()) {
210
+ throw new Error('Kite3D authoring metadata requires a supported role and stable id')
211
+ }
212
+ if (metadata.sourceId !== undefined && (typeof metadata.sourceId !== 'string' || !metadata.sourceId.trim())) {
213
+ throw new Error('Kite3D authoring sourceId must be a non-empty string')
214
+ }
215
+ if (metadata.allowCameraInside !== undefined && typeof metadata.allowCameraInside !== 'boolean') {
216
+ throw new Error('Kite3D allowCameraInside must be a boolean')
217
+ }
218
+ return {
219
+ role: metadata.role,
220
+ id: metadata.id.trim(),
221
+ ...(metadata.sourceId ? {sourceId: metadata.sourceId.trim()} : {}),
222
+ ...(metadata.allowCameraInside !== undefined ? {allowCameraInside: metadata.allowCameraInside} : {}),
223
+ }
224
+ }
225
+
226
+ function cloneMaterials(object: IObject3D): void {
227
+ const renderable = object as IObject3D & {material?: {clone?: () => unknown} | Array<{clone?: () => unknown}>}
228
+ if (Array.isArray(renderable.material)) {
229
+ renderable.material = renderable.material.map((material) => material?.clone?.() || material) as typeof renderable.material
230
+ } else if (renderable.material) {
231
+ renderable.material = (renderable.material.clone?.() || renderable.material) as typeof renderable.material
232
+ }
233
+ }
234
+
235
+ function findTrackedScene(object: IObject3D): RuntimeScene | undefined {
236
+ for (let current: IObject3D | null = object; current; current = current.parent as IObject3D | null) {
237
+ const record = trackedRuntimeObjects.get(current)
238
+ if (record?.scene) return record.scene
239
+ }
240
+ return undefined
241
+ }
242
+
243
+ function findRuntimeScene(object: IObject3D): RuntimeScene | undefined {
244
+ let current: IObject3D | null = object
245
+ while (current?.parent) current = current.parent as IObject3D
246
+ return current && 'modelRoot' in current ? current as RuntimeScene : undefined
247
+ }
248
+
249
+ function isDescendantOf(object: IObject3D, ancestor: IObject3D): boolean {
250
+ for (let current: IObject3D | null = object; current; current = current.parent as IObject3D | null) {
251
+ if (current === ancestor) return true
252
+ }
253
+ return false
254
+ }
255
+
256
+ function collectResources(object: IObject3D, target: Set<object>): void {
257
+ object.traverse((child) => {
258
+ const resourceOwner = child as IObject3D & {geometry?: object, material?: object | object[]}
259
+ if (resourceOwner.geometry) target.add(resourceOwner.geometry)
260
+ const materials = Array.isArray(resourceOwner.material) ? resourceOwner.material : [resourceOwner.material]
261
+ for (const material of materials) {
262
+ if (!material) continue
263
+ target.add(material)
264
+ for (const value of Object.values(material)) {
265
+ if (isRecord(value) && value.isTexture === true) target.add(value)
266
+ }
267
+ }
268
+ })
269
+ }
270
+
271
+ function disposeObjectResources(object: IObject3D, protectedResources: Set<object>, disposed: Set<object>): void {
272
+ const dispose = (resource: unknown) => {
273
+ if (!isRecord(resource) || protectedResources.has(resource) || disposed.has(resource)) return
274
+ disposed.add(resource)
275
+ if (typeof resource.dispose === 'function') resource.dispose()
276
+ }
277
+ object.traverse((child) => {
278
+ const resourceOwner = child as IObject3D & {geometry?: object, material?: object | object[]}
279
+ dispose(resourceOwner.geometry)
280
+ const materials = Array.isArray(resourceOwner.material) ? resourceOwner.material : [resourceOwner.material]
281
+ for (const material of materials) {
282
+ if (!material) continue
283
+ for (const value of Object.values(material)) {
284
+ if (isRecord(value) && value.isTexture === true) dispose(value)
285
+ }
286
+ dispose(material)
287
+ }
288
+ })
289
+ }
290
+
291
+ function isRecord(value: unknown): value is Record<string, unknown> {
292
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
293
+ }