@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,275 @@
|
|
|
1
|
+
import * as ThreePipe from 'threepipe'
|
|
2
|
+
import {
|
|
3
|
+
type ComponentDefn,
|
|
4
|
+
type IObject3D,
|
|
5
|
+
Object3DComponent,
|
|
6
|
+
type ThreeViewer,
|
|
7
|
+
} from 'threepipe'
|
|
8
|
+
import {getAuthoringMetadata, setAuthoringMetadata, type AuthoringMetadata} from '../authoring.ts'
|
|
9
|
+
|
|
10
|
+
export interface GeneratorParams {
|
|
11
|
+
[key: string]: unknown
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GeneratorContext {
|
|
15
|
+
node: IObject3D
|
|
16
|
+
params: GeneratorParams
|
|
17
|
+
viewer: ThreeViewer
|
|
18
|
+
engine: Record<string, unknown>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GeneratorModule {
|
|
22
|
+
default?: (context: GeneratorContext) => unknown | Promise<unknown>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface GeneratorViewerOptions {
|
|
26
|
+
base: string | URL
|
|
27
|
+
onError?: (error: unknown) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface GeneratorViewerConfig {
|
|
31
|
+
base: URL
|
|
32
|
+
onError?: (error: unknown) => void
|
|
33
|
+
pending: Set<Promise<void>>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const viewerConfigs = new WeakMap<ThreeViewer, GeneratorViewerConfig>()
|
|
37
|
+
let generatorImportRevision = 0
|
|
38
|
+
|
|
39
|
+
export class GeneratorComponent extends Object3DComponent {
|
|
40
|
+
static ComponentType = 'Generator'
|
|
41
|
+
static StateProperties: ComponentDefn['StateProperties'] = ['module', 'params']
|
|
42
|
+
|
|
43
|
+
module = ''
|
|
44
|
+
params: GeneratorParams = {}
|
|
45
|
+
private runRevision = 0
|
|
46
|
+
|
|
47
|
+
static configureViewer(viewer: ThreeViewer, options: GeneratorViewerOptions): void {
|
|
48
|
+
viewerConfigs.set(viewer, {
|
|
49
|
+
base: typeof options.base === 'string' ? new URL(options.base) : options.base,
|
|
50
|
+
onError: options.onError,
|
|
51
|
+
pending: new Set(),
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static async waitForViewer(viewer: ThreeViewer): Promise<void> {
|
|
56
|
+
const config = viewerConfigs.get(viewer)
|
|
57
|
+
while (config?.pending.size) await Promise.all([...config.pending])
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
init(object: IObject3D, state: Record<string, unknown>): void {
|
|
61
|
+
super.init(object, state)
|
|
62
|
+
const run = () => { void this.run().catch((error) => reportGeneratorError(this.ctx.viewer, error)) }
|
|
63
|
+
this.onStateChange('module', run)
|
|
64
|
+
this.onStateChange('params', run)
|
|
65
|
+
run()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async run(): Promise<void> {
|
|
69
|
+
const viewer = this.ctx.viewer
|
|
70
|
+
const config = viewerConfigs.get(viewer)
|
|
71
|
+
if (!config) throw new Error('Generator viewer is not configured')
|
|
72
|
+
const revision = ++this.runRevision
|
|
73
|
+
const task = runGenerator({
|
|
74
|
+
node: this.object,
|
|
75
|
+
params: this.params,
|
|
76
|
+
viewer,
|
|
77
|
+
module: this.module,
|
|
78
|
+
base: config.base,
|
|
79
|
+
revision: ++generatorImportRevision,
|
|
80
|
+
isCurrent: () => revision === this.runRevision,
|
|
81
|
+
}).then(() => {
|
|
82
|
+
if (revision === this.runRevision) viewer.setDirty(this)
|
|
83
|
+
}).finally(() => config.pending.delete(task))
|
|
84
|
+
config.pending.add(task)
|
|
85
|
+
await task
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async bake(): Promise<number> {
|
|
89
|
+
await this.run()
|
|
90
|
+
const node = this.object
|
|
91
|
+
const generated = node.children.filter((child) => child.userData.kite3dGenerated === true)
|
|
92
|
+
const bakedFrom = {
|
|
93
|
+
module: this.module,
|
|
94
|
+
params: JSON.parse(JSON.stringify(this.params)) as GeneratorParams,
|
|
95
|
+
ts: new Date().toISOString(),
|
|
96
|
+
}
|
|
97
|
+
for (const child of generated) unmarkGenerated(child as IObject3D)
|
|
98
|
+
this.ctx.ecp.removeComponent(node, this.uuid)
|
|
99
|
+
const metadata = getAuthoringMetadata(node)
|
|
100
|
+
if (metadata?.role === 'generator' && !metadata.sourceId) {
|
|
101
|
+
setAuthoringMetadata(node, {
|
|
102
|
+
role: 'direct',
|
|
103
|
+
id: metadata.id,
|
|
104
|
+
...(metadata.allowCameraInside !== undefined ? {allowCameraInside: metadata.allowCameraInside} : {}),
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
node.userData.kite3dBakedFrom = bakedFrom
|
|
108
|
+
node._sChildren = [...node.children]
|
|
109
|
+
node.setDirty?.({change: 'userData.kite3dBakedFrom', source: 'kite3d bake'})
|
|
110
|
+
return generated.length
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
destroy(): Record<string, unknown> {
|
|
114
|
+
this.runRevision += 1
|
|
115
|
+
removeGeneratedChildren(this.object)
|
|
116
|
+
return super.destroy()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface RunGeneratorOptions extends Omit<GeneratorContext, 'engine'> {
|
|
121
|
+
module: string
|
|
122
|
+
base: URL
|
|
123
|
+
revision?: number
|
|
124
|
+
isCurrent?: () => boolean
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function runGenerator({
|
|
128
|
+
node,
|
|
129
|
+
params,
|
|
130
|
+
viewer,
|
|
131
|
+
module,
|
|
132
|
+
base,
|
|
133
|
+
revision = 0,
|
|
134
|
+
isCurrent,
|
|
135
|
+
}: RunGeneratorOptions): Promise<IObject3D[]> {
|
|
136
|
+
removeGeneratedChildren(node)
|
|
137
|
+
if (!module) return []
|
|
138
|
+
const source = ensureGeneratorMetadata(node)
|
|
139
|
+
const moduleUrl = resolveGeneratorModule(module, base)
|
|
140
|
+
if (revision) moduleUrl.searchParams.set('kite3d-generator', String(revision))
|
|
141
|
+
const loaded = await importGeneratorModule(moduleUrl.href)
|
|
142
|
+
if (typeof loaded.default !== 'function') {
|
|
143
|
+
throw new Error(`Generator module must have a default generate function: ${module}`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const existingChildren = new Set(node.children)
|
|
147
|
+
const returned = await loaded.default({
|
|
148
|
+
node,
|
|
149
|
+
params,
|
|
150
|
+
viewer,
|
|
151
|
+
engine: ThreePipe as unknown as Record<string, unknown>,
|
|
152
|
+
})
|
|
153
|
+
const returnedObjects = normalizeGeneratedResult(returned)
|
|
154
|
+
for (const [index, child] of returnedObjects.entries()) {
|
|
155
|
+
if (child.parent === node) continue
|
|
156
|
+
markGenerated(child, source.id, index)
|
|
157
|
+
node.add(child)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const generated = node.children.filter((child) => !existingChildren.has(child)) as IObject3D[]
|
|
161
|
+
if (isCurrent && !isCurrent()) {
|
|
162
|
+
for (const child of generated) removeGeneratedObject(child)
|
|
163
|
+
return []
|
|
164
|
+
}
|
|
165
|
+
generated.forEach((child, index) => markGenerated(child, source.id, index))
|
|
166
|
+
return generated
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function removeGeneratedChildren(node: IObject3D): void {
|
|
170
|
+
for (const child of [...node.children] as IObject3D[]) {
|
|
171
|
+
if (child.userData.kite3dGenerated !== true) continue
|
|
172
|
+
removeGeneratedObject(child)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function markGenerated(object: IObject3D, sourceId?: string, outputIndex = 0): void {
|
|
177
|
+
let descendantIndex = 0
|
|
178
|
+
object.traverse((child: IObject3D) => {
|
|
179
|
+
child.userData.kite3dGenerated = true
|
|
180
|
+
child.userData.excludeFromExport = true
|
|
181
|
+
if (sourceId) {
|
|
182
|
+
child.userData.kite3dAuthoring = {
|
|
183
|
+
role: 'generator',
|
|
184
|
+
id: `${sourceId}:preview:${outputIndex}:${descendantIndex++}`,
|
|
185
|
+
sourceId,
|
|
186
|
+
} satisfies AuthoringMetadata
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function unmarkGenerated(object: IObject3D): void {
|
|
192
|
+
object.traverse((child: IObject3D) => {
|
|
193
|
+
delete child.userData.kite3dGenerated
|
|
194
|
+
delete child.userData.excludeFromExport
|
|
195
|
+
const metadata = getAuthoringMetadata(child)
|
|
196
|
+
if (metadata?.role === 'generator' && metadata.sourceId) delete child.userData.kite3dAuthoring
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function ensureGeneratorMetadata(node: IObject3D): AuthoringMetadata {
|
|
201
|
+
const current = getAuthoringMetadata(node)
|
|
202
|
+
if (current?.role === 'generator' && !current.sourceId) return current
|
|
203
|
+
const savedId = typeof node.userData.gltfUUID === 'string' && node.userData.gltfUUID.trim()
|
|
204
|
+
? node.userData.gltfUUID.trim()
|
|
205
|
+
: current?.id || generatorPathId(node)
|
|
206
|
+
setAuthoringMetadata(node, {role: 'generator', id: savedId})
|
|
207
|
+
return getAuthoringMetadata(node)!
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function generatorPathId(node: IObject3D): string {
|
|
211
|
+
const parts: string[] = []
|
|
212
|
+
for (let current: IObject3D | null = node; current?.parent; current = current.parent as IObject3D) {
|
|
213
|
+
const index = current.parent.children.indexOf(current)
|
|
214
|
+
parts.push(`${current.name || current.type}:${index}`)
|
|
215
|
+
if (current.parent.userData?.rootSceneModelRoot) break
|
|
216
|
+
}
|
|
217
|
+
return `generator:${parts.reverse().join('/')}`
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function removeGeneratedObject(object: IObject3D): void {
|
|
221
|
+
object.removeFromParent()
|
|
222
|
+
const disposed = new Set<object>()
|
|
223
|
+
object.traverse((child) => {
|
|
224
|
+
const renderable = child as IObject3D & {geometry?: {dispose?(): void}, material?: unknown | unknown[]}
|
|
225
|
+
disposeResource(renderable.geometry, disposed)
|
|
226
|
+
const materials = Array.isArray(renderable.material) ? renderable.material : [renderable.material]
|
|
227
|
+
for (const material of materials) {
|
|
228
|
+
if (!material || typeof material !== 'object') continue
|
|
229
|
+
for (const value of Object.values(material)) {
|
|
230
|
+
if (value && typeof value === 'object' && 'isTexture' in value) disposeResource(value, disposed)
|
|
231
|
+
}
|
|
232
|
+
disposeResource(material, disposed)
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function disposeResource(resource: unknown, disposed: Set<object>): void {
|
|
238
|
+
if (!resource || typeof resource !== 'object' || disposed.has(resource)) return
|
|
239
|
+
disposed.add(resource)
|
|
240
|
+
;(resource as {dispose?(): void}).dispose?.()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function resolveGeneratorModule(module: string, base: URL): URL {
|
|
244
|
+
if (!module || module.startsWith('/') || /^[a-z][a-z\d+.-]*:/i.test(module)) {
|
|
245
|
+
throw new Error(`Generator module must be a project-relative path: ${module}`)
|
|
246
|
+
}
|
|
247
|
+
const resolved = new URL(module, base)
|
|
248
|
+
if (resolved.origin !== base.origin) {
|
|
249
|
+
throw new Error(`Generator module must be same-origin: ${resolved.href}`)
|
|
250
|
+
}
|
|
251
|
+
return resolved
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function normalizeGeneratedResult(value: unknown): IObject3D[] {
|
|
255
|
+
if (!value) return []
|
|
256
|
+
const values = Array.isArray(value) ? value : [value]
|
|
257
|
+
const objects: IObject3D[] = []
|
|
258
|
+
for (const candidate of values) {
|
|
259
|
+
if (!(candidate as IObject3D | undefined)?.isObject3D) {
|
|
260
|
+
throw new Error('Generator output must be an Object3D, an array of Object3D values, or undefined')
|
|
261
|
+
}
|
|
262
|
+
objects.push(candidate as IObject3D)
|
|
263
|
+
}
|
|
264
|
+
return objects
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function importGeneratorModule(url: string): Promise<GeneratorModule> {
|
|
268
|
+
return import(/* @vite-ignore */ url) as Promise<GeneratorModule>
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function reportGeneratorError(viewer: ThreeViewer, error: unknown): void {
|
|
272
|
+
const onError = viewerConfigs.get(viewer)?.onError
|
|
273
|
+
if (onError) onError(error)
|
|
274
|
+
else console.error('[kite3d] Generator error', error)
|
|
275
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// /**
|
|
2
|
+
// * Example usage of HtmlUiComponent
|
|
3
|
+
// *
|
|
4
|
+
// * This file demonstrates how to use the HtmlUiComponent with threepipe
|
|
5
|
+
// */
|
|
6
|
+
//
|
|
7
|
+
// import { HtmlUiComponent } from './HtmlUiComponent'
|
|
8
|
+
// import { IViewer, EntityComponentPlugin, IObject3D } from 'threepipe'
|
|
9
|
+
//
|
|
10
|
+
// /**
|
|
11
|
+
// * Initialize the HtmlUiComponent for a viewer
|
|
12
|
+
// */
|
|
13
|
+
// export function initHtmlUiComponent(viewer: IViewer) {
|
|
14
|
+
// // The EntityComponentPlugin should already be added to the viewer
|
|
15
|
+
// // If not, add it:
|
|
16
|
+
// // viewer.addPluginSync(EntityComponentPlugin)
|
|
17
|
+
// }
|
|
18
|
+
//
|
|
19
|
+
// /**
|
|
20
|
+
// * Add a simple label to an object that follows it in 3D space
|
|
21
|
+
// */
|
|
22
|
+
// export function addObjectLabel(object: IObject3D, labelText: string) {
|
|
23
|
+
// return EntityComponentPlugin.AddComponent(object, HtmlUiComponent, {
|
|
24
|
+
// htmlData: `
|
|
25
|
+
// <div style="
|
|
26
|
+
// background: rgba(0, 0, 0, 0.8);
|
|
27
|
+
// color: white;
|
|
28
|
+
// padding: 8px 12px;
|
|
29
|
+
// border-radius: 5px;
|
|
30
|
+
// font-family: Arial, sans-serif;
|
|
31
|
+
// font-size: 14px;
|
|
32
|
+
// white-space: nowrap;
|
|
33
|
+
// box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
34
|
+
// ">
|
|
35
|
+
// ${labelText}
|
|
36
|
+
// </div>
|
|
37
|
+
// `,
|
|
38
|
+
// width: 150,
|
|
39
|
+
// height: 40,
|
|
40
|
+
// positionMode: 'world',
|
|
41
|
+
// offsetY: -50, // Position above the object
|
|
42
|
+
// scrollable: false,
|
|
43
|
+
// interactable: false,
|
|
44
|
+
// visible: true,
|
|
45
|
+
// zIndex: 1000
|
|
46
|
+
// })
|
|
47
|
+
// }
|
|
48
|
+
//
|
|
49
|
+
// /**
|
|
50
|
+
// * Add an info panel to an object
|
|
51
|
+
// */
|
|
52
|
+
// export function addInfoPanel(object: IObject3D, title: string, content: string) {
|
|
53
|
+
// return EntityComponentPlugin.AddComponent(object, HtmlUiComponent, {
|
|
54
|
+
// htmlData: `
|
|
55
|
+
// <div style="
|
|
56
|
+
// background: white;
|
|
57
|
+
// padding: 15px;
|
|
58
|
+
// border-radius: 8px;
|
|
59
|
+
// font-family: Arial, sans-serif;
|
|
60
|
+
// box-shadow: 0 4px 16px rgba(0,0,0,0.2);
|
|
61
|
+
// border: 1px solid #ccc;
|
|
62
|
+
// ">
|
|
63
|
+
// <h3 style="margin: 0 0 10px 0; font-size: 16px; color: #333;">${title}</h3>
|
|
64
|
+
// <p style="margin: 0; font-size: 13px; color: #666; line-height: 1.5;">${content}</p>
|
|
65
|
+
// </div>
|
|
66
|
+
// `,
|
|
67
|
+
// width: 250,
|
|
68
|
+
// height: 150,
|
|
69
|
+
// positionMode: 'world',
|
|
70
|
+
// offsetX: 100,
|
|
71
|
+
// offsetY: -100,
|
|
72
|
+
// scrollable: true,
|
|
73
|
+
// interactable: true,
|
|
74
|
+
// visible: false, // Start hidden
|
|
75
|
+
// zIndex: 1000
|
|
76
|
+
// })
|
|
77
|
+
// }
|
|
78
|
+
//
|
|
79
|
+
// /**
|
|
80
|
+
// * Add a fixed HUD element
|
|
81
|
+
// */
|
|
82
|
+
// export function addHudElement(object: IObject3D, htmlContent: string, x: number, y: number) {
|
|
83
|
+
// return EntityComponentPlugin.AddComponent(object, HtmlUiComponent, {
|
|
84
|
+
// htmlData: htmlContent,
|
|
85
|
+
// width: 200,
|
|
86
|
+
// height: 100,
|
|
87
|
+
// positionMode: 'screen',
|
|
88
|
+
// offsetX: x,
|
|
89
|
+
// offsetY: y,
|
|
90
|
+
// scrollable: false,
|
|
91
|
+
// interactable: true,
|
|
92
|
+
// visible: true,
|
|
93
|
+
// zIndex: 1001
|
|
94
|
+
// })
|
|
95
|
+
// }
|
|
96
|
+
//
|
|
97
|
+
// /**
|
|
98
|
+
// * Add an interactive button panel
|
|
99
|
+
// */
|
|
100
|
+
// export function addButtonPanel(object: IObject3D, onButtonClick: () => void) {
|
|
101
|
+
// const component = EntityComponentPlugin.AddComponent(object, HtmlUiComponent, {
|
|
102
|
+
// htmlData: `
|
|
103
|
+
// <div style="
|
|
104
|
+
// background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
105
|
+
// padding: 15px;
|
|
106
|
+
// border-radius: 10px;
|
|
107
|
+
// box-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
|
108
|
+
// ">
|
|
109
|
+
// <button id="action-button" style="
|
|
110
|
+
// background: white;
|
|
111
|
+
// border: none;
|
|
112
|
+
// padding: 10px 20px;
|
|
113
|
+
// border-radius: 5px;
|
|
114
|
+
// cursor: pointer;
|
|
115
|
+
// font-weight: bold;
|
|
116
|
+
// color: #667eea;
|
|
117
|
+
// font-size: 14px;
|
|
118
|
+
// transition: transform 0.2s;
|
|
119
|
+
// ">
|
|
120
|
+
// Click Me
|
|
121
|
+
// </button>
|
|
122
|
+
// </div>
|
|
123
|
+
// `,
|
|
124
|
+
// width: 140,
|
|
125
|
+
// height: 70,
|
|
126
|
+
// positionMode: 'world',
|
|
127
|
+
// offsetY: 50,
|
|
128
|
+
// scrollable: false,
|
|
129
|
+
// interactable: true,
|
|
130
|
+
// visible: true,
|
|
131
|
+
// zIndex: 1000
|
|
132
|
+
// })
|
|
133
|
+
//
|
|
134
|
+
// // Add event listener after component is initialized
|
|
135
|
+
// setTimeout(() => {
|
|
136
|
+
// const button = component.element?.querySelector('#action-button')
|
|
137
|
+
// if (button) {
|
|
138
|
+
// button.addEventListener('click', onButtonClick)
|
|
139
|
+
// }
|
|
140
|
+
// }, 0)
|
|
141
|
+
//
|
|
142
|
+
// return component
|
|
143
|
+
// }
|
|
144
|
+
//
|
|
145
|
+
// /**
|
|
146
|
+
// * Example: Complete usage scenario
|
|
147
|
+
// */
|
|
148
|
+
// export function exampleUsage(viewer: IViewer, targetObject: IObject3D) {
|
|
149
|
+
// // Initialize
|
|
150
|
+
// initHtmlUiComponent(viewer)
|
|
151
|
+
//
|
|
152
|
+
// // Add a label that follows the object
|
|
153
|
+
// const label = addObjectLabel(targetObject, 'Important Object')
|
|
154
|
+
//
|
|
155
|
+
// // Add an info panel that can be toggled
|
|
156
|
+
// const infoPanel = addInfoPanel(
|
|
157
|
+
// targetObject,
|
|
158
|
+
// 'Object Details',
|
|
159
|
+
// 'This object has special properties. Click to learn more about its configuration.'
|
|
160
|
+
// )
|
|
161
|
+
//
|
|
162
|
+
// // Add an interactive button
|
|
163
|
+
// const buttonPanel = addButtonPanel(targetObject, () => {
|
|
164
|
+
// // Toggle info panel visibility
|
|
165
|
+
// infoPanel.visible = !infoPanel.visible
|
|
166
|
+
// console.log('Button clicked! Info panel toggled.')
|
|
167
|
+
// })
|
|
168
|
+
//
|
|
169
|
+
// // Add a fixed HUD
|
|
170
|
+
// const hud = addHudElement(
|
|
171
|
+
// targetObject,
|
|
172
|
+
// `<div style="background: rgba(0,0,0,0.8); color: white; padding: 10px; border-radius: 5px;">
|
|
173
|
+
// <strong>Status:</strong> Active
|
|
174
|
+
// </div>`,
|
|
175
|
+
// 10,
|
|
176
|
+
// 10
|
|
177
|
+
// )
|
|
178
|
+
//
|
|
179
|
+
// // Return components for further manipulation
|
|
180
|
+
// return { label, infoPanel, buttonPanel, hud }
|
|
181
|
+
// }
|
|
182
|
+
//
|
|
183
|
+
// /**
|
|
184
|
+
// * Dynamic content update example
|
|
185
|
+
// */
|
|
186
|
+
// export function updateLabelContent(component: HtmlUiComponent, newText: string) {
|
|
187
|
+
// component.setHtml(`
|
|
188
|
+
// <div style="
|
|
189
|
+
// background: rgba(0, 0, 0, 0.8);
|
|
190
|
+
// color: white;
|
|
191
|
+
// padding: 8px 12px;
|
|
192
|
+
// border-radius: 5px;
|
|
193
|
+
// font-family: Arial, sans-serif;
|
|
194
|
+
// font-size: 14px;
|
|
195
|
+
// white-space: nowrap;
|
|
196
|
+
// box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
197
|
+
// ">
|
|
198
|
+
// ${newText}
|
|
199
|
+
// </div>
|
|
200
|
+
// `)
|
|
201
|
+
// }
|
|
202
|
+
//
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# HtmlUiComponent
|
|
2
|
+
|
|
3
|
+
A component for attaching HTML UI elements to 3D objects in threepipe.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **HTML Content**: Render any HTML content in a div element
|
|
8
|
+
- **Position Modes**:
|
|
9
|
+
- `world`: Follow a 3D object in world space (automatically projects to screen)
|
|
10
|
+
- `screen`: Fixed screen position (top-left corner)
|
|
11
|
+
- `viewport`: Viewport-relative positioning (using percentages)
|
|
12
|
+
- **Customizable Properties**:
|
|
13
|
+
- Size (width, height in pixels)
|
|
14
|
+
- Scrollable content
|
|
15
|
+
- Interactable (pointer events)
|
|
16
|
+
- Visibility toggle
|
|
17
|
+
- Z-index control
|
|
18
|
+
- Offset positioning
|
|
19
|
+
|
|
20
|
+
## Usage Example
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { HtmlUiComponent } from './plugins/HtmlUiComponent'
|
|
24
|
+
import { EntityComponentPlugin } from 'threepipe'
|
|
25
|
+
|
|
26
|
+
// Register the component
|
|
27
|
+
viewer.addPluginSync(EntityComponentPlugin)
|
|
28
|
+
|
|
29
|
+
// Add component to an object
|
|
30
|
+
const component = EntityComponentPlugin.AddComponent(myObject, HtmlUiComponent, {
|
|
31
|
+
htmlData: '<div style="background: white; padding: 10px; border-radius: 5px;">Object Label</div>',
|
|
32
|
+
width: 150,
|
|
33
|
+
height: 50,
|
|
34
|
+
positionMode: 'world',
|
|
35
|
+
offsetY: -50, // Position above the object
|
|
36
|
+
scrollable: false,
|
|
37
|
+
interactable: true,
|
|
38
|
+
visible: true,
|
|
39
|
+
zIndex: 1000
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Update HTML content
|
|
43
|
+
component.setHtml('<div>Updated content</div>')
|
|
44
|
+
|
|
45
|
+
// Change position
|
|
46
|
+
component.setPosition(10, 20) // offsetX, offsetY
|
|
47
|
+
|
|
48
|
+
// Change size
|
|
49
|
+
component.setSize(200, 100)
|
|
50
|
+
|
|
51
|
+
// Show/hide
|
|
52
|
+
component.show()
|
|
53
|
+
component.hide()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Position Modes
|
|
57
|
+
|
|
58
|
+
### World Mode
|
|
59
|
+
Follows the 3D object's position in world space and projects it to screen coordinates.
|
|
60
|
+
Perfect for labels, tooltips, or info panels attached to 3D objects.
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
component.positionMode = 'world'
|
|
64
|
+
component.offsetX = 0 // Pixel offset from projected position
|
|
65
|
+
component.offsetY = -50 // Position 50px above the object
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Screen Mode
|
|
69
|
+
Fixed position relative to the top-left corner of the screen.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
component.positionMode = 'screen'
|
|
73
|
+
component.offsetX = 100 // 100px from left edge
|
|
74
|
+
component.offsetY = 50 // 50px from top edge
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Viewport Mode
|
|
78
|
+
Position relative to the viewport size (0-100 representing percentages).
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
component.positionMode = 'viewport'
|
|
82
|
+
component.offsetX = 50 // 50% from left (center)
|
|
83
|
+
component.offsetY = 50 // 50% from top (center)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Properties
|
|
87
|
+
|
|
88
|
+
| Property | Type | Default | Description |
|
|
89
|
+
|----------|------|---------|-------------|
|
|
90
|
+
| `htmlData` | `string` | `'<div>Hello World</div>'` | HTML content to render |
|
|
91
|
+
| `width` | `number` | `200` | Width in pixels (set to `-1` for auto) |
|
|
92
|
+
| `height` | `number` | `150` | Height in pixels (set to `-1` for auto) |
|
|
93
|
+
| `scrollable` | `boolean` | `false` | Enable scrolling for overflow content |
|
|
94
|
+
| `interactable` | `boolean` | `true` | Enable pointer events (clickable) |
|
|
95
|
+
| `visible` | `boolean` | `true` | Show/hide the element |
|
|
96
|
+
| `appendToBody` | `boolean` | `false` | Append to `document.body` (true) or `viewer.container` (false, default). Can be changed dynamically - element will be moved. |
|
|
97
|
+
| `positionMode` | `'world' \| 'screen' \| 'viewport'` | `'world'` | Position mode |
|
|
98
|
+
| `offsetX` | `number` | `0` | X-axis offset in pixels (or % for viewport mode) |
|
|
99
|
+
| `offsetY` | `number` | `0` | Y-axis offset in pixels (or % for viewport mode) |
|
|
100
|
+
| `zIndex` | `number` | `1000` | CSS z-index value |
|
|
101
|
+
|
|
102
|
+
## Methods
|
|
103
|
+
|
|
104
|
+
### `setHtml(html: string)`
|
|
105
|
+
Update the HTML content.
|
|
106
|
+
|
|
107
|
+
### `setPosition(x: number, y: number)`
|
|
108
|
+
Set the offset position.
|
|
109
|
+
|
|
110
|
+
### `setSize(width: number, height: number)`
|
|
111
|
+
Set the size of the element.
|
|
112
|
+
|
|
113
|
+
### `show()`
|
|
114
|
+
Show the element (sets `visible` to `true`).
|
|
115
|
+
|
|
116
|
+
### `hide()`
|
|
117
|
+
Hide the element (sets `visible` to `false`).
|
|
118
|
+
|
|
119
|
+
## Advanced Examples
|
|
120
|
+
|
|
121
|
+
### Interactive Button Panel
|
|
122
|
+
```typescript
|
|
123
|
+
const buttonPanel = EntityComponentPlugin.AddComponent(object, HtmlUiComponent, {
|
|
124
|
+
htmlData: `
|
|
125
|
+
<div style="background: rgba(0,0,0,0.8); color: white; padding: 10px; border-radius: 8px;">
|
|
126
|
+
<button onclick="alert('Button clicked!')">Click Me</button>
|
|
127
|
+
</div>
|
|
128
|
+
`,
|
|
129
|
+
width: 120,
|
|
130
|
+
height: 60,
|
|
131
|
+
positionMode: 'world',
|
|
132
|
+
interactable: true
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Information Panel with Scroll
|
|
137
|
+
```typescript
|
|
138
|
+
const infoPanel = EntityComponentPlugin.AddComponent(object, HtmlUiComponent, {
|
|
139
|
+
htmlData: `
|
|
140
|
+
<div style="background: white; padding: 15px; font-family: Arial;">
|
|
141
|
+
<h3>Object Information</h3>
|
|
142
|
+
<p>Lorem ipsum dolor sit amet...</p>
|
|
143
|
+
<ul>
|
|
144
|
+
<li>Property 1</li>
|
|
145
|
+
<li>Property 2</li>
|
|
146
|
+
<li>Property 3</li>
|
|
147
|
+
</ul>
|
|
148
|
+
</div>
|
|
149
|
+
`,
|
|
150
|
+
width: 250,
|
|
151
|
+
height: 200,
|
|
152
|
+
scrollable: true,
|
|
153
|
+
positionMode: 'world'
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Fixed HUD Element
|
|
158
|
+
```typescript
|
|
159
|
+
const hud = EntityComponentPlugin.AddComponent(object, HtmlUiComponent, {
|
|
160
|
+
htmlData: `
|
|
161
|
+
<div style="background: rgba(255,255,255,0.9); padding: 10px;">
|
|
162
|
+
<strong>Score:</strong> <span id="score">0</span>
|
|
163
|
+
</div>
|
|
164
|
+
`,
|
|
165
|
+
width: 150,
|
|
166
|
+
height: 40,
|
|
167
|
+
positionMode: 'screen',
|
|
168
|
+
offsetX: 10,
|
|
169
|
+
offsetY: 10,
|
|
170
|
+
interactable: false
|
|
171
|
+
})
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Auto-Sized Element
|
|
175
|
+
```typescript
|
|
176
|
+
const autoSized = EntityComponentPlugin.AddComponent(object, HtmlUiComponent, {
|
|
177
|
+
htmlData: `
|
|
178
|
+
<div style="background: white; padding: 15px; border-radius: 5px;">
|
|
179
|
+
<h3>This content determines the size</h3>
|
|
180
|
+
<p>Width and height are automatic!</p>
|
|
181
|
+
</div>
|
|
182
|
+
`,
|
|
183
|
+
width: -1, // Auto width
|
|
184
|
+
height: -1, // Auto height
|
|
185
|
+
positionMode: 'world'
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Dynamically Switch Container
|
|
190
|
+
```typescript
|
|
191
|
+
const component = EntityComponentPlugin.AddComponent(object, HtmlUiComponent, {
|
|
192
|
+
htmlData: '<div style="background: white; padding: 10px;">My UI</div>',
|
|
193
|
+
width: 200,
|
|
194
|
+
height: 100,
|
|
195
|
+
appendToBody: false // Start in viewer.container
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// Later, move to document.body (useful for full-screen overlays)
|
|
199
|
+
component.appendToBody = true
|
|
200
|
+
|
|
201
|
+
// Move back to viewer.container
|
|
202
|
+
component.appendToBody = false
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Styling Tips
|
|
206
|
+
|
|
207
|
+
- Use inline styles in the HTML for better control
|
|
208
|
+
- Set `pointer-events: none` on non-interactive elements within the HTML if needed
|
|
209
|
+
- Use `rgba()` colors for transparency
|
|
210
|
+
- The element has CSS class `threepipe-html-ui` for global styling
|
|
211
|
+
|
|
212
|
+
## Notes
|
|
213
|
+
|
|
214
|
+
- Elements are automatically cleaned up when the component is destroyed
|
|
215
|
+
- World-positioned elements use `requestAnimationFrame` for smooth tracking
|
|
216
|
+
- By default, elements are appended to `viewer.container` (set `appendToBody: true` to append to `document.body` instead)
|
|
217
|
+
- The element is positioned absolutely within its container
|
|
218
|
+
- Transform origin is centered for world-positioned elements (`translate(-50%, -50%)`)
|
|
219
|
+
|