@c4h/chuci 0.2.4 → 0.2.6

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.
@@ -1,496 +1,496 @@
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: calc(var(--cc-viewer-z-index-each, 1000) + 5);">
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: calc(var(--cc-viewer-z-index-each, 1000) + 10);
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: calc(var(--cc-viewer-z-index-each, 1000) + 10);
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
- // Get z-index from host style
329
- const hostStyle = window.getComputedStyle(this)
330
- const zIndexVar = hostStyle.getPropertyValue('--cc-viewer-z-index-each').trim()
331
- const baseZIndex = zIndexVar ? parseInt(zIndexVar, 10) : 1000
332
- canvas.style.zIndex = `${baseZIndex + 1}`
333
- document.body.appendChild(canvas)
334
-
335
- // Store canvas reference for cleanup
336
- this.externalCanvas = canvas
337
-
338
-
339
- // Setup controls - use the canvas in document body
340
- this.controls = new OrbitControls(this.camera, canvas)
341
- this.controls.enableDamping = true
342
- this.controls.dampingFactor = 0.05
343
-
344
- // Setup lights
345
- const ambientLight = new THREE.AmbientLight(0x404040, 2)
346
- const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
347
- directionalLight.position.set(1, 1, 1)
348
- this.scene.add(ambientLight, directionalLight)
349
-
350
- // Load model
351
- await this.loadModel()
352
-
353
- // Setup resize handler
354
- this.resizeObserver = new ResizeObserver((entries) => {
355
- // Ignore resize events with zero size
356
- for (const entry of entries) {
357
- const { width, height } = entry.contentRect
358
- if (width > 0 && height > 0) {
359
- this.handleResize()
360
- }
361
- }
362
- })
363
- this.resizeObserver.observe(this.container)
364
-
365
- // Start animation
366
- this.animateLoop()
367
-
368
- } catch (error) {
369
- throw error
370
- }
371
- }
372
-
373
- private async loadModel() {
374
- const objLoader = new OBJLoader()
375
-
376
- try {
377
- // Load materials if provided
378
- if (this.materialUrl) {
379
- const mtlLoader = new MTLLoader()
380
- const basePath = this.materialUrl.substring(0, this.materialUrl.lastIndexOf('/') + 1)
381
- mtlLoader.setPath(basePath)
382
-
383
- const materials = await new Promise<MTLLoader.MaterialCreator>((resolve, reject) => {
384
- const filename = this.materialUrl.substring(this.materialUrl.lastIndexOf('/') + 1)
385
- mtlLoader.load(filename, resolve, undefined, reject)
386
- })
387
-
388
- materials.preload()
389
- objLoader.setMaterials(materials)
390
- }
391
-
392
- // Load OBJ
393
- const basePath = this.modelUrl.substring(0, this.modelUrl.lastIndexOf('/') + 1)
394
- objLoader.setPath(basePath)
395
-
396
- const object = await new Promise<THREE.Group>((resolve, reject) => {
397
- const filename = this.modelUrl.substring(this.modelUrl.lastIndexOf('/') + 1)
398
- objLoader.load(filename, resolve, undefined, reject)
399
- })
400
-
401
- // Center and scale the model
402
- const box = new THREE.Box3().setFromObject(object)
403
- const size = new THREE.Vector3()
404
- box.getSize(size)
405
- const maxDim = Math.max(size.x, size.y, size.z)
406
- object.scale.multiplyScalar(3.0 / maxDim)
407
-
408
- const center = new THREE.Vector3()
409
- box.getCenter(center)
410
- object.position.sub(center.multiplyScalar(object.scale.x))
411
-
412
- // Check if scene still exists (viewer might have been closed)
413
- if (!this.scene) {
414
- return
415
- }
416
-
417
- this.scene.add(object)
418
- this.currentModel = object
419
-
420
- // Store original materials for texture toggling
421
- this.storeOriginalMaterials(object)
422
-
423
- // Position camera from attributes
424
- const camPos = this.cameraPosition.split(',').map(n => parseFloat(n.trim()))
425
- const camTarget = this.cameraTarget.split(',').map(n => parseFloat(n.trim()))
426
-
427
- if (camPos.length === 3) {
428
- this.camera!.position.set(camPos[0], camPos[1], camPos[2])
429
- }
430
- if (camTarget.length === 3) {
431
- this.camera!.lookAt(camTarget[0], camTarget[1], camTarget[2])
432
- this.controls!.target.set(camTarget[0], camTarget[1], camTarget[2])
433
- }
434
- this.controls!.update()
435
-
436
-
437
- // Force initial render
438
- if (this.renderer && this.scene && this.camera) {
439
- this.renderer.render(this.scene, this.camera)
440
- }
441
-
442
- } catch (error) {
443
- throw error
444
- }
445
- }
446
-
447
- private animateLoop = () => {
448
- // Check if renderer still exists before continuing
449
- if (!this.renderer || !this.scene || !this.camera) {
450
- return
451
- }
452
-
453
- this.animationId = requestAnimationFrame(this.animateLoop)
454
-
455
- if (this.controls) {
456
- this.controls.update()
457
- }
458
-
459
- this.renderer.render(this.scene, this.camera)
460
-
461
- // Update debug info if in debug mode
462
- this.updateDebugInfo()
463
- }
464
-
465
- private handleResize() {
466
- if (!this.container || !this.camera || !this.renderer) return
467
-
468
- const rect = this.container.getBoundingClientRect()
469
- const width = rect.width
470
- const height = rect.height
471
-
472
-
473
- if (width > 0 && height > 0) {
474
- this.camera.aspect = width / height
475
- this.camera.updateProjectionMatrix()
476
- this.renderer.setSize(width, height)
477
-
478
- // Update canvas position
479
- const canvas = this.renderer.domElement
480
- canvas.style.left = `${rect.left}px`
481
- canvas.style.top = `${rect.top}px`
482
- canvas.style.width = `${rect.width}px`
483
- canvas.style.height = `${rect.height}px`
484
- }
485
- }
486
- }
487
-
488
- if (!customElements.get('cc-viewer-3dmodel')) {
489
- customElements.define('cc-viewer-3dmodel', CcViewer3DModel)
490
- }
491
-
492
- declare global {
493
- interface HTMLElementTagNameMap {
494
- 'cc-viewer-3dmodel': CcViewer3DModel
495
- }
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: calc(var(--cc-viewer-z-index-each, 1000) + 5);">
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: calc(var(--cc-viewer-z-index-each, 1000) + 10);
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: calc(var(--cc-viewer-z-index-each, 1000) + 10);
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
+ // Get z-index from host style
329
+ const hostStyle = window.getComputedStyle(this)
330
+ const zIndexVar = hostStyle.getPropertyValue('--cc-viewer-z-index-each').trim()
331
+ const baseZIndex = zIndexVar ? parseInt(zIndexVar, 10) : 1000
332
+ canvas.style.zIndex = `${baseZIndex + 1}`
333
+ document.body.appendChild(canvas)
334
+
335
+ // Store canvas reference for cleanup
336
+ this.externalCanvas = canvas
337
+
338
+
339
+ // Setup controls - use the canvas in document body
340
+ this.controls = new OrbitControls(this.camera, canvas)
341
+ this.controls.enableDamping = true
342
+ this.controls.dampingFactor = 0.05
343
+
344
+ // Setup lights
345
+ const ambientLight = new THREE.AmbientLight(0x404040, 2)
346
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
347
+ directionalLight.position.set(1, 1, 1)
348
+ this.scene.add(ambientLight, directionalLight)
349
+
350
+ // Load model
351
+ await this.loadModel()
352
+
353
+ // Setup resize handler
354
+ this.resizeObserver = new ResizeObserver((entries) => {
355
+ // Ignore resize events with zero size
356
+ for (const entry of entries) {
357
+ const { width, height } = entry.contentRect
358
+ if (width > 0 && height > 0) {
359
+ this.handleResize()
360
+ }
361
+ }
362
+ })
363
+ this.resizeObserver.observe(this.container)
364
+
365
+ // Start animation
366
+ this.animateLoop()
367
+
368
+ } catch (error) {
369
+ throw error
370
+ }
371
+ }
372
+
373
+ private async loadModel() {
374
+ const objLoader = new OBJLoader()
375
+
376
+ try {
377
+ // Load materials if provided
378
+ if (this.materialUrl) {
379
+ const mtlLoader = new MTLLoader()
380
+ const basePath = this.materialUrl.substring(0, this.materialUrl.lastIndexOf('/') + 1)
381
+ mtlLoader.setPath(basePath)
382
+
383
+ const materials = await new Promise<MTLLoader.MaterialCreator>((resolve, reject) => {
384
+ const filename = this.materialUrl.substring(this.materialUrl.lastIndexOf('/') + 1)
385
+ mtlLoader.load(filename, resolve, undefined, reject)
386
+ })
387
+
388
+ materials.preload()
389
+ objLoader.setMaterials(materials)
390
+ }
391
+
392
+ // Load OBJ
393
+ const basePath = this.modelUrl.substring(0, this.modelUrl.lastIndexOf('/') + 1)
394
+ objLoader.setPath(basePath)
395
+
396
+ const object = await new Promise<THREE.Group>((resolve, reject) => {
397
+ const filename = this.modelUrl.substring(this.modelUrl.lastIndexOf('/') + 1)
398
+ objLoader.load(filename, resolve, undefined, reject)
399
+ })
400
+
401
+ // Center and scale the model
402
+ const box = new THREE.Box3().setFromObject(object)
403
+ const size = new THREE.Vector3()
404
+ box.getSize(size)
405
+ const maxDim = Math.max(size.x, size.y, size.z)
406
+ object.scale.multiplyScalar(3.0 / maxDim)
407
+
408
+ const center = new THREE.Vector3()
409
+ box.getCenter(center)
410
+ object.position.sub(center.multiplyScalar(object.scale.x))
411
+
412
+ // Check if scene still exists (viewer might have been closed)
413
+ if (!this.scene) {
414
+ return
415
+ }
416
+
417
+ this.scene.add(object)
418
+ this.currentModel = object
419
+
420
+ // Store original materials for texture toggling
421
+ this.storeOriginalMaterials(object)
422
+
423
+ // Position camera from attributes
424
+ const camPos = this.cameraPosition.split(',').map(n => parseFloat(n.trim()))
425
+ const camTarget = this.cameraTarget.split(',').map(n => parseFloat(n.trim()))
426
+
427
+ if (camPos.length === 3) {
428
+ this.camera!.position.set(camPos[0], camPos[1], camPos[2])
429
+ }
430
+ if (camTarget.length === 3) {
431
+ this.camera!.lookAt(camTarget[0], camTarget[1], camTarget[2])
432
+ this.controls!.target.set(camTarget[0], camTarget[1], camTarget[2])
433
+ }
434
+ this.controls!.update()
435
+
436
+
437
+ // Force initial render
438
+ if (this.renderer && this.scene && this.camera) {
439
+ this.renderer.render(this.scene, this.camera)
440
+ }
441
+
442
+ } catch (error) {
443
+ throw error
444
+ }
445
+ }
446
+
447
+ private animateLoop = () => {
448
+ // Check if renderer still exists before continuing
449
+ if (!this.renderer || !this.scene || !this.camera) {
450
+ return
451
+ }
452
+
453
+ this.animationId = requestAnimationFrame(this.animateLoop)
454
+
455
+ if (this.controls) {
456
+ this.controls.update()
457
+ }
458
+
459
+ this.renderer.render(this.scene, this.camera)
460
+
461
+ // Update debug info if in debug mode
462
+ this.updateDebugInfo()
463
+ }
464
+
465
+ private handleResize() {
466
+ if (!this.container || !this.camera || !this.renderer) return
467
+
468
+ const rect = this.container.getBoundingClientRect()
469
+ const width = rect.width
470
+ const height = rect.height
471
+
472
+
473
+ if (width > 0 && height > 0) {
474
+ this.camera.aspect = width / height
475
+ this.camera.updateProjectionMatrix()
476
+ this.renderer.setSize(width, height)
477
+
478
+ // Update canvas position
479
+ const canvas = this.renderer.domElement
480
+ canvas.style.left = `${rect.left}px`
481
+ canvas.style.top = `${rect.top}px`
482
+ canvas.style.width = `${rect.width}px`
483
+ canvas.style.height = `${rect.height}px`
484
+ }
485
+ }
486
+ }
487
+
488
+ if (!customElements.get('cc-viewer-3dmodel')) {
489
+ customElements.define('cc-viewer-3dmodel', CcViewer3DModel)
490
+ }
491
+
492
+ declare global {
493
+ interface HTMLElementTagNameMap {
494
+ 'cc-viewer-3dmodel': CcViewer3DModel
495
+ }
496
496
  }