const setPoints = (points, p, i) => { points[i++] = p[0] points[i++] = p[1] points[i++] = p[2] || 0 } function CSG2Vertices(csg) { let vLen = 0, iLen = 0 for (const poly of csg.polygons) { const len = poly.vertices.length vLen += len * 3 iLen += 3 * (len - 2) } const vertices = new Float32Array(vLen) const indices = vLen > 65535 ? new Uint32Array(iLen) : new Uint16Array(iLen) let vertOffset = 0 let indOffset = 0 let posOffset = 0 let first = 0 for (const poly of csg.polygons) { const arr = poly.vertices const len = arr.length first = posOffset vertices.set(arr[0], vertOffset) vertOffset += 3 vertices.set(arr[1], vertOffset) vertOffset += 3 posOffset += 2 for (let i = 2; i < len; i++) { vertices.set(arr[i], vertOffset) indices[indOffset++] = first indices[indOffset++] = first + i - 1 indices[indOffset++] = first + i vertOffset += 3 posOffset += 1 } } return { vertices, indices, type: 'mesh' } } function CSG2LineVertices(csg) { let vLen = csg.points.length * 3 if (csg.isClosed) vLen += 3 const vertices = new Float32Array(vLen) csg.points.forEach((p, idx) => setPoints(vertices, p, idx * 3)) if (csg.isClosed) { setPoints(vertices, csg.points[0], vertices.length - 3) } return { vertices, type: 'line' } } function CSG2LineSegmentsVertices(csg) { const vLen = csg.sides.length * 6 const vertices = new Float32Array(vLen) csg.sides.forEach((side, idx) => { const i = idx * 6 setPoints(vertices, side[0], i) setPoints(vertices, side[1], i + 3) }) return { vertices, type: 'lines' } } function CSGCached(func, data, cacheKey, transferable) { cacheKey = cacheKey || data let geo = CSGToBuffers.cache.get(cacheKey) if (geo) return geo geo = func(data) // fill transferable array for postMessage optimization if (transferable) { const { vertices, indices } = geo transferable.push(vertices) if (indices) transferable.push(indices) } CSGToBuffers.cache.set(cacheKey, geo) return geo } function CSGToBuffers(csg, transferable) { let obj if (csg.polygons) obj = CSGCached(CSG2Vertices, csg, csg.polygons, transferable) if (csg.sides && !csg.points) obj = CSGCached(CSG2LineSegmentsVertices, csg, csg.sides, transferable) if (csg.points) obj = CSGCached(CSG2LineVertices, csg, csg.points, transferable) return obj } CSGToBuffers.clearCache = () => { CSGToBuffers.cache = new WeakMap() } CSGToBuffers.clearCache() let workerBaseURI function require(url) { url = require.alias[url] || url if (url[0] != '/' && url.substr(0, 2) != './' && url.substr(0, 4) != 'http') url = 'https://unpkg.com/' + url let exports = require.cache[url] //get from cache if (!exports) { //not cached const module = requireModule(url) require.cache[url] = exports = module.exports //cache obj exported by module } return exports //require returns object exported by module } function requireFile(url) { try { const X = new XMLHttpRequest() X.open('GET', new URL(url, workerBaseURI), 0) // sync X.send() if (X.status && X.status !== 200) throw new Error(X.statusText) return X.responseText } catch (e) { console.log( 'problem loading url ', url, 'base', workerBaseURI, ' error:', e.message ) throw e } } function requireModule(url, source) { try { const exports = {} if (!source) source = requireFile(url) const module = { id: url, uri: url, exports: exports, source } //according to node.js modules // fix, add comment to show source on Chrome Dev Tools source = '//@ sourceURL=' + url + '\n' + source //------ const anonFn = new Function('require', 'exports', 'module', source) //create a Fn with module code, and 3 params: require, exports & module anonFn(require, exports, module) // call the Fn, Execute the module return module } catch (err) { console.error('Error loading module ' + url, err.message) throw err } } require.cache = {} require.alias = {} const cmdHandler = (handlers) => (cmd) => { const fn = handlers[cmd.action] if (!fn) throw new Error('no handler for type: ' + cmd.action) fn(cmd) } function parseParams(script) { let lines = script.split('\n').map((l) => l.trim()) lines = lines .map((l, i) => { return { code: l, line: i + 1, group: l[0] == '/' && !lines[i + 1] } }) .filter((l) => l.code) let i = 0, line, lineNum while (i < lines.length) { line = lines[i].code.trim() i++ if (line.length > 12 && line.substring(line.length - 13) == '//jscadparams') break if (line.length > 12 && line.indexOf('@jscad-params') != -1) break } let groupIndex = 1 const defs = [] while (i < lines.length) { line = lines[i].code lineNum = lines[i].line if (line[0] === '}') break if (line[0] === '/') { // group const def = parseComment(line, lineNum) let name = '_group_' + groupIndex++ let caption = def.caption const idx = caption.lastIndexOf(':') if (idx !== -1) { name = caption.substring(idx + 1).trim() caption = caption.substring(0, idx).trim() } defs.push({ name, type: 'group', caption, ...def.options }) } else { const idx = line.indexOf('/') if (idx === -1) { const def = parseDef(line, lineNum) def.caption = def.name defs.push(def) } else { defs.push( parseOne( line.substring(idx).trim(), line.substring(0, idx).trim(), lineNum, lineNum ) ) } } i++ } return defs } function parseOne(comment, code, line1, line2) { const { caption, options } = parseComment(comment, line1) let def = { caption, ...parseDef(code, line2) } def.caption = def.caption || def.name if (options) { def = { ...def, ...options } if (def.type === 'checkbox' && def.hasOwnProperty('initial')) def.checked = true if (def.type === 'slider') { if (def.min === undefined) { def.min = 0 } if (def.max === undefined) { def.max = def.initial * 2 || 100 } } } return def } function parseComment(comment, line) { const prefix = comment.substring(0, 2) if (prefix === '//') comment = comment.substring(2) if (prefix === '/*') comment = comment.substring(2, comment.length - 2) comment = comment.trim() const ret = {} const idx = comment.indexOf('{') if (idx !== -1) { try { ret.options = eval('(' + comment.substring(idx) + ')') } catch (e) { console.log('Error in line ' + line) console.log(comment) throw e } comment = comment.substring(0, idx).trim() } ret.caption = comment return ret } function parseDef(code, line) { if (code[code.length - 1] == ',') code = code.substring(0, code.length - 1).trim() let idx = code.indexOf('=') if (idx == -1) idx = code.indexOf(':') if (idx == -1) { return { name: code, type: 'text' } } else { const initial = code.substring(idx + 1).trim() const ret = { type: 'text', name: code.substring(0, idx).trim() } if (initial === 'true' || initial === 'false') { ret.type = 'checkbox' ret.checked = initial === 'true' } else if (/^[0-9]+$/.test(initial)) { ret.type = 'int' ret.initial = parseFloat(initial) } else if (/^[0-9]+\.[0-9]+$/.test(initial)) { ret.type = 'number' ret.initial = parseFloat(initial) } else { try { ret.initial = eval(initial) } catch (e) { console.log('Error in line ' + line) console.log(code) console.log('problem evaluating inital value:', initial) e = new EvalError(e.message, 'code', line) e.lineNumber = line throw e } } return ret } } const makeScriptWorker = ({ callback, convertToSolids }) => { let onInit, main, scriptStats, entities, lastParamsDef function runMain(params = {}) { if(lastParamsDef) lastParamsDef.forEach(def=>{ if(!(def.name in params) && 'initial' in def) params[def.name] = def.initial }) let time = Date.now() let solids const transfer = [] try { const tmp = main(params) solids = [] function flatten(arr) { if (arr) { if (arr instanceof Array) arr.forEach(flatten) else solids.push(arr) } } flatten(tmp) } catch (e) { callback( { action: 'entities', worker: 'render', error: e.message, stack: e.stack.toString(), }, transfer ) return } const solidsTime = Date.now() - time scriptStats = `generate solids ${solidsTime}ms` if (convertToSolids === 'buffers') { CSGToBuffers.clearCache() entities = solids .filter((s) => s) .map((csg) => { const obj = CSGToBuffers(csg, transfer) obj.color = csg.color obj.transforms = csg.transforms return obj }) } else if (convertToSolids === 'regl') { const { entitiesFromSolids } = require('@jscad/regl-renderer') time = Date.now() entities = entitiesFromSolids({}, solids) scriptStats += ` convert to entities ${Date.now() - time}ms` } else { entities = solids } callback( { action: 'entities', worker: 'render', entities, scriptStats }, transfer ) } let initialized = false const handlers = { runScript: ({ script, url, params = {} }) => { if (!initialized) { onInit = () => handlers.runScript({ script, url, params }) } let script_module try { script_module = requireModule(url, script) } catch (e) { callback({ action: 'entities', worker: 'render', error: e.message, stack: e.stack.toString(), }) return } main = script_module.exports.main const gp = script_module.exports.getParameterDefinitions const paramsDef = parseParams(script) || [] if (gp) { gp().forEach((p) => { const idx = paramsDef.findIndex((old) => old.name == p.name) if (idx === -1) { paramsDef.push(p) } else { paramsDef[idx] = p } }) } console.log('paramsDef', paramsDef) lastParamsDef = paramsDef callback({ action: 'parameterDefinitions', worker: 'main', data: paramsDef, }) runMain(params) }, updateParams: ({ params = {} }) => { runMain(params) }, init: (params) => { let baseURI = params.baseURI const alias = params.alias || [] if (!baseURI && typeof document != 'undefined' && document.baseURI) { baseURI = document.baseURI } if (baseURI) workerBaseURI = baseURI.toString() alias.forEach((arr) => { const [orig, ...aliases] = arr aliases.forEach((a) => { require.alias[a] = orig if (a.toLowerCase().substr(-3) !== '.js') require.alias[a + '.js'] = orig }) }) initialized = true if (onInit) onInit() }, } return { // called from outside to pass mesasges into worker postMessage: cmdHandler(handlers), } } /** Make render worker */ const makeRenderWorker = () => { let perspectiveCamera const state = {} const rotateSpeed = 0.002 const panSpeed = 1 const zoomSpeed = 0.08 let rotateDelta = [0, 0] let panDelta = [0, 0] let zoomDelta = 0 let updateRender = true let orbitControls, renderOptions, gridOptions, axisOptions, renderer let entities = [] function createContext(canvas, contextAttributes) { function get(type) { try { return { gl: canvas.getContext(type, contextAttributes), type } } catch (e) { return null } } return ( get('webgl2') || get('webgl') || get('experimental-webgl') || get('webgl-experimental') ) } const startRenderer = ({ canvas, cameraPosition, cameraTarget, axis = {}, grid = {}, }) => { const { prepareRender, drawCommands, cameras, controls, } = require('@jscad/regl-renderer') perspectiveCamera = cameras.perspective orbitControls = controls.orbit state.canvas = canvas state.camera = { ...perspectiveCamera.defaults } if (cameraPosition) state.camera.position = cameraPosition if (cameraTarget) state.camera.target = cameraTarget resize({ width: canvas.width, height: canvas.height }) state.controls = orbitControls.defaults const { gl, type } = createContext(canvas) // prepare the renderer const setupOptions = { glOptions: { gl }, } if (type == 'webgl') { setupOptions.glOptions.optionalExtensions = ['oes_element_index_uint'] } renderer = prepareRender(setupOptions) gridOptions = { visuals: { drawCmd: 'drawGrid', show: grid.show || grid.show === undefined, color: grid.color || [0, 0, 0, 1], subColor: grid.subColor || [0, 0, 1, 0.5], fadeOut: false, transparent: true, }, size: grid.size || [200, 200], ticks: grid.ticks || [10, 1], } axisOptions = { visuals: { drawCmd: 'drawAxis', show: axis.show || axis.show === undefined, }, size: axis.size || 100, } // assemble the options for rendering renderOptions = { camera: state.camera, drawCommands: { drawAxis: drawCommands.drawAxis, drawGrid: drawCommands.drawGrid, drawLines: drawCommands.drawLines, drawMesh: drawCommands.drawMesh, }, // define the visual content entities: [gridOptions, axisOptions, ...entities], } // the heart of rendering, as themes, controls, etc change updateView() } let renderTimer const tmFunc = typeof requestAnimationFrame === 'undefined' ? setTimeout : requestAnimationFrame function updateView(delay = 8) { if (renderTimer || !renderer) return renderTimer = tmFunc(updateAndRender, delay) } const doRotatePanZoom = () => { if (rotateDelta[0] || rotateDelta[1]) { const updated = orbitControls.rotate( { controls: state.controls, camera: state.camera, speed: rotateSpeed }, rotateDelta ) state.controls = { ...state.controls, ...updated.controls } rotateDelta = [0, 0] } if (panDelta[0] || panDelta[1]) { const updated = orbitControls.pan( { controls: state.controls, camera: state.camera, speed: panSpeed }, panDelta ) state.controls = { ...state.controls, ...updated.controls } panDelta = [0, 0] state.camera.position = updated.camera.position state.camera.target = updated.camera.target } if (zoomDelta) { const updated = orbitControls.zoom( { controls: state.controls, camera: state.camera, speed: zoomSpeed }, zoomDelta ) state.controls = { ...state.controls, ...updated.controls } zoomDelta = 0 } } const updateAndRender = () => { renderTimer = null doRotatePanZoom() const updates = orbitControls.update({ controls: state.controls, camera: state.camera, }) state.controls = { ...state.controls, ...updates.controls } if (state.controls.changed) updateView(16) // for elasticity in rotate / zoom state.camera.position = updates.camera.position perspectiveCamera.update(state.camera) renderOptions.entities = [gridOptions, axisOptions, ...entities] renderer(renderOptions) if (updateRender) { updateRender = '' } } function resize({ width, height }) { state.canvas.width = width state.canvas.height = height perspectiveCamera.setProjection(state.camera, state.camera, { width, height, }) perspectiveCamera.update(state.camera, state.camera) updateView() } const handlers = { pan: ({ dx, dy }) => { panDelta[0] += dx panDelta[1] += dy updateView() }, rotate: ({ dx, dy }) => { rotateDelta[0] -= dx rotateDelta[1] -= dy updateView() }, zoom: ({ dy }) => { zoomDelta += dy updateView() }, resize, entities: (params) => { entities = params.entities updateRender = params.scriptStats updateView() }, init: (params) => { if (params.canvas) startRenderer(params) initialized = true }, } return { // called from outside to pass mesasges into worker postMessage: cmdHandler(handlers), } } function start(params) { const { callback = () => {}, convertToSolids = false } = params // by default 'render' messages go outside of this instance (result of modeling) let scriptWorker const sendCmd = (params, transfer) => { if (params.worker === 'script') scriptWorker.postMessage(params, transfer) else { callback(params, transfer) } } scriptWorker = makeScriptWorker({ callback: sendCmd, convertToSolids }) callback({ action: 'workerInit', worker: 'main' }) return { updateParams: ({ params = {} }) => sendCmd({ action: 'updateParams', worker: 'script', params }), runScript: ({ script, url = '' }) => sendCmd({ action: 'runScript', worker: 'script', script, url }), postMessage: sendCmd, } } const init = start({ convertToSolids: 'buffers', callback: (params) => self.postMessage(params), }) self.onmessage = ({ data }) => { if (data.action === 'init') { workerBaseURI = data.baseURI } init.postMessage(data, null) }