@c4h/chuci 0.1.0 → 0.2.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/CHANGELOG.md +32 -32
- package/LICENSE +20 -20
- package/README.ja.md +143 -143
- package/README.md +224 -224
- package/dist/chuci.js +9067 -6595
- package/dist/chuci.umd.js +164 -165
- package/dist/index.d.ts +1 -0
- package/package.json +36 -33
- package/src/components/swiper/cc-swiper-slide.ts +49 -49
- package/src/components/swiper/cc-swiper-styles.ts +28 -28
- package/src/components/swiper/cc-swiper.ts +379 -361
- package/src/components/swiper/swiper-styles.css +4 -4
- package/src/components/viewer/cc-viewer-3dmodel.ts +491 -491
- package/src/components/viewer/cc-viewer-base.ts +278 -278
- package/src/components/viewer/cc-viewer-gaussian.ts +380 -380
- package/src/components/viewer/cc-viewer-image.ts +189 -189
- package/src/components/viewer/cc-viewer-panorama.ts +85 -85
- package/src/components/viewer/cc-viewer-styles.ts +55 -55
- package/src/components/viewer/cc-viewer-video.ts +109 -109
- package/src/components/viewer/cc-viewer-youtube.ts +75 -75
- package/src/components/viewer/cc-viewer.ts +290 -290
- package/src/index.ts +24 -24
- package/src/types/css-modules.d.ts +1 -1
- package/src/types/global.d.ts +10 -10
- package/src/utils/base-element.ts +76 -76
- package/dist/assets/azumaya_panorama1.png +0 -0
- package/dist/chuci.cjs +0 -4483
- package/dist/index-8VMexD2a.cjs +0 -255
- package/dist/index-kvsisbKS.js +0 -2125
- package/dist/index.html +0 -241
- package/dist/test-image.html +0 -63
|
@@ -1,381 +1,381 @@
|
|
|
1
|
-
import { CcViewerBase } from './cc-viewer-base'
|
|
2
|
-
|
|
3
|
-
export class CcViewerGaussian extends CcViewerBase {
|
|
4
|
-
private splatUrl = ''
|
|
5
|
-
private debugMode = false
|
|
6
|
-
private cameraPosition = '3,3,3'
|
|
7
|
-
private _cameraTarget = '0,0,0' // TODO: Implement camera target functionality
|
|
8
|
-
|
|
9
|
-
// gsplat.js library types are not available, using any for external library objects
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
-
private scene?: any
|
|
12
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
-
private camera?: any
|
|
14
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
-
private renderer?: any
|
|
16
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
-
private controls?: any
|
|
18
|
-
private animationId?: number
|
|
19
|
-
private canvas?: HTMLCanvasElement
|
|
20
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
-
private swiper?: any
|
|
22
|
-
|
|
23
|
-
static get observedAttributes() {
|
|
24
|
-
return ['show', 'debug-mode', 'camera-position', 'camera-target']
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
|
28
|
-
if (name === 'show') {
|
|
29
|
-
this.isShow = newValue === 'true'
|
|
30
|
-
} else if (name === 'debug-mode') {
|
|
31
|
-
this.debugMode = newValue === 'true'
|
|
32
|
-
} else if (name === 'camera-position') {
|
|
33
|
-
this.cameraPosition = newValue || '3,3,3'
|
|
34
|
-
} else if (name === 'camera-target') {
|
|
35
|
-
this._cameraTarget = newValue || '0,0,0'
|
|
36
|
-
}
|
|
37
|
-
super.attributeChangedCallback(name, oldValue, newValue)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Implementation of abstract methods from base class
|
|
41
|
-
protected async doOpen(url: string): Promise<void> {
|
|
42
|
-
this.splatUrl = url
|
|
43
|
-
await this.initializeViewer()
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
protected doClose(): void {
|
|
47
|
-
this.cleanup()
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
protected getViewerContent(): string {
|
|
51
|
-
// This won't be used since we override render, but required by abstract class
|
|
52
|
-
return ''
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Override to use custom rendering due to special canvas handling
|
|
56
|
-
protected shouldUseCustomRender(): boolean {
|
|
57
|
-
return true
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Custom render implementation
|
|
61
|
-
protected customRender(): void {
|
|
62
|
-
const styles = this.css`
|
|
63
|
-
:host {
|
|
64
|
-
--cc-viewer-z-index-each: 1000;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.backdrop {
|
|
68
|
-
justify-content: center;
|
|
69
|
-
align-items: center;
|
|
70
|
-
position: fixed;
|
|
71
|
-
left: 0;
|
|
72
|
-
right: 0;
|
|
73
|
-
top: 0;
|
|
74
|
-
bottom: 0;
|
|
75
|
-
width: 100%;
|
|
76
|
-
height: 100%;
|
|
77
|
-
background-color: rgba(0, 0, 0, 0.9);
|
|
78
|
-
z-index: 1000;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.viewer {
|
|
82
|
-
position: absolute;
|
|
83
|
-
width: 90%;
|
|
84
|
-
height: 85%;
|
|
85
|
-
inset: 0px;
|
|
86
|
-
margin: auto;
|
|
87
|
-
align-self: center;
|
|
88
|
-
background-color: #000;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.gaussian-container {
|
|
92
|
-
width: 100%;
|
|
93
|
-
height: 100%;
|
|
94
|
-
position: relative;
|
|
95
|
-
background: #000;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
.gaussian-container canvas {
|
|
99
|
-
width: 100% !important;
|
|
100
|
-
height: 100% !important;
|
|
101
|
-
display: block;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
.loading {
|
|
105
|
-
position: absolute;
|
|
106
|
-
top: 50%;
|
|
107
|
-
left: 50%;
|
|
108
|
-
transform: translate(-50%, -50%);
|
|
109
|
-
color: #fff;
|
|
110
|
-
font-size: 1.2rem;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.error {
|
|
114
|
-
position: absolute;
|
|
115
|
-
top: 50%;
|
|
116
|
-
left: 50%;
|
|
117
|
-
transform: translate(-50%, -50%);
|
|
118
|
-
color: #e74c3c;
|
|
119
|
-
text-align: center;
|
|
120
|
-
padding: 20px;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
canvas {
|
|
124
|
-
display: block;
|
|
125
|
-
width: 100%;
|
|
126
|
-
height: 100%;
|
|
127
|
-
touch-action: none;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.debug-info {
|
|
131
|
-
position: absolute;
|
|
132
|
-
top: 10px;
|
|
133
|
-
left: 10px;
|
|
134
|
-
background: rgba(0, 0, 0, 0.85);
|
|
135
|
-
color: #00ff00;
|
|
136
|
-
padding: 12px;
|
|
137
|
-
font-family: 'Courier New', monospace;
|
|
138
|
-
font-size: 11px;
|
|
139
|
-
line-height: 1.4;
|
|
140
|
-
border-radius: 6px;
|
|
141
|
-
border: 1px solid rgba(0, 255, 0, 0.3);
|
|
142
|
-
pointer-events: none;
|
|
143
|
-
white-space: pre-line;
|
|
144
|
-
min-width: 200px;
|
|
145
|
-
z-index: 1003;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
${this.getNavigationStyles()}
|
|
149
|
-
`
|
|
150
|
-
|
|
151
|
-
const gaussianContent = `
|
|
152
|
-
<canvas style="display: none;"></canvas>
|
|
153
|
-
${this.isLoading ? '<div class="loading">Loading...</div>' : ''}
|
|
154
|
-
${!this.isLoading && this.debugMode ? `
|
|
155
|
-
<div class="debug-info">
|
|
156
|
-
📍 Camera Position:
|
|
157
|
-
${this.getCameraDebugInfo()}
|
|
158
|
-
|
|
159
|
-
🎯 Camera Target:
|
|
160
|
-
${this.getTargetDebugInfo()}
|
|
161
|
-
|
|
162
|
-
🎮 Controls:
|
|
163
|
-
• Rotate: Left-drag
|
|
164
|
-
• Zoom: Scroll wheel
|
|
165
|
-
• Pan: Right-drag or Shift+Left-drag
|
|
166
|
-
|
|
167
|
-
📊 Status: ${this.scene ? 'Splat loaded' : 'Loading...'}
|
|
168
|
-
</div>
|
|
169
|
-
` : ''}
|
|
170
|
-
`
|
|
171
|
-
|
|
172
|
-
const html = `
|
|
173
|
-
${styles}
|
|
174
|
-
<div class="backdrop" style="${this.isShow ? 'visibility: visible' : 'visibility: hidden'}">
|
|
175
|
-
<div class="viewer">
|
|
176
|
-
<div class="gaussian-container">
|
|
177
|
-
${gaussianContent}
|
|
178
|
-
</div>
|
|
179
|
-
</div>
|
|
180
|
-
${this.getNavigationButtons()}
|
|
181
|
-
</div>
|
|
182
|
-
`
|
|
183
|
-
|
|
184
|
-
this.updateShadowRoot(html)
|
|
185
|
-
|
|
186
|
-
// Add navigation listeners after render
|
|
187
|
-
setTimeout(() => {
|
|
188
|
-
this.addNavigationListeners()
|
|
189
|
-
}, 0)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private cleanup() {
|
|
193
|
-
if (this.animationId) {
|
|
194
|
-
cancelAnimationFrame(this.animationId)
|
|
195
|
-
this.animationId = undefined
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (this.renderer && typeof this.renderer.dispose === 'function') {
|
|
199
|
-
this.renderer.dispose()
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Remove all gaussian canvases from document.body
|
|
203
|
-
const existingCanvases = document.querySelectorAll('canvas[id^="gaussian-canvas-"]')
|
|
204
|
-
existingCanvases.forEach(canvas => {
|
|
205
|
-
if (canvas.parentNode === document.body) {
|
|
206
|
-
document.body.removeChild(canvas)
|
|
207
|
-
}
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
this.scene = undefined
|
|
211
|
-
this.camera = undefined
|
|
212
|
-
this.renderer = undefined
|
|
213
|
-
this.controls = undefined
|
|
214
|
-
this.canvas = undefined
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private getCameraDebugInfo(): string {
|
|
218
|
-
if (!this.camera || !this.camera.position) return 'Position: unavailable'
|
|
219
|
-
const pos = this.camera.position
|
|
220
|
-
try {
|
|
221
|
-
return `X: ${pos.x.toFixed(3)}, Y: ${pos.y.toFixed(3)}, Z: ${pos.z.toFixed(3)}`
|
|
222
|
-
} catch (_error) {
|
|
223
|
-
return `Position: ${JSON.stringify(pos)}`
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
private getTargetDebugInfo(): string {
|
|
228
|
-
if (!this.controls) return 'Target: controls unavailable'
|
|
229
|
-
// gsplat.js OrbitControls might not have a target property
|
|
230
|
-
// Return available control info instead
|
|
231
|
-
try {
|
|
232
|
-
return `Controls active (no target property in gsplat.js)`
|
|
233
|
-
} catch (_error) {
|
|
234
|
-
return `Target: ${JSON.stringify(this.controls)}`
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private updateDebugInfo() {
|
|
239
|
-
// Update debug display
|
|
240
|
-
const debugEl = this.query('.debug-info')
|
|
241
|
-
if (debugEl) {
|
|
242
|
-
debugEl.innerHTML = `
|
|
243
|
-
📍 Camera Position:
|
|
244
|
-
${this.getCameraDebugInfo()}
|
|
245
|
-
|
|
246
|
-
🎯 Camera Target:
|
|
247
|
-
${this.getTargetDebugInfo()}
|
|
248
|
-
|
|
249
|
-
🎮 Controls:
|
|
250
|
-
• Rotate: Left-drag
|
|
251
|
-
• Zoom: Scroll wheel
|
|
252
|
-
• Pan: Right-drag or Shift+Left-drag
|
|
253
|
-
|
|
254
|
-
📊 Status: ${this.scene ? 'Splat loaded' : 'Loading...'}
|
|
255
|
-
`
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
private async initializeViewer() {
|
|
260
|
-
// Create a unique ID for this instance
|
|
261
|
-
const canvasId = `gaussian-canvas-${Date.now()}`
|
|
262
|
-
|
|
263
|
-
// Get the viewer container dimensions
|
|
264
|
-
const viewerEl = this.query('.viewer') as HTMLElement
|
|
265
|
-
if (!viewerEl) return
|
|
266
|
-
|
|
267
|
-
const rect = viewerEl.getBoundingClientRect()
|
|
268
|
-
|
|
269
|
-
// Check if canvas already exists in normal DOM
|
|
270
|
-
let normalCanvas = document.getElementById(canvasId) as HTMLCanvasElement
|
|
271
|
-
if (!normalCanvas) {
|
|
272
|
-
normalCanvas = document.createElement('canvas')
|
|
273
|
-
normalCanvas.id = canvasId
|
|
274
|
-
normalCanvas.style.position = 'fixed'
|
|
275
|
-
normalCanvas.style.top = `${rect.top}px`
|
|
276
|
-
normalCanvas.style.left = `${rect.left}px`
|
|
277
|
-
normalCanvas.style.width = `${rect.width}px`
|
|
278
|
-
normalCanvas.style.height = `${rect.height}px`
|
|
279
|
-
normalCanvas.style.zIndex = '1001' // Above backdrop but below buttons
|
|
280
|
-
normalCanvas.style.pointerEvents = 'auto' // Keep mouse events for 3D controls
|
|
281
|
-
normalCanvas.style.display = 'block'
|
|
282
|
-
normalCanvas.style.background = 'transparent'
|
|
283
|
-
document.body.appendChild(normalCanvas)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Update canvas position in case viewer moved
|
|
287
|
-
normalCanvas.style.top = `${rect.top}px`
|
|
288
|
-
normalCanvas.style.left = `${rect.left}px`
|
|
289
|
-
normalCanvas.style.width = `${rect.width}px`
|
|
290
|
-
normalCanvas.style.height = `${rect.height}px`
|
|
291
|
-
|
|
292
|
-
this.canvas = normalCanvas
|
|
293
|
-
|
|
294
|
-
try {
|
|
295
|
-
const container = this.query('.gaussian-container') as HTMLDivElement
|
|
296
|
-
if (!container) return
|
|
297
|
-
|
|
298
|
-
// Import gsplat using the proven API approach
|
|
299
|
-
const SPLAT = await import('gsplat')
|
|
300
|
-
|
|
301
|
-
// Setup GSplat components exactly like the React example
|
|
302
|
-
this.scene = new SPLAT.Scene()
|
|
303
|
-
this.camera = new SPLAT.Camera()
|
|
304
|
-
this.renderer = new SPLAT.WebGLRenderer(this.canvas)
|
|
305
|
-
this.controls = new SPLAT.OrbitControls(this.camera, this.canvas)
|
|
306
|
-
|
|
307
|
-
// Load the splat file
|
|
308
|
-
await SPLAT.Loader.LoadAsync(this.splatUrl, this.scene)
|
|
309
|
-
|
|
310
|
-
// Start render loop
|
|
311
|
-
let frameCount = 0
|
|
312
|
-
const animate = () => {
|
|
313
|
-
// Check if renderer still exists before continuing
|
|
314
|
-
if (!this.renderer || !this.scene || !this.camera) {
|
|
315
|
-
return
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
this.animationId = requestAnimationFrame(animate)
|
|
319
|
-
|
|
320
|
-
if (this.controls) {
|
|
321
|
-
this.controls.update()
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
try {
|
|
325
|
-
this.renderer.render(this.scene, this.camera)
|
|
326
|
-
} catch (_e) {
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Log first frame only
|
|
330
|
-
if (frameCount === 0 && this.canvas) {
|
|
331
|
-
frameCount++
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (this.debugMode) {
|
|
335
|
-
this.updateDebugInfo()
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
animate()
|
|
339
|
-
|
|
340
|
-
// Setup resize handler
|
|
341
|
-
const resizeObserver = new ResizeObserver(() => {
|
|
342
|
-
this.handleResize()
|
|
343
|
-
})
|
|
344
|
-
resizeObserver.observe(container)
|
|
345
|
-
|
|
346
|
-
} catch (error) {
|
|
347
|
-
throw error
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
private handleResize() {
|
|
352
|
-
const container = this.query('.gaussian-container') as HTMLDivElement
|
|
353
|
-
if (!container || !this.renderer || !this.camera) return
|
|
354
|
-
|
|
355
|
-
const width = container.clientWidth
|
|
356
|
-
const height = container.clientHeight
|
|
357
|
-
|
|
358
|
-
// Update renderer size
|
|
359
|
-
if (typeof this.renderer.setSize === 'function') {
|
|
360
|
-
this.renderer.setSize(width, height)
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Update camera aspect ratio
|
|
364
|
-
if (typeof this.camera.aspect !== 'undefined') {
|
|
365
|
-
this.camera.aspect = width / height
|
|
366
|
-
if (typeof this.camera.updateProjectionMatrix === 'function') {
|
|
367
|
-
this.camera.updateProjectionMatrix()
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (!customElements.get('cc-viewer-gaussian')) {
|
|
374
|
-
customElements.define('cc-viewer-gaussian', CcViewerGaussian)
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
declare global {
|
|
378
|
-
interface HTMLElementTagNameMap {
|
|
379
|
-
'cc-viewer-gaussian': CcViewerGaussian
|
|
380
|
-
}
|
|
1
|
+
import { CcViewerBase } from './cc-viewer-base'
|
|
2
|
+
|
|
3
|
+
export class CcViewerGaussian extends CcViewerBase {
|
|
4
|
+
private splatUrl = ''
|
|
5
|
+
private debugMode = false
|
|
6
|
+
private cameraPosition = '3,3,3'
|
|
7
|
+
private _cameraTarget = '0,0,0' // TODO: Implement camera target functionality
|
|
8
|
+
|
|
9
|
+
// gsplat.js library types are not available, using any for external library objects
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
private scene?: any
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
private camera?: any
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
private renderer?: any
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
private controls?: any
|
|
18
|
+
private animationId?: number
|
|
19
|
+
private canvas?: HTMLCanvasElement
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
private swiper?: any
|
|
22
|
+
|
|
23
|
+
static get observedAttributes() {
|
|
24
|
+
return ['show', 'debug-mode', 'camera-position', 'camera-target']
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
|
28
|
+
if (name === 'show') {
|
|
29
|
+
this.isShow = newValue === 'true'
|
|
30
|
+
} else if (name === 'debug-mode') {
|
|
31
|
+
this.debugMode = newValue === 'true'
|
|
32
|
+
} else if (name === 'camera-position') {
|
|
33
|
+
this.cameraPosition = newValue || '3,3,3'
|
|
34
|
+
} else if (name === 'camera-target') {
|
|
35
|
+
this._cameraTarget = newValue || '0,0,0'
|
|
36
|
+
}
|
|
37
|
+
super.attributeChangedCallback(name, oldValue, newValue)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Implementation of abstract methods from base class
|
|
41
|
+
protected async doOpen(url: string): Promise<void> {
|
|
42
|
+
this.splatUrl = url
|
|
43
|
+
await this.initializeViewer()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected doClose(): void {
|
|
47
|
+
this.cleanup()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected getViewerContent(): string {
|
|
51
|
+
// This won't be used since we override render, but required by abstract class
|
|
52
|
+
return ''
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Override to use custom rendering due to special canvas handling
|
|
56
|
+
protected shouldUseCustomRender(): boolean {
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Custom render implementation
|
|
61
|
+
protected customRender(): void {
|
|
62
|
+
const styles = this.css`
|
|
63
|
+
:host {
|
|
64
|
+
--cc-viewer-z-index-each: 1000;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.backdrop {
|
|
68
|
+
justify-content: center;
|
|
69
|
+
align-items: center;
|
|
70
|
+
position: fixed;
|
|
71
|
+
left: 0;
|
|
72
|
+
right: 0;
|
|
73
|
+
top: 0;
|
|
74
|
+
bottom: 0;
|
|
75
|
+
width: 100%;
|
|
76
|
+
height: 100%;
|
|
77
|
+
background-color: rgba(0, 0, 0, 0.9);
|
|
78
|
+
z-index: 1000;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.viewer {
|
|
82
|
+
position: absolute;
|
|
83
|
+
width: 90%;
|
|
84
|
+
height: 85%;
|
|
85
|
+
inset: 0px;
|
|
86
|
+
margin: auto;
|
|
87
|
+
align-self: center;
|
|
88
|
+
background-color: #000;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.gaussian-container {
|
|
92
|
+
width: 100%;
|
|
93
|
+
height: 100%;
|
|
94
|
+
position: relative;
|
|
95
|
+
background: #000;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.gaussian-container canvas {
|
|
99
|
+
width: 100% !important;
|
|
100
|
+
height: 100% !important;
|
|
101
|
+
display: block;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.loading {
|
|
105
|
+
position: absolute;
|
|
106
|
+
top: 50%;
|
|
107
|
+
left: 50%;
|
|
108
|
+
transform: translate(-50%, -50%);
|
|
109
|
+
color: #fff;
|
|
110
|
+
font-size: 1.2rem;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.error {
|
|
114
|
+
position: absolute;
|
|
115
|
+
top: 50%;
|
|
116
|
+
left: 50%;
|
|
117
|
+
transform: translate(-50%, -50%);
|
|
118
|
+
color: #e74c3c;
|
|
119
|
+
text-align: center;
|
|
120
|
+
padding: 20px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
canvas {
|
|
124
|
+
display: block;
|
|
125
|
+
width: 100%;
|
|
126
|
+
height: 100%;
|
|
127
|
+
touch-action: none;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.debug-info {
|
|
131
|
+
position: absolute;
|
|
132
|
+
top: 10px;
|
|
133
|
+
left: 10px;
|
|
134
|
+
background: rgba(0, 0, 0, 0.85);
|
|
135
|
+
color: #00ff00;
|
|
136
|
+
padding: 12px;
|
|
137
|
+
font-family: 'Courier New', monospace;
|
|
138
|
+
font-size: 11px;
|
|
139
|
+
line-height: 1.4;
|
|
140
|
+
border-radius: 6px;
|
|
141
|
+
border: 1px solid rgba(0, 255, 0, 0.3);
|
|
142
|
+
pointer-events: none;
|
|
143
|
+
white-space: pre-line;
|
|
144
|
+
min-width: 200px;
|
|
145
|
+
z-index: 1003;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
${this.getNavigationStyles()}
|
|
149
|
+
`
|
|
150
|
+
|
|
151
|
+
const gaussianContent = `
|
|
152
|
+
<canvas style="display: none;"></canvas>
|
|
153
|
+
${this.isLoading ? '<div class="loading">Loading...</div>' : ''}
|
|
154
|
+
${!this.isLoading && this.debugMode ? `
|
|
155
|
+
<div class="debug-info">
|
|
156
|
+
📍 Camera Position:
|
|
157
|
+
${this.getCameraDebugInfo()}
|
|
158
|
+
|
|
159
|
+
🎯 Camera Target:
|
|
160
|
+
${this.getTargetDebugInfo()}
|
|
161
|
+
|
|
162
|
+
🎮 Controls:
|
|
163
|
+
• Rotate: Left-drag
|
|
164
|
+
• Zoom: Scroll wheel
|
|
165
|
+
• Pan: Right-drag or Shift+Left-drag
|
|
166
|
+
|
|
167
|
+
📊 Status: ${this.scene ? 'Splat loaded' : 'Loading...'}
|
|
168
|
+
</div>
|
|
169
|
+
` : ''}
|
|
170
|
+
`
|
|
171
|
+
|
|
172
|
+
const html = `
|
|
173
|
+
${styles}
|
|
174
|
+
<div class="backdrop" style="${this.isShow ? 'visibility: visible' : 'visibility: hidden'}">
|
|
175
|
+
<div class="viewer">
|
|
176
|
+
<div class="gaussian-container">
|
|
177
|
+
${gaussianContent}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
${this.getNavigationButtons()}
|
|
181
|
+
</div>
|
|
182
|
+
`
|
|
183
|
+
|
|
184
|
+
this.updateShadowRoot(html)
|
|
185
|
+
|
|
186
|
+
// Add navigation listeners after render
|
|
187
|
+
setTimeout(() => {
|
|
188
|
+
this.addNavigationListeners()
|
|
189
|
+
}, 0)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private cleanup() {
|
|
193
|
+
if (this.animationId) {
|
|
194
|
+
cancelAnimationFrame(this.animationId)
|
|
195
|
+
this.animationId = undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (this.renderer && typeof this.renderer.dispose === 'function') {
|
|
199
|
+
this.renderer.dispose()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Remove all gaussian canvases from document.body
|
|
203
|
+
const existingCanvases = document.querySelectorAll('canvas[id^="gaussian-canvas-"]')
|
|
204
|
+
existingCanvases.forEach(canvas => {
|
|
205
|
+
if (canvas.parentNode === document.body) {
|
|
206
|
+
document.body.removeChild(canvas)
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
this.scene = undefined
|
|
211
|
+
this.camera = undefined
|
|
212
|
+
this.renderer = undefined
|
|
213
|
+
this.controls = undefined
|
|
214
|
+
this.canvas = undefined
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private getCameraDebugInfo(): string {
|
|
218
|
+
if (!this.camera || !this.camera.position) return 'Position: unavailable'
|
|
219
|
+
const pos = this.camera.position
|
|
220
|
+
try {
|
|
221
|
+
return `X: ${pos.x.toFixed(3)}, Y: ${pos.y.toFixed(3)}, Z: ${pos.z.toFixed(3)}`
|
|
222
|
+
} catch (_error) {
|
|
223
|
+
return `Position: ${JSON.stringify(pos)}`
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private getTargetDebugInfo(): string {
|
|
228
|
+
if (!this.controls) return 'Target: controls unavailable'
|
|
229
|
+
// gsplat.js OrbitControls might not have a target property
|
|
230
|
+
// Return available control info instead
|
|
231
|
+
try {
|
|
232
|
+
return `Controls active (no target property in gsplat.js)`
|
|
233
|
+
} catch (_error) {
|
|
234
|
+
return `Target: ${JSON.stringify(this.controls)}`
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private updateDebugInfo() {
|
|
239
|
+
// Update debug display
|
|
240
|
+
const debugEl = this.query('.debug-info')
|
|
241
|
+
if (debugEl) {
|
|
242
|
+
debugEl.innerHTML = `
|
|
243
|
+
📍 Camera Position:
|
|
244
|
+
${this.getCameraDebugInfo()}
|
|
245
|
+
|
|
246
|
+
🎯 Camera Target:
|
|
247
|
+
${this.getTargetDebugInfo()}
|
|
248
|
+
|
|
249
|
+
🎮 Controls:
|
|
250
|
+
• Rotate: Left-drag
|
|
251
|
+
• Zoom: Scroll wheel
|
|
252
|
+
• Pan: Right-drag or Shift+Left-drag
|
|
253
|
+
|
|
254
|
+
📊 Status: ${this.scene ? 'Splat loaded' : 'Loading...'}
|
|
255
|
+
`
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private async initializeViewer() {
|
|
260
|
+
// Create a unique ID for this instance
|
|
261
|
+
const canvasId = `gaussian-canvas-${Date.now()}`
|
|
262
|
+
|
|
263
|
+
// Get the viewer container dimensions
|
|
264
|
+
const viewerEl = this.query('.viewer') as HTMLElement
|
|
265
|
+
if (!viewerEl) return
|
|
266
|
+
|
|
267
|
+
const rect = viewerEl.getBoundingClientRect()
|
|
268
|
+
|
|
269
|
+
// Check if canvas already exists in normal DOM
|
|
270
|
+
let normalCanvas = document.getElementById(canvasId) as HTMLCanvasElement
|
|
271
|
+
if (!normalCanvas) {
|
|
272
|
+
normalCanvas = document.createElement('canvas')
|
|
273
|
+
normalCanvas.id = canvasId
|
|
274
|
+
normalCanvas.style.position = 'fixed'
|
|
275
|
+
normalCanvas.style.top = `${rect.top}px`
|
|
276
|
+
normalCanvas.style.left = `${rect.left}px`
|
|
277
|
+
normalCanvas.style.width = `${rect.width}px`
|
|
278
|
+
normalCanvas.style.height = `${rect.height}px`
|
|
279
|
+
normalCanvas.style.zIndex = '1001' // Above backdrop but below buttons
|
|
280
|
+
normalCanvas.style.pointerEvents = 'auto' // Keep mouse events for 3D controls
|
|
281
|
+
normalCanvas.style.display = 'block'
|
|
282
|
+
normalCanvas.style.background = 'transparent'
|
|
283
|
+
document.body.appendChild(normalCanvas)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Update canvas position in case viewer moved
|
|
287
|
+
normalCanvas.style.top = `${rect.top}px`
|
|
288
|
+
normalCanvas.style.left = `${rect.left}px`
|
|
289
|
+
normalCanvas.style.width = `${rect.width}px`
|
|
290
|
+
normalCanvas.style.height = `${rect.height}px`
|
|
291
|
+
|
|
292
|
+
this.canvas = normalCanvas
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const container = this.query('.gaussian-container') as HTMLDivElement
|
|
296
|
+
if (!container) return
|
|
297
|
+
|
|
298
|
+
// Import gsplat using the proven API approach
|
|
299
|
+
const SPLAT = await import('gsplat')
|
|
300
|
+
|
|
301
|
+
// Setup GSplat components exactly like the React example
|
|
302
|
+
this.scene = new SPLAT.Scene()
|
|
303
|
+
this.camera = new SPLAT.Camera()
|
|
304
|
+
this.renderer = new SPLAT.WebGLRenderer(this.canvas)
|
|
305
|
+
this.controls = new SPLAT.OrbitControls(this.camera, this.canvas)
|
|
306
|
+
|
|
307
|
+
// Load the splat file
|
|
308
|
+
await SPLAT.Loader.LoadAsync(this.splatUrl, this.scene)
|
|
309
|
+
|
|
310
|
+
// Start render loop
|
|
311
|
+
let frameCount = 0
|
|
312
|
+
const animate = () => {
|
|
313
|
+
// Check if renderer still exists before continuing
|
|
314
|
+
if (!this.renderer || !this.scene || !this.camera) {
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
this.animationId = requestAnimationFrame(animate)
|
|
319
|
+
|
|
320
|
+
if (this.controls) {
|
|
321
|
+
this.controls.update()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
this.renderer.render(this.scene, this.camera)
|
|
326
|
+
} catch (_e) {
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Log first frame only
|
|
330
|
+
if (frameCount === 0 && this.canvas) {
|
|
331
|
+
frameCount++
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (this.debugMode) {
|
|
335
|
+
this.updateDebugInfo()
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
animate()
|
|
339
|
+
|
|
340
|
+
// Setup resize handler
|
|
341
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
342
|
+
this.handleResize()
|
|
343
|
+
})
|
|
344
|
+
resizeObserver.observe(container)
|
|
345
|
+
|
|
346
|
+
} catch (error) {
|
|
347
|
+
throw error
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private handleResize() {
|
|
352
|
+
const container = this.query('.gaussian-container') as HTMLDivElement
|
|
353
|
+
if (!container || !this.renderer || !this.camera) return
|
|
354
|
+
|
|
355
|
+
const width = container.clientWidth
|
|
356
|
+
const height = container.clientHeight
|
|
357
|
+
|
|
358
|
+
// Update renderer size
|
|
359
|
+
if (typeof this.renderer.setSize === 'function') {
|
|
360
|
+
this.renderer.setSize(width, height)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Update camera aspect ratio
|
|
364
|
+
if (typeof this.camera.aspect !== 'undefined') {
|
|
365
|
+
this.camera.aspect = width / height
|
|
366
|
+
if (typeof this.camera.updateProjectionMatrix === 'function') {
|
|
367
|
+
this.camera.updateProjectionMatrix()
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!customElements.get('cc-viewer-gaussian')) {
|
|
374
|
+
customElements.define('cc-viewer-gaussian', CcViewerGaussian)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
declare global {
|
|
378
|
+
interface HTMLElementTagNameMap {
|
|
379
|
+
'cc-viewer-gaussian': CcViewerGaussian
|
|
380
|
+
}
|
|
381
381
|
}
|