@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,289 @@
1
+ import type {ThreeViewer} from 'threepipe'
2
+
3
+ export interface SerializeSceneGltfOptions {
4
+ scenePath?: string
5
+ }
6
+
7
+ export interface SerializedSceneFile {
8
+ path: string
9
+ bytes: Uint8Array
10
+ }
11
+
12
+ export interface SerializedSceneGltf {
13
+ document: Record<string, unknown>
14
+ gltf: Uint8Array
15
+ files: SerializedSceneFile[]
16
+ }
17
+
18
+ interface GltfBuffer {
19
+ byteLength?: number
20
+ uri?: string
21
+ [key: string]: unknown
22
+ }
23
+
24
+ interface GltfBufferView {
25
+ buffer: number
26
+ byteOffset?: number
27
+ [key: string]: unknown
28
+ }
29
+
30
+ interface GltfImage {
31
+ uri?: string
32
+ mimeType?: string
33
+ [key: string]: unknown
34
+ }
35
+
36
+ interface GltfDocument extends Record<string, unknown> {
37
+ buffers?: GltfBuffer[]
38
+ bufferViews?: GltfBufferView[]
39
+ images?: GltfImage[]
40
+ }
41
+
42
+ const encoder = new TextEncoder()
43
+
44
+ /** Export a viewer scene as deterministic text glTF plus external resources. */
45
+ export async function serializeSceneGltf(
46
+ viewer: Pick<ThreeViewer, 'exportScene'>,
47
+ options: SerializeSceneGltfOptions = {},
48
+ ): Promise<SerializedSceneGltf> {
49
+ const blob = await viewer.exportScene({
50
+ exportExt: 'gltf',
51
+ preserveUUIDs: true,
52
+ viewerConfig: true,
53
+ embedUrlImages: false,
54
+ onlyVisible: true,
55
+ jsonSpaces: 2,
56
+ }, false)
57
+ if (!blob) throw new Error('The scene exporter returned no glTF data')
58
+ return serializeSceneGltfDocument(JSON.parse(await blob.text()), options)
59
+ }
60
+
61
+ /** Canonicalize JSON glTF and extract every embedded resource. */
62
+ export async function serializeSceneGltfDocument(
63
+ input: unknown,
64
+ options: SerializeSceneGltfOptions = {},
65
+ ): Promise<SerializedSceneGltf> {
66
+ if (!isRecord(input) || !isRecord(input.asset)) {
67
+ throw new Error('The scene is not a JSON glTF document')
68
+ }
69
+ const document = cloneJson(input) as GltfDocument
70
+ removeVolatileViewerIds(document)
71
+ removeUnreferencedUuids(document)
72
+ const scenePath = normalizeProjectPath(options.scenePath || 'assets/main.scene.gltf')
73
+ const sceneDirectory = directoryName(scenePath)
74
+ const files: SerializedSceneFile[] = []
75
+
76
+ extractBuffers(document, sceneDirectory, fileStem(scenePath), files)
77
+ await extractImages(document, sceneDirectory, files, 16)
78
+
79
+ const sorted = sortObjectKeys(document) as GltfDocument
80
+ return {
81
+ document: sorted,
82
+ gltf: encoder.encode(`${JSON.stringify(sorted, null, 2)}\n`),
83
+ files: files.sort((left, right) => left.path.localeCompare(right.path)),
84
+ }
85
+ }
86
+
87
+ function removeVolatileViewerIds(value: unknown): void {
88
+ if (Array.isArray(value)) {
89
+ value.forEach(removeVolatileViewerIds)
90
+ return
91
+ }
92
+ if (!isRecord(value)) return
93
+ if (value.rootSceneModelRoot === true) delete value.gltfUUID
94
+ if (isRecord(value.WEBGI_viewer)) {
95
+ canonicalizeViewerConfig(value.WEBGI_viewer)
96
+ const scene = value.WEBGI_viewer.scene
97
+ const camera = isRecord(scene) ? scene.defaultCamera : undefined
98
+ const object = isRecord(camera) ? camera.object : undefined
99
+ if (isRecord(object)) delete object.uuid
100
+ }
101
+ Object.values(value).forEach(removeVolatileViewerIds)
102
+ }
103
+
104
+ function removeUnreferencedUuids(document: GltfDocument): void {
105
+ const uses = new Map<string, number>()
106
+ visitJson(document, (key, value) => {
107
+ uses.set(key, (uses.get(key) || 0) + 1)
108
+ if (typeof value === 'string') uses.set(value, (uses.get(value) || 0) + 1)
109
+ })
110
+ visitJson(document, (key, value, owner) => {
111
+ if (key === 'uuid' && typeof value === 'string' && uses.get(value) === 1) delete owner[key]
112
+ })
113
+ }
114
+
115
+ function visitJson(
116
+ value: unknown,
117
+ visit: (key: string, value: unknown, owner: Record<string, unknown>) => void,
118
+ ): void {
119
+ if (Array.isArray(value)) {
120
+ value.forEach((child) => visitJson(child, visit))
121
+ return
122
+ }
123
+ if (!isRecord(value)) return
124
+ for (const [key, child] of Object.entries(value)) {
125
+ visit(key, child, value)
126
+ visitJson(child, visit)
127
+ }
128
+ }
129
+
130
+ function canonicalizeViewerConfig(value: unknown): void {
131
+ if (Array.isArray(value)) {
132
+ value.forEach(canonicalizeViewerConfig)
133
+ return
134
+ }
135
+ if (!isRecord(value)) return
136
+ if (value.isEuler === true) {
137
+ if (typeof value.order !== 'string') value.order = 'XYZ'
138
+ if (typeof value.x !== 'number') value.x = 0
139
+ if (typeof value.y !== 'number') value.y = 0
140
+ if (typeof value.z !== 'number') value.z = 0
141
+ }
142
+ if (value.autoAspect === true) {
143
+ delete value.aspect
144
+ if (isRecord(value.object)) delete value.object.aspect
145
+ }
146
+ Object.values(value).forEach(canonicalizeViewerConfig)
147
+ }
148
+
149
+ function extractBuffers(
150
+ document: GltfDocument,
151
+ sceneDirectory: string,
152
+ baseName: string,
153
+ files: SerializedSceneFile[],
154
+ ): void {
155
+ const buffers = document.buffers
156
+ if (!buffers?.length) return
157
+
158
+ const embedded = buffers.map((buffer) => buffer.uri?.startsWith('data:') ? decodeDataUrl(buffer.uri).bytes : undefined)
159
+ if (embedded.every((value) => value === undefined)) return
160
+ if (embedded.some((value, index) => value === undefined && buffers[index]?.uri)) {
161
+ throw new Error('A scene cannot combine embedded and external buffers during serialization')
162
+ }
163
+
164
+ let byteLength = 0
165
+ const offsets = embedded.map((bytes) => {
166
+ const offset = align4(byteLength)
167
+ byteLength = offset + (bytes?.byteLength || 0)
168
+ return offset
169
+ })
170
+ const combined = new Uint8Array(byteLength)
171
+ embedded.forEach((bytes, index) => {
172
+ if (bytes) combined.set(bytes, offsets[index])
173
+ })
174
+
175
+ for (const view of document.bufferViews || []) {
176
+ const offset = offsets[view.buffer]
177
+ if (offset === undefined) throw new Error(`Invalid glTF buffer index: ${view.buffer}`)
178
+ view.byteOffset = (view.byteOffset || 0) + offset
179
+ view.buffer = 0
180
+ }
181
+
182
+ const binName = `${baseName}.bin`
183
+ document.buffers = [{byteLength: combined.byteLength, uri: binName}]
184
+ files.push({path: joinProjectPath(sceneDirectory, binName), bytes: combined})
185
+ }
186
+
187
+ async function extractImages(
188
+ document: GltfDocument,
189
+ sceneDirectory: string,
190
+ files: SerializedSceneFile[],
191
+ hashLength: number,
192
+ ): Promise<void> {
193
+ const byPath = new Map<string, Uint8Array>()
194
+ for (const image of document.images || []) {
195
+ if (!image.uri?.startsWith('data:')) continue
196
+ const decoded = decodeDataUrl(image.uri)
197
+ const mimeType = decoded.mimeType || image.mimeType || 'application/octet-stream'
198
+ const hash = await sha256(decoded.bytes)
199
+ const path = `assets/textures/${hash.slice(0, hashLength)}.${imageExtension(mimeType)}`
200
+ byPath.set(path, decoded.bytes)
201
+ image.uri = relativeProjectPath(sceneDirectory, path)
202
+ image.mimeType = mimeType
203
+ }
204
+ for (const [path, bytes] of byPath) files.push({path, bytes})
205
+ }
206
+
207
+ function decodeDataUrl(uri: string): {bytes: Uint8Array, mimeType?: string} {
208
+ const match = /^data:([^;,]*)(;base64)?,(.*)$/s.exec(uri)
209
+ if (!match) throw new Error('Invalid data URL in glTF')
210
+ return {
211
+ bytes: match[2] ? decodeBase64(match[3]) : encoder.encode(decodeURIComponent(match[3])),
212
+ mimeType: match[1] || undefined,
213
+ }
214
+ }
215
+
216
+ function decodeBase64(value: string): Uint8Array {
217
+ const decoded = atob(value)
218
+ return Uint8Array.from(decoded, (character) => character.charCodeAt(0))
219
+ }
220
+
221
+ async function sha256(bytes: Uint8Array): Promise<string> {
222
+ const digest = await crypto.subtle.digest('SHA-256', bytes as BufferSource)
223
+ return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, '0')).join('')
224
+ }
225
+
226
+ function imageExtension(mimeType: string): string {
227
+ const normalized = mimeType.toLowerCase().split(';', 1)[0]
228
+ const extensions: Record<string, string> = {
229
+ 'image/avif': 'avif',
230
+ 'image/gif': 'gif',
231
+ 'image/jpeg': 'jpg',
232
+ 'image/ktx2': 'ktx2',
233
+ 'image/png': 'png',
234
+ 'image/svg+xml': 'svg',
235
+ 'image/webp': 'webp',
236
+ }
237
+ return extensions[normalized] || normalized.split('/')[1]?.replace(/[^a-z0-9.+-]/g, '') || 'bin'
238
+ }
239
+
240
+ function sortObjectKeys(value: unknown): unknown {
241
+ if (Array.isArray(value)) return value.map(sortObjectKeys)
242
+ if (!isRecord(value)) return value
243
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortObjectKeys(value[key])]))
244
+ }
245
+
246
+ function cloneJson<T>(value: T): T {
247
+ return JSON.parse(JSON.stringify(value)) as T
248
+ }
249
+
250
+ function isRecord(value: unknown): value is Record<string, unknown> {
251
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
252
+ }
253
+
254
+ function normalizeProjectPath(path: string): string {
255
+ const normalized = path.replace(/\\/g, '/').replace(/^\.\//, '')
256
+ if (!normalized || normalized.startsWith('/') || normalized.split('/').includes('..')) {
257
+ throw new Error(`Invalid project path: ${path}`)
258
+ }
259
+ return normalized
260
+ }
261
+
262
+ function directoryName(path: string): string {
263
+ const end = path.lastIndexOf('/')
264
+ return end < 0 ? '' : path.slice(0, end)
265
+ }
266
+
267
+ function fileStem(path: string): string {
268
+ const name = path.slice(path.lastIndexOf('/') + 1)
269
+ const end = name.lastIndexOf('.')
270
+ return end < 0 ? name : name.slice(0, end)
271
+ }
272
+
273
+ function joinProjectPath(directory: string, name: string): string {
274
+ return directory ? `${directory}/${name}` : name
275
+ }
276
+
277
+ function relativeProjectPath(fromDirectory: string, target: string): string {
278
+ const from = fromDirectory ? fromDirectory.split('/') : []
279
+ const to = target.split('/')
280
+ while (from.length && to.length && from[0] === to[0]) {
281
+ from.shift()
282
+ to.shift()
283
+ }
284
+ return [...from.map(() => '..'), ...to].join('/') || '.'
285
+ }
286
+
287
+ function align4(value: number): number {
288
+ return (value + 3) & ~3
289
+ }
package/src/scripts.ts ADDED
@@ -0,0 +1,52 @@
1
+ import {Class, EntityComponentPlugin, IViewerPlugin, ThreeViewer, TObject3DComponent} from 'threepipe'
2
+
3
+ export type ScriptModule = Record<string, unknown>
4
+
5
+ export interface WalkedScriptExports {
6
+ plugins: Array<{name: string, value: Class<IViewerPlugin>}>
7
+ components: Array<{name: string, value: TObject3DComponent}>
8
+ }
9
+
10
+ export function walkScriptExports(module: ScriptModule): WalkedScriptExports {
11
+ const plugins: WalkedScriptExports['plugins'] = []
12
+ const components: WalkedScriptExports['components'] = []
13
+ for (const [name, value] of Object.entries(module)) {
14
+ if (isPluginType(value)) plugins.push({name, value})
15
+ if (isComponentType(value)) components.push({name, value})
16
+ }
17
+ return {plugins, components}
18
+ }
19
+
20
+ /** Register every plugin and Object3D component exported by project modules. */
21
+ export async function registerScripts(viewer: ThreeViewer, modules: Iterable<ScriptModule>): Promise<WalkedScriptExports> {
22
+ const found: WalkedScriptExports = {plugins: [], components: []}
23
+ const entityComponents = viewer.getPlugin(EntityComponentPlugin)
24
+ if (!entityComponents) throw new Error('EntityComponentPlugin must be added before project scripts')
25
+
26
+ for (const module of modules) {
27
+ const walked = walkScriptExports(module)
28
+ found.plugins.push(...walked.plugins)
29
+ found.components.push(...walked.components)
30
+ }
31
+ for (const component of found.components) {
32
+ const existing = entityComponents.componentTypes.get(component.value.ComponentType)
33
+ if (existing !== component.value) {
34
+ if (existing) entityComponents.removeComponentType(existing)
35
+ await entityComponents.addComponentType(component.value)
36
+ }
37
+ }
38
+ for (const plugin of found.plugins) {
39
+ if (!viewer.getPlugin(plugin.value)) await viewer.addPlugin(plugin.value)
40
+ }
41
+ return found
42
+ }
43
+
44
+ export function isPluginType(value: unknown): value is Class<IViewerPlugin> {
45
+ return typeof value === 'function'
46
+ && typeof (value as unknown as {PluginType?: unknown}).PluginType === 'string'
47
+ }
48
+
49
+ export function isComponentType(value: unknown): value is TObject3DComponent {
50
+ return typeof value === 'function'
51
+ && typeof (value as unknown as {ComponentType?: unknown}).ComponentType === 'string'
52
+ }