diff --git a/app/api/package.json b/app/api/package.json index a57d377..0314a75 100644 --- a/app/api/package.json +++ b/app/api/package.json @@ -6,14 +6,15 @@ "@netlify/functions": "^0.7.2", "@redwoodjs/api": "^0.34.1", "@sentry/node": "^6.5.1", - "chrome-aws-lambda": "^10.1.0", + "chrome-aws-lambda": "^9.1.0", "cloudinary": "^1.23.0", "human-id": "^2.0.1", "nodemailer": "^6.6.2", - "puppeteer": "^10.1.0", - "puppeteer-core": "^10.1.0" + "puppeteer-core": "^9.1.1" }, "devDependencies": { + "puppeteer": "^9.1.1", + "@netlify/functions": "^0.7.2", "@types/nodemailer": "^6.4.2" } } diff --git a/app/api/src/services/og-image-generator.ts.md b/app/api/src/services/og-image-generator.ts.md new file mode 100644 index 0000000..93adbdd --- /dev/null +++ b/app/api/src/services/og-image-generator.ts.md @@ -0,0 +1,71 @@ +// TODO this should be in the functions folder. +// Got the proof of concept working locally, but even though chrome-aws-lambda is supposed to fit into a AWS lambda it did not for me +// in the mean time this is causing builds to fail so moved it out here. +import { builder } from '@netlify/functions' +const { headless, executablePath, puppeteer } = require('chrome-aws-lambda') + +const captureWidth = 1200 +const captureHeight = 630 +const clipY = 0 + +async function unwrappedHandler(event, context) { + const path = event.path + .replace(/.+\/og-image-generator/, '') + .replace(/\/og-image-.+\.jpg/, '') + + const url = `${process.env.URL}/u${path}/social-card` + + const browser = await puppeteer.launch({ + executablePath: process.env.URL?.includes('localhost') + ? null + : await executablePath, + args: [ + '--no-sandbox', + '--disable-web-security', + '--disable-gpu', + '--hide-scrollbars', + '--disable-setuid-sandbox', + ], + // args: chromium.args, + defaultViewport: { + width: captureWidth, + height: captureHeight + clipY, + }, + headless: headless, + }) + const page = await browser.newPage() + + await page.goto(url, { waitUntil: 'networkidle0' }) + + const screenshot = await page.screenshot({ + type: 'jpeg', + // netlify functions can only return strings, so base64 it is + encoding: 'base64', + quality: 70, + clip: { + x: 0, + y: clipY, + width: captureWidth, + height: captureHeight, + }, + }) + + await browser.close() + + if (typeof screenshot !== 'string') { + return { + statusCode: 400, + } + } + + return { + statusCode: 200, + headers: { + 'Content-Type': 'image/jpg', + }, + body: screenshot, + isBase64Encoded: true, + } +} + +export const handler = builder(unwrappedHandler) diff --git a/app/web/public/demo-worker.js b/app/web/public/demo-worker.js new file mode 100644 index 0000000..ebbb97c --- /dev/null +++ b/app/web/public/demo-worker.js @@ -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; 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); +} + + + + + + + + + +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 +}); diff --git a/app/web/src/components/EditorMenu/helpers.ts b/app/web/src/components/EditorMenu/helpers.ts index ff050c6..0ced278 100644 --- a/app/web/src/components/EditorMenu/helpers.ts +++ b/app/web/src/components/EditorMenu/helpers.ts @@ -35,17 +35,19 @@ export const makeStlDownloadHandler = ideType, }: makeStlDownloadHandlerArgs) => () => { - const makeStlBlobFromGeo = flow( - (geo) => new Mesh(geo, new MeshBasicMaterial()), - (mesh) => new Scene().add(mesh), + const makeStlBlobFromMesh = flow( + (...meshes) => new Scene().add(...meshes), (scene) => new STLExporter().parse(scene), (stl) => new Blob([stl], { 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() diff --git a/app/web/src/components/IdeViewer/IdeViewer.tsx b/app/web/src/components/IdeViewer/IdeViewer.tsx index 85675b6..8930b9f 100644 --- a/app/web/src/components/IdeViewer/IdeViewer.tsx +++ b/app/web/src/components/IdeViewer/IdeViewer.tsx @@ -21,6 +21,12 @@ function Asset({ geometry: incomingGeo }) { } }, [incomingGeo]) if (!incomingGeo) return null + + if (incomingGeo.length) + return incomingGeo.map((shape, index) => ( + + )) + return ( @@ -209,8 +215,8 @@ const IdeViewer = ({ Loading }) => { }) }} /> - - + + {state.objectData?.type === 'png' && ( diff --git a/app/web/src/components/Seo/Seo.tsx b/app/web/src/components/Seo/Seo.tsx index 1697b8d..7c8adc5 100644 --- a/app/web/src/components/Seo/Seo.tsx +++ b/app/web/src/components/Seo/Seo.tsx @@ -1,6 +1,16 @@ import { Helmet } from 'react-helmet' -const Seo = ({ title, description, lang, socialImageUrl}: { title: string; description: string; lang: string; socialImageUrl?: string}) => { +const Seo = ({ + title, + description, + lang, + socialImageUrl, +}: { + title: string + description: string + lang: string + socialImageUrl?: string +}) => { return ( <> +
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 = 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) => { - // do your magic - return createUnhealthyResponse( new Date(), 'JSCAD controller not implemented yet') + 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 + }) + + await waitResult + resolveReference = null + return response } const jsCadController: DefaultKernelExport = { diff --git a/app/web/src/helpers/hooks/useIdeState.ts b/app/web/src/helpers/hooks/useIdeState.ts index c38d483..9b671be 100644 --- a/app/web/src/helpers/hooks/useIdeState.ts +++ b/app/web/src/helpers/hooks/useIdeState.ts @@ -11,7 +11,7 @@ function withThunk(dispatch, getState) { export type CadPackage = 'openscad' | 'cadquery' | 'jscad' -const initCodeMap: {[key in CadPackage]: string} = { +const initCodeMap: { [key in CadPackage]: string } = { openscad: `// involute donut // ^ first comment is used for download title (i.e "involute-donut.stl") @@ -42,9 +42,33 @@ result = (cq.Workplane().circle(diam).extrude(20.0) show_object(result) `, -jscad: ` -// TODO implement example JSCAD code. -` + 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} +`, } const codeStorageKey = 'Last-editor-code' diff --git a/app/web/src/pages/ProjectPage/ProjectPage.tsx b/app/web/src/pages/ProjectPage/ProjectPage.tsx index 24577be..187db51 100644 --- a/app/web/src/pages/ProjectPage/ProjectPage.tsx +++ b/app/web/src/pages/ProjectPage/ProjectPage.tsx @@ -9,11 +9,20 @@ import { Toaster } from '@redwoodjs/web/toast' const ProjectPage = ({ userName, projectTitle }) => { const { currentUser } = useAuth() const [state, thunkDispatch] = useIdeState() - const cacheInvalidator = new Date().toISOString().split('-').slice(0, 2).join('-') + const cacheInvalidator = new Date() + .toISOString() + .split('-') + .slice(0, 2) + .join('-') const socialImageUrl = `/.netlify/functions/og-image-generator/${userName}/${projectTitle}/og-image-${cacheInvalidator}.jpg` return ( <> - + -Click the share button in the top right, then select the "external srcipt" tab. +Click the share button in the top right, then select the "external script" tab. diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index d750155..07c9e8a 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -94,12 +94,12 @@ module.exports = { docs: { sidebarPath: require.resolve('./sidebars.js'), editUrl: - 'https://github.com/facebook/docusaurus/edit/master/website/', + 'https://github.com/Irev-Dev/cadhub/blob/main/docs/', }, blog: { showReadingTime: true, editUrl: - 'https://github.com/facebook/docusaurus/edit/master/website/blog/', + 'https://github.com/Irev-Dev/cadhub/blob/main/docs/', }, theme: { customCss: require.resolve('./src/css/custom.css'),