UNPKG

awv3

Version:
365 lines (325 loc) 12.6 kB
// TODO // 1. doesn't need to be coupled to canvas AT ALL // 2. Canvas events should be renamed to broadcasts to make sense import * as THREE from 'three'; import v4 from 'uuid-v4'; import * as Error from './error'; import { Performance } from './helpers'; import Unpack from 'awv3-protocol/unpack'; import Object3 from '../three/object3'; import Region from '../three/region'; import Defaults from './defaults'; import { parseMesh, parseLine } from './geometry'; // options.pool = THREE.Object3D // options.factory = function() // options. // when a pool exists operations can be fulfilled without needing a complex factory // this applies to assemblies, or even connected classcad transmissions // operations should always be awaited, the geometry parser should return promises as well // structural elements should be visible and uncompressed, only geometry should go to the worker // if the structure is laid out clear it can be processed in sequence export default class Parser { constructor() { // ... } async stream(url = Error.log('Url undefined'), options = {}) { // Always assume an array let requests = Array.isArray(url) ? url : [url]; // Map request-array into an array of promises and await them through Promises.all let results = await Promise.all( requests.map(async item => { // This function is allowed to run async, each request must race // A for-of loop instead would run requests in sequence, but it would waste performance // Create context for each request let context = createContext(options); // Request URL, wait until downloaded, then parse let response = await fetch(item).then(r => r.json()); // Handle response and wait (will cause recursive async calls to webworkers) await handleResult(context, response); // There may be additional promises to fulfill await Promise.all(context.promises); context.mapitem = item; // Return result return context; }), ); // All promises have been fulfilled, return results let context = mergeContext(results); context.options.callback({ type: Parser.Factory.Finished, context }); return context; } // Same pattern as above async parse(blob = Error.log('Blob undefined'), options = {}) { let requests = Array.isArray(blob) ? blob : [blob]; let results = await Promise.all( requests.map(async item => { let context = createContext(options); await handleResult(context, item); await Promise.all(context.promises); return context; }), ); let context = mergeContext(results); context.options.callback({ type: Parser.Factory.Finished, context }); return context; } } Parser.Factory = { Blob: 'Blob', Link: 'Link', Assembly: 'Assembly', Part: 'Part', Model: 'Model', Mesh: 'Mesh', Line: 'Line', Cone: 'Cone', Vertex: 'Vertex', Csys: 'Csys', Text: 'Text', Transform: 'Transform', Remove: 'Remove', Started: 'Started', Finished: 'Finished', }; export function createContext(options = {}, resolve = undefined, reject = undefined, command = '') { // Set defaults options = { callback: typeof options === 'function' ? options : () => null, session: undefined, id: v4(), ...Defaults.all, ...options, }; return { // Transaction ID id: options.id, // Transaction resolve resolve: resolve, // Transaction reject reject: reject, // All generated promises promises: [], // Hint for mapping results later command: command || '', // Options options: options, // An array of context objects, for instance when several urls are parsed at once array: [], // Part hashtable using load-time hints like filenames map: {}, // An array of resulting 3D models models: [], // An array of JSON patches patches: [], // An array of ClassCAD results results: [], // An array of ClassCAD error messages errors: [], // Bytes processed (compressed) bytes: 0, // Bytes processed (uncompressed) bytesUncompressed: 0, // Timer socketTime: null, // Parsing time time: 0, }; } export function mergeContext(context = Error.log('Context undefined')) { if (Array.isArray(context)) { if (context.length === 1) return mergeContext(context[0]); else { let result = createContext(); result.array = context; return mergeContext(result); } } delete context.resolve; delete context.reject; delete context.promises; delete context.mapitem; if (context.array.length > 0 && context.models.length === 0) { for (let part of context.array) { if (!!part.mapitem) context.map[part.mapitem] = part; let partContext = mergeContext(part); partContext.models.forEach(model => context.models.push(model)); partContext.patches.forEach(patch => context.patches.push(patch)); } } context.firstModel = undefined; if (context.models.length > 0) context.firstModel = context.models[0]; context.firstResult = undefined; if (context.results.length > 0) context.firstResult = context.results[0].result; return context; } export function parseGeometry(data, context) { let { models, options } = context; let { min, max, meta, material } = data.properties; let { color, emissive, opacity, map } = material; let model = new THREE.Group(); model.name = 'geometry'; model.userData = { id: data.id, meta }; if (color) { data.properties.material.color = new THREE.Color(color[0] / 255, color[1] / 255, color[2] / 255); } if (emissive) { data.properties.material.emissive = new THREE.Color(emissive[0] / 255, emissive[1] / 255, emissive[2] / 255); } if (map && options.session) { let map = null; if (data.properties.material.map) { let key = data.properties.material.map; map = options.session.materials[key]; if (!map) { let path = session.globals.resources[key]; map = options.session.materials[key] = new THREE.TextureLoader().setCrossOrigin('anonymous').load(path); map.wrapS = THREE.RepeatWrapping; map.wrapT = THREE.RepeatWrapping; } } data.properties.material.map = map; } if (!!min && !!max) { data.properties.box = new THREE.Box3(new THREE.Vector3(...min), new THREE.Vector3(...max)); data.properties.sphere = data.properties.box.getBoundingSphere(); model.bounds = model.bounds || { box: new THREE.Box3(), sphere: new THREE.Sphere() }; model.bounds.box = data.properties.box; model.bounds.sphere = data.properties.sphere; } if (data.mesh !== undefined) { let mesh = parseMesh(data, context); if (mesh) { model.add(mesh); options.callback({ type: Parser.Factory.Mesh, model, data: mesh, meta: mesh.meta, material: mesh.material, }); } } if (data.line !== undefined) { let mesh = parseLine(data, context); if (mesh) { model.add(mesh); options.callback({ type: Parser.Factory.Line, model, data: mesh, meta: mesh.meta, material: mesh.material, }); } } // cones if (!!data.cones) for (let cone of data.cones) options.callback({ type: Parser.Factory.Cone, model, data: cone, meta: cone.meta, material: cone.material, }); // vertices if (!!data.vertices) { let mesh = new Region(); for (let vertex of data.vertices) mesh.points.push({ id: vertex.id, meta: { ...vertex.meta, type: 'point', position: new THREE.Vector3(...vertex.p), layer: data.properties.layer, }, }); options.callback({ type: Parser.Factory.Vertex, model, data: mesh }); model.add(mesh); } // csys if (!!data.coordinateSystems) for (let csys of data.coordinateSystems) options.callback({ type: Parser.Factory.Csys, model, data: csys, meta: csys.meta }); //text if (!!data.text) for (let text of data.text) options.callback({ type: Parser.Factory.Text, model, data: text, meta: text.meta }); models.push(model); options.callback({ type: Parser.Factory.Model, model, data, meta }); } export async function handleResult(context = Error.log('Context undefined'), object = Error.log('Object undefined')) { let promise = new Promise(async resolve => { if (object.type === 'Buffer' && !!object.data) { // Node Buffer object = Uint8Array.from(object.data); } if (object instanceof Uint8Array) { // Wrap binary data into json Blob object = { command: 'Blob', type: 'Binary', data: object }; } if (typeof object === 'string' || object instanceof String) { // Plain text package let length = object.length; object = JSON.parse(object); context.bytes += length; if (object.command !== 'Blob') context.bytesUncompressed += length; } if (Array.isArray(object)) { // Collection of commands await Promise.all(object.map(item => handleResult(context, item))); } else if (object.command === 'Endpoint') { // Scaled endpoints context.results.push({ command: 'Result', from: 'ip', result: object, }); } else if (object.command === 'Blob') { // Pass options to worker object.options = { materials: context.options.materials }; // Process package in worker threads let core = Unpack.getFreeCore(); let result = await core.post(object, data => { if (context.options.materials.lazy) requestAnimationFrame(() => parseGeometry(data, context)); else parseGeometry(data, context); }); } else if ( context.resolve && object.command === 'Result' && object.from === 'EndFrame' && object.transactionID === context.id ) { // EndFrames are valid only for transactions that have explicit resolves marked in their respective contexts // Everything else will be resolved manually through context.promises let tempResolve = context.resolve; let results = await Promise.all(context.promises); context = mergeContext(context); tempResolve(context); } else if (object.command === 'Result' && object.from !== 'BeginFrame' && object.from !== 'EndFrame') { // Decode result, it *should* be JSON try { let json = JSON.parse(object.result); object.result = json; } catch (e) {} context.results.push(object); } else if (object.command === 'Patch') { context.patches = [...context.patches, ...object.ops]; } else if (object.command === 'ErrorMessage') { // ClassCAD error messages context.errors.push(object.attributes); console.warn( 'ClassCAD > State: ' + object.attributes.errorState + ', Code: ' + object.attributes.errorCode + ', Message: ' + object.attributes.errorMessage, ); } resolve(context); }); context.promises.push(promise); return await promise; }