UNPKG

prophetk-3d-viewer

Version:

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

343 lines (302 loc) • 11.9 kB
const ModelRenderer = require('../../src/core/ModelRenderer.js'); const WechatPlatformAdapter = require('../../src/adapters/wechat/WechatPlatformAdapter.js'); const DEFAULT_SCENE_CONFIG = require('../../src/core/defaultSceneConfig.js'); function appendNoCacheParam(url) { const sep = url.indexOf('?') === -1 ? '?' : '&'; return url + sep + '_t=' + Date.now(); } Component({ properties: { bgColor: { type: String, value: '' }, canvasId: { type: String, value: 'model-viewer-canvas' }, renderMode: { type: String, value: 'on-demand' }, }, data: { loading: false, error: '', }, lifetimes: { ready() { setTimeout(() => this._initRenderer(), 300); }, detached() { this._inputEpoch = (this._inputEpoch || 0) + 1; this._touchQueue = null; this._detached = true; if (this._renderer) this._renderer.dispose(); this._renderer = null; }, }, pageLifetimes: { hide() { this.onTouchCancel({}, 'blur'); }, }, methods: { loadModel(url, options = {}) { if (!this._renderer) { return new Promise((resolve, reject) => { this._pendingLoad = { url, options, resolve, reject }; }); } return this._doLoad(url, options); }, loadScene(source) { // A new scene supplies its own default. Later API calls take precedence, // including while initialization or a remote scene read is pending. this._cameraModeOverride = null; if (!this._renderer) { return new Promise((resolve, reject) => { this._pendingScene = { source, resolve, reject }; }); } return this._doLoadScene(source); }, switchModel(url, options = {}) { if (!this._renderer) return this.loadModel(url, options); return this._renderer.switchModel(url, options); }, clearModel(target) { if (!this._renderer) return 0; return this._renderer.clearModel(target); }, ClearModel(target) { return this.clearModel(target); }, clearAllModels() { if (!this._renderer) return 0; return this._renderer.clearAllModels(); }, ClearAllModels() { return this.clearAllModels(); }, setParent(modelId, parentId) { if (!this._renderer) throw new Error('renderer not ready'); this._renderer.setParent(modelId, parentId); }, SetParent(modelId, parentId) { this.setParent(modelId, parentId); }, getSelectedModelId() { return this._renderer ? this._renderer.getSelectedModelId() : null; }, getModelTransforms() { return this._renderer ? this._renderer.getModelTransforms() : '[]'; }, clearSelection() { if (this._renderer) this._renderer.clearSelection(); }, getCameraControlMode() { if (this._cameraModeOverride) return this._cameraModeOverride; return this._renderer ? this._renderer.getCameraControlMode() : 'orbit'; }, setCameraControlMode(mode) { if (mode !== 'orbit' && mode !== 'pan') { throw new RangeError('camera.controlMode must be "orbit" or "pan"'); } this._cameraModeOverride = mode; if (this._renderer) this._renderer.setCameraControlMode(mode); }, applyDecal(url, options = {}) { if (!this._renderer) return Promise.reject(new Error('renderer not ready')); return this._renderer.applyDecal(url, options); }, updateDecal(angle, height, scaleX = 1.0, scaleY = 1.0) { if (this._renderer) this._renderer.updateDecal(angle, height, scaleX, scaleY); }, updateProjectorDecal(options = {}) { if (this._renderer) this._renderer.updateProjectorDecal(options); }, removeDecal() { if (this._renderer) this._renderer.removeDecal(); }, setUiLabelText(id, text) { if (!this._renderer) return false; return this._renderer.setUiLabelText(id, text); }, onTouchStart(e) { this._dispatchTouch('onTouchStart', e); }, onTouchMove(e) { this._dispatchTouch('onTouchMove', e); }, onTouchEnd(e) { this._dispatchTouch('onTouchEnd', e); }, onWheel(e) { // DevTools dispatches wheel deltas in detail (without DOM ctrlKey). // The canvas consumes both ordinary and Ctrl-modified wheel gestures. if (!this._renderer || this._touchQueue) return false; return this._renderer.onWheel(e.detail || e); }, onTouchCancel(e, reason = 'touchcancel') { this._inputEpoch = (this._inputEpoch || 0) + 1; this._touchQueue = null; this._touchActive = false; if (this._renderer) this._renderer.onTouchCancel(e, reason); }, _dispatchTouch(method, e) { if (!this._renderer) return; const snapshot = { touches: Array.from(e.touches || [], (t) => ({ ...t })), changedTouches: Array.from(e.changedTouches || [], (t) => ({ ...t })), pointerType: 'touch', }; const first = method === 'onTouchStart' && !this._touchActive; this._touchActive = snapshot.touches.length > 0; if (this._touchQueue) { this._touchQueue.push({ method, event: snapshot }); } else if (first) { // Refresh the canvas rectangle after page scrolling. Preserve input order // while the Mini Program's asynchronous selector query is pending. this._touchQueue = [{ method, event: snapshot }]; const epoch = this._inputEpoch || 0; wx.createSelectorQuery().in(this).select('#' + this.data.canvasId) .boundingClientRect().exec((res) => { if (!this._renderer || epoch !== (this._inputEpoch || 0)) return; const rect = res && res[0]; if (!rect || !rect.width || !rect.height) { this.onTouchCancel({}); return; } const renderer = this._renderer; if (rect.width !== renderer._width || rect.height !== renderer._height) { renderer.resize(rect.width, rect.height, rect); } else { renderer._canvasRect = rect; } const queue = this._touchQueue || []; this._touchQueue = null; queue.forEach((item) => { if (this._renderer && epoch === (this._inputEpoch || 0)) this._deliverTouch(item.method, item.event); }); }); } else { this._deliverTouch(method, snapshot); } }, _deliverTouch(method, event) { const rect = this._renderer._canvasRect; [event.touches, event.changedTouches].forEach((touches) => touches.forEach((touch) => { // CanvasTouch uses local x/y on runtimes that omit client coordinates. if (typeof touch.clientX !== 'number') touch.clientX = touch.x + rect.left; if (typeof touch.clientY !== 'number') touch.clientY = touch.y + rect.top; })); this._renderer[method](event); }, _initRenderer() { const tryQuery = () => { if (this._detached) return; wx.createSelectorQuery() .in(this) .select('#' + this.data.canvasId) .fields({ node: true, size: true, rect: true }) .exec((res) => { if (this._detached) return; if (!res || !res[0] || !res[0].node) { setTimeout(tryQuery, 200); return; } const { node, width, height } = res[0]; const platform = new WechatPlatformAdapter({ wx, canvas: node }); this._renderer = new ModelRenderer(node, { platform, bgColor: this.data.bgColor, renderMode: this.data.renderMode, width, height, left: res[0].left || 0, top: res[0].top || 0, onUiEvent: (detail) => { this.triggerEvent('uiEvent', detail); this.triggerEvent('uievent', detail); }, onModelEvent: (detail) => { if (detail.type === 'dragblocked') { wx.showToast({ title: detail.message, icon: 'none', duration: 2600 }); } this.triggerEvent('modelevent', detail); }, }); if (this._cameraModeOverride) this._renderer.setCameraControlMode(this._cameraModeOverride); const runPendingLoad = (beforeLoad) => { if (!this._pendingLoad) return; const { url, options, resolve, reject } = this._pendingLoad; this._pendingLoad = null; beforeLoad.then(() => this._doLoad(url, options)).then(resolve).catch(reject); }; if (this._pendingScene) { const { source, resolve, reject } = this._pendingScene; this._pendingScene = null; const sceneLoad = this._doLoadScene(source); sceneLoad.then(resolve).catch(reject); runPendingLoad(sceneLoad); } else if (this._pendingLoad) { const { url, options, resolve, reject } = this._pendingLoad; this._pendingLoad = null; this._doLoad(url, options).then(resolve).catch(reject); } }); }; tryQuery(); }, _doLoad(url, options = {}) { this.setData({ loading: true, error: '' }); return this._renderer.loadModel(url, options) .then(() => this.setData({ loading: false })) .catch((err) => { console.error('[model-viewer] load error:', err); this.setData({ loading: false, error: String(err.message || err) }); throw err; }); }, _doLoadScene(source) { this.setData({ loading: true, error: '' }); return this._readSceneSource(source) .then((config) => { const loaded = this._renderer.loadScene(config); if (this._cameraModeOverride) this._renderer.setCameraControlMode(this._cameraModeOverride); return loaded; }) .then(() => { this.setData({ loading: false }); this.triggerEvent('sceneLoaded'); }) .catch((err) => { console.error('[model-viewer] scene load error:', err); this.setData({ loading: false, error: String(err.message || err) }); this.triggerEvent('sceneError', { error: String(err.message || err) }); throw err; }); }, _readSceneSource(source) { if (source && typeof source === 'object') return Promise.resolve(source); if (!source) return Promise.resolve(DEFAULT_SCENE_CONFIG); if (typeof source !== 'string') return Promise.reject(new Error('scene source is empty')); if (/^https?:\/\//i.test(source)) { const platform = this._renderer && this._renderer._platform; if (platform) return platform.requestJson(appendNoCacheParam(source)); } return new Promise((resolve, reject) => { const fs = wx.getFileSystemManager(); const paths = source.charAt(0) === '/' ? [source, source.slice(1)] : [source, '/' + source]; const tryRead = (index) => { if (index >= paths.length) return reject(new Error('scene json not found: ' + source)); fs.readFile({ filePath: paths[index], encoding: 'utf8', success: (res) => { try { resolve(JSON.parse(res.data)); } catch (e) { reject(e); } }, fail: () => tryRead(index + 1), }); }; tryRead(0); }); }, }, });