initial jscad integration #428
1
.vscode/settings.json
vendored
@@ -2,6 +2,7 @@
|
||||
"cSpell.words": [
|
||||
"Hutten",
|
||||
"cadquery",
|
||||
"jscad",
|
||||
"openscad",
|
||||
"sendmail"
|
||||
]
|
||||
|
||||
@@ -41,6 +41,7 @@ model User {
|
||||
enum CadPackage {
|
||||
openscad
|
||||
cadquery
|
||||
// jscad // TODO #422, add jscad to db schema when were ready to enable saving of jscad projects
|
||||
}
|
||||
|
||||
model Project {
|
||||
|
||||
625
app/web/public/demo-worker.js
Normal file
@@ -0,0 +1,625 @@
|
||||
(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; 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
|
||||
|
||||
var 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){
|
||||
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){
|
||||
|
👌 out of curiosity are we currently locked into the old node 👌 out of curiosity are we currently locked into the old node `require` syntax only? Wondering if you have ideas for `import` syntax?
it is just a piece of code I was able to hack-up that works. Like that hack it is just a piece of code I was able to hack-up that works. Like that hack `useThree` for react-fiber. The worker needs more refinement for sure.
yup, sweet. yup, sweet.
|
||||
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) => {
|
||||
|
I don't quiet follow how these handlers fit in? especially since this runs in the worker. Could you explain it a little? I don't quiet follow how these handlers fit in? especially since this runs in the worker. Could you explain it a little?
it is code when canvas is used directly
I was not sure if we would go in direction of direct canvass access. for now, this integration only "script eval" part is needed. I curently copy/pasta the worker from my other prototype. After I refine the prototype worker, I will have a version that only has "script eval" part. it is code when canvas is used directly
The worker has 3 parts:
- renderer
- orbit controls
- script eval
I was not sure if we would go in direction of direct canvass access. for now, this integration only "script eval" part is needed.
I curently copy/pasta the worker from my other prototype. After I refine the prototype worker, I will have a version that only has "script eval" part.
okay yup, cool. okay yup, cool.
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const makeScriptWorker = ({callback, convertToSolids})=>{
|
||||
let workerBaseURI, onInit
|
||||
|
||||
function runMain(params={}){
|
||||
let time = Date.now()
|
||||
let solids
|
||||
try{
|
||||
solids = main(params)
|
||||
}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`
|
||||
|
||||
let transfer = []
|
||||
if(convertToSolids === 'buffers'){
|
||||
CSGToBuffers.clearCache()
|
||||
entities = solids.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
|
||||
if(gp){
|
||||
callback({action:'parameterDefinitions', worker:'main', data:gp()})
|
||||
}
|
||||
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){
|
||||
console.log(updateRender, ' first render', Date.now()-time);
|
||||
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){
|
||||
console.log('render in scope: '+scope);
|
||||
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
|
||||
});
|
||||
@@ -35,8 +35,7 @@ export const makeStlDownloadHandler =
|
||||
ideType,
|
||||
}: makeStlDownloadHandlerArgs) =>
|
||||
() => {
|
||||
const makeStlBlobFromGeo = flow(
|
||||
(geo) => new Mesh(geo, new MeshBasicMaterial()),
|
||||
const makeStlBlobFromMesh = flow(
|
||||
(mesh) => new Scene().add(mesh),
|
||||
(scene) => new STLExporter().parse(scene),
|
||||
(stl) =>
|
||||
@@ -44,8 +43,11 @@ export const makeStlDownloadHandler =
|
||||
type: 'text/plain',
|
||||
})
|
||||
)
|
||||
const saveFile = (geometry) => {
|
||||
const blob = makeStlBlobFromGeo(geometry)
|
||||
const makeStlBlobFromGeo = flow(
|
||||
(geo) => new Mesh(geo, new MeshBasicMaterial()),
|
||||
(mesh) => makeStlBlobFromMesh(mesh)
|
||||
)
|
||||
const saveFile = (blob) => {
|
||||
fileSave(blob, {
|
||||
fileName,
|
||||
extensions: ['.stl'],
|
||||
@@ -56,7 +58,9 @@ export const makeStlDownloadHandler =
|
||||
type === 'geometry' &&
|
||||
(quality === 'high' || ideType === 'openscad')
|
||||
) {
|
||||
saveFile(geometry)
|
||||
saveFile(makeStlBlobFromGeo(geometry))
|
||||
} else if (ideType == 'jscad') {
|
||||
saveFile(makeStlBlobFromMesh(geometry))
|
||||
} else {
|
||||
thunkDispatch((dispatch, getState) => {
|
||||
const state = getState()
|
||||
|
||||
@@ -4,6 +4,8 @@ import { makeCodeStoreKey, requestRender } from 'src/helpers/hooks/useIdeState'
|
||||
import Editor, { useMonaco } from '@monaco-editor/react'
|
||||
import { theme } from 'src/../tailwind.config'
|
||||
import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode'
|
||||
import type { CadPackage as CadPackageType } from 'src/helpers/hooks/useIdeState'
|
||||
import CadPackage from '../CadPackage/CadPackage'
|
||||
|
||||
const colors = theme.extend.colors
|
||||
|
||||
@@ -12,9 +14,10 @@ const IdeEditor = ({ Loading }) => {
|
||||
const [theme, setTheme] = useState('vs-dark')
|
||||
const saveCode = useSaveCode()
|
||||
|
||||
const ideTypeToLanguageMap = {
|
||||
const ideTypeToLanguageMap: { [key in CadPackageType]: string } = {
|
||||
cadquery: 'python',
|
||||
openscad: 'cpp',
|
||||
jscad: 'javascript',
|
||||
}
|
||||
const monaco = useMonaco()
|
||||
useEffect(() => {
|
||||
@@ -21,6 +21,12 @@ function Asset({ geometry: incomingGeo }) {
|
||||
}
|
||||
}, [incomingGeo])
|
||||
if (!incomingGeo) return null
|
||||
|
||||
if (incomingGeo.length)
|
||||
return incomingGeo.map((shape, index) => (
|
||||
<primitive object={shape} key={index} />
|
||||
))
|
||||
|
||||
return (
|
||||
<mesh ref={mesh} scale={[1, 1, 1]}>
|
||||
<bufferGeometry attach="geometry" ref={ref} />
|
||||
@@ -209,8 +215,8 @@ const IdeViewer = ({ Loading }) => {
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ambientLight intensity={1} />
|
||||
<pointLight position={[15, 5, 10]} intensity={4} />
|
||||
<ambientLight intensity={0.3} />
|
||||
<pointLight position={[15, 5, 10]} intensity={0.1} />
|
||||
<pointLight position={[-1000, -1000, -1000]} intensity={1} />
|
||||
<pointLight position={[-1000, 0, 1000]} intensity={1} />
|
||||
{state.objectData?.type === 'png' && (
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
import Svg from 'src/components/Svg/Svg'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import type { CadPackage } from 'src/helpers/hooks/useIdeState'
|
||||
|
||||
const menuOptions: {
|
||||
name: string
|
||||
sub: string
|
||||
ideType: CadPackage
|
||||
}[] = [
|
||||
{
|
||||
name: 'OpenSCAD',
|
||||
sub: 'beta',
|
||||
ideType: 'openscad',
|
||||
},
|
||||
{ name: 'CadQuery', sub: 'beta', ideType: 'cadquery' },
|
||||
// { name: 'JSCAD', sub: 'alpha', ideType: 'jscad' }, // TODO #422, add jscad to db schema when were ready to enable saving of jscad projects
|
||||
]
|
||||
|
||||
const NavPlusButton: React.FC = () => {
|
||||
return (
|
||||
@@ -11,14 +26,7 @@ const NavPlusButton: React.FC = () => {
|
||||
|
||||
<Popover.Panel className="absolute z-10 right-0">
|
||||
<ul className="bg-gray-200 mt-4 rounded shadow-md overflow-hidden">
|
||||
{[
|
||||
{
|
||||
name: 'OpenSCAD',
|
||||
sub: 'beta',
|
||||
ideType: 'openscad',
|
||||
},
|
||||
{ name: 'CadQuery', sub: 'beta', ideType: 'cadquery' },
|
||||
].map(({ name, sub, ideType }) => (
|
||||
{menuOptions.map(({ name, sub, ideType }) => (
|
||||
<li
|
||||
key={name}
|
||||
className="px-4 py-2 hover:bg-gray-400 text-gray-800"
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { DefaultKernelExport } from './common'
|
||||
import type { CadPackage } from 'src/helpers/hooks/useIdeState'
|
||||
|
||||
import openscad from './openScadController'
|
||||
import cadquery from './cadQueryController'
|
||||
import jscad from './jsCadController'
|
||||
|
||||
export const cadPackages = {
|
||||
export const cadPackages: { [key in CadPackage]: DefaultKernelExport } = {
|
||||
openscad,
|
||||
cadquery,
|
||||
jscad,
|
||||
}
|
||||
|
||||
142
app/web/src/helpers/cadPackages/jsCadController.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
RenderArgs,
|
||||
DefaultKernelExport,
|
||||
createUnhealthyResponse,
|
||||
createHealthyResponse,
|
||||
} from './common'
|
||||
import {
|
||||
MeshPhongMaterial,
|
||||
LineBasicMaterial,
|
||||
BufferGeometry,
|
||||
BufferAttribute,
|
||||
Line,
|
||||
LineSegments,
|
||||
Color,
|
||||
Mesh,
|
||||
Group,
|
||||
} from 'three/build/three.module'
|
||||
|
||||
const materials = {
|
||||
mesh: {
|
||||
def: new MeshPhongMaterial({ color: 0x0084d1, flatShading: true }),
|
||||
material: (params) => new MeshPhongMaterial(params),
|
||||
},
|
||||
line: {
|
||||
def: new LineBasicMaterial({ color: 0x0000ff }),
|
||||
material: ({ color, opacity, transparent }) =>
|
||||
new LineBasicMaterial({ color, opacity, transparent }),
|
||||
},
|
||||
lines: null,
|
||||
}
|
||||
materials.lines = materials.line
|
||||
|
||||
function CSG2Object3D(obj) {
|
||||
const { vertices, indices, color, transforms } = obj
|
||||
|
||||
const materialDef = materials[obj.type]
|
||||
if (!materialDef) {
|
||||
console.error('Can not hangle object type: ' + obj.type, obj)
|
||||
return null
|
||||
}
|
||||
|
||||
let material = materialDef.def
|
||||
if (color) {
|
||||
const c = color
|
||||
material = new materialDef.material({
|
||||
color: new Color(c[0], c[1], c[2]),
|
||||
flatShading: true,
|
||||
opacity: c[3] === void 0 ? 1 : c[3],
|
||||
transparent: c[3] != 1 && c[3] !== void 0,
|
||||
})
|
||||
}
|
||||
|
||||
const geo = new BufferGeometry()
|
||||
geo.setAttribute('position', new BufferAttribute(vertices, 3))
|
||||
|
||||
let mesh
|
||||
switch (obj.type) {
|
||||
case 'mesh':
|
||||
geo.setIndex(new BufferAttribute(indices, 1))
|
||||
mesh = new Mesh(geo, material)
|
||||
break
|
||||
case 'line':
|
||||
mesh = new Line(geo, material)
|
||||
break
|
||||
case 'lines':
|
||||
mesh = new LineSegments(geo, material)
|
||||
break
|
||||
}
|
||||
if (transforms) mesh.applyMatrix4({ elements: transforms })
|
||||
return mesh
|
||||
}
|
||||
|
||||
let scriptWorker
|
||||
const scriptUrl = '/demo-worker.js'
|
||||
let resolveReference = null
|
||||
let response = null
|
||||
|
||||
const callResolve = () => {
|
||||
if (resolveReference) resolveReference()
|
||||
resolveReference
|
||||
}
|
||||
|
||||
export const render: DefaultKernelExport['render'] = async ({
|
||||
code,
|
||||
settings,
|
||||
}: RenderArgs) => {
|
||||
if (!scriptWorker) {
|
||||
const baseURI = document.baseURI.toString()
|
||||
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.addEventListener('message', (e) => {
|
||||
const data = e.data
|
||||
if (data.action == 'entities') {
|
||||
if (data.error) {
|
||||
response = createUnhealthyResponse(new Date(), data.error)
|
||||
} else {
|
||||
response = createHealthyResponse({
|
||||
type: 'geometry',
|
||||
data: [...data.entities.map(CSG2Object3D).filter((o) => o)],
|
||||
consoleMessage: data.scriptStats,
|
||||
date: new Date(),
|
||||
})
|
||||
}
|
||||
callResolve()
|
||||
}
|
||||
})
|
||||
|
||||
callResolve()
|
||||
response = null
|
||||
scriptWorker.postMessage({ action: 'init', baseURI, alias: [] })
|
||||
}
|
||||
scriptWorker.postMessage({
|
||||
action: 'runScript',
|
||||
worker: 'script',
|
||||
script: code,
|
||||
url: 'jscad_script',
|
||||
})
|
||||
|
||||
const waitResult = new Promise((resolve) => {
|
||||
resolveReference = resolve
|
||||
|
hmmm I might have a bit of think of how we might be able to change this, and a few other bits related to turning a worker message into a promise. assigning the resolve function to an upper scope var smells a bit off to me. hmmm I might have a bit of think of how we might be able to change this, and a few other bits related to turning a worker message into a promise. assigning the resolve function to an upper scope var smells a bit off to me.
I was thinking that I need to stop pervious job if next one comes before first one is finished. hence the upper scope. I was thinking that I need to stop pervious job if next one comes before first one is finished. hence the upper scope.
|
||||
})
|
||||
|
||||
await waitResult
|
||||
resolveReference = null
|
||||
return response
|
||||
}
|
||||
|
||||
const jsCadController: DefaultKernelExport = {
|
||||
render,
|
||||
}
|
||||
|
||||
export default jsCadController
|
||||
@@ -9,7 +9,9 @@ function withThunk(dispatch, getState) {
|
||||
: dispatch(actionOrThunk)
|
||||
}
|
||||
|
||||
const initCodeMap = {
|
||||
export type CadPackage = 'openscad' | 'cadquery' | 'jscad'
|
||||
|
||||
const initCodeMap: { [key in CadPackage]: string } = {
|
||||
openscad: `// involute donut
|
||||
|
||||
// ^ first comment is used for download title (i.e "involute-donut.stl")
|
||||
@@ -39,6 +41,33 @@ result = (cq.Workplane().circle(diam).extrude(20.0)
|
||||
.edges("%CIRCLE").chamfer(0.15))
|
||||
|
||||
show_object(result)
|
||||
`,
|
||||
jscad: `
|
||||
const { booleans, colors, primitives } = require('@jscad/modeling') // modeling comes from the included MODELING library
|
||||
|
||||
const { intersect, subtract } = booleans
|
||||
const { colorize } = colors
|
||||
const { cube, cuboid, line, sphere, star } = primitives
|
||||
|
||||
const main = ({scale=1}) => {
|
||||
const logo = [
|
||||
colorize([1.0, 0.4, 1.0], subtract(
|
||||
cube({ size: 300 }),
|
||||
sphere({ radius: 200 })
|
||||
)),
|
||||
colorize([1.0, 1.0, 0], intersect(
|
||||
sphere({ radius: 130 }),
|
||||
cube({ size: 210 })
|
||||
))
|
||||
]
|
||||
|
||||
const transpCube = colorize([1, 0, 0, 0.75], cuboid({ size: [100 * scale, 100, 210 + (200 * scale)] }))
|
||||
const star2D = star({ vertices: 8, innerRadius: 150, outerRadius: 200 })
|
||||
const line2D = colorize([1.0, 0, 0], line([[220, 220], [-220, 220], [-220, -220], [220, -220], [220, 220]]))
|
||||
|
||||
return [transpCube, star2D, line2D, ...logo]
|
||||
}
|
||||
module.exports = {main}
|
||||
`,
|
||||
}
|
||||
|
||||
@@ -53,7 +82,7 @@ interface XYZ {
|
||||
}
|
||||
|
||||
export interface State {
|
||||
ideType: 'INIT' | 'openscad' | 'cadquery'
|
||||
ideType: 'INIT' | CadPackage
|
||||
consoleMessages: { type: 'message' | 'error'; message: string; time: Date }[]
|
||||
code: string
|
||||
objectData: {
|
||||
|
||||
I understand why you've done it, having this file just hanging in the public folder is not something I'd like to have long term, mostly because unlike the rest of the repo this isn't transpiled so suddenly we have to be concious of old browsers just for this one file.
There are 2 ways to run a worker.
option 2. would enable bundling the code together with rest of the app and can be done in two ways
worker is still heavily under construction, and currently this way it is easier to debug for me. After it is further improved we will look to integrate it instead of having it like this in public folder.
Fair enough.
Once you're happy with it/it's settled down a little, I'd be happy to work this into the bundle. I did something similar in another webpack project
https://github.com/zalo/CascadeStudio/pull/58/files#diff-d4a06c27ddf0c24f2ba6029bffc9b20062214e07d6ee72822997fb6941b2534d