@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.
- package/dist/authoring.d.ts +54 -0
- package/dist/authoring.js +165 -0
- package/dist/authoring.js.map +1 -0
- package/dist/authoringValidation.d.ts +110 -0
- package/dist/authoringValidation.js +488 -0
- package/dist/authoringValidation.js.map +1 -0
- package/dist/defaults.d.ts +2 -0
- package/dist/fileTypes.d.ts +5 -0
- package/dist/fileTypes.js +51 -0
- package/dist/fileTypes.js.map +1 -0
- package/dist/importMap.d.ts +15 -0
- package/dist/importMap.js +62 -0
- package/dist/importMap.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +2756 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations.js +5 -0
- package/dist/migrations.js.map +1 -0
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +13 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugins/GeneratorComponent.d.ts +40 -0
- package/dist/plugins/HtmlUiComponent.d.ts +52 -0
- package/dist/plugins/HtmlUiComponent.example.d.ts +0 -0
- package/dist/plugins/cannon/Cannon3DBodyComponent.d.ts +27 -0
- package/dist/plugins/cannon/Cannon3DShapeComponent.d.ts +57 -0
- package/dist/plugins/cannon/CannonPhysicsPlugin.d.ts +63 -0
- package/dist/plugins/cannon/CannonRagdollComponent.d.ts +148 -0
- package/dist/plugins/cannon/helper.d.ts +24 -0
- package/dist/plugins/cannon/threeToCannon.d.ts +63 -0
- package/dist/plugins/cannon/utils.d.ts +9 -0
- package/dist/projectFormat.js +108 -0
- package/dist/projectFormat.js.map +1 -0
- package/dist/runtime/createGame.d.ts +29 -0
- package/dist/runtime/index.d.ts +18 -0
- package/dist/runtime/migrations.d.ts +5 -0
- package/dist/runtime/nestedAssets.d.ts +26 -0
- package/dist/runtime/projectFormat.d.ts +83 -0
- package/dist/runtime/version.d.ts +1 -0
- package/dist/runtime.js +72835 -0
- package/dist/runtime.js.map +1 -0
- package/dist/sceneSerialization.d.ts +17 -0
- package/dist/sceneSerialization.js +174 -0
- package/dist/sceneSerialization.js.map +1 -0
- package/dist/scripts.d.ts +17 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/package.json +86 -0
- package/src/authoring.ts +293 -0
- package/src/authoringValidation.ts +815 -0
- package/src/defaults.ts +277 -0
- package/src/fileTypes.ts +53 -0
- package/src/importMap.ts +105 -0
- package/src/index.ts +15 -0
- package/src/paths.ts +9 -0
- package/src/plugins/GeneratorComponent.ts +275 -0
- package/src/plugins/HtmlUiComponent.example.ts +202 -0
- package/src/plugins/HtmlUiComponent.md +219 -0
- package/src/plugins/HtmlUiComponent.ts +320 -0
- package/src/plugins/cannon/Cannon3DBodyComponent.ts +227 -0
- package/src/plugins/cannon/Cannon3DShapeComponent.ts +258 -0
- package/src/plugins/cannon/CannonPhysicsPlugin.ts +409 -0
- package/src/plugins/cannon/CannonRagdollComponent.ts +1170 -0
- package/src/plugins/cannon/helper.ts +255 -0
- package/src/plugins/cannon/threeToCannon.ts +441 -0
- package/src/plugins/cannon/utils.ts +185 -0
- package/src/runtime/createGame.ts +337 -0
- package/src/runtime/index.ts +19 -0
- package/src/runtime/migrations.ts +6 -0
- package/src/runtime/nestedAssets.ts +226 -0
- package/src/runtime/projectFormat.ts +233 -0
- package/src/runtime/version.ts +3 -0
- package/src/sceneSerialization.ts +289 -0
- 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
|
+
}
|