Add stl download for OpenSCAD and CadQuery IDEs #331
@@ -1,4 +1,4 @@
|
|||||||
const { runScad } = require('./runScad')
|
const { runScad, stlExport } = require('./runScad')
|
||||||
const middy = require('middy')
|
const middy = require('middy')
|
||||||
const { cors } = require('middy/middlewares')
|
const { cors } = require('middy/middlewares')
|
||||||
const AWS = require('aws-sdk')
|
const AWS = require('aws-sdk')
|
||||||
@@ -12,25 +12,52 @@ const {
|
|||||||
|
|
||||||
const s3 = new AWS.S3()
|
const s3 = new AWS.S3()
|
||||||
|
|
||||||
|
const openScadStlKey = (eventBody) => {
|
||||||
|
const { file } = JSON.parse(eventBody)
|
||||||
|
return `${makeHash(JSON.stringify(file))}.stl`
|
||||||
|
}
|
||||||
|
|
||||||
const preview = async (req, _context, callback) => {
|
const preview = async (req, _context, callback) => {
|
||||||
_context.callbackWaitsForEmptyEventLoop = false
|
_context.callbackWaitsForEmptyEventLoop = false
|
||||||
const eventBody = req.body
|
const eventBody = req.body
|
||||||
console.log('eventBody', eventBody)
|
console.log('eventBody', eventBody)
|
||||||
const key = `${makeHash(eventBody)}.png`
|
const key = `${makeHash(eventBody)}.png`
|
||||||
|
const stlKey = openScadStlKey(eventBody)
|
||||||
|
|
||||||
console.log('key', key)
|
console.log('key', key)
|
||||||
|
|
||||||
|
const stlParams = {
|
||||||
|
Bucket: process.env.BUCKET,
|
||||||
|
Key: stlKey,
|
||||||
|
}
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: process.env.BUCKET,
|
Bucket: process.env.BUCKET,
|
||||||
Key: key,
|
Key: key,
|
||||||
}
|
}
|
||||||
const previousAsset = await checkIfAlreadyExists(params, s3)
|
const [previousAssetStl, previousAssetPng] = await Promise.all([
|
||||||
|
checkIfAlreadyExists(stlParams, s3),
|
||||||
|
checkIfAlreadyExists(params, s3),
|
||||||
|
])
|
||||||
|
const type = previousAssetStl.isAlreadyInBucket ? 'stl' : 'png'
|
||||||
|
const previousAsset = previousAssetStl.isAlreadyInBucket
|
||||||
|
? previousAssetStl
|
||||||
|
: previousAssetPng
|
||||||
if (previousAsset.isAlreadyInBucket) {
|
if (previousAsset.isAlreadyInBucket) {
|
||||||
console.log('already in bucket')
|
console.log('already in bucket')
|
||||||
const response = {
|
const response = {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
url: getObjectUrl(params, s3),
|
url: getObjectUrl(
|
||||||
consoleMessage: previousAsset.consoleMessage,
|
{
|
||||||
|
Bucket: process.env.BUCKET,
|
||||||
|
Key: previousAssetStl.isAlreadyInBucket ? stlKey : key,
|
||||||
|
},
|
||||||
|
s3
|
||||||
|
),
|
||||||
|
consoleMessage:
|
||||||
|
previousAsset.consoleMessage || previousAssetPng.consoleMessage,
|
||||||
|
type,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
callback(null, response)
|
callback(null, response)
|
||||||
@@ -50,39 +77,46 @@ const preview = async (req, _context, callback) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// const stl = async (req, _context, callback) => {
|
const stl = async (req, _context, callback) => {
|
||||||
// _context.callbackWaitsForEmptyEventLoop = false
|
_context.callbackWaitsForEmptyEventLoop = false
|
||||||
// const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
|
const eventBody = req.body
|
||||||
// console.log(eventBody, 'eventBody')
|
console.log(eventBody, 'eventBody')
|
||||||
// const { file } = JSON.parse(eventBody)
|
const stlKey = openScadStlKey(eventBody)
|
||||||
// const { error, result, tempFile } = await stlExport({ file })
|
|
||||||
// if (error) {
|
console.log('key', stlKey)
|
||||||
// const response = {
|
|
||||||
// statusCode: 400,
|
const params = {
|
||||||
// body: { error, tempFile },
|
Bucket: process.env.BUCKET,
|
||||||
// }
|
Key: stlKey,
|
||||||
// callback(null, response)
|
}
|
||||||
// } else {
|
console.log('original params', params)
|
||||||
// console.log(`got result in route: ${result}, file is: ${tempFile}`)
|
const previousAsset = await checkIfAlreadyExists(params, s3)
|
||||||
// const fs = require('fs')
|
if (previousAsset.isAlreadyInBucket) {
|
||||||
// const stl = fs.readFileSync(`/tmp/${tempFile}/output.stl`, {
|
console.log('already in bucket')
|
||||||
// encoding: 'base64',
|
const response = {
|
||||||
// })
|
statusCode: 200,
|
||||||
// console.log('encoded stl', stl)
|
body: JSON.stringify({
|
||||||
// const response = {
|
url: getObjectUrl({ ...params }, s3),
|
||||||
// statusCode: 200,
|
consoleMessage: previousAsset.consoleMessage,
|
||||||
// headers: {
|
}),
|
||||||
// 'content-type': 'application/stl',
|
}
|
||||||
// },
|
callback(null, response)
|
||||||
// body: stl,
|
return
|
||||||
// isBase64Encoded: true,
|
}
|
||||||
// }
|
const { file } = JSON.parse(eventBody)
|
||||||
// console.log('callback fired')
|
const { error, consoleMessage, fullPath } = await stlExport({ file })
|
||||||
// callback(null, response)
|
await storeAssetAndReturnUrl({
|
||||||
// }
|
error,
|
||||||
// }
|
callback,
|
||||||
|
fullPath,
|
||||||
|
consoleMessage,
|
||||||
|
key: stlKey,
|
||||||
|
s3,
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// stl: middy(stl).use(cors()),
|
stl: middy(stl).use(cors()),
|
||||||
preview: middy(loggerWrap(preview)).use(cors()),
|
preview: middy(loggerWrap(preview)).use(cors()),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,14 +35,15 @@ module.exports.runScad = async ({
|
|||||||
|
|
||||||
module.exports.stlExport = async ({ file } = {}) => {
|
module.exports.stlExport = async ({ file } = {}) => {
|
||||||
const tempFile = await makeFile(file, '.scad', nanoid)
|
const tempFile = await makeFile(file, '.scad', nanoid)
|
||||||
|
const fullPath = `/tmp/${tempFile}/output.stl`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await runCommand(
|
const consoleMessage = await runCommand(
|
||||||
`openscad -o /tmp/${tempFile}/output.stl /tmp/${tempFile}/main.scad`,
|
`xvfb-run --auto-servernum --server-args "-screen 0 1024x768x24" openscad -o ${fullPath} /tmp/${tempFile}/main.scad`,
|
||||||
300000 // lambda will time out before this, we might need to look at background jobs if we do git integration stl generation
|
60000 // lambda will time out before this, we might need to look at background jobs if we do git integration stl generation
|
||||||
)
|
)
|
||||||
return { result, tempFile }
|
return { consoleMessage, fullPath }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { error, tempFile }
|
return { error, fullPath }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,18 +63,21 @@ functions:
|
|||||||
timeout: 25
|
timeout: 25
|
||||||
environment:
|
environment:
|
||||||
BUCKET: cad-preview-bucket-prod-001
|
BUCKET: cad-preview-bucket-prod-001
|
||||||
# openscadstl:
|
openscadstl:
|
||||||
# image:
|
image:
|
||||||
# name: openscadimage
|
name: openscadimage
|
||||||
# command:
|
command:
|
||||||
# - openscad.stl
|
- openscad.stl
|
||||||
# entryPoint:
|
entryPoint:
|
||||||
# - '/entrypoint.sh'
|
- '/entrypoint.sh'
|
||||||
# events:
|
events:
|
||||||
# - http:
|
- http:
|
||||||
# path: openscad/stl
|
path: openscad/stl
|
||||||
# method: post
|
method: post
|
||||||
# timeout: 30
|
cors: true
|
||||||
|
timeout: 30
|
||||||
|
environment:
|
||||||
|
BUCKET: cad-preview-bucket-prod-001
|
||||||
cadquerystl:
|
cadquerystl:
|
||||||
image:
|
image:
|
||||||
name: cadqueryimage
|
name: cadqueryimage
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"@redwoodjs/forms": "^0.31.0",
|
"@redwoodjs/forms": "^0.31.0",
|
||||||
"@redwoodjs/router": "^0.31.0",
|
"@redwoodjs/router": "^0.31.0",
|
||||||
"@redwoodjs/web": "^0.31.0",
|
"@redwoodjs/web": "^0.31.0",
|
||||||
|
"browser-fs-access": "^0.17.2",
|
||||||
"cloudinary-react": "^1.6.7",
|
"cloudinary-react": "^1.6.7",
|
||||||
"controlkit": "^0.1.9",
|
"controlkit": "^0.1.9",
|
||||||
"get-active-classes": "^0.0.11",
|
"get-active-classes": "^0.0.11",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const IdeContainer = () => {
|
|||||||
})
|
})
|
||||||
thunkDispatch((dispatch, getState) => {
|
thunkDispatch((dispatch, getState) => {
|
||||||
const state = getState()
|
const state = getState()
|
||||||
if (state.ideType === 'openScad') {
|
if (['png', 'INIT'].includes(state.objectData?.type)) {
|
||||||
dispatch({ type: 'setLoading' })
|
dispatch({ type: 'setLoading' })
|
||||||
requestRender({
|
requestRender({
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import { useIdeState, codeStorageKey } from 'src/helpers/hooks/useIdeState'
|
|||||||
import { copyTextToClipboard } from 'src/helpers/clipboard'
|
import { copyTextToClipboard } from 'src/helpers/clipboard'
|
||||||
import { requestRender } from 'src/helpers/hooks/useIdeState'
|
import { requestRender } from 'src/helpers/hooks/useIdeState'
|
||||||
import { encode, decode } from 'src/helpers/compress'
|
import { encode, decode } from 'src/helpers/compress'
|
||||||
import { flow } from 'lodash/fp'
|
import { flow, identity } from 'lodash/fp'
|
||||||
|
import { fileSave } from 'browser-fs-access'
|
||||||
|
import { MeshBasicMaterial, Mesh, Scene } from 'three'
|
||||||
|
|
|||||||
|
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'
|
||||||
|
|
||||||
export const githubSafe = (url) =>
|
export const githubSafe = (url) =>
|
||||||
url.includes('github.com')
|
url.includes('github.com')
|
||||||
@@ -80,6 +83,63 @@ const IdeToolbarNew = ({ cadPackage }) => {
|
|||||||
copyTextToClipboard(window.location.href)
|
copyTextToClipboard(window.location.href)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const PullTitleFromFirstLine = (code) => {
|
||||||
|
const firstLine = code.split('\n').filter(identity)[0]
|
||||||
|
if (!(firstLine.startsWith('//') || firstLine.startsWith('#'))) {
|
||||||
|
return 'object.stl'
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
(firstLine.replace(/^(\/\/|#)\s*(.+)/, (_, __, titleWithSpaces) =>
|
||||||
|
titleWithSpaces.replaceAll(/\s/g, '-')
|
||||||
|
) || 'object') + '.stl'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStlDownload = (({ geometry, fileName, type }) => () => {
|
||||||
|
not needed, but didn't want the not needed, but didn't want the `{256 hash}.stl` to be the file name, so if there's a comment as the first line it will use that for the title.
|
|||||||
|
const makeStlBlobFromGeo = flow(
|
||||||
|
(geo) => new Mesh(geo, new MeshBasicMaterial()),
|
||||||
|
(mesh) => new Scene().add(mesh),
|
||||||
|
(scene) => new STLExporter().parse(scene),
|
||||||
|
(stl) =>
|
||||||
|
new Blob([stl], {
|
||||||
|
type: 'text/plain',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const saveFile = (geometry) => {
|
||||||
|
const blob = makeStlBlobFromGeo(geometry)
|
||||||
|
fileSave(blob, {
|
||||||
|
fileName,
|
||||||
|
extensions: ['.stl'],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (geometry) {
|
||||||
|
if (type === 'geometry') {
|
||||||
|
saveFile(geometry)
|
||||||
|
} else {
|
||||||
|
thunkDispatch((dispatch, getState) => {
|
||||||
|
const state = getState()
|
||||||
|
if (state.ideType === 'openScad') {
|
||||||
|
thunkDispatch((dispatch, getState) => {
|
||||||
|
const state = getState()
|
||||||
|
dispatch({ type: 'setLoading' })
|
||||||
|
requestRender({
|
||||||
|
state,
|
||||||
|
dispatch,
|
||||||
|
code: state.code,
|
||||||
|
if we're currently showing 3d in the preview than we're golden, if not than it must be openscad, so we should trigger an stl download. if we're currently showing 3d in the preview than we're golden, if not than it must be openscad, so we should trigger an stl download.
|
|||||||
|
viewerSize: state.viewerSize,
|
||||||
|
camera: state.camera,
|
||||||
|
specialCadProcess: 'stl',
|
||||||
|
}).then((result) => result && saveFile(result.data))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})({
|
||||||
|
type: state.objectData?.type,
|
||||||
|
geometry: state.objectData?.data,
|
||||||
|
fileName: PullTitleFromFirstLine(state.code),
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IdeContext.Provider value={{ state, thunkDispatch }}>
|
<IdeContext.Provider value={{ state, thunkDispatch }}>
|
||||||
@@ -97,6 +157,12 @@ const IdeToolbarNew = ({ cadPackage }) => {
|
|||||||
>
|
>
|
||||||
Copy link
|
Copy link
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleStlDownload}
|
||||||
|
className="border-2 text-gray-700 px-2 text-sm m-1 ml-2"
|
||||||
|
>
|
||||||
|
Download STL
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
<IdeContainer />
|
<IdeContainer />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,33 +9,16 @@ import {
|
|||||||
} from 'react-three-fiber'
|
} from 'react-three-fiber'
|
||||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||||
import { Vector3 } from 'three'
|
import { Vector3 } from 'three'
|
||||||
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
|
|
||||||
import { requestRender } from 'src/helpers/hooks/useIdeState'
|
import { requestRender } from 'src/helpers/hooks/useIdeState'
|
||||||
|
|
||||||
extend({ OrbitControls })
|
extend({ OrbitControls })
|
||||||
|
|
||||||
function Asset({ url, resetLoading, setLoading }) {
|
function Asset({ geometry: incomingGeo }) {
|
||||||
const [loadedGeometry, setLoadedGeometry] = useState()
|
|
||||||
const mesh = useRef()
|
const mesh = useRef()
|
||||||
const ref = useUpdate((geometry) => {
|
const ref = useUpdate((geometry) => {
|
||||||
geometry.attributes = loadedGeometry.attributes
|
geometry.attributes = incomingGeo.attributes
|
||||||
|
Asset component is much cleaner now as we're making the three geometry higher in the stack. Asset component is much cleaner now as we're making the three geometry higher in the stack.
|
|||||||
})
|
})
|
||||||
useEffect(() => {
|
if (!incomingGeo) return null
|
||||||
if (url) {
|
|
||||||
const loader = new STLLoader()
|
|
||||||
setLoading()
|
|
||||||
loader.load(
|
|
||||||
url,
|
|
||||||
(geometry) => {
|
|
||||||
setLoadedGeometry(geometry)
|
|
||||||
resetLoading()
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
resetLoading
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [url])
|
|
||||||
if (!loadedGeometry) return null
|
|
||||||
return (
|
return (
|
||||||
<mesh ref={mesh} scale={[1, 1, 1]}>
|
<mesh ref={mesh} scale={[1, 1, 1]}>
|
||||||
<bufferGeometry attach="geometry" ref={ref} />
|
<bufferGeometry attach="geometry" ref={ref} />
|
||||||
@@ -158,9 +141,6 @@ const IdeViewer = () => {
|
|||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
const [image, setImage] = useState()
|
const [image, setImage] = useState()
|
||||||
|
|
||||||
const resetLoading = () => thunkDispatch({ type: 'resetLoading' })
|
|
||||||
const setLoading = () => thunkDispatch({ type: 'setLoading' })
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setImage(state.objectData?.type === 'png' && state.objectData?.data)
|
setImage(state.objectData?.type === 'png' && state.objectData?.data)
|
||||||
setIsDragging(false)
|
setIsDragging(false)
|
||||||
@@ -192,7 +172,7 @@ const IdeViewer = () => {
|
|||||||
)}
|
)}
|
||||||
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
|
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
|
||||||
className={`opacity-0 absolute inset-0 transition-opacity duration-500 ${
|
className={`opacity-0 absolute inset-0 transition-opacity duration-500 ${
|
||||||
!(isDragging || state.ideType !== 'openScad')
|
!(isDragging || state.objectData?.type !== 'png')
|
||||||
? 'hover:opacity-50'
|
? 'hover:opacity-50'
|
||||||
: 'opacity-100'
|
: 'opacity-100'
|
||||||
}`}
|
}`}
|
||||||
@@ -208,7 +188,7 @@ const IdeViewer = () => {
|
|||||||
})
|
})
|
||||||
thunkDispatch((dispatch, getState) => {
|
thunkDispatch((dispatch, getState) => {
|
||||||
const state = getState()
|
const state = getState()
|
||||||
if (state.ideType === 'openScad') {
|
if (['png', 'INIT'].includes(state.objectData?.type)) {
|
||||||
dispatch({ type: 'setLoading' })
|
dispatch({ type: 'setLoading' })
|
||||||
requestRender({
|
requestRender({
|
||||||
state,
|
state,
|
||||||
@@ -223,7 +203,7 @@ const IdeViewer = () => {
|
|||||||
/>
|
/>
|
||||||
<ambientLight />
|
<ambientLight />
|
||||||
<pointLight position={[15, 5, 10]} />
|
<pointLight position={[15, 5, 10]} />
|
||||||
{state.ideType === 'openScad' && (
|
{state.objectData?.type === 'png' && (
|
||||||
<>
|
<>
|
||||||
<Sphere position={[0, 0, 0]} color={pink400} />
|
<Sphere position={[0, 0, 0]} color={pink400} />
|
||||||
<Box position={[0, 50, 0]} size={[1, 100, 1]} color={indigo900} />
|
<Box position={[0, 50, 0]} size={[1, 100, 1]} color={indigo900} />
|
||||||
@@ -235,13 +215,11 @@ const IdeViewer = () => {
|
|||||||
<Box position={[50, 0, 0]} size={[100, 1, 1]} color={pink400} />
|
<Box position={[50, 0, 0]} size={[100, 1, 1]} color={pink400} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{state.ideType === 'cadQuery' && (
|
<Asset
|
||||||
<Asset
|
geometry={
|
||||||
url={state.objectData?.type === 'stl' && state.objectData?.data}
|
state.objectData?.type === 'geometry' && state.objectData?.data
|
||||||
resetLoading={resetLoading}
|
}
|
||||||
setLoading={setLoading}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</div>
|
</div>
|
||||||
{state.isLoading && (
|
{state.isLoading && (
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { lambdaBaseURL } from './common'
|
import {
|
||||||
|
lambdaBaseURL,
|
||||||
|
stlToGeometry,
|
||||||
|
createHealthyResponse,
|
||||||
|
createUnhealthyResponse,
|
||||||
|
} from './common'
|
||||||
|
|
||||||
export const render = async ({ code }) => {
|
export const render = async ({ code }) => {
|
||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
@@ -14,47 +19,26 @@ export const render = async ({ code }) => {
|
|||||||
body,
|
body,
|
||||||
})
|
})
|
||||||
if (response.status === 400) {
|
if (response.status === 400) {
|
||||||
// TODO add proper error messages for CadQuery
|
|
||||||
const { error } = await response.json()
|
const { error } = await response.json()
|
||||||
const cleanedErrorMessage = error.replace(
|
|
||||||
/["|']\/tmp\/.+\/main.scad["|']/g,
|
|
||||||
"'main.scad'"
|
|
||||||
)
|
|
||||||
return {
|
return {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: {
|
message: {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: cleanedErrorMessage,
|
message: error,
|
||||||
time: new Date(),
|
time: new Date(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
return {
|
const geometry = await stlToGeometry(data.url)
|
||||||
status: 'healthy',
|
return createHealthyResponse({
|
||||||
objectData: {
|
type: 'geometry',
|
||||||
type: 'stl',
|
data: geometry,
|
||||||
data: data.url,
|
consoleMessage: data.consoleMessage,
|
||||||
},
|
date: new Date(),
|
||||||
message: {
|
})
|
||||||
type: 'message',
|
|
||||||
message: data.consoleMessage || 'Successful Render',
|
|
||||||
time: new Date(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// TODO handle errors better
|
return createUnhealthyResponse(new Date())
|
||||||
// I think we should display something overlayed on the viewer window something like "network issue try again"
|
|
||||||
// and in future I think we need timeouts differently as they maybe from a user trying to render something too complex
|
|
||||||
// or something with minkowski in it :/ either way something like "render timed out, try again or here are tips to reduce part complexity" with a link talking about $fn and minkowski etc
|
|
||||||
return {
|
|
||||||
status: 'error',
|
|
||||||
message: {
|
|
||||||
type: 'error',
|
|
||||||
message: 'network issue',
|
|
||||||
time: new Date(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,40 @@
|
|||||||
|
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
|
||||||
|
|
||||||
export const lambdaBaseURL =
|
export const lambdaBaseURL =
|
||||||
process.env.CAD_LAMBDA_BASE_URL ||
|
process.env.CAD_LAMBDA_BASE_URL ||
|
||||||
'https://2inlbple1b.execute-api.us-east-1.amazonaws.com/prod2'
|
'https://2inlbple1b.execute-api.us-east-1.amazonaws.com/prod2'
|
||||||
|
|
||||||
|
export const stlToGeometry = (url) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
new STLLoader().load(url, resolve, null, reject)
|
||||||
|
})
|
||||||
|
|
||||||
|
export function createHealthyResponse({ date, data, consoleMessage, type }) {
|
||||||
|
return {
|
||||||
|
status: 'healthy',
|
||||||
|
objectData: {
|
||||||
|
type,
|
||||||
|
data: data,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: 'message',
|
||||||
|
message: consoleMessage,
|
||||||
|
time: date,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createUnhealthyResponse(date, message = 'network issue') {
|
||||||
|
// TODO handle errors better
|
||||||
|
// I think we should display something overlayed on the viewer window something like "network issue try again"
|
||||||
|
// and in future I think we need timeouts differently as they maybe from a user trying to render something too complex
|
||||||
|
// or something with minkowski in it :/ either way something like "render timed out, try again or here are tips to reduce part complexity" with a link talking about $fn and minkowski etc
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: {
|
||||||
|
type: 'error',
|
||||||
|
message,
|
||||||
|
time: date,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { lambdaBaseURL } from './common'
|
import {
|
||||||
|
lambdaBaseURL,
|
||||||
|
stlToGeometry,
|
||||||
|
createHealthyResponse,
|
||||||
|
createUnhealthyResponse,
|
||||||
|
} from './common'
|
||||||
|
|
||||||
export const render = async ({ code, settings }) => {
|
export const render = async ({ code, settings }) => {
|
||||||
const pixelRatio = window.devicePixelRatio || 1
|
const pixelRatio = window.devicePixelRatio || 1
|
||||||
@@ -41,51 +46,61 @@ export const render = async ({ code, settings }) => {
|
|||||||
})
|
})
|
||||||
if (response.status === 400) {
|
if (response.status === 400) {
|
||||||
const { error } = await response.json()
|
const { error } = await response.json()
|
||||||
const cleanedErrorMessage = error.replace(
|
const cleanedErrorMessage = cleanError(error)
|
||||||
/["|']\/tmp\/.+\/main.scad["|']/g,
|
return createUnhealthyResponse(new Date(), cleanedErrorMessage)
|
||||||
"'main.scad'"
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
status: 'error',
|
|
||||||
message: {
|
|
||||||
type: 'error',
|
|
||||||
message: cleanedErrorMessage,
|
|
||||||
time: new Date(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
return {
|
const type = data.type !== 'stl' ? 'png' : 'geometry'
|
||||||
status: 'healthy',
|
const newData = data.type !== 'stl' ? data.url : stlToGeometry(data.url)
|
||||||
objectData: {
|
return createHealthyResponse({
|
||||||
type: 'png',
|
type,
|
||||||
data: data.url,
|
data: await newData,
|
||||||
},
|
consoleMessage: data.consoleMessage,
|
||||||
message: {
|
date: new Date(),
|
||||||
type: 'message',
|
})
|
||||||
message: data.consoleMessage,
|
|
||||||
time: new Date(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// TODO handle errors better
|
return createUnhealthyResponse(new Date())
|
||||||
// I think we should display something overlayed on the viewer window something like "network issue try again"
|
}
|
||||||
// and in future I think we need timeouts differently as they maybe from a user trying to render something too complex
|
}
|
||||||
// or something with minkowski in it :/ either way something like "render timed out, try again or here are tips to reduce part complexity" with a link talking about $fn and minkowski etc
|
|
||||||
return {
|
export const stl = async ({ code, settings }) => {
|
||||||
status: 'error',
|
const body = JSON.stringify({
|
||||||
message: {
|
settings: {},
|
||||||
type: 'error',
|
file: code,
|
||||||
message: 'network issue',
|
})
|
||||||
time: new Date(),
|
try {
|
||||||
|
const response = await fetch(lambdaBaseURL + '/openscad/stl', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
if (response.status === 400) {
|
||||||
|
const { error } = await response.json()
|
||||||
|
const cleanedErrorMessage = cleanError(error)
|
||||||
|
return createUnhealthyResponse(new Date(), cleanedErrorMessage)
|
||||||
}
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
const geometry = await stlToGeometry(data.url)
|
||||||
|
return createHealthyResponse({
|
||||||
|
type: 'geometry',
|
||||||
|
data: geometry,
|
||||||
|
consoleMessage: data.consoleMessage,
|
||||||
|
Previously we were storing the aws url here and than was downloading and managing the geometry, but instead doing that here, it's much easier to grab that geometry again to download the stl Previously we were storing the aws url here and than <Asset /> was downloading and managing the geometry, but instead doing that here, it's much easier to grab that geometry again to download the stl
|
|||||||
|
date: new Date(),
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
return createUnhealthyResponse(new Date())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openScad = {
|
const openScad = {
|
||||||
render,
|
render,
|
||||||
// more functions to come
|
stl,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default openScad
|
export default openScad
|
||||||
|
|
||||||
|
function cleanError(error) {
|
||||||
|
return error.replace(/["|']\/tmp\/.+\/main.scad["|']/g, "'main.scad'")
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ function withThunk(dispatch, getState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initCodeMap = {
|
const initCodeMap = {
|
||||||
openScad: `
|
openScad: `// involute donut
|
||||||
|
|
||||||
|
// ^ first comment is used for download title (i.e "involute-donut.stl")
|
||||||
|
|
||||||
color(c="DarkGoldenrod")rotate_extrude()translate([20,0])circle(d=30);
|
color(c="DarkGoldenrod")rotate_extrude()translate([20,0])circle(d=30);
|
||||||
donut();
|
donut();
|
||||||
module donut() {
|
module donut() {
|
||||||
@@ -23,7 +26,11 @@ module stick(basewid, angl){
|
|||||||
translate([0,0,10])sphere(9);
|
translate([0,0,10])sphere(9);
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
cadQuery: `import cadquery as cq
|
cadQuery: `# demo shaft coupler
|
||||||
|
|
||||||
|
# ^ first comment is used for download title (i.e. "demo-shaft-coupler.stl")
|
||||||
|
|
||||||
|
import cadquery as cq
|
||||||
from cadquery import exporters
|
from cadquery import exporters
|
||||||
|
|
||||||
diam = 5.0
|
diam = 5.0
|
||||||
@@ -49,7 +56,7 @@ export const useIdeState = () => {
|
|||||||
],
|
],
|
||||||
code,
|
code,
|
||||||
objectData: {
|
objectData: {
|
||||||
type: 'stl',
|
type: 'INIT',
|
||||||
data: null,
|
data: null,
|
||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
@@ -138,17 +145,22 @@ export const requestRender = ({
|
|||||||
code,
|
code,
|
||||||
camera,
|
camera,
|
||||||
viewerSize,
|
viewerSize,
|
||||||
|
specialCadProcess = null,
|
||||||
}) => {
|
}) => {
|
||||||
state.ideType !== 'INIT' &&
|
if (
|
||||||
!state.isLoading &&
|
state.ideType !== 'INIT' &&
|
||||||
cadPackages[state.ideType]
|
(!state.isLoading || state.objectData?.type === 'INIT')
|
||||||
.render({
|
) {
|
||||||
code,
|
const renderFn = specialCadProcess
|
||||||
settings: {
|
? cadPackages[state.ideType][specialCadProcess]
|
||||||
camera,
|
: cadPackages[state.ideType].render
|
||||||
viewerSize,
|
return renderFn({
|
||||||
},
|
code,
|
||||||
})
|
settings: {
|
||||||
|
camera,
|
||||||
|
viewerSize,
|
||||||
|
},
|
||||||
|
})
|
||||||
.then(({ objectData, message, status }) => {
|
.then(({ objectData, message, status }) => {
|
||||||
if (status === 'error') {
|
if (status === 'error') {
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -160,7 +172,9 @@ export const requestRender = ({
|
|||||||
type: 'healthyRender',
|
type: 'healthyRender',
|
||||||
payload: { objectData, message, lastRunCode: code },
|
payload: { objectData, message, lastRunCode: code },
|
||||||
})
|
})
|
||||||
|
return objectData
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here
|
.catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3097,6 +3097,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/line-column/-/line-column-1.0.0.tgz#fa5a59c21e885fef3739a273b43dacf55b63437f"
|
resolved "https://registry.yarnpkg.com/@types/line-column/-/line-column-1.0.0.tgz#fa5a59c21e885fef3739a273b43dacf55b63437f"
|
||||||
integrity sha512-wbw+IDRw/xY/RGy+BL6f4Eey4jsUgHQrMuA4Qj0CSG3x/7C2Oc57pmRoM2z3M4DkylWRz+G1pfX06sCXQm0J+w==
|
integrity sha512-wbw+IDRw/xY/RGy+BL6f4Eey4jsUgHQrMuA4Qj0CSG3x/7C2Oc57pmRoM2z3M4DkylWRz+G1pfX06sCXQm0J+w==
|
||||||
|
|
||||||
|
"@types/lodash@^4.14.170":
|
||||||
|
version "4.14.170"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6"
|
||||||
|
integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==
|
||||||
|
|
||||||
"@types/long@^4.0.0":
|
"@types/long@^4.0.0":
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
|
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
|
||||||
@@ -4822,6 +4827,11 @@ brorand@^1.0.1, brorand@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
||||||
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
|
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
|
||||||
|
|
||||||
|
browser-fs-access@^0.17.2:
|
||||||
|
version "0.17.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/browser-fs-access/-/browser-fs-access-0.17.2.tgz#6debfe35ebce77a1eecca7822a449c740a8de9db"
|
||||||
|
integrity sha512-z3H37rU3fmKkGJ1r09v3hAwByUI0vYIh8a7LsUaQnnMxdVUJm1UzsffvNK6Qnvk9jGqvY7uiX2DPu4BZXavcWA==
|
||||||
|
|
||||||
browser-process-hrtime@^1.0.0:
|
browser-process-hrtime@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
|
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
|
||||||
|
|||||||
Reference in New Issue
Block a user
nice library that abstracts a bunch browser compatibility issues with saving files from the browser.
more context : https://github.com/zalo/CascadeStudio/pull/39#issuecomment-766911556