UNPKG

prophetk-3d-viewer

Version:

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

155 lines (143 loc) • 5.87 kB
const traverseModel = require('./modelTraversal.js'); function parentId(value) { if (value === undefined || value === null) return null; if (typeof value !== 'string' || !value.trim()) throw new Error('parent must be a non-empty model ID or null'); return value; } class ModelHierarchy { constructor(renderer) { this.renderer = renderer; this.records = new Map(); Object.keys(renderer._modelNodes).forEach(id => { traverseModel.registerRoot(renderer._modelNodes[id]); this.records.set(id, { id, parent: null, object: renderer._modelNodes[id], state: 'ready' }); }); } static validate(items) { const graph = new Map(); items.forEach((item, index) => { const id = item.id || item.name || ('model_' + index); if (graph.has(id)) throw new Error('Duplicate model ID: ' + id); graph.set(id, { parent: parentId(item.parent) }); }); graph.forEach((record, id) => ModelHierarchy.checkCycle(graph, id, record.parent)); } static checkCycle(records, id, parent) { const seen = new Set([id]); for (let next = parent; next !== null && next !== undefined;) { if (seen.has(next)) throw new Error('Cyclic model parent: ' + id); seen.add(next); const record = records.get(next); next = record ? record.parent : null; } } load(item, id, normalize) { const r = this.renderer; const parent = parentId(item.parent); if (r._disposed) throw new Error('renderer disposed'); const previous = this.records.get(id); if (previous && previous.state !== 'failed') throw new Error('Duplicate model ID: ' + id); ModelHierarchy.checkCycle(this.records, id, parent); const record = { id, parent, url: item.url, invisible: item.invisible === true, state: 'loading', object: null }; this.records.set(id, record); return new Promise((resolve, reject) => { record.reject = reject; Promise.resolve().then(() => r._loadGltfObject(item.url, { normalize })).then(object => { if (this.records.get(id) !== record || r._disposed) { r._disposeObject(object); return; } r._applyTransform(object, item); object.name = id; object.userData.modelId = id; object.userData.modelUrl = item.url; record.object = object; record.state = 'ready'; r._modelNodes[id] = object; traverseModel.registerRoot(object); // Register before attaching descendants so geometry belongs to one model. r._interaction.register(id, object, item.isMovable, item.clickEvent); this.sync(); record.reject = null; resolve(object); }).catch(error => { if (this.records.get(id) === record) record.state = 'failed'; record.reject = null; reject(error); }); }); } sync() { const r = this.renderer; const ready = new Map(); const connected = record => { if (ready.has(record.id)) return ready.get(record.id); const parent = this.records.get(record.parent); const value = record.state === 'ready' && (record.parent === null || !!parent && connected(parent)); ready.set(record.id, value); return value; }; this.records.forEach(record => { if (!record.object) return; const target = connected(record) ? (record.parent === null ? r._sceneRoot : this.records.get(record.parent).object) : null; if (record.object.parent !== target) { if (target) target.add(record.object); else if (record.object.parent) record.object.parent.remove(record.object); } }); const selected = this.records.get(r._interaction.selectedId); if (selected && !connected(selected)) r._interaction.select(null, 'parentchange'); r._interaction.updateHelper(); r._requestRender(); } descendants(ids) { const result = new Set(ids); let changed = true; while (changed) { changed = false; this.records.forEach(record => { if (result.has(record.parent) && !result.has(record.id)) { result.add(record.id); changed = true; } }); } return result; } setParent(id, value) { const record = this.records.get(id); if (!record) throw new Error('Unknown model ID: ' + id); const parent = parentId(value); ModelHierarchy.checkCycle(this.records, id, parent); if (record.parent === parent) return; this.cancelAffected(this.descendants([id]), 'parentchange'); if (this.records.get(id) !== record) return; // A cancellation callback may remove this model. ModelHierarchy.checkCycle(this.records, id, parent); record.parent = parent; this.sync(); } cancelAffected(ids, reason) { const interaction = this.renderer._interaction; const g = interaction.gesture; if (g && [g.entry, g.clickEntry].some(entry => entry && ids.has(entry.id))) interaction.cancel(reason); } remove(ids, reason = 'modelremoved') { const r = this.renderer; const affected = this.descendants(ids); this.cancelAffected(affected, reason); const records = Array.from(affected).map(id => this.records.get(id)).filter(Boolean); // Invalidate every record before callbacks or asynchronous results can run. records.forEach(record => { this.records.delete(record.id); delete r._modelNodes[record.id]; }); records.forEach(record => { r._interaction.remove(record.id, reason); if (record.reject) record.reject(new Error('Model load cancelled: ' + record.id)); if (record.object && record.object.parent) record.object.parent.remove(record.object); }); records.forEach(record => { if (record.object) r._clearModelObject(record.object); }); r._refreshCurrentModel(); if (records.length) r._requestRender(); return records.length; } } module.exports = ModelHierarchy;