@@ -29,7 +29,7 @@ const IdeContainer = () => {
|
||||
})
|
||||
thunkDispatch((dispatch, getState) => {
|
||||
const state = getState()
|
||||
if (state.ideType === 'openScad') {
|
||||
if (['png', 'INIT'].includes(state.objectData?.type)) {
|
||||
dispatch({ type: 'setLoading' })
|
||||
requestRender({
|
||||
state,
|
||||
|
||||
@@ -5,7 +5,10 @@ import { useIdeState, codeStorageKey } from 'src/helpers/hooks/useIdeState'
|
||||
import { copyTextToClipboard } from 'src/helpers/clipboard'
|
||||
import { requestRender } from 'src/helpers/hooks/useIdeState'
|
||||
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) =>
|
||||
url.includes('github.com')
|
||||
@@ -80,6 +83,63 @@ const IdeToolbarNew = ({ cadPackage }) => {
|
||||
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 }) => () => {
|
||||
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,
|
||||
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 (
|
||||
<IdeContext.Provider value={{ state, thunkDispatch }}>
|
||||
@@ -97,6 +157,12 @@ const IdeToolbarNew = ({ cadPackage }) => {
|
||||
>
|
||||
Copy link
|
||||
</button>
|
||||
<button
|
||||
onClick={handleStlDownload}
|
||||
className="border-2 text-gray-700 px-2 text-sm m-1 ml-2"
|
||||
>
|
||||
Download STL
|
||||
</button>
|
||||
</nav>
|
||||
<IdeContainer />
|
||||
</div>
|
||||
|
||||
@@ -9,33 +9,16 @@ import {
|
||||
} from 'react-three-fiber'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { Vector3 } from 'three'
|
||||
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
|
||||
import { requestRender } from 'src/helpers/hooks/useIdeState'
|
||||
|
||||
extend({ OrbitControls })
|
||||
|
||||
function Asset({ url, resetLoading, setLoading }) {
|
||||
const [loadedGeometry, setLoadedGeometry] = useState()
|
||||
function Asset({ geometry: incomingGeo }) {
|
||||
const mesh = useRef()
|
||||
const ref = useUpdate((geometry) => {
|
||||
geometry.attributes = loadedGeometry.attributes
|
||||
geometry.attributes = incomingGeo.attributes
|
||||
})
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
const loader = new STLLoader()
|
||||
setLoading()
|
||||
loader.load(
|
||||
url,
|
||||
(geometry) => {
|
||||
setLoadedGeometry(geometry)
|
||||
resetLoading()
|
||||
},
|
||||
null,
|
||||
resetLoading
|
||||
)
|
||||
}
|
||||
}, [url])
|
||||
if (!loadedGeometry) return null
|
||||
if (!incomingGeo) return null
|
||||
return (
|
||||
<mesh ref={mesh} scale={[1, 1, 1]}>
|
||||
<bufferGeometry attach="geometry" ref={ref} />
|
||||
@@ -158,9 +141,6 @@ const IdeViewer = () => {
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [image, setImage] = useState()
|
||||
|
||||
const resetLoading = () => thunkDispatch({ type: 'resetLoading' })
|
||||
const setLoading = () => thunkDispatch({ type: 'setLoading' })
|
||||
|
||||
useEffect(() => {
|
||||
setImage(state.objectData?.type === 'png' && state.objectData?.data)
|
||||
setIsDragging(false)
|
||||
@@ -192,7 +172,7 @@ const IdeViewer = () => {
|
||||
)}
|
||||
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
|
||||
className={`opacity-0 absolute inset-0 transition-opacity duration-500 ${
|
||||
!(isDragging || state.ideType !== 'openScad')
|
||||
!(isDragging || state.objectData?.type !== 'png')
|
||||
? 'hover:opacity-50'
|
||||
: 'opacity-100'
|
||||
}`}
|
||||
@@ -208,7 +188,7 @@ const IdeViewer = () => {
|
||||
})
|
||||
thunkDispatch((dispatch, getState) => {
|
||||
const state = getState()
|
||||
if (state.ideType === 'openScad') {
|
||||
if (['png', 'INIT'].includes(state.objectData?.type)) {
|
||||
dispatch({ type: 'setLoading' })
|
||||
requestRender({
|
||||
state,
|
||||
@@ -223,7 +203,7 @@ const IdeViewer = () => {
|
||||
/>
|
||||
<ambientLight />
|
||||
<pointLight position={[15, 5, 10]} />
|
||||
{state.ideType === 'openScad' && (
|
||||
{state.objectData?.type === 'png' && (
|
||||
<>
|
||||
<Sphere position={[0, 0, 0]} color={pink400} />
|
||||
<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} />
|
||||
</>
|
||||
)}
|
||||
{state.ideType === 'cadQuery' && (
|
||||
<Asset
|
||||
url={state.objectData?.type === 'stl' && state.objectData?.data}
|
||||
resetLoading={resetLoading}
|
||||
setLoading={setLoading}
|
||||
/>
|
||||
)}
|
||||
<Asset
|
||||
geometry={
|
||||
state.objectData?.type === 'geometry' && state.objectData?.data
|
||||
}
|
||||
/>
|
||||
</Canvas>
|
||||
</div>
|
||||
{state.isLoading && (
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { lambdaBaseURL } from './common'
|
||||
import {
|
||||
lambdaBaseURL,
|
||||
stlToGeometry,
|
||||
createHealthyResponse,
|
||||
createUnhealthyResponse,
|
||||
} from './common'
|
||||
|
||||
export const render = async ({ code }) => {
|
||||
const body = JSON.stringify({
|
||||
@@ -14,47 +19,26 @@ export const render = async ({ code }) => {
|
||||
body,
|
||||
})
|
||||
if (response.status === 400) {
|
||||
// TODO add proper error messages for CadQuery
|
||||
const { error } = await response.json()
|
||||
const cleanedErrorMessage = error.replace(
|
||||
/["|']\/tmp\/.+\/main.scad["|']/g,
|
||||
"'main.scad'"
|
||||
)
|
||||
return {
|
||||
status: 'error',
|
||||
message: {
|
||||
type: 'error',
|
||||
message: cleanedErrorMessage,
|
||||
message: error,
|
||||
time: new Date(),
|
||||
},
|
||||
}
|
||||
}
|
||||
const data = await response.json()
|
||||
return {
|
||||
status: 'healthy',
|
||||
objectData: {
|
||||
type: 'stl',
|
||||
data: data.url,
|
||||
},
|
||||
message: {
|
||||
type: 'message',
|
||||
message: data.consoleMessage || 'Successful Render',
|
||||
time: new Date(),
|
||||
},
|
||||
}
|
||||
const geometry = await stlToGeometry(data.url)
|
||||
return createHealthyResponse({
|
||||
type: 'geometry',
|
||||
data: geometry,
|
||||
consoleMessage: data.consoleMessage,
|
||||
date: new Date(),
|
||||
})
|
||||
} catch (e) {
|
||||
// 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: 'network issue',
|
||||
time: new Date(),
|
||||
},
|
||||
}
|
||||
return createUnhealthyResponse(new Date())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,40 @@
|
||||
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
|
||||
|
||||
export const lambdaBaseURL =
|
||||
process.env.CAD_LAMBDA_BASE_URL ||
|
||||
'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 }) => {
|
||||
const pixelRatio = window.devicePixelRatio || 1
|
||||
@@ -41,51 +46,61 @@ export const render = async ({ code, settings }) => {
|
||||
})
|
||||
if (response.status === 400) {
|
||||
const { error } = await response.json()
|
||||
const cleanedErrorMessage = error.replace(
|
||||
/["|']\/tmp\/.+\/main.scad["|']/g,
|
||||
"'main.scad'"
|
||||
)
|
||||
return {
|
||||
status: 'error',
|
||||
message: {
|
||||
type: 'error',
|
||||
message: cleanedErrorMessage,
|
||||
time: new Date(),
|
||||
},
|
||||
}
|
||||
const cleanedErrorMessage = cleanError(error)
|
||||
return createUnhealthyResponse(new Date(), cleanedErrorMessage)
|
||||
}
|
||||
const data = await response.json()
|
||||
return {
|
||||
status: 'healthy',
|
||||
objectData: {
|
||||
type: 'png',
|
||||
data: data.url,
|
||||
},
|
||||
message: {
|
||||
type: 'message',
|
||||
message: data.consoleMessage,
|
||||
time: new Date(),
|
||||
},
|
||||
}
|
||||
const type = data.type !== 'stl' ? 'png' : 'geometry'
|
||||
const newData = data.type !== 'stl' ? data.url : stlToGeometry(data.url)
|
||||
return createHealthyResponse({
|
||||
type,
|
||||
data: await newData,
|
||||
consoleMessage: data.consoleMessage,
|
||||
date: new Date(),
|
||||
})
|
||||
} catch (e) {
|
||||
// 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: 'network issue',
|
||||
time: new Date(),
|
||||
return createUnhealthyResponse(new Date())
|
||||
}
|
||||
}
|
||||
|
||||
export const stl = async ({ code, settings }) => {
|
||||
const body = JSON.stringify({
|
||||
settings: {},
|
||||
file: code,
|
||||
})
|
||||
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,
|
||||
date: new Date(),
|
||||
})
|
||||
} catch (e) {
|
||||
return createUnhealthyResponse(new Date())
|
||||
}
|
||||
}
|
||||
|
||||
const openScad = {
|
||||
render,
|
||||
// more functions to come
|
||||
stl,
|
||||
}
|
||||
|
||||
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 = {
|
||||
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);
|
||||
donut();
|
||||
module donut() {
|
||||
@@ -23,7 +26,11 @@ module stick(basewid, angl){
|
||||
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
|
||||
|
||||
diam = 5.0
|
||||
@@ -49,7 +56,7 @@ export const useIdeState = () => {
|
||||
],
|
||||
code,
|
||||
objectData: {
|
||||
type: 'stl',
|
||||
type: 'INIT',
|
||||
data: null,
|
||||
},
|
||||
layout: {
|
||||
@@ -138,17 +145,22 @@ export const requestRender = ({
|
||||
code,
|
||||
camera,
|
||||
viewerSize,
|
||||
specialCadProcess = null,
|
||||
}) => {
|
||||
state.ideType !== 'INIT' &&
|
||||
!state.isLoading &&
|
||||
cadPackages[state.ideType]
|
||||
.render({
|
||||
code,
|
||||
settings: {
|
||||
camera,
|
||||
viewerSize,
|
||||
},
|
||||
})
|
||||
if (
|
||||
state.ideType !== 'INIT' &&
|
||||
(!state.isLoading || state.objectData?.type === 'INIT')
|
||||
) {
|
||||
const renderFn = specialCadProcess
|
||||
? cadPackages[state.ideType][specialCadProcess]
|
||||
: cadPackages[state.ideType].render
|
||||
return renderFn({
|
||||
code,
|
||||
settings: {
|
||||
camera,
|
||||
viewerSize,
|
||||
},
|
||||
})
|
||||
.then(({ objectData, message, status }) => {
|
||||
if (status === 'error') {
|
||||
dispatch({
|
||||
@@ -160,7 +172,9 @@ export const requestRender = ({
|
||||
type: 'healthyRender',
|
||||
payload: { objectData, message, lastRunCode: code },
|
||||
})
|
||||
return objectData
|
||||
}
|
||||
})
|
||||
.catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user