awv3
Version:
⚡ AWV3 embedded CAD
365 lines (325 loc) • 12.6 kB
JavaScript
// 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;
}