initial jscad integration #428

Merged
hrgdavor merged 15 commits from kurt/411-demo-branch into main 2021-08-01 11:35:56 +02:00
10 changed files with 843 additions and 19 deletions

View File

@@ -2,6 +2,7 @@
"cSpell.words": [
"Hutten",
"cadquery",
"jscad",
"openscad",
"sendmail"
]

View File

@@ -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 {

View File

@@ -0,0 +1,625 @@
(function(f) {
Irev-Dev commented 2021-08-01 10:31:46 +02:00 (Migrated from github.com)
Review

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.

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.
hrgdavor commented 2021-08-01 10:38:28 +02:00 (Migrated from github.com)
Review

There are 2 ways to run a worker.

  1. js file
  2. generated code in a blob

option 2. would enable bundling the code together with rest of the app and can be done in two ways

  • paste the whole worker code into a string
  • wrap the worker code in a function and use funtion.toString() to extract the code later

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.

There are 2 ways to run a worker. 1. js file 2. generated code in a blob option 2. would enable bundling the code together with rest of the app and can be done in two ways - paste the whole worker code into a string - wrap the worker code in a function and use funtion.toString() to extract the code later 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.
Irev-Dev commented 2021-08-01 11:25:37 +02:00 (Migrated from github.com)
Review

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

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
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){
Irev-Dev commented 2021-08-01 10:37:05 +02:00 (Migrated from github.com)
Review

👌 out of curiosity are we currently locked into the old node require syntax only? Wondering if you have ideas for import syntax?

👌 out of curiosity are we currently locked into the old node `require` syntax only? Wondering if you have ideas for `import` syntax?
hrgdavor commented 2021-08-01 10:41:07 +02:00 (Migrated from github.com)
Review

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.

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.
Irev-Dev commented 2021-08-01 11:19:32 +02:00 (Migrated from github.com)
Review

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) => {
Irev-Dev commented 2021-08-01 10:39:39 +02:00 (Migrated from github.com)
Review

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?
hrgdavor commented 2021-08-01 10:46:30 +02:00 (Migrated from github.com)
Review

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.

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.
Irev-Dev commented 2021-08-01 11:19:06 +02:00 (Migrated from github.com)
Review

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
});

View File

@@ -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()

View File

@@ -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(() => {

View File

@@ -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' && (

View File

@@ -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"

View File

@@ -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,
}

View 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
Irev-Dev commented 2021-08-01 11:04:09 +02:00 (Migrated from github.com)
Review

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.
hrgdavor commented 2021-08-01 11:20:51 +02:00 (Migrated from github.com)
Review

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

View File

@@ -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: {