@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.
@@ -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
  }