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
JavaScript
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,
};