@kite3d/engine 0.16.0 → 0.18.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/generatorParams.d.ts +23 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1278 -1176
- package/dist/index.js.map +1 -1
- package/dist/plugins/GeneratorComponent.d.ts +5 -1
- package/dist/projectFormat.js +70 -53
- package/dist/projectFormat.js.map +1 -1
- package/dist/runtime/projectFormat.d.ts +4 -0
- package/dist/runtime.js +7617 -7503
- package/dist/runtime.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/generatorParams.ts +182 -0
- package/src/index.ts +1 -0
- package/src/plugins/GeneratorComponent.ts +12 -0
- package/src/runtime/createGame.ts +2 -18
- package/src/runtime/projectFormat.ts +28 -0
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
export const generatorParamTypes = [
|
|
2
|
+
'boolean',
|
|
3
|
+
'number',
|
|
4
|
+
'integer',
|
|
5
|
+
'string',
|
|
6
|
+
'select',
|
|
7
|
+
'vector',
|
|
8
|
+
'color',
|
|
9
|
+
'json',
|
|
10
|
+
] as const
|
|
11
|
+
|
|
12
|
+
export type GeneratorParamType = typeof generatorParamTypes[number]
|
|
13
|
+
|
|
14
|
+
export type GeneratorParamOption = string | number | {
|
|
15
|
+
value: string | number
|
|
16
|
+
label: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GeneratorParamDefinition {
|
|
20
|
+
type?: GeneratorParamType
|
|
21
|
+
label?: string
|
|
22
|
+
help?: string
|
|
23
|
+
default?: unknown
|
|
24
|
+
options?: GeneratorParamOption[]
|
|
25
|
+
min?: number
|
|
26
|
+
max?: number
|
|
27
|
+
step?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface GeneratorParamSchemaEntry extends Omit<GeneratorParamDefinition, 'type'> {
|
|
31
|
+
type: GeneratorParamType
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type GeneratorParamSchema = Record<string, GeneratorParamSchemaEntry>
|
|
35
|
+
|
|
36
|
+
export function parseGeneratorParamsSchema(
|
|
37
|
+
exported: unknown,
|
|
38
|
+
module: string,
|
|
39
|
+
warn: (message: string) => void = console.warn,
|
|
40
|
+
): GeneratorParamSchema {
|
|
41
|
+
if (exported === undefined) return {}
|
|
42
|
+
if (!isRecord(exported)) {
|
|
43
|
+
warn(`[kite3d] Ignoring invalid generator params export from ${module}: expected an object`)
|
|
44
|
+
return {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const schema: GeneratorParamSchema = {}
|
|
48
|
+
for (const [key, value] of Object.entries(exported)) {
|
|
49
|
+
const parsed = parseEntry(value)
|
|
50
|
+
if (typeof parsed === 'string') {
|
|
51
|
+
warn(`[kite3d] Ignoring invalid generator param "${key}" from ${module}: ${parsed}`)
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
schema[key] = parsed
|
|
55
|
+
}
|
|
56
|
+
return schema
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function inferGeneratorParam(value: unknown): GeneratorParamSchemaEntry {
|
|
60
|
+
if (typeof value === 'boolean') return {type: 'boolean'}
|
|
61
|
+
if (typeof value === 'number') return {type: 'number'}
|
|
62
|
+
if (isNumberVector(value)) return {type: 'vector'}
|
|
63
|
+
if (typeof value === 'string') return {type: 'string'}
|
|
64
|
+
return {type: 'json'}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function validateGeneratorParam(value: unknown, entry: GeneratorParamSchemaEntry): string | undefined {
|
|
68
|
+
const name = entry.label || 'Value'
|
|
69
|
+
switch (entry.type) {
|
|
70
|
+
case 'boolean':
|
|
71
|
+
return typeof value === 'boolean' ? undefined : `${name} must be true or false.`
|
|
72
|
+
case 'number':
|
|
73
|
+
case 'integer': {
|
|
74
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return `${name} must be a number.`
|
|
75
|
+
if (entry.type === 'integer' && !Number.isInteger(value)) return `${name} must be an integer.`
|
|
76
|
+
if (entry.min !== undefined && value < entry.min) return `${name} must be at least ${entry.min}.`
|
|
77
|
+
if (entry.max !== undefined && value > entry.max) return `${name} must be at most ${entry.max}.`
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
case 'string':
|
|
81
|
+
return typeof value === 'string' ? undefined : `${name} must be text.`
|
|
82
|
+
case 'select': {
|
|
83
|
+
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
84
|
+
return `${name} must be one of the available options.`
|
|
85
|
+
}
|
|
86
|
+
if (entry.options?.length && !entry.options.some((option) => Object.is(optionValue(option), value))) {
|
|
87
|
+
return `${name} must be one of the available options.`
|
|
88
|
+
}
|
|
89
|
+
return undefined
|
|
90
|
+
}
|
|
91
|
+
case 'vector':
|
|
92
|
+
return isNumberVector(value) ? undefined : `${name} must be an array of 2 to 4 numbers.`
|
|
93
|
+
case 'color':
|
|
94
|
+
return typeof value === 'string' && /^#[\da-f]{6}$/i.test(value)
|
|
95
|
+
? undefined
|
|
96
|
+
: `${name} must be a six-digit hex color.`
|
|
97
|
+
case 'json':
|
|
98
|
+
return isJsonValue(value) ? undefined : `${name} must be a JSON value.`
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseEntry(value: unknown): GeneratorParamSchemaEntry | string {
|
|
103
|
+
if (!isRecord(value)) return 'expected an object'
|
|
104
|
+
if (value.type !== undefined && !generatorParamTypes.includes(value.type as GeneratorParamType)) {
|
|
105
|
+
return `unknown type ${JSON.stringify(value.type)}`
|
|
106
|
+
}
|
|
107
|
+
if (value.label !== undefined && typeof value.label !== 'string') return 'label must be a string'
|
|
108
|
+
if (value.help !== undefined && typeof value.help !== 'string') return 'help must be a string'
|
|
109
|
+
if (value.options !== undefined && !isOptions(value.options)) return 'options must contain strings, numbers, or labeled values'
|
|
110
|
+
for (const bound of ['min', 'max', 'step'] as const) {
|
|
111
|
+
if (value[bound] !== undefined && !isFiniteNumber(value[bound])) return `${bound} must be a finite number`
|
|
112
|
+
}
|
|
113
|
+
if (isFiniteNumber(value.step) && value.step <= 0) return 'step must be greater than zero'
|
|
114
|
+
if (isFiniteNumber(value.min) && isFiniteNumber(value.max) && value.min > value.max) {
|
|
115
|
+
return 'min must not be greater than max'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const type = value.type as GeneratorParamType | undefined
|
|
119
|
+
?? (value.options !== undefined ? 'select' : inferGeneratorParam(value.default).type)
|
|
120
|
+
if ((value.min !== undefined || value.max !== undefined || value.step !== undefined)
|
|
121
|
+
&& type !== 'number' && type !== 'integer') {
|
|
122
|
+
return 'min, max, and step require a number or integer type'
|
|
123
|
+
}
|
|
124
|
+
if (value.options !== undefined && type !== 'select') return 'options require the select type'
|
|
125
|
+
|
|
126
|
+
const entry: GeneratorParamSchemaEntry = {
|
|
127
|
+
type,
|
|
128
|
+
...(value.label !== undefined ? {label: value.label} : {}),
|
|
129
|
+
...(value.help !== undefined ? {help: value.help} : {}),
|
|
130
|
+
...(Object.prototype.hasOwnProperty.call(value, 'default') ? {default: value.default} : {}),
|
|
131
|
+
...(value.options !== undefined ? {options: value.options} : {}),
|
|
132
|
+
...(isFiniteNumber(value.min) ? {min: value.min} : {}),
|
|
133
|
+
...(isFiniteNumber(value.max) ? {max: value.max} : {}),
|
|
134
|
+
...(isFiniteNumber(value.step) ? {step: value.step} : {}),
|
|
135
|
+
}
|
|
136
|
+
if (Object.prototype.hasOwnProperty.call(entry, 'default')) {
|
|
137
|
+
const error = validateGeneratorParam(entry.default, entry)
|
|
138
|
+
if (error) return `invalid default: ${error}`
|
|
139
|
+
}
|
|
140
|
+
return entry
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isOptions(value: unknown): value is GeneratorParamOption[] {
|
|
144
|
+
return Array.isArray(value) && value.every((option) => {
|
|
145
|
+
if (typeof option === 'string' || typeof option === 'number') return true
|
|
146
|
+
return isRecord(option)
|
|
147
|
+
&& (typeof option.value === 'string' || typeof option.value === 'number')
|
|
148
|
+
&& typeof option.label === 'string'
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function optionValue(option: GeneratorParamOption): string | number {
|
|
153
|
+
return typeof option === 'object' ? option.value : option
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isNumberVector(value: unknown): value is number[] {
|
|
157
|
+
return Array.isArray(value)
|
|
158
|
+
&& value.length >= 2
|
|
159
|
+
&& value.length <= 4
|
|
160
|
+
&& value.every(isFiniteNumber)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
164
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isJsonValue(value: unknown, seen = new Set<object>()): boolean {
|
|
168
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return true
|
|
169
|
+
if (typeof value === 'number') return Number.isFinite(value)
|
|
170
|
+
if (!value || typeof value !== 'object' || seen.has(value)) return false
|
|
171
|
+
seen.add(value)
|
|
172
|
+
const valid = Array.isArray(value)
|
|
173
|
+
? value.every((item) => isJsonValue(item, seen))
|
|
174
|
+
: Object.getPrototypeOf(value) === Object.prototype
|
|
175
|
+
&& Object.values(value).every((item) => isJsonValue(item, seen))
|
|
176
|
+
seen.delete(value)
|
|
177
|
+
return valid
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
181
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
182
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './paths.ts'
|
|
|
7
7
|
export * from './sceneSerialization.ts'
|
|
8
8
|
export * from './authoring.ts'
|
|
9
9
|
export * from './authoringValidation.ts'
|
|
10
|
+
export * from './generatorParams.ts'
|
|
10
11
|
export * from './plugins/HtmlUiComponent.ts'
|
|
11
12
|
export * from './plugins/GeneratorComponent.ts'
|
|
12
13
|
export * from './plugins/cannon/CannonPhysicsPlugin.ts'
|
|
@@ -6,6 +6,11 @@ import {
|
|
|
6
6
|
type ThreeViewer,
|
|
7
7
|
} from 'threepipe'
|
|
8
8
|
import {getAuthoringMetadata, setAuthoringMetadata, type AuthoringMetadata} from '../authoring.ts'
|
|
9
|
+
import {
|
|
10
|
+
parseGeneratorParamsSchema,
|
|
11
|
+
type GeneratorParamDefinition,
|
|
12
|
+
type GeneratorParamSchema,
|
|
13
|
+
} from '../generatorParams.ts'
|
|
9
14
|
|
|
10
15
|
export interface GeneratorParams {
|
|
11
16
|
[key: string]: unknown
|
|
@@ -20,6 +25,7 @@ export interface GeneratorContext {
|
|
|
20
25
|
|
|
21
26
|
export interface GeneratorModule {
|
|
22
27
|
default?: (context: GeneratorContext) => unknown | Promise<unknown>
|
|
28
|
+
params?: Record<string, GeneratorParamDefinition>
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
export interface GeneratorViewerOptions {
|
|
@@ -42,6 +48,7 @@ export class GeneratorComponent extends Object3DComponent {
|
|
|
42
48
|
|
|
43
49
|
module = ''
|
|
44
50
|
params: GeneratorParams = {}
|
|
51
|
+
schema: GeneratorParamSchema = {}
|
|
45
52
|
private runRevision = 0
|
|
46
53
|
|
|
47
54
|
static configureViewer(viewer: ThreeViewer, options: GeneratorViewerOptions): void {
|
|
@@ -78,6 +85,7 @@ export class GeneratorComponent extends Object3DComponent {
|
|
|
78
85
|
base: config.base,
|
|
79
86
|
revision: ++generatorImportRevision,
|
|
80
87
|
isCurrent: () => revision === this.runRevision,
|
|
88
|
+
onSchema: (schema) => { this.schema = schema },
|
|
81
89
|
}).then(() => {
|
|
82
90
|
if (revision === this.runRevision) viewer.setDirty(this)
|
|
83
91
|
}).finally(() => config.pending.delete(task))
|
|
@@ -122,6 +130,7 @@ export interface RunGeneratorOptions extends Omit<GeneratorContext, 'engine'> {
|
|
|
122
130
|
base: URL
|
|
123
131
|
revision?: number
|
|
124
132
|
isCurrent?: () => boolean
|
|
133
|
+
onSchema?: (schema: GeneratorParamSchema) => void
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
export async function runGenerator({
|
|
@@ -132,13 +141,16 @@ export async function runGenerator({
|
|
|
132
141
|
base,
|
|
133
142
|
revision = 0,
|
|
134
143
|
isCurrent,
|
|
144
|
+
onSchema,
|
|
135
145
|
}: RunGeneratorOptions): Promise<IObject3D[]> {
|
|
136
146
|
removeGeneratedChildren(node)
|
|
147
|
+
onSchema?.({})
|
|
137
148
|
if (!module) return []
|
|
138
149
|
const source = ensureGeneratorMetadata(node)
|
|
139
150
|
const moduleUrl = resolveGeneratorModule(module, base)
|
|
140
151
|
if (revision) moduleUrl.searchParams.set('kite3d-generator', String(revision))
|
|
141
152
|
const loaded = await importGeneratorModule(moduleUrl.href)
|
|
153
|
+
if (!isCurrent || isCurrent()) onSchema?.(parseGeneratorParamsSchema(loaded.params, module))
|
|
142
154
|
if (typeof loaded.default !== 'function') {
|
|
143
155
|
throw new Error(`Generator module must have a default generate function: ${module}`)
|
|
144
156
|
}
|
|
@@ -29,8 +29,8 @@ import {
|
|
|
29
29
|
} from '../authoringValidation.ts'
|
|
30
30
|
import {RuntimeNestedAssetLoader} from './nestedAssets.ts'
|
|
31
31
|
import {
|
|
32
|
-
assetUrlPrefix,
|
|
33
32
|
AssetsJSONManifest,
|
|
33
|
+
createProjectAssetURLModifier,
|
|
34
34
|
ExternalPlugin,
|
|
35
35
|
isDependencyModuleSpecifier,
|
|
36
36
|
parseAssetsJSONManifest,
|
|
@@ -142,7 +142,7 @@ async function createProjectGame({
|
|
|
142
142
|
|
|
143
143
|
// Three's LoadingManager delegates through this importer hook. It covers
|
|
144
144
|
// glTF buffers/textures and nested imports without patching global fetch.
|
|
145
|
-
const urlModifier =
|
|
145
|
+
const urlModifier = createProjectAssetURLModifier(baseUrl, assetsManifest)
|
|
146
146
|
viewer.assetManager.importer.addURLModifier(urlModifier)
|
|
147
147
|
removeURLModifier = () => viewer?.assetManager.importer.removeURLModifier(urlModifier)
|
|
148
148
|
|
|
@@ -246,22 +246,6 @@ async function registerProjectScripts(
|
|
|
246
246
|
await registerScripts(viewer, modules)
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
function createURLModifier(base: URL, assets: AssetsJSONManifest) {
|
|
250
|
-
const assetIdPrefix = `${assetUrlPrefix}@`
|
|
251
|
-
return (url: string): string => {
|
|
252
|
-
if (url.startsWith(assetIdPrefix)) {
|
|
253
|
-
const id = url.slice(assetIdPrefix.length).split('/', 1)[0]
|
|
254
|
-
const asset = assets.files[id]
|
|
255
|
-
if (!asset?.path) throw new Error(`Unknown asset id in URL: ${id}`)
|
|
256
|
-
return new URL(asset.path, base).href
|
|
257
|
-
}
|
|
258
|
-
if (url.startsWith(assetUrlPrefix)) {
|
|
259
|
-
return new URL(url.slice(assetUrlPrefix.length), base).href
|
|
260
|
-
}
|
|
261
|
-
return url
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
249
|
function resolvePluginSpecifier(
|
|
266
250
|
definition: ExternalPlugin,
|
|
267
251
|
packageJson: ProjectPackageJSON,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {parse, ParseError} from 'jsonc-parser'
|
|
2
|
+
import type {GeneratorParamSchema} from '../generatorParams.ts'
|
|
2
3
|
|
|
3
4
|
export const settingsKey = 'kite3d'
|
|
4
5
|
export const assetUrlPrefix = `/${settingsKey}/`
|
|
@@ -12,15 +13,41 @@ export interface ProjectGeneratorState {
|
|
|
12
13
|
nodeIndex: number
|
|
13
14
|
nodeName: string
|
|
14
15
|
params: Record<string, unknown>
|
|
16
|
+
schema: GeneratorParamSchema
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface AssetsJSONManifest {
|
|
18
20
|
files: Record<string, {
|
|
19
21
|
path: string
|
|
22
|
+
files?: Record<string, string>
|
|
20
23
|
}>
|
|
21
24
|
version: number
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
export function createProjectAssetURLModifier(base: URL, assets: AssetsJSONManifest) {
|
|
28
|
+
const assetIdPrefix = `${assetUrlPrefix}@`
|
|
29
|
+
return (url: string): string => {
|
|
30
|
+
if (url.startsWith(assetIdPrefix)) {
|
|
31
|
+
const request = new URL(url, 'https://kite3d.invalid')
|
|
32
|
+
const relative = decodeURIComponent(request.pathname.slice(assetIdPrefix.length))
|
|
33
|
+
const slash = relative.indexOf('/')
|
|
34
|
+
const id = slash < 0 ? relative : relative.slice(0, slash)
|
|
35
|
+
const assetPath = slash < 0 ? '' : relative.slice(slash + 1)
|
|
36
|
+
const asset = assets.files[id]
|
|
37
|
+
if (!asset?.path) throw new Error(`Unknown asset id in URL: ${id}`)
|
|
38
|
+
const registered = asset.files?.[assetPath]
|
|
39
|
+
if (registered) return new URL(registered, base).href
|
|
40
|
+
if (asset.files) throw new Error(`Unknown file for asset ${id}: ${assetPath}`)
|
|
41
|
+
if (/^f\.[^/]+$/i.test(assetPath)) return new URL(asset.path, base).href
|
|
42
|
+
return new URL(assetPath, new URL(asset.path, base)).href
|
|
43
|
+
}
|
|
44
|
+
if (url.startsWith(assetUrlPrefix)) {
|
|
45
|
+
return new URL(url.slice(assetUrlPrefix.length), base).href
|
|
46
|
+
}
|
|
47
|
+
return url
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
24
51
|
export interface ProjectDependency {
|
|
25
52
|
key: string
|
|
26
53
|
version: string
|
|
@@ -140,6 +167,7 @@ export function readProjectGeneratorStates(text: string): ProjectGeneratorState[
|
|
|
140
167
|
nodeIndex,
|
|
141
168
|
nodeName: typeof node.name === 'string' ? node.name : `Node ${nodeIndex}`,
|
|
142
169
|
params: isRecord(component.state.params) ? component.state.params : {},
|
|
170
|
+
schema: {},
|
|
143
171
|
})
|
|
144
172
|
}
|
|
145
173
|
}
|