diff --git a/app/web/config/worker-loader.d.ts b/app/web/config/worker-loader.d.ts new file mode 100644 index 0000000..cf7fd6b --- /dev/null +++ b/app/web/config/worker-loader.d.ts @@ -0,0 +1,10 @@ +declare module "worker-loader!*" { + // You need to change `Worker`, if you specified a different value for the `workerType` option + class WebpackWorker extends Worker { + constructor(); + } + + // Uncomment this if you set the `esModule` option to `false` + // export = WebpackWorker; + export default WebpackWorker; +} diff --git a/app/web/package.json b/app/web/package.json index 76b4728..77285a4 100644 --- a/app/web/package.json +++ b/app/web/package.json @@ -45,7 +45,8 @@ "react-tabs": "^3.2.2", "rich-markdown-editor": "^11.0.2", "styled-components": "^5.2.0", - "three": "^0.130.1" + "three": "^0.130.1", + "worker-loader": "^3.0.8" }, "devDependencies": { "@types/lodash": "^4.14.170", @@ -56,4 +57,4 @@ "postcss-loader": "^6.1.1", "tailwindcss": "^2.2.7" } -} \ No newline at end of file +} diff --git a/app/web/public/demo-worker.js b/app/web/public/demo-worker.js deleted file mode 100644 index f49e365..0000000 --- a/app/web/public/demo-worker.js +++ /dev/null @@ -1,786 +0,0 @@ -(function(f) { - if (typeof exports === "object" && typeof module !== "undefined") { - module.exports = f() - } else if (typeof define === "function" && define.amd) { - define([], f) - } else { - var g; - if (typeof window !== "undefined") { - g = window - } else if (typeof global !== "undefined") { - g = global - } else if (typeof self !== "undefined") { - g = self - } else { - g = this - } - g.jscadWorker = f() - } -})(function() { -// multi purpose module - - - -const setPoints = (points, p, i)=>{ - points[i++] = p[0] - points[i++] = p[1] - points[i++] = p[2] || 0 -} - -function CSG2Vertices(csg){ - let idx = 0 - - let vLen = 0, iLen = 0 - for (let poly of csg.polygons){ - let 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 (let poly of csg.polygons){ - let arr = poly.vertices - let 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; isetPoints(vertices, p, idx * 3 )) - - if(csg.isClosed){ - setPoints(vertices, csg.points[0], vertices.length - 3 ) - } - return {vertices, type:'line'} -} - -function CSG2LineSegmentsVertices(csg){ - let vLen = csg.sides.length * 6 - - var vertices = new Float32Array(vLen) - csg.sides.forEach((side,idx)=>{ - let 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 - let 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{ - let 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 initCanvas = (canvas, callback)=>{ - - // convert HTML events (mouse movement) to viewer changes - let lastX = 0 - let lastY = 0 - - let pointerDown = false - - const moveHandler = (ev) => { - if(!pointerDown) return - const cmd = { - worker: 'render', - dx: lastX - ev.pageX, - dy: ev.pageY - lastY - } - - const shiftKey = (ev.shiftKey === true) || (ev.touches && ev.touches.length > 2) - cmd.action = shiftKey ? 'pan':'rotate' - callback(cmd) - - lastX = ev.pageX - lastY = ev.pageY - - ev.preventDefault() - } - const downHandler = (ev) => { - pointerDown = true - lastX = ev.pageX - lastY = ev.pageY - canvas.setPointerCapture(ev.pointerId) - ev.preventDefault() - } - - const upHandler = (ev) => { - pointerDown = false - canvas.releasePointerCapture(ev.pointerId) - ev.preventDefault() - } - - const wheelHandler = (ev) => { - callback({action:'zoom', dy:ev.deltaY, worker: 'render'}) - ev.preventDefault() - } - - canvas.onpointermove = moveHandler - canvas.onpointerdown = downHandler - canvas.onpointerup = upHandler - canvas.onwheel = wheelHandler -} - -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, next, lineNum - while(i12 && line.substring(line.length-13) == '//jscadparams') break; - if(line.length>12 && line.indexOf('@jscad-params') != -1) break; - } - - let groupIndex = 1 - const defs = [] - - while(i{ - let workerBaseURI, onInit - - - function runMain(params={}){ - let time = Date.now() - let solids - let transfer = [] - try{ - let 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 - } - let solidsTime = Date.now() - time - scriptStats = `generate solids ${solidsTime}ms` - - if(convertToSolids === 'buffers'){ - CSGToBuffers.clearCache() - entities = solids.filter(s=>s).map((csg)=>{ - let 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 - let gp = script_module.exports.getParameterDefinitions - let paramsDef = parseParams(script) || [] - if(gp){ - gp().forEach(p=>{ - let idx = paramsDef.findIndex(old=>old.name == p.name) - if(idx === -1){ - paramsDef.push(p) - }else{ - paramsDef.splice(idx,1,p) - } - }) - } - if(paramsDef.length) callback({action:'parameterDefinitions', worker:'main', data:paramsDef}) - - runMain(params) - }, - updateParams: ({params={}})=>{ - runMain(params) - }, - init: (params)=>{ - let {baseURI, alias=[]} = params - if(!baseURI && typeof document != 'undefined' && document.baseURI){ - baseURI = document.baseURI - } - - if(baseURI) workerBaseURI = baseURI.toString() - - alias.forEach(arr=>{ - let [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 = Object.assign({}, 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 = (timestamp) => { - 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 - ] - let time = Date.now() - 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), - } -} - - - - - - - - - - -return (params)=>{ - let { canvas, baseURI=(typeof document === 'undefined') ? '':document.location.toString(), scope='main', renderInWorker, render, callback=()=>{}, scriptUrl='demo-worker.js', alias, convertToSolids=false } = params - // by default 'render' messages go outside of this instance (result of modeling) - let sendToRender = callback - let scriptWorker, renderWorker - workerBaseURI = baseURI - - const sendCmd = (params, transfer)=>{ - if(params.worker === 'render') - sendToRender(params, transfer) - else if(params.worker === 'script') - scriptWorker.postMessage(params, transfer) - else{ - // parameter definitions will arrive from scriptWorker - callback(params, transfer) - } - } - - const updateSize = function({width,height}){ - sendCmd({ action:'resize', worker:'render', width: canvas.offsetWidth, height: canvas.offsetHeight}) - } - - - renderInWorker = !!(canvas && renderInWorker && canvas.transferControlToOffscreen) - const makeRenderWorkerHere = (scope === 'main' && canvas && !renderInWorker) || (scope === 'worker' && render) - // worker is in current thread - if(makeRenderWorkerHere){ - renderWorker = makeRenderWorker({callback:sendCmd}) - sendToRender = (params, transfer)=>renderWorker.postMessage(params, transfer) - } - - if(scope === 'main'){ -// let extraScript = renderInWorker ? `,'https://unpkg.com/@jscad/regl-renderer'`:'' - let script =`let baseURI = '${baseURI}' -importScripts(new URL('${scriptUrl}',baseURI)) -let worker = jscadWorker({ - baseURI: baseURI, - convertToSolids: ${convertToSolids}, - scope:'worker', - callback:(params)=>self.postMessage(params), - render:${renderInWorker} -}) -self.addEventListener('message', (e)=>worker.postMessage(e.data)) -` - let blob = new Blob([script],{type: 'text/javascript'}) - scriptWorker = new Worker(window.URL.createObjectURL(blob)) - scriptWorker.addEventListener('message',(e)=>sendCmd(e.data)) - scriptWorker.postMessage({action:'init', baseURI, alias}) - if(renderInWorker) renderWorker = scriptWorker - - if(canvas){ - initCanvas(canvas, sendCmd) - window.addEventListener('resize',updateSize) - } - }else{ - scriptWorker = makeScriptWorker({callback:sendCmd, convertToSolids}) - callback({action:'workerInit',worker:'main'}) - } - - if(canvas){ - // redirect 'render' messages to renderWorker - sendToRender = (params, transfer)=>renderWorker.postMessage(params, transfer) - let width = canvas.width = canvas.clientWidth - let height = canvas.height = canvas.clientHeight - if(scope == 'main'){ - const offscreen = renderInWorker ? canvas.transferControlToOffscreen() : canvas - renderWorker.postMessage({action:'init', worker:'render', canvas:offscreen, width, height}, [offscreen]) - } - } - - return { - updateSize, - updateParams:({params={}})=>sendCmd({ action:'updateParams', worker:'script', params}), - runScript: ({script,url=''})=>sendCmd({ action:'runScript', worker:'script', script, url}), - postMessage: sendCmd, - } -} - - -// multi purpose module -}); diff --git a/app/web/src/helpers/cadPackages/jsCad/jsCadController.tsx b/app/web/src/helpers/cadPackages/jsCad/jsCadController.tsx index 4f8c2f2..5562de7 100644 --- a/app/web/src/helpers/cadPackages/jsCad/jsCadController.tsx +++ b/app/web/src/helpers/cadPackages/jsCad/jsCadController.tsx @@ -16,6 +16,7 @@ import { Mesh, } from 'three' import { jsCadToCadhubParams } from './jscadParams' +import TheWorker from 'worker-loader!./jscadWorker' const materials = { mesh: { @@ -134,19 +135,7 @@ export const render: DefaultKernelExport['render'] = async ({ }: RenderArgs) => { if (!scriptWorker) { const baseURI = document.baseURI.toString() - const scriptUrl = '/demo-worker.js' - const script = `let baseURI = '${baseURI}' - importScripts(new URL('${scriptUrl}',baseURI)) - let worker = jscadWorker({ - baseURI: baseURI, - scope:'worker', - convertToSolids: 'buffers', - callback:(params)=>self.postMessage(params), - }) - self.addEventListener('message', (e)=>worker.postMessage(e.data)) - ` - const blob = new Blob([script], { type: 'text/javascript' }) - scriptWorker = new Worker(window.URL.createObjectURL(blob)) + scriptWorker = new TheWorker() let parameterDefinitions = [] scriptWorker.addEventListener('message', ({ data }) => { if (data.action == 'parameterDefinitions') { diff --git a/app/web/src/helpers/cadPackages/jsCad/jscadWorker.ts b/app/web/src/helpers/cadPackages/jsCad/jscadWorker.ts new file mode 100644 index 0000000..3f8f6a1 --- /dev/null +++ b/app/web/src/helpers/cadPackages/jsCad/jscadWorker.ts @@ -0,0 +1,695 @@ +const setPoints = (points, p, i) => { + points[i++] = p[0] + points[i++] = p[1] + points[i++] = p[2] || 0 +} + +function CSG2Vertices(csg) { + const idx = 0 + + 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, + next, + 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 + next = lines[i + 1] ? lines[i + 1].code : '' + 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 + + function runMain(params = {}) { + 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.splice(idx, 1, p) + } + }) + } + if (paramsDef.length) + callback({ + action: 'parameterDefinitions', + worker: 'main', + data: paramsDef, + }) + + runMain(params) + }, + updateParams: ({ params = {} }) => { + runMain(params) + }, + init: (params) => { + let { baseURI, alias = [] } = params + 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 = (timestamp) => { + 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] + const time = Date.now() + 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) +} diff --git a/app/yarn.lock b/app/yarn.lock index 3adfcb0..f75c025 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -19057,6 +19057,14 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" +worker-loader@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-3.0.8.tgz#5fc5cda4a3d3163d9c274a4e3a811ce8b60dbb37" + integrity sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + worker-rpc@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/worker-rpc/-/worker-rpc-0.1.1.tgz#cb565bd6d7071a8f16660686051e969ad32f54d5"