UNPKG

prophetk-3d-viewer

Version:

A Three.js r128 based GLB/GLTF model viewer for WeChat Mini Program and Web.

234 lines (203 loc) • 8.41 kB
const ModelRenderer = require('../../core/ModelRenderer.js'); const WebPlatformAdapter = require('../../adapters/web/WebPlatformAdapter.js'); class WebModelViewer { constructor(options = {}) { const container = typeof options.container === 'string' ? document.querySelector(options.container) : options.container; if (!container) throw new Error('WebModelViewer requires a container'); const canvas = options.canvas || document.createElement('canvas'); if (!options.canvas) container.appendChild(canvas); canvas.className = options.className || 'wr-model-viewer-canvas'; canvas.style.width = '100%'; canvas.style.height = '100%'; canvas.style.display = 'block'; this._previousTouchAction = canvas.style.touchAction; canvas.style.touchAction = 'none'; const rect = container.getBoundingClientRect(); const width = options.width || rect.width || container.clientWidth || 300; const height = options.height || rect.height || container.clientHeight || 150; const platform = options.platform || new WebPlatformAdapter(); this.canvas = canvas; this.container = container; this.platform = platform; this.renderer = new ModelRenderer(canvas, { platform, bgColor: options.bgColor, renderMode: options.renderMode || 'continuous', width, height, left: rect.left || 0, top: rect.top || 0, onUiEvent: (detail) => { if (typeof options.onUiEvent === 'function') options.onUiEvent(detail); canvas.dispatchEvent(new CustomEvent('uiEvent', { detail })); }, onModelEvent: (detail) => { if (detail.type === 'dragblocked') this._showModelHint(detail.message); if (typeof options.onModelEvent === 'function') options.onModelEvent(detail); canvas.dispatchEvent(new CustomEvent('modelevent', { detail })); }, }); this._bindEvents(); if (typeof ResizeObserver !== 'undefined') { this._resizeObserver = new ResizeObserver(() => this.resize()); this._resizeObserver.observe(container); } } loadModel(url, options) { return this.renderer.loadModel(url, options); } switchModel(url, options) { return this.renderer.switchModel(url, options); } loadScene(config) { return this.renderer.loadScene(config); } clearModel(target) { return this.renderer.clearModel(target); } clearAllModels() { return this.renderer.clearAllModels(); } setParent(modelId, parentId) { this.renderer.setParent(modelId, parentId); } SetParent(modelId, parentId) { this.setParent(modelId, parentId); } getSelectedModelId() { return this.renderer.getSelectedModelId(); } getModelTransforms() { return this.renderer.getModelTransforms(); } clearSelection() { this.renderer.clearSelection(); } getCameraControlMode() { return this.renderer.getCameraControlMode(); } setCameraControlMode(mode) { this.renderer.setCameraControlMode(mode); } applyDecal(url, options) { return this.renderer.applyDecal(url, options); } updateDecal(angle, height, scaleX, scaleY) { this.renderer.updateDecal(angle, height, scaleX, scaleY); } updateProjectorDecal(options) { this.renderer.updateProjectorDecal(options); } removeDecal() { this.renderer.removeDecal(); } setUiLabelText(id, text) { return this.renderer.setUiLabelText(id, text); } resize() { const rect = this.container.getBoundingClientRect(); const width = rect.width || this.container.clientWidth; const height = rect.height || this.container.clientHeight; if (!width || !height) return; this.renderer.resize(width, height, rect); } dispose() { if (this._resizeObserver) this._resizeObserver.disconnect(); this._unbindEvents(); this.renderer.dispose(); clearTimeout(this._hintTimer); if (this._modelHint) this._modelHint.remove(); this.canvas.style.touchAction = this._previousTouchAction; if (this.platform.dispose) this.platform.dispose(); } _bindEvents() { // Pointer events unify touch and mouse without synthesized mouse duplicates. this._pointers = new Map(); const point = (e) => ({ identifier: e.pointerId, clientX: e.clientX, clientY: e.clientY }); const event = (e) => ({ touches: Array.from(this._pointers.values()), changedTouches: [point(e)], pointerType: e.pointerType }); this._onPointerDown = (e) => { if (e.pointerType === 'mouse' && e.button !== 0) return; e.preventDefault(); this._pointers.set(e.pointerId, point(e)); if (this.canvas.setPointerCapture) this.canvas.setPointerCapture(e.pointerId); this.renderer.onTouchStart(event(e)); }; this._onPointerMove = (e) => { if (!this._pointers.has(e.pointerId)) return; e.preventDefault(); this._pointers.set(e.pointerId, point(e)); this.renderer.onTouchMove(event(e)); }; this._onPointerUp = (e) => { if (!this._pointers.has(e.pointerId)) return; this._pointers.delete(e.pointerId); this.renderer.onTouchEnd(event(e)); }; this._cancelPointers = (reason) => { const ids = Array.from(this._pointers.keys()); this._pointers.clear(); this.renderer.onTouchCancel({}, reason); ids.forEach((id) => { if (this.canvas.hasPointerCapture && this.canvas.hasPointerCapture(id)) { this.canvas.releasePointerCapture(id); } }); }; this._onPointerCancel = (e) => { if (this._pointers.has(e.pointerId)) this._cancelPointers('touchcancel'); }; this._onBlur = () => this._cancelPointers('blur'); this._onWheel = (e) => { // A non-passive listener prevents Ctrl+wheel from zooming the web page. // Keep ordinary wheel zoom, and consume the gesture only on this canvas. e.preventDefault(); e.stopPropagation(); this.renderer.onWheel(e); }; this.canvas.addEventListener('pointerdown', this._onPointerDown); window.addEventListener('pointermove', this._onPointerMove, { passive: false }); window.addEventListener('pointerup', this._onPointerUp); window.addEventListener('pointercancel', this._onPointerCancel); this.canvas.addEventListener('lostpointercapture', this._onPointerCancel); window.addEventListener('blur', this._onBlur); this.canvas.addEventListener('wheel', this._onWheel, { passive: false }); } _unbindEvents() { this.canvas.removeEventListener('pointerdown', this._onPointerDown); window.removeEventListener('pointermove', this._onPointerMove); window.removeEventListener('pointerup', this._onPointerUp); window.removeEventListener('pointercancel', this._onPointerCancel); this.canvas.removeEventListener('lostpointercapture', this._onPointerCancel); window.removeEventListener('blur', this._onBlur); this.canvas.removeEventListener('wheel', this._onWheel); this._pointers.forEach((point, id) => { if (this.canvas.hasPointerCapture && this.canvas.hasPointerCapture(id)) { this.canvas.releasePointerCapture(id); } }); this._pointers.clear(); } _showModelHint(message) { if (!this._modelHint) { this._modelHint = document.createElement('div'); this._modelHint.setAttribute('role', 'status'); this._modelHint.style.cssText = 'position:fixed;z-index:1000;pointer-events:none;' + 'padding:8px 12px;border-radius:6px;background:rgba(0,0,0,.75);color:white;' + 'font:14px sans-serif;max-width:280px;box-sizing:border-box;'; document.body.appendChild(this._modelHint); } const rect = this.canvas.getBoundingClientRect(); this._modelHint.textContent = message; this._modelHint.style.left = Math.max(0, rect.left + 8) + 'px'; this._modelHint.style.top = Math.max(0, rect.top + 8) + 'px'; this._modelHint.style.display = 'block'; clearTimeout(this._hintTimer); this._hintTimer = setTimeout(() => { this._modelHint.style.display = 'none'; }, 2600); } } function createWebModelViewer(options) { return new WebModelViewer(options); } module.exports = { WebModelViewer, createWebModelViewer, };