@c4h/chuci 0.1.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 +33 -0
- package/LICENSE +21 -0
- package/README.ja.md +144 -0
- package/README.md +225 -0
- package/dist/assets/azumaya_panorama1.png +0 -0
- package/dist/chuci.cjs +4483 -0
- package/dist/chuci.js +21710 -0
- package/dist/chuci.umd.js +4737 -0
- package/dist/index-8VMexD2a.cjs +255 -0
- package/dist/index-kvsisbKS.js +2125 -0
- package/dist/index.d.ts +203 -0
- package/dist/index.html +241 -0
- package/dist/test-image.html +63 -0
- package/package.json +86 -0
- package/src/components/swiper/cc-swiper-slide.ts +50 -0
- package/src/components/swiper/cc-swiper-styles.ts +29 -0
- package/src/components/swiper/cc-swiper.ts +362 -0
- package/src/components/swiper/swiper-styles.css +5 -0
- package/src/components/viewer/cc-viewer-3dmodel.ts +492 -0
- package/src/components/viewer/cc-viewer-base.ts +279 -0
- package/src/components/viewer/cc-viewer-gaussian.ts +381 -0
- package/src/components/viewer/cc-viewer-image.ts +190 -0
- package/src/components/viewer/cc-viewer-panorama.ts +86 -0
- package/src/components/viewer/cc-viewer-styles.ts +56 -0
- package/src/components/viewer/cc-viewer-video.ts +110 -0
- package/src/components/viewer/cc-viewer-youtube.ts +76 -0
- package/src/components/viewer/cc-viewer.ts +291 -0
- package/src/index.ts +25 -0
- package/src/types/css-modules.d.ts +1 -0
- package/src/types/global.d.ts +11 -0
- package/src/utils/base-element.ts +77 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { ChuciElement } from '@/utils/base-element'
|
|
2
|
+
|
|
3
|
+
export abstract class CcViewerBase extends ChuciElement {
|
|
4
|
+
private _showPrevButton = true
|
|
5
|
+
private _showNextButton = true
|
|
6
|
+
protected isShow = false
|
|
7
|
+
protected isLoading = false
|
|
8
|
+
|
|
9
|
+
get showPrevButton() {
|
|
10
|
+
return this._showPrevButton
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
set showPrevButton(value: boolean) {
|
|
14
|
+
this._showPrevButton = value
|
|
15
|
+
this.updateNavigationVisibility()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get showNextButton() {
|
|
19
|
+
return this._showNextButton
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
set showNextButton(value: boolean) {
|
|
23
|
+
this._showNextButton = value
|
|
24
|
+
this.updateNavigationVisibility()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Template method pattern - subclasses implement these
|
|
28
|
+
protected abstract doOpen(url: string): void | Promise<void>
|
|
29
|
+
protected abstract doClose(): void
|
|
30
|
+
protected abstract getViewerContent(): string
|
|
31
|
+
|
|
32
|
+
// Common lifecycle methods
|
|
33
|
+
open(url: string): void {
|
|
34
|
+
this.isShow = true
|
|
35
|
+
this.isLoading = true
|
|
36
|
+
|
|
37
|
+
// Call subclass implementation first
|
|
38
|
+
const openPromise = Promise.resolve(this.doOpen(url))
|
|
39
|
+
|
|
40
|
+
// Use microtask to ensure doOpen runs first (even if synchronous)
|
|
41
|
+
Promise.resolve().then(() => {
|
|
42
|
+
// Initial render for loading state
|
|
43
|
+
this.render()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Update after loading completes
|
|
47
|
+
openPromise.then(() => {
|
|
48
|
+
this.isLoading = false
|
|
49
|
+
this.render()
|
|
50
|
+
}).catch(_error => {
|
|
51
|
+
this.isLoading = false
|
|
52
|
+
this.render()
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
close(): void {
|
|
57
|
+
this.cleanupNavigationListeners()
|
|
58
|
+
this.doClose()
|
|
59
|
+
this.isShow = false
|
|
60
|
+
this.isLoading = false
|
|
61
|
+
this.render()
|
|
62
|
+
this.dispatch('close')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
protected cleanupNavigationListeners() {
|
|
66
|
+
// Remove data-listener-attached attributes so listeners can be re-added
|
|
67
|
+
const prevBtn = this.query('.nav-prev')
|
|
68
|
+
const nextBtn = this.query('.nav-next')
|
|
69
|
+
const closeBtn = this.query('.nav-close')
|
|
70
|
+
|
|
71
|
+
if (prevBtn) prevBtn.removeAttribute('data-listener-attached')
|
|
72
|
+
if (nextBtn) nextBtn.removeAttribute('data-listener-attached')
|
|
73
|
+
if (closeBtn) closeBtn.removeAttribute('data-listener-attached')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Common render method
|
|
77
|
+
protected render() {
|
|
78
|
+
// Allow subclasses to opt out of common rendering
|
|
79
|
+
if (this.shouldUseCustomRender()) {
|
|
80
|
+
this.customRender()
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const styles = this.css`
|
|
85
|
+
:host {
|
|
86
|
+
--cc-viewer-z-index-each: 1000;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.backdrop {
|
|
90
|
+
justify-content: center;
|
|
91
|
+
align-items: center;
|
|
92
|
+
position: fixed;
|
|
93
|
+
left: 0;
|
|
94
|
+
right: 0;
|
|
95
|
+
top: 0;
|
|
96
|
+
bottom: 0;
|
|
97
|
+
width: 100%;
|
|
98
|
+
height: 100%;
|
|
99
|
+
background-color: rgba(0, 0, 0, 0.9);
|
|
100
|
+
z-index: var(--cc-viewer-z-index-each);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.viewer {
|
|
104
|
+
position: absolute;
|
|
105
|
+
width: 90%;
|
|
106
|
+
height: 85%;
|
|
107
|
+
inset: 0px;
|
|
108
|
+
margin: auto;
|
|
109
|
+
align-self: center;
|
|
110
|
+
background-color: #000;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
${this.getNavigationStyles()}
|
|
114
|
+
${this.getCustomStyles()}
|
|
115
|
+
`
|
|
116
|
+
|
|
117
|
+
const html = `
|
|
118
|
+
${styles}
|
|
119
|
+
<div class="backdrop" style="${this.isShow ? 'visibility: visible' : 'visibility: hidden'}">
|
|
120
|
+
${this.getNavigationButtons()}
|
|
121
|
+
<div class="viewer">
|
|
122
|
+
${this.getViewerContent()}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
`
|
|
126
|
+
|
|
127
|
+
this.updateShadowRoot(html)
|
|
128
|
+
|
|
129
|
+
// Add listeners after render
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
this.addNavigationListeners()
|
|
132
|
+
this.onAfterRender()
|
|
133
|
+
}, 0)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Hook for viewers that need completely custom rendering (e.g., image viewer)
|
|
137
|
+
protected shouldUseCustomRender(): boolean {
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
protected customRender(): void {
|
|
142
|
+
// Override in subclasses that need custom rendering
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Hook methods for subclasses
|
|
146
|
+
protected getCustomStyles(): string {
|
|
147
|
+
return ''
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
protected onAfterRender(): void {
|
|
151
|
+
// Subclasses can override for custom listeners
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
protected navigatePrev() {
|
|
155
|
+
this.dispatch('navigate-prev')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
protected navigateNext() {
|
|
159
|
+
this.dispatch('navigate-next')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
protected getNavigationButtons(): string {
|
|
163
|
+
const prevStyle = this.showPrevButton ? '' : 'display: none;'
|
|
164
|
+
const nextStyle = this.showNextButton ? '' : 'display: none;'
|
|
165
|
+
|
|
166
|
+
return `
|
|
167
|
+
<button class="nav-button nav-prev" style="${prevStyle}" aria-label="Previous">
|
|
168
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
169
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
170
|
+
</svg>
|
|
171
|
+
</button>
|
|
172
|
+
<button class="nav-button nav-next" style="${nextStyle}" aria-label="Next">
|
|
173
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
174
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
|
175
|
+
</svg>
|
|
176
|
+
</button>
|
|
177
|
+
<button class="nav-button nav-close" aria-label="Close">
|
|
178
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
179
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
180
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
181
|
+
</svg>
|
|
182
|
+
</button>
|
|
183
|
+
`
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
protected getNavigationStyles(): string {
|
|
187
|
+
return `
|
|
188
|
+
.nav-button {
|
|
189
|
+
position: absolute;
|
|
190
|
+
top: 50%;
|
|
191
|
+
transform: translateY(-50%);
|
|
192
|
+
background: rgba(0, 0, 0, 0.5);
|
|
193
|
+
color: white;
|
|
194
|
+
border: none;
|
|
195
|
+
border-radius: 4px;
|
|
196
|
+
width: 48px;
|
|
197
|
+
height: 48px;
|
|
198
|
+
display: flex;
|
|
199
|
+
align-items: center;
|
|
200
|
+
justify-content: center;
|
|
201
|
+
cursor: pointer;
|
|
202
|
+
transition: background 0.3s;
|
|
203
|
+
z-index: 1002;
|
|
204
|
+
pointer-events: auto;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.nav-button:hover {
|
|
208
|
+
background: rgba(0, 0, 0, 0.7);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.nav-prev {
|
|
212
|
+
left: 20px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.nav-next {
|
|
216
|
+
right: 20px;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.nav-close {
|
|
220
|
+
top: 20px;
|
|
221
|
+
right: 20px;
|
|
222
|
+
transform: none;
|
|
223
|
+
}
|
|
224
|
+
`
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
protected addNavigationListeners() {
|
|
228
|
+
|
|
229
|
+
// Check if this viewer is actually visible
|
|
230
|
+
const backdrop = this.query('.backdrop') as HTMLElement
|
|
231
|
+
if (backdrop && backdrop.style.visibility === 'hidden') {
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
const prevBtn = this.query('.nav-prev')
|
|
237
|
+
const nextBtn = this.query('.nav-next')
|
|
238
|
+
const closeBtn = this.query('.nav-close')
|
|
239
|
+
|
|
240
|
+
if (prevBtn && !prevBtn.hasAttribute('data-listener-attached')) {
|
|
241
|
+
prevBtn.setAttribute('data-listener-attached', 'true')
|
|
242
|
+
prevBtn.addEventListener('click', (e) => {
|
|
243
|
+
e.stopPropagation()
|
|
244
|
+
e.preventDefault()
|
|
245
|
+
this.navigatePrev()
|
|
246
|
+
}, true)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (nextBtn && !nextBtn.hasAttribute('data-listener-attached')) {
|
|
250
|
+
nextBtn.setAttribute('data-listener-attached', 'true')
|
|
251
|
+
nextBtn.addEventListener('click', (e) => {
|
|
252
|
+
e.stopPropagation()
|
|
253
|
+
this.navigateNext()
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (closeBtn && !closeBtn.hasAttribute('data-listener-attached')) {
|
|
258
|
+
closeBtn.setAttribute('data-listener-attached', 'true')
|
|
259
|
+
closeBtn.addEventListener('click', (e) => {
|
|
260
|
+
e.stopPropagation()
|
|
261
|
+
this.close()
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
}, 0)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
protected updateNavigationVisibility() {
|
|
268
|
+
const prevBtn = this.query('.nav-prev')
|
|
269
|
+
const nextBtn = this.query('.nav-next')
|
|
270
|
+
|
|
271
|
+
if (prevBtn) {
|
|
272
|
+
(prevBtn as HTMLElement).style.display = this._showPrevButton ? '' : 'none'
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (nextBtn) {
|
|
276
|
+
(nextBtn as HTMLElement).style.display = this._showNextButton ? '' : 'none'
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
381
|
+
}
|