@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,226 @@
1
+ import {
2
+ copyObject3DUserData,
3
+ getPartialProps,
4
+ IMaterial,
5
+ ImportResult,
6
+ ImportResultExtras,
7
+ IObject3D,
8
+ Quaternion,
9
+ setPartialProps,
10
+ ThreeViewer,
11
+ Vector3,
12
+ } from 'threepipe'
13
+
14
+ type RuntimeErrorHandler = (error: unknown) => void
15
+
16
+ const defaultObjectOverrides = ['visible', 'name', 'position', 'quaternion', 'scale']
17
+ const defaultMaterialOverrides = ['name']
18
+ const objectProperties: (keyof IObject3D)[] = [
19
+ 'position',
20
+ 'quaternion',
21
+ 'scale',
22
+ 'visible',
23
+ 'castShadow',
24
+ 'receiveShadow',
25
+ 'frustumCulled',
26
+ 'renderOrder',
27
+ ]
28
+
29
+ /**
30
+ * Loads the embedded rootPath references written by the editor's AssetTracker.
31
+ * This is deliberately runtime-only: it keeps root overrides, but has no file
32
+ * watching, picking, export hooks, refresh subscriptions, or editor registry.
33
+ */
34
+ export class RuntimeNestedAssetLoader {
35
+ private readonly importer: ThreeViewer['assetManager']['importer']
36
+ private readonly cache = new Map<string, Promise<ImportResult | undefined>>()
37
+ private readonly pending = new Set<Promise<void>>()
38
+
39
+ constructor(
40
+ private readonly viewer: ThreeViewer,
41
+ private readonly onError: RuntimeErrorHandler,
42
+ ) {
43
+ this.importer = viewer.assetManager.importer
44
+ this.importer.addEventListener('processRaw', this.onProcessRaw)
45
+ }
46
+
47
+ dispose() {
48
+ this.importer.removeEventListener('processRaw', this.onProcessRaw)
49
+ this.cache.clear()
50
+ this.pending.clear()
51
+ }
52
+
53
+ async loadObjectDependencies(object: IObject3D): Promise<void> {
54
+ if (!object.traverse) return
55
+
56
+ const objects: IObject3D[] = []
57
+ const materials = new Set<IMaterial>()
58
+ object.traverse((child: IObject3D) => {
59
+ objects.push(child)
60
+ const material = (child as IObject3D & {material?: IMaterial | IMaterial[]}).material
61
+ if (material) {
62
+ const entries = Array.isArray(material) ? material : [material]
63
+ entries.forEach((entry) => entry?.isMaterial && materials.add(entry))
64
+ }
65
+ })
66
+
67
+ const loads: Promise<void>[] = []
68
+ for (const child of objects) {
69
+ if (!this.isEmbeddedReference(child)) continue
70
+ if (child._loadingPromise) {
71
+ loads.push(child._loadingPromise.then(() => undefined))
72
+ continue
73
+ }
74
+
75
+ if (!(child.userData.sProperties as string[]).length) {
76
+ child.userData.sProperties = [...defaultObjectOverrides]
77
+ }
78
+ child._sChildren ||= []
79
+ const load = this.loadReference(child)
80
+ child._loadingPromise = load
81
+ loads.push(load)
82
+ }
83
+
84
+ for (const material of materials) {
85
+ if (!this.isEmbeddedReference(material)) continue
86
+ if (material._loadingPromise) {
87
+ loads.push(material._loadingPromise.then(() => undefined))
88
+ continue
89
+ }
90
+
91
+ if (!(material.userData.sProperties as string[]).length) {
92
+ material.userData.sProperties = [...defaultMaterialOverrides]
93
+ }
94
+ const load = this.loadReference(material)
95
+ material._loadingPromise = load
96
+ loads.push(load)
97
+ }
98
+
99
+ if (!loads.length) return
100
+ const loading = Promise.allSettled(loads).then((results) => {
101
+ for (const result of results) {
102
+ if (result.status === 'rejected') this.onError(result.reason)
103
+ }
104
+ })
105
+ object._loadingPromise = loading
106
+ this.track(loading)
107
+ await loading
108
+ }
109
+
110
+ async waitForPending(): Promise<void> {
111
+ while (this.pending.size) {
112
+ await Promise.all([...this.pending])
113
+ }
114
+ }
115
+
116
+ private readonly onProcessRaw = (event: {data?: ImportResult}) => {
117
+ const object = event.data
118
+ if (!object?.isObject3D) return
119
+ const loading = this.loadObjectDependencies(object as IObject3D)
120
+ this.track(loading)
121
+ }
122
+
123
+ private isEmbeddedReference(value: IObject3D | IMaterial): boolean {
124
+ const rootPath = value.userData?.rootPath
125
+ return typeof rootPath === 'string'
126
+ && !(value as ImportResultExtras).__rootPath
127
+ && !value.userData.rootPathRefresh
128
+ && Array.isArray(value.userData.sProperties)
129
+ }
130
+
131
+ private loadAsset(path: string, options: Record<string, unknown> | undefined) {
132
+ let loading = this.cache.get(path)
133
+ if (!loading) {
134
+ loading = this.importer.importSingle(path, options || {})
135
+ this.cache.set(path, loading)
136
+ }
137
+ return loading
138
+ }
139
+
140
+ private async loadReference(target: IObject3D | IMaterial): Promise<void> {
141
+ const path = target.userData.rootPath as string
142
+ const imported = await this.loadAsset(path, target.userData.rootPathOptions)
143
+ if (!imported) throw new Error(`Unable to load nested asset from ${path}`)
144
+ if (imported._loadingPromise) await imported._loadingPromise
145
+
146
+ const targetObject = target as IObject3D
147
+ const targetMaterial = target as IMaterial
148
+ if (targetObject.isObject3D && imported.isObject3D) {
149
+ this.updateObject(imported as IObject3D, targetObject)
150
+ return
151
+ }
152
+ if (targetMaterial.isMaterial && imported.isMaterial) {
153
+ this.updateMaterial(imported as IMaterial, targetMaterial)
154
+ return
155
+ }
156
+ throw new Error(`Nested asset at ${path} does not match its reference type`)
157
+ }
158
+
159
+ private updateObject(source: IObject3D, target: IObject3D) {
160
+ if (target._sChildren) {
161
+ for (const child of [...target.children]) {
162
+ if (!target._sChildren.includes(child)) child.removeFromParent()
163
+ }
164
+ }
165
+
166
+ for (const child of source.children) {
167
+ const clone = cloneObject(child)
168
+ clone.userData.excludeFromExport = true
169
+ target.add(clone)
170
+ }
171
+
172
+ const overrideProperties = target.userData.sProperties as string[]
173
+ const name = target.name
174
+ const visible = target.visible
175
+ for (const property of objectProperties) {
176
+ if (overrideProperties.includes(property)) continue
177
+ const value = source[property]
178
+ const current = target[property]
179
+ if (value && current && (value as Vector3).isVector3) {
180
+ (current as Vector3).copy(value as Vector3)
181
+ } else if (value && current && (value as Quaternion).isQuaternion) {
182
+ (current as Quaternion).copy(value as Quaternion)
183
+ } else if (value !== undefined) {
184
+ Object.assign(target, {[property]: value})
185
+ }
186
+ }
187
+
188
+ const targetUserData = target.userData
189
+ target.userData = {}
190
+ copyObject3DUserData(target.userData, source.userData, ['uuid', 'sProperties'])
191
+ Object.assign(target.userData, targetUserData)
192
+ target.name = name
193
+ target.visible = visible
194
+ target.userData.uuid = target.uuid
195
+ target.userData.sProperties = overrideProperties
196
+ target.setDirty?.({source: 'RuntimeNestedAssetLoader'})
197
+ }
198
+
199
+ private updateMaterial(source: IMaterial, target: IMaterial) {
200
+ const overrideProperties = target.userData.sProperties as string[]
201
+ const overrides = getPartialProps(target, overrideProperties)
202
+ const name = target.name
203
+ target.setValues(source)
204
+ target.name = name
205
+ setPartialProps(overrides, target)
206
+ target.userData.uuid = target.uuid
207
+ target.userData.sProperties = overrideProperties
208
+ }
209
+
210
+ private track(promise: Promise<void>) {
211
+ this.pending.add(promise)
212
+ const remove = () => this.pending.delete(promise)
213
+ promise.then(remove, remove)
214
+ }
215
+ }
216
+
217
+ function cloneObject(source: IObject3D): IObject3D {
218
+ const clone = source.clone(false) as IObject3D
219
+ const sourceRoot = source as IObject3D & {_tpRootPath?: string, _tpRootUid?: string}
220
+ const cloneRoot = clone as IObject3D & {_tpRootPath?: string, _tpRootUid?: string}
221
+ cloneRoot._tpRootPath = sourceRoot._tpRootPath
222
+ cloneRoot._tpRootUid = sourceRoot._tpRootUid || source.uuid
223
+ delete clone.userData.cloneParent
224
+ for (const child of source.children) clone.add(cloneObject(child))
225
+ return clone
226
+ }
@@ -0,0 +1,233 @@
1
+ import {parse, ParseError} from 'jsonc-parser'
2
+
3
+ export const settingsKey = 'kite3d'
4
+ export const assetUrlPrefix = `/${settingsKey}/`
5
+
6
+ export type JSONValue = string | number | boolean | null | JSONValue[] | {[key: string]: JSONValue}
7
+ export type ProjectPackageJSON = Record<string, JSONValue> & {mainScene: string}
8
+
9
+ export interface ProjectGeneratorState {
10
+ componentId: string
11
+ module: string
12
+ nodeIndex: number
13
+ nodeName: string
14
+ params: Record<string, unknown>
15
+ }
16
+
17
+ export interface AssetsJSONManifest {
18
+ files: Record<string, {
19
+ path: string
20
+ }>
21
+ version: number
22
+ }
23
+
24
+ export interface ProjectDependency {
25
+ key: string
26
+ version: string
27
+ url?: string
28
+ }
29
+
30
+ export interface ExternalPlugin {
31
+ import: string
32
+ /** @default `default` */
33
+ className?: string
34
+ /** @default true */
35
+ active?: boolean
36
+ /** Constructor parameters. */
37
+ params?: JSONValue[]
38
+ }
39
+
40
+ export interface ExternalScript {
41
+ import: string
42
+ /** @default true */
43
+ active?: boolean
44
+ }
45
+
46
+ export interface ProjectViewerSettings {
47
+ msaa?: boolean
48
+ rgbm?: boolean
49
+ zPrepass?: boolean
50
+ renderScale?: number | 'auto'
51
+ maxRenderScale?: number
52
+ backgroundColor?: string | number | null
53
+ modelRootScale?: number
54
+ stencil?: boolean
55
+ debug?: boolean
56
+ tonemap?: boolean
57
+ camera?: {
58
+ type?: 'perspective' | 'orthographic'
59
+ controlsMode?: string
60
+ position?: [number, number, number]
61
+ target?: [number, number, number]
62
+ }
63
+ maxHDRIntensity?: number
64
+ powerPreference?: 'default' | 'high-performance' | 'low-power'
65
+ }
66
+
67
+ export interface ProjectConfigSettings {
68
+ plugins: ExternalPlugin[]
69
+ scripts: ExternalScript[]
70
+ dependencies: ProjectDependency[]
71
+ viewer: ProjectViewerSettings
72
+ }
73
+
74
+ export interface ProjectConfigSettingsJSON {
75
+ plugins?: (ExternalPlugin | string)[]
76
+ imports?: Record<string, string>
77
+ scripts?: (ExternalScript | string)[]
78
+ viewer?: ProjectViewerSettings
79
+ }
80
+
81
+ /** A configured script or plugin is package-backed only when it names a declared dependency exactly. */
82
+ export function isDependencyModuleSpecifier(
83
+ specifier: string,
84
+ packageJson: ProjectPackageJSON,
85
+ ): boolean {
86
+ const dependencies = packageJson.dependencies
87
+ return isRecord(dependencies) && Object.prototype.hasOwnProperty.call(dependencies, specifier)
88
+ }
89
+
90
+ export function parsePackageJSON(text: string): ProjectPackageJSON {
91
+ const json = parse(text) as unknown
92
+ if (!json || typeof json !== 'object' || Array.isArray(json)) {
93
+ throw new Error('Invalid package.json file: expected an object')
94
+ }
95
+ const packageJson = json as ProjectPackageJSON
96
+ if (typeof packageJson.mainScene !== 'string' || !packageJson.mainScene.toLowerCase().endsWith('.gltf')) {
97
+ throw new Error('package.json mainScene must name a text .gltf file')
98
+ }
99
+ return packageJson
100
+ }
101
+
102
+ export function parseAssetsJSONManifest(text: string): AssetsJSONManifest {
103
+ const json = parse(text) as AssetsJSONManifest
104
+
105
+ if (json.files && typeof json.files !== 'object') {
106
+ throw new Error('Invalid assets.json file: files should be an object')
107
+ }
108
+ if (json.version !== undefined && typeof json.version !== 'number') {
109
+ throw new Error('Invalid assets.json file: version should be a number')
110
+ }
111
+
112
+ if (!json.files) json.files = {}
113
+ if (!json.version) json.version = 1
114
+
115
+ return json
116
+ }
117
+
118
+ export function validateSceneSource(path: string, text: string): void {
119
+ const document = JSON.parse(text) as {asset?: unknown}
120
+ if (!document || typeof document !== 'object' || !document.asset) {
121
+ throw new Error(`${path} is not a JSON glTF document`)
122
+ }
123
+ }
124
+
125
+ export function readProjectGeneratorStates(text: string): ProjectGeneratorState[] {
126
+ try {
127
+ const document = JSON.parse(text) as {
128
+ nodes?: Array<{
129
+ name?: unknown
130
+ extras?: {EntityComponentPlugin?: Record<string, {type?: unknown, state?: unknown}>}
131
+ }>
132
+ }
133
+ const generators: ProjectGeneratorState[] = []
134
+ for (const [nodeIndex, node] of (document.nodes || []).entries()) {
135
+ for (const [componentId, component] of Object.entries(node.extras?.EntityComponentPlugin || {})) {
136
+ if (component.type !== 'Generator' || !isRecord(component.state)) continue
137
+ generators.push({
138
+ componentId,
139
+ module: typeof component.state.module === 'string' ? component.state.module : '',
140
+ nodeIndex,
141
+ nodeName: typeof node.name === 'string' ? node.name : `Node ${nodeIndex}`,
142
+ params: isRecord(component.state.params) ? component.state.params : {},
143
+ })
144
+ }
145
+ }
146
+ return generators
147
+ } catch {
148
+ return []
149
+ }
150
+ }
151
+
152
+ export function updateProjectGeneratorState(
153
+ text: string,
154
+ generator: Pick<ProjectGeneratorState, 'componentId' | 'nodeIndex' | 'nodeName'>,
155
+ update: Partial<Pick<ProjectGeneratorState, 'module' | 'params'>>,
156
+ ): {text: string, state: Record<string, unknown>} {
157
+ const document = JSON.parse(text) as {
158
+ nodes?: Array<{extras?: {EntityComponentPlugin?: Record<string, {state?: unknown}>}}>
159
+ }
160
+ const state = document.nodes?.[generator.nodeIndex]?.extras?.EntityComponentPlugin?.[generator.componentId]?.state
161
+ if (!isRecord(state)) throw new Error(`Generator component is missing on ${generator.nodeName}`)
162
+ Object.assign(state, update)
163
+ return {text: JSON.stringify(document, null, 2), state}
164
+ }
165
+
166
+ export async function parsePackageJsonSettingsConfig(json: ProjectPackageJSON, _project?: unknown): Promise<ProjectConfigSettings> {
167
+ const config = (json[settingsKey] ?? {}) as ProjectConfigSettingsJSON
168
+ const dependencies: ProjectDependency[] = []
169
+ const packageDependencies = json.dependencies && typeof json.dependencies === 'object' && !Array.isArray(json.dependencies)
170
+ ? json.dependencies as Record<string, string>
171
+ : {}
172
+ const deps: Record<string, string> = {
173
+ ...packageDependencies,
174
+ }
175
+
176
+ dependencies.push(...Object.entries(deps).map(([key, version]) => ({key, version})))
177
+
178
+ if (config.imports) {
179
+ dependencies.push(...Object.entries(config.imports).map(([key, url]) => ({
180
+ key,
181
+ url: !url.startsWith('@') ? url : undefined,
182
+ version: url.startsWith('@') ? url.slice(1) : '',
183
+ })))
184
+ }
185
+
186
+ const plugins = config.plugins?.map((entry) => {
187
+ if (typeof entry !== 'string') return entry
188
+
189
+ const match = entry.match(/\((.*)\)$/)
190
+ let params: JSONValue[] | undefined
191
+ let spec = entry
192
+
193
+ if (match) {
194
+ const paramString = match[1].trim()
195
+ spec = entry.slice(0, match.index).trim()
196
+ if (paramString) {
197
+ try {
198
+ const errors: ParseError[] = []
199
+ params = parse(`[${paramString}]`, errors)
200
+ if (!Array.isArray(params) || errors.length) {
201
+ console.error(errors)
202
+ throw new Error('Plugin params is not a valid array')
203
+ }
204
+ } catch (error) {
205
+ console.error('Unable to parse plugin params, skipping plugin', entry, paramString, error)
206
+ return null
207
+ }
208
+ }
209
+ }
210
+
211
+ const separator = spec.lastIndexOf(':')
212
+ return {
213
+ import: separator !== -1 ? spec.slice(0, separator) : spec,
214
+ className: separator !== -1 ? spec.slice(separator + 1) : undefined,
215
+ params,
216
+ } satisfies ExternalPlugin
217
+ }) || []
218
+
219
+ const scripts = config.scripts?.map((entry) =>
220
+ typeof entry === 'string' ? {import: entry} : entry
221
+ ) || []
222
+
223
+ return {
224
+ plugins: plugins.filter(Boolean) as ExternalPlugin[],
225
+ scripts,
226
+ dependencies,
227
+ viewer: config.viewer || {},
228
+ }
229
+ }
230
+
231
+ function isRecord(value: unknown): value is Record<string, unknown> {
232
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
233
+ }
@@ -0,0 +1,3 @@
1
+ import enginePackage from '../../package.json'
2
+
3
+ export const RUNTIME_VERSION = enginePackage.version