@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,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
+