@c4h/chuci 0.1.0 → 0.2.1
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 +9068 -6596
- package/dist/chuci.umd.js +165 -166
- 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,492 +1,492 @@
|
|
|
1
|
-
import { CcViewerBase } from './cc-viewer-base'
|
|
2
|
-
import * as THREE from 'three'
|
|
3
|
-
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'
|
|
4
|
-
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js'
|
|
5
|
-
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
|
6
|
-
|
|
7
|
-
export class CcViewer3DModel extends CcViewerBase {
|
|
8
|
-
private modelUrl = ''
|
|
9
|
-
private materialUrl = ''
|
|
10
|
-
private debugMode = false
|
|
11
|
-
private cameraPosition = '3,3,3'
|
|
12
|
-
private cameraTarget = '0,0,0'
|
|
13
|
-
private showTexture = true
|
|
14
|
-
|
|
15
|
-
private scene?: THREE.Scene
|
|
16
|
-
private camera?: THREE.PerspectiveCamera
|
|
17
|
-
private renderer?: THREE.WebGLRenderer
|
|
18
|
-
private controls?: OrbitControls
|
|
19
|
-
private animationId?: number
|
|
20
|
-
private container?: HTMLDivElement
|
|
21
|
-
private currentModel?: THREE.Group
|
|
22
|
-
private originalMaterials: Map<THREE.Mesh, THREE.Material | THREE.Material[]> = new Map()
|
|
23
|
-
private resizeObserver?: ResizeObserver
|
|
24
|
-
private externalCanvas?: HTMLCanvasElement
|
|
25
|
-
|
|
26
|
-
static get observedAttributes() {
|
|
27
|
-
return ['show', 'debug-mode', 'camera-position', 'camera-target', 'show-texture', 'material-url']
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
|
31
|
-
|
|
32
|
-
if (name === 'show') {
|
|
33
|
-
this.isShow = newValue === 'true'
|
|
34
|
-
} else if (name === 'debug-mode') {
|
|
35
|
-
this.debugMode = newValue === 'true' || newValue === ''
|
|
36
|
-
} else if (name === 'camera-position') {
|
|
37
|
-
this.cameraPosition = newValue || '3,3,3'
|
|
38
|
-
} else if (name === 'camera-target') {
|
|
39
|
-
this.cameraTarget = newValue || '0,0,0'
|
|
40
|
-
} else if (name === 'show-texture') {
|
|
41
|
-
this.showTexture = newValue !== 'false'
|
|
42
|
-
} else if (name === 'material-url') {
|
|
43
|
-
this.materialUrl = newValue || ''
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Re-render if viewer is already open and debug mode changes
|
|
47
|
-
if (name === 'debug-mode' && this.isShow) {
|
|
48
|
-
this.render()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
super.attributeChangedCallback(name, oldValue, newValue)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Implementation of abstract methods from base class
|
|
55
|
-
protected async doOpen(url: string): Promise<void> {
|
|
56
|
-
|
|
57
|
-
this.modelUrl = url
|
|
58
|
-
// materialUrl is set via attribute
|
|
59
|
-
this.showTexture = true
|
|
60
|
-
|
|
61
|
-
await this.initializeViewer()
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
protected doClose(): void {
|
|
65
|
-
this.cleanup()
|
|
66
|
-
this.modelUrl = ''
|
|
67
|
-
this.materialUrl = ''
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
protected getViewerContent(): string {
|
|
71
|
-
|
|
72
|
-
const modelContent = !this.modelUrl ?
|
|
73
|
-
'<div class="error">No model URL provided</div>' :
|
|
74
|
-
`
|
|
75
|
-
${this.isLoading ? '<div class="loading">Loading...</div>' : ''}
|
|
76
|
-
${!this.isLoading && this.debugMode ? `
|
|
77
|
-
<div class="debug-info" style="z-index: 1005;">
|
|
78
|
-
Camera Position: ${this.getCameraDebugInfo()}<br>
|
|
79
|
-
Camera Target: ${this.getTargetDebugInfo()}<br>
|
|
80
|
-
Controls: Rotate (drag), Zoom (scroll), Pan (right-drag)
|
|
81
|
-
</div>
|
|
82
|
-
` : ''}
|
|
83
|
-
${!this.isLoading ? `
|
|
84
|
-
<button class="texture-toggle">
|
|
85
|
-
Texture: ${this.showTexture ? 'ON' : 'OFF'}
|
|
86
|
-
</button>
|
|
87
|
-
` : ''}
|
|
88
|
-
`
|
|
89
|
-
|
|
90
|
-
return `
|
|
91
|
-
<div class="model-container">
|
|
92
|
-
${modelContent}
|
|
93
|
-
</div>
|
|
94
|
-
`
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Custom styles for 3D viewer
|
|
98
|
-
protected getCustomStyles(): string {
|
|
99
|
-
return `
|
|
100
|
-
.model-container {
|
|
101
|
-
width: 100%;
|
|
102
|
-
height: 100%;
|
|
103
|
-
position: relative;
|
|
104
|
-
background: transparent;
|
|
105
|
-
z-index: 1;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
.model-container canvas {
|
|
109
|
-
position: absolute;
|
|
110
|
-
top: 0;
|
|
111
|
-
left: 0;
|
|
112
|
-
width: 100%;
|
|
113
|
-
height: 100%;
|
|
114
|
-
z-index: 1;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.loading {
|
|
118
|
-
position: absolute;
|
|
119
|
-
top: 50%;
|
|
120
|
-
left: 50%;
|
|
121
|
-
transform: translate(-50%, -50%);
|
|
122
|
-
color: #666;
|
|
123
|
-
font-size: 1.2rem;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
.error {
|
|
127
|
-
position: absolute;
|
|
128
|
-
top: 50%;
|
|
129
|
-
left: 50%;
|
|
130
|
-
transform: translate(-50%, -50%);
|
|
131
|
-
color: #e74c3c;
|
|
132
|
-
text-align: center;
|
|
133
|
-
padding: 20px;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
canvas {
|
|
137
|
-
display: block;
|
|
138
|
-
width: 100%;
|
|
139
|
-
height: 100%;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
.debug-info {
|
|
143
|
-
position: absolute;
|
|
144
|
-
top: 10px;
|
|
145
|
-
left: 10px;
|
|
146
|
-
background: rgba(0, 0, 0, 0.8);
|
|
147
|
-
color: white;
|
|
148
|
-
padding: 10px;
|
|
149
|
-
font-family: monospace;
|
|
150
|
-
font-size: 12px;
|
|
151
|
-
border-radius: 4px;
|
|
152
|
-
pointer-events: none;
|
|
153
|
-
z-index: 1010;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
.texture-toggle {
|
|
157
|
-
position: absolute;
|
|
158
|
-
top: 10px;
|
|
159
|
-
right: 10px;
|
|
160
|
-
background: rgba(0, 0, 0, 0.8);
|
|
161
|
-
color: white;
|
|
162
|
-
border: 1px solid #666;
|
|
163
|
-
padding: 8px 16px;
|
|
164
|
-
cursor: pointer;
|
|
165
|
-
border-radius: 4px;
|
|
166
|
-
transition: background 0.3s;
|
|
167
|
-
z-index: 1010;
|
|
168
|
-
pointer-events: auto;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
.texture-toggle:hover {
|
|
172
|
-
background: rgba(0, 0, 0, 0.9);
|
|
173
|
-
}
|
|
174
|
-
`
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Add texture toggle listener after render
|
|
178
|
-
protected onAfterRender(): void {
|
|
179
|
-
const textureBtn = this.query('.texture-toggle')
|
|
180
|
-
if (textureBtn) {
|
|
181
|
-
textureBtn.addEventListener('click', () => this.toggleTexture())
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private cleanup() {
|
|
186
|
-
|
|
187
|
-
if (this.animationId) {
|
|
188
|
-
cancelAnimationFrame(this.animationId)
|
|
189
|
-
this.animationId = undefined
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (this.renderer) {
|
|
193
|
-
this.renderer.dispose()
|
|
194
|
-
// Remove canvas from document body
|
|
195
|
-
if (this.renderer.domElement.parentNode === document.body) {
|
|
196
|
-
document.body.removeChild(this.renderer.domElement)
|
|
197
|
-
}
|
|
198
|
-
this.renderer = undefined
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (this.controls) {
|
|
202
|
-
this.controls.dispose()
|
|
203
|
-
this.controls = undefined
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Clear Three.js objects
|
|
207
|
-
if (this.scene) {
|
|
208
|
-
this.scene.traverse((child) => {
|
|
209
|
-
if (child instanceof THREE.Mesh) {
|
|
210
|
-
child.geometry?.dispose()
|
|
211
|
-
if (Array.isArray(child.material)) {
|
|
212
|
-
child.material.forEach(m => m.dispose())
|
|
213
|
-
} else {
|
|
214
|
-
child.material?.dispose()
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
})
|
|
218
|
-
this.scene.clear()
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
this.scene = undefined
|
|
222
|
-
this.camera = undefined
|
|
223
|
-
this.currentModel = undefined
|
|
224
|
-
this.originalMaterials.clear()
|
|
225
|
-
|
|
226
|
-
if (this.resizeObserver) {
|
|
227
|
-
this.resizeObserver.disconnect()
|
|
228
|
-
this.resizeObserver = undefined
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
this.container = undefined
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
private storeOriginalMaterials(object: THREE.Group) {
|
|
235
|
-
object.traverse((child) => {
|
|
236
|
-
if (child instanceof THREE.Mesh) {
|
|
237
|
-
this.originalMaterials.set(child, child.material)
|
|
238
|
-
}
|
|
239
|
-
})
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
private toggleTexture() {
|
|
243
|
-
this.showTexture = !this.showTexture
|
|
244
|
-
|
|
245
|
-
if (!this.currentModel) return
|
|
246
|
-
|
|
247
|
-
this.currentModel.traverse((child) => {
|
|
248
|
-
if (child instanceof THREE.Mesh) {
|
|
249
|
-
if (this.showTexture) {
|
|
250
|
-
// Restore original materials
|
|
251
|
-
const original = this.originalMaterials.get(child)
|
|
252
|
-
if (original) {
|
|
253
|
-
child.material = original
|
|
254
|
-
}
|
|
255
|
-
} else {
|
|
256
|
-
// Apply simple color material
|
|
257
|
-
const color = new THREE.Color(0xcccccc)
|
|
258
|
-
child.material = new THREE.MeshPhongMaterial({ color })
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
this.render()
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private getCameraDebugInfo(): string {
|
|
267
|
-
if (!this.camera) return 'N/A'
|
|
268
|
-
const pos = this.camera.position
|
|
269
|
-
return `${pos.x.toFixed(2)}, ${pos.y.toFixed(2)}, ${pos.z.toFixed(2)}`
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
private getTargetDebugInfo(): string {
|
|
273
|
-
if (!this.controls) return 'N/A'
|
|
274
|
-
const target = this.controls.target
|
|
275
|
-
return `${target.x.toFixed(2)}, ${target.y.toFixed(2)}, ${target.z.toFixed(2)}`
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
private updateDebugInfo() {
|
|
279
|
-
// Update debug display
|
|
280
|
-
const debugEl = this.query('.debug-info')
|
|
281
|
-
if (debugEl && this.debugMode) {
|
|
282
|
-
debugEl.innerHTML = `
|
|
283
|
-
Camera Position: ${this.getCameraDebugInfo()}<br>
|
|
284
|
-
Camera Target: ${this.getTargetDebugInfo()}<br>
|
|
285
|
-
Controls: Rotate (drag), Zoom (scroll), Pan (right-drag)
|
|
286
|
-
`
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
private async initializeViewer() {
|
|
291
|
-
|
|
292
|
-
// Wait a moment to ensure DOM is ready
|
|
293
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
294
|
-
|
|
295
|
-
this.container = this.query('.model-container') as HTMLDivElement
|
|
296
|
-
if (!this.container) {
|
|
297
|
-
return
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Get the actual position of the container in the viewport
|
|
301
|
-
const rect = this.container.getBoundingClientRect()
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
// Setup scene
|
|
306
|
-
this.scene = new THREE.Scene()
|
|
307
|
-
this.scene.background = new THREE.Color(0x303030) // Dark gray background
|
|
308
|
-
|
|
309
|
-
// Setup camera
|
|
310
|
-
const width = this.container.clientWidth || this.container.offsetWidth
|
|
311
|
-
const height = this.container.clientHeight || this.container.offsetHeight
|
|
312
|
-
this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
|
|
313
|
-
|
|
314
|
-
// Setup renderer
|
|
315
|
-
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
|
|
316
|
-
this.renderer.setSize(width, height)
|
|
317
|
-
this.renderer.setPixelRatio(window.devicePixelRatio)
|
|
318
|
-
this.renderer.shadowMap.enabled = true
|
|
319
|
-
|
|
320
|
-
// Create canvas outside shadow DOM
|
|
321
|
-
const canvas = this.renderer.domElement
|
|
322
|
-
canvas.style.position = 'fixed'
|
|
323
|
-
canvas.style.left = `${rect.left}px`
|
|
324
|
-
canvas.style.top = `${rect.top}px`
|
|
325
|
-
canvas.style.width = `${rect.width}px`
|
|
326
|
-
canvas.style.height = `${rect.height}px`
|
|
327
|
-
canvas.style.pointerEvents = 'auto'
|
|
328
|
-
canvas.style.zIndex = '1001'
|
|
329
|
-
document.body.appendChild(canvas)
|
|
330
|
-
|
|
331
|
-
// Store canvas reference for cleanup
|
|
332
|
-
this.externalCanvas = canvas
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
// Setup controls - use the canvas in document body
|
|
336
|
-
this.controls = new OrbitControls(this.camera, canvas)
|
|
337
|
-
this.controls.enableDamping = true
|
|
338
|
-
this.controls.dampingFactor = 0.05
|
|
339
|
-
|
|
340
|
-
// Setup lights
|
|
341
|
-
const ambientLight = new THREE.AmbientLight(0x404040, 2)
|
|
342
|
-
const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
|
|
343
|
-
directionalLight.position.set(1, 1, 1)
|
|
344
|
-
this.scene.add(ambientLight, directionalLight)
|
|
345
|
-
|
|
346
|
-
// Load model
|
|
347
|
-
await this.loadModel()
|
|
348
|
-
|
|
349
|
-
// Setup resize handler
|
|
350
|
-
this.resizeObserver = new ResizeObserver((entries) => {
|
|
351
|
-
// Ignore resize events with zero size
|
|
352
|
-
for (const entry of entries) {
|
|
353
|
-
const { width, height } = entry.contentRect
|
|
354
|
-
if (width > 0 && height > 0) {
|
|
355
|
-
this.handleResize()
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
})
|
|
359
|
-
this.resizeObserver.observe(this.container)
|
|
360
|
-
|
|
361
|
-
// Start animation
|
|
362
|
-
this.animateLoop()
|
|
363
|
-
|
|
364
|
-
} catch (error) {
|
|
365
|
-
throw error
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
private async loadModel() {
|
|
370
|
-
const objLoader = new OBJLoader()
|
|
371
|
-
|
|
372
|
-
try {
|
|
373
|
-
// Load materials if provided
|
|
374
|
-
if (this.materialUrl) {
|
|
375
|
-
const mtlLoader = new MTLLoader()
|
|
376
|
-
const basePath = this.materialUrl.substring(0, this.materialUrl.lastIndexOf('/') + 1)
|
|
377
|
-
mtlLoader.setPath(basePath)
|
|
378
|
-
|
|
379
|
-
const materials = await new Promise<MTLLoader.MaterialCreator>((resolve, reject) => {
|
|
380
|
-
const filename = this.materialUrl.substring(this.materialUrl.lastIndexOf('/') + 1)
|
|
381
|
-
mtlLoader.load(filename, resolve, undefined, reject)
|
|
382
|
-
})
|
|
383
|
-
|
|
384
|
-
materials.preload()
|
|
385
|
-
objLoader.setMaterials(materials)
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Load OBJ
|
|
389
|
-
const basePath = this.modelUrl.substring(0, this.modelUrl.lastIndexOf('/') + 1)
|
|
390
|
-
objLoader.setPath(basePath)
|
|
391
|
-
|
|
392
|
-
const object = await new Promise<THREE.Group>((resolve, reject) => {
|
|
393
|
-
const filename = this.modelUrl.substring(this.modelUrl.lastIndexOf('/') + 1)
|
|
394
|
-
objLoader.load(filename, resolve, undefined, reject)
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
// Center and scale the model
|
|
398
|
-
const box = new THREE.Box3().setFromObject(object)
|
|
399
|
-
const size = new THREE.Vector3()
|
|
400
|
-
box.getSize(size)
|
|
401
|
-
const maxDim = Math.max(size.x, size.y, size.z)
|
|
402
|
-
object.scale.multiplyScalar(3.0 / maxDim)
|
|
403
|
-
|
|
404
|
-
const center = new THREE.Vector3()
|
|
405
|
-
box.getCenter(center)
|
|
406
|
-
object.position.sub(center.multiplyScalar(object.scale.x))
|
|
407
|
-
|
|
408
|
-
// Check if scene still exists (viewer might have been closed)
|
|
409
|
-
if (!this.scene) {
|
|
410
|
-
return
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
this.scene.add(object)
|
|
414
|
-
this.currentModel = object
|
|
415
|
-
|
|
416
|
-
// Store original materials for texture toggling
|
|
417
|
-
this.storeOriginalMaterials(object)
|
|
418
|
-
|
|
419
|
-
// Position camera from attributes
|
|
420
|
-
const camPos = this.cameraPosition.split(',').map(n => parseFloat(n.trim()))
|
|
421
|
-
const camTarget = this.cameraTarget.split(',').map(n => parseFloat(n.trim()))
|
|
422
|
-
|
|
423
|
-
if (camPos.length === 3) {
|
|
424
|
-
this.camera!.position.set(camPos[0], camPos[1], camPos[2])
|
|
425
|
-
}
|
|
426
|
-
if (camTarget.length === 3) {
|
|
427
|
-
this.camera!.lookAt(camTarget[0], camTarget[1], camTarget[2])
|
|
428
|
-
this.controls!.target.set(camTarget[0], camTarget[1], camTarget[2])
|
|
429
|
-
}
|
|
430
|
-
this.controls!.update()
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
// Force initial render
|
|
434
|
-
if (this.renderer && this.scene && this.camera) {
|
|
435
|
-
this.renderer.render(this.scene, this.camera)
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
} catch (error) {
|
|
439
|
-
throw error
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
private animateLoop = () => {
|
|
444
|
-
// Check if renderer still exists before continuing
|
|
445
|
-
if (!this.renderer || !this.scene || !this.camera) {
|
|
446
|
-
return
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
this.animationId = requestAnimationFrame(this.animateLoop)
|
|
450
|
-
|
|
451
|
-
if (this.controls) {
|
|
452
|
-
this.controls.update()
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
this.renderer.render(this.scene, this.camera)
|
|
456
|
-
|
|
457
|
-
// Update debug info if in debug mode
|
|
458
|
-
this.updateDebugInfo()
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
private handleResize() {
|
|
462
|
-
if (!this.container || !this.camera || !this.renderer) return
|
|
463
|
-
|
|
464
|
-
const rect = this.container.getBoundingClientRect()
|
|
465
|
-
const width = rect.width
|
|
466
|
-
const height = rect.height
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (width > 0 && height > 0) {
|
|
470
|
-
this.camera.aspect = width / height
|
|
471
|
-
this.camera.updateProjectionMatrix()
|
|
472
|
-
this.renderer.setSize(width, height)
|
|
473
|
-
|
|
474
|
-
// Update canvas position
|
|
475
|
-
const canvas = this.renderer.domElement
|
|
476
|
-
canvas.style.left = `${rect.left}px`
|
|
477
|
-
canvas.style.top = `${rect.top}px`
|
|
478
|
-
canvas.style.width = `${rect.width}px`
|
|
479
|
-
canvas.style.height = `${rect.height}px`
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (!customElements.get('cc-viewer-3dmodel')) {
|
|
485
|
-
customElements.define('cc-viewer-3dmodel', CcViewer3DModel)
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
declare global {
|
|
489
|
-
interface HTMLElementTagNameMap {
|
|
490
|
-
'cc-viewer-3dmodel': CcViewer3DModel
|
|
491
|
-
}
|
|
1
|
+
import { CcViewerBase } from './cc-viewer-base'
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'
|
|
4
|
+
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js'
|
|
5
|
+
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
|
6
|
+
|
|
7
|
+
export class CcViewer3DModel extends CcViewerBase {
|
|
8
|
+
private modelUrl = ''
|
|
9
|
+
private materialUrl = ''
|
|
10
|
+
private debugMode = false
|
|
11
|
+
private cameraPosition = '3,3,3'
|
|
12
|
+
private cameraTarget = '0,0,0'
|
|
13
|
+
private showTexture = true
|
|
14
|
+
|
|
15
|
+
private scene?: THREE.Scene
|
|
16
|
+
private camera?: THREE.PerspectiveCamera
|
|
17
|
+
private renderer?: THREE.WebGLRenderer
|
|
18
|
+
private controls?: OrbitControls
|
|
19
|
+
private animationId?: number
|
|
20
|
+
private container?: HTMLDivElement
|
|
21
|
+
private currentModel?: THREE.Group
|
|
22
|
+
private originalMaterials: Map<THREE.Mesh, THREE.Material | THREE.Material[]> = new Map()
|
|
23
|
+
private resizeObserver?: ResizeObserver
|
|
24
|
+
private externalCanvas?: HTMLCanvasElement
|
|
25
|
+
|
|
26
|
+
static get observedAttributes() {
|
|
27
|
+
return ['show', 'debug-mode', 'camera-position', 'camera-target', 'show-texture', 'material-url']
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
|
31
|
+
|
|
32
|
+
if (name === 'show') {
|
|
33
|
+
this.isShow = newValue === 'true'
|
|
34
|
+
} else if (name === 'debug-mode') {
|
|
35
|
+
this.debugMode = newValue === 'true' || newValue === ''
|
|
36
|
+
} else if (name === 'camera-position') {
|
|
37
|
+
this.cameraPosition = newValue || '3,3,3'
|
|
38
|
+
} else if (name === 'camera-target') {
|
|
39
|
+
this.cameraTarget = newValue || '0,0,0'
|
|
40
|
+
} else if (name === 'show-texture') {
|
|
41
|
+
this.showTexture = newValue !== 'false'
|
|
42
|
+
} else if (name === 'material-url') {
|
|
43
|
+
this.materialUrl = newValue || ''
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Re-render if viewer is already open and debug mode changes
|
|
47
|
+
if (name === 'debug-mode' && this.isShow) {
|
|
48
|
+
this.render()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
super.attributeChangedCallback(name, oldValue, newValue)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Implementation of abstract methods from base class
|
|
55
|
+
protected async doOpen(url: string): Promise<void> {
|
|
56
|
+
|
|
57
|
+
this.modelUrl = url
|
|
58
|
+
// materialUrl is set via attribute
|
|
59
|
+
this.showTexture = true
|
|
60
|
+
|
|
61
|
+
await this.initializeViewer()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected doClose(): void {
|
|
65
|
+
this.cleanup()
|
|
66
|
+
this.modelUrl = ''
|
|
67
|
+
this.materialUrl = ''
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
protected getViewerContent(): string {
|
|
71
|
+
|
|
72
|
+
const modelContent = !this.modelUrl ?
|
|
73
|
+
'<div class="error">No model URL provided</div>' :
|
|
74
|
+
`
|
|
75
|
+
${this.isLoading ? '<div class="loading">Loading...</div>' : ''}
|
|
76
|
+
${!this.isLoading && this.debugMode ? `
|
|
77
|
+
<div class="debug-info" style="z-index: 1005;">
|
|
78
|
+
Camera Position: ${this.getCameraDebugInfo()}<br>
|
|
79
|
+
Camera Target: ${this.getTargetDebugInfo()}<br>
|
|
80
|
+
Controls: Rotate (drag), Zoom (scroll), Pan (right-drag)
|
|
81
|
+
</div>
|
|
82
|
+
` : ''}
|
|
83
|
+
${!this.isLoading ? `
|
|
84
|
+
<button class="texture-toggle">
|
|
85
|
+
Texture: ${this.showTexture ? 'ON' : 'OFF'}
|
|
86
|
+
</button>
|
|
87
|
+
` : ''}
|
|
88
|
+
`
|
|
89
|
+
|
|
90
|
+
return `
|
|
91
|
+
<div class="model-container">
|
|
92
|
+
${modelContent}
|
|
93
|
+
</div>
|
|
94
|
+
`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Custom styles for 3D viewer
|
|
98
|
+
protected getCustomStyles(): string {
|
|
99
|
+
return `
|
|
100
|
+
.model-container {
|
|
101
|
+
width: 100%;
|
|
102
|
+
height: 100%;
|
|
103
|
+
position: relative;
|
|
104
|
+
background: transparent;
|
|
105
|
+
z-index: 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.model-container canvas {
|
|
109
|
+
position: absolute;
|
|
110
|
+
top: 0;
|
|
111
|
+
left: 0;
|
|
112
|
+
width: 100%;
|
|
113
|
+
height: 100%;
|
|
114
|
+
z-index: 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.loading {
|
|
118
|
+
position: absolute;
|
|
119
|
+
top: 50%;
|
|
120
|
+
left: 50%;
|
|
121
|
+
transform: translate(-50%, -50%);
|
|
122
|
+
color: #666;
|
|
123
|
+
font-size: 1.2rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.error {
|
|
127
|
+
position: absolute;
|
|
128
|
+
top: 50%;
|
|
129
|
+
left: 50%;
|
|
130
|
+
transform: translate(-50%, -50%);
|
|
131
|
+
color: #e74c3c;
|
|
132
|
+
text-align: center;
|
|
133
|
+
padding: 20px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
canvas {
|
|
137
|
+
display: block;
|
|
138
|
+
width: 100%;
|
|
139
|
+
height: 100%;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.debug-info {
|
|
143
|
+
position: absolute;
|
|
144
|
+
top: 10px;
|
|
145
|
+
left: 10px;
|
|
146
|
+
background: rgba(0, 0, 0, 0.8);
|
|
147
|
+
color: white;
|
|
148
|
+
padding: 10px;
|
|
149
|
+
font-family: monospace;
|
|
150
|
+
font-size: 12px;
|
|
151
|
+
border-radius: 4px;
|
|
152
|
+
pointer-events: none;
|
|
153
|
+
z-index: 1010;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.texture-toggle {
|
|
157
|
+
position: absolute;
|
|
158
|
+
top: 10px;
|
|
159
|
+
right: 10px;
|
|
160
|
+
background: rgba(0, 0, 0, 0.8);
|
|
161
|
+
color: white;
|
|
162
|
+
border: 1px solid #666;
|
|
163
|
+
padding: 8px 16px;
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
border-radius: 4px;
|
|
166
|
+
transition: background 0.3s;
|
|
167
|
+
z-index: 1010;
|
|
168
|
+
pointer-events: auto;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.texture-toggle:hover {
|
|
172
|
+
background: rgba(0, 0, 0, 0.9);
|
|
173
|
+
}
|
|
174
|
+
`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Add texture toggle listener after render
|
|
178
|
+
protected onAfterRender(): void {
|
|
179
|
+
const textureBtn = this.query('.texture-toggle')
|
|
180
|
+
if (textureBtn) {
|
|
181
|
+
textureBtn.addEventListener('click', () => this.toggleTexture())
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private cleanup() {
|
|
186
|
+
|
|
187
|
+
if (this.animationId) {
|
|
188
|
+
cancelAnimationFrame(this.animationId)
|
|
189
|
+
this.animationId = undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (this.renderer) {
|
|
193
|
+
this.renderer.dispose()
|
|
194
|
+
// Remove canvas from document body
|
|
195
|
+
if (this.renderer.domElement.parentNode === document.body) {
|
|
196
|
+
document.body.removeChild(this.renderer.domElement)
|
|
197
|
+
}
|
|
198
|
+
this.renderer = undefined
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (this.controls) {
|
|
202
|
+
this.controls.dispose()
|
|
203
|
+
this.controls = undefined
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Clear Three.js objects
|
|
207
|
+
if (this.scene) {
|
|
208
|
+
this.scene.traverse((child) => {
|
|
209
|
+
if (child instanceof THREE.Mesh) {
|
|
210
|
+
child.geometry?.dispose()
|
|
211
|
+
if (Array.isArray(child.material)) {
|
|
212
|
+
child.material.forEach(m => m.dispose())
|
|
213
|
+
} else {
|
|
214
|
+
child.material?.dispose()
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
this.scene.clear()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this.scene = undefined
|
|
222
|
+
this.camera = undefined
|
|
223
|
+
this.currentModel = undefined
|
|
224
|
+
this.originalMaterials.clear()
|
|
225
|
+
|
|
226
|
+
if (this.resizeObserver) {
|
|
227
|
+
this.resizeObserver.disconnect()
|
|
228
|
+
this.resizeObserver = undefined
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.container = undefined
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private storeOriginalMaterials(object: THREE.Group) {
|
|
235
|
+
object.traverse((child) => {
|
|
236
|
+
if (child instanceof THREE.Mesh) {
|
|
237
|
+
this.originalMaterials.set(child, child.material)
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private toggleTexture() {
|
|
243
|
+
this.showTexture = !this.showTexture
|
|
244
|
+
|
|
245
|
+
if (!this.currentModel) return
|
|
246
|
+
|
|
247
|
+
this.currentModel.traverse((child) => {
|
|
248
|
+
if (child instanceof THREE.Mesh) {
|
|
249
|
+
if (this.showTexture) {
|
|
250
|
+
// Restore original materials
|
|
251
|
+
const original = this.originalMaterials.get(child)
|
|
252
|
+
if (original) {
|
|
253
|
+
child.material = original
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
// Apply simple color material
|
|
257
|
+
const color = new THREE.Color(0xcccccc)
|
|
258
|
+
child.material = new THREE.MeshPhongMaterial({ color })
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
this.render()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private getCameraDebugInfo(): string {
|
|
267
|
+
if (!this.camera) return 'N/A'
|
|
268
|
+
const pos = this.camera.position
|
|
269
|
+
return `${pos.x.toFixed(2)}, ${pos.y.toFixed(2)}, ${pos.z.toFixed(2)}`
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private getTargetDebugInfo(): string {
|
|
273
|
+
if (!this.controls) return 'N/A'
|
|
274
|
+
const target = this.controls.target
|
|
275
|
+
return `${target.x.toFixed(2)}, ${target.y.toFixed(2)}, ${target.z.toFixed(2)}`
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private updateDebugInfo() {
|
|
279
|
+
// Update debug display
|
|
280
|
+
const debugEl = this.query('.debug-info')
|
|
281
|
+
if (debugEl && this.debugMode) {
|
|
282
|
+
debugEl.innerHTML = `
|
|
283
|
+
Camera Position: ${this.getCameraDebugInfo()}<br>
|
|
284
|
+
Camera Target: ${this.getTargetDebugInfo()}<br>
|
|
285
|
+
Controls: Rotate (drag), Zoom (scroll), Pan (right-drag)
|
|
286
|
+
`
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private async initializeViewer() {
|
|
291
|
+
|
|
292
|
+
// Wait a moment to ensure DOM is ready
|
|
293
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
294
|
+
|
|
295
|
+
this.container = this.query('.model-container') as HTMLDivElement
|
|
296
|
+
if (!this.container) {
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Get the actual position of the container in the viewport
|
|
301
|
+
const rect = this.container.getBoundingClientRect()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
// Setup scene
|
|
306
|
+
this.scene = new THREE.Scene()
|
|
307
|
+
this.scene.background = new THREE.Color(0x303030) // Dark gray background
|
|
308
|
+
|
|
309
|
+
// Setup camera
|
|
310
|
+
const width = this.container.clientWidth || this.container.offsetWidth
|
|
311
|
+
const height = this.container.clientHeight || this.container.offsetHeight
|
|
312
|
+
this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
|
|
313
|
+
|
|
314
|
+
// Setup renderer
|
|
315
|
+
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
|
|
316
|
+
this.renderer.setSize(width, height)
|
|
317
|
+
this.renderer.setPixelRatio(window.devicePixelRatio)
|
|
318
|
+
this.renderer.shadowMap.enabled = true
|
|
319
|
+
|
|
320
|
+
// Create canvas outside shadow DOM
|
|
321
|
+
const canvas = this.renderer.domElement
|
|
322
|
+
canvas.style.position = 'fixed'
|
|
323
|
+
canvas.style.left = `${rect.left}px`
|
|
324
|
+
canvas.style.top = `${rect.top}px`
|
|
325
|
+
canvas.style.width = `${rect.width}px`
|
|
326
|
+
canvas.style.height = `${rect.height}px`
|
|
327
|
+
canvas.style.pointerEvents = 'auto'
|
|
328
|
+
canvas.style.zIndex = '1001'
|
|
329
|
+
document.body.appendChild(canvas)
|
|
330
|
+
|
|
331
|
+
// Store canvas reference for cleanup
|
|
332
|
+
this.externalCanvas = canvas
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
// Setup controls - use the canvas in document body
|
|
336
|
+
this.controls = new OrbitControls(this.camera, canvas)
|
|
337
|
+
this.controls.enableDamping = true
|
|
338
|
+
this.controls.dampingFactor = 0.05
|
|
339
|
+
|
|
340
|
+
// Setup lights
|
|
341
|
+
const ambientLight = new THREE.AmbientLight(0x404040, 2)
|
|
342
|
+
const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
|
|
343
|
+
directionalLight.position.set(1, 1, 1)
|
|
344
|
+
this.scene.add(ambientLight, directionalLight)
|
|
345
|
+
|
|
346
|
+
// Load model
|
|
347
|
+
await this.loadModel()
|
|
348
|
+
|
|
349
|
+
// Setup resize handler
|
|
350
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
351
|
+
// Ignore resize events with zero size
|
|
352
|
+
for (const entry of entries) {
|
|
353
|
+
const { width, height } = entry.contentRect
|
|
354
|
+
if (width > 0 && height > 0) {
|
|
355
|
+
this.handleResize()
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
this.resizeObserver.observe(this.container)
|
|
360
|
+
|
|
361
|
+
// Start animation
|
|
362
|
+
this.animateLoop()
|
|
363
|
+
|
|
364
|
+
} catch (error) {
|
|
365
|
+
throw error
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private async loadModel() {
|
|
370
|
+
const objLoader = new OBJLoader()
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
// Load materials if provided
|
|
374
|
+
if (this.materialUrl) {
|
|
375
|
+
const mtlLoader = new MTLLoader()
|
|
376
|
+
const basePath = this.materialUrl.substring(0, this.materialUrl.lastIndexOf('/') + 1)
|
|
377
|
+
mtlLoader.setPath(basePath)
|
|
378
|
+
|
|
379
|
+
const materials = await new Promise<MTLLoader.MaterialCreator>((resolve, reject) => {
|
|
380
|
+
const filename = this.materialUrl.substring(this.materialUrl.lastIndexOf('/') + 1)
|
|
381
|
+
mtlLoader.load(filename, resolve, undefined, reject)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
materials.preload()
|
|
385
|
+
objLoader.setMaterials(materials)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Load OBJ
|
|
389
|
+
const basePath = this.modelUrl.substring(0, this.modelUrl.lastIndexOf('/') + 1)
|
|
390
|
+
objLoader.setPath(basePath)
|
|
391
|
+
|
|
392
|
+
const object = await new Promise<THREE.Group>((resolve, reject) => {
|
|
393
|
+
const filename = this.modelUrl.substring(this.modelUrl.lastIndexOf('/') + 1)
|
|
394
|
+
objLoader.load(filename, resolve, undefined, reject)
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
// Center and scale the model
|
|
398
|
+
const box = new THREE.Box3().setFromObject(object)
|
|
399
|
+
const size = new THREE.Vector3()
|
|
400
|
+
box.getSize(size)
|
|
401
|
+
const maxDim = Math.max(size.x, size.y, size.z)
|
|
402
|
+
object.scale.multiplyScalar(3.0 / maxDim)
|
|
403
|
+
|
|
404
|
+
const center = new THREE.Vector3()
|
|
405
|
+
box.getCenter(center)
|
|
406
|
+
object.position.sub(center.multiplyScalar(object.scale.x))
|
|
407
|
+
|
|
408
|
+
// Check if scene still exists (viewer might have been closed)
|
|
409
|
+
if (!this.scene) {
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
this.scene.add(object)
|
|
414
|
+
this.currentModel = object
|
|
415
|
+
|
|
416
|
+
// Store original materials for texture toggling
|
|
417
|
+
this.storeOriginalMaterials(object)
|
|
418
|
+
|
|
419
|
+
// Position camera from attributes
|
|
420
|
+
const camPos = this.cameraPosition.split(',').map(n => parseFloat(n.trim()))
|
|
421
|
+
const camTarget = this.cameraTarget.split(',').map(n => parseFloat(n.trim()))
|
|
422
|
+
|
|
423
|
+
if (camPos.length === 3) {
|
|
424
|
+
this.camera!.position.set(camPos[0], camPos[1], camPos[2])
|
|
425
|
+
}
|
|
426
|
+
if (camTarget.length === 3) {
|
|
427
|
+
this.camera!.lookAt(camTarget[0], camTarget[1], camTarget[2])
|
|
428
|
+
this.controls!.target.set(camTarget[0], camTarget[1], camTarget[2])
|
|
429
|
+
}
|
|
430
|
+
this.controls!.update()
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
// Force initial render
|
|
434
|
+
if (this.renderer && this.scene && this.camera) {
|
|
435
|
+
this.renderer.render(this.scene, this.camera)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
} catch (error) {
|
|
439
|
+
throw error
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private animateLoop = () => {
|
|
444
|
+
// Check if renderer still exists before continuing
|
|
445
|
+
if (!this.renderer || !this.scene || !this.camera) {
|
|
446
|
+
return
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
this.animationId = requestAnimationFrame(this.animateLoop)
|
|
450
|
+
|
|
451
|
+
if (this.controls) {
|
|
452
|
+
this.controls.update()
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
this.renderer.render(this.scene, this.camera)
|
|
456
|
+
|
|
457
|
+
// Update debug info if in debug mode
|
|
458
|
+
this.updateDebugInfo()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private handleResize() {
|
|
462
|
+
if (!this.container || !this.camera || !this.renderer) return
|
|
463
|
+
|
|
464
|
+
const rect = this.container.getBoundingClientRect()
|
|
465
|
+
const width = rect.width
|
|
466
|
+
const height = rect.height
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
if (width > 0 && height > 0) {
|
|
470
|
+
this.camera.aspect = width / height
|
|
471
|
+
this.camera.updateProjectionMatrix()
|
|
472
|
+
this.renderer.setSize(width, height)
|
|
473
|
+
|
|
474
|
+
// Update canvas position
|
|
475
|
+
const canvas = this.renderer.domElement
|
|
476
|
+
canvas.style.left = `${rect.left}px`
|
|
477
|
+
canvas.style.top = `${rect.top}px`
|
|
478
|
+
canvas.style.width = `${rect.width}px`
|
|
479
|
+
canvas.style.height = `${rect.height}px`
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!customElements.get('cc-viewer-3dmodel')) {
|
|
485
|
+
customElements.define('cc-viewer-3dmodel', CcViewer3DModel)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
declare global {
|
|
489
|
+
interface HTMLElementTagNameMap {
|
|
490
|
+
'cc-viewer-3dmodel': CcViewer3DModel
|
|
491
|
+
}
|
|
492
492
|
}
|