Attempt to at move app into app sub dir
This commit is contained in:
66
app/web/src/helpers/cadPackages/cadQueryController.js
Normal file
66
app/web/src/helpers/cadPackages/cadQueryController.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { lambdaBaseURL } from './common'
|
||||
|
||||
export const render = async ({ code }) => {
|
||||
const body = JSON.stringify({
|
||||
settings: {},
|
||||
file: code,
|
||||
})
|
||||
try {
|
||||
const response = await fetch(lambdaBaseURL + '/cadquery/stl', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
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,
|
||||
time: new Date(),
|
||||
},
|
||||
}
|
||||
}
|
||||
const data = await response.json()
|
||||
return {
|
||||
status: 'healthy',
|
||||
objectData: {
|
||||
type: 'stl',
|
||||
data: data.imageBase64,
|
||||
},
|
||||
message: {
|
||||
type: 'message',
|
||||
message: data.result || 'Successful Render',
|
||||
time: 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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const openScad = {
|
||||
render,
|
||||
// more functions to come
|
||||
}
|
||||
|
||||
export default openScad
|
||||
3
app/web/src/helpers/cadPackages/common.js
Normal file
3
app/web/src/helpers/cadPackages/common.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const lambdaBaseURL =
|
||||
process.env.CAD_LAMBDA_BASE_URL ||
|
||||
'https://wzab9s632b.execute-api.us-east-1.amazonaws.com/prod'
|
||||
7
app/web/src/helpers/cadPackages/index.js
Normal file
7
app/web/src/helpers/cadPackages/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import openScad from './openScadController'
|
||||
import cadQuery from './cadQueryController'
|
||||
|
||||
export const cadPackages = {
|
||||
openScad,
|
||||
cadQuery,
|
||||
}
|
||||
76
app/web/src/helpers/cadPackages/openScadController.js
Normal file
76
app/web/src/helpers/cadPackages/openScadController.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { lambdaBaseURL } from './common'
|
||||
|
||||
export const render = async ({ code, settings }) => {
|
||||
const pixelRatio = window.devicePixelRatio || 1
|
||||
const size = {
|
||||
x: Math.round(settings.viewerSize?.width * pixelRatio),
|
||||
y: Math.round(settings.viewerSize?.height * pixelRatio),
|
||||
}
|
||||
const body = JSON.stringify({
|
||||
settings: {
|
||||
size,
|
||||
camera: settings.camera,
|
||||
},
|
||||
file: code,
|
||||
})
|
||||
if (!settings.camera.position) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const response = await fetch(lambdaBaseURL + '/openscad/preview', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body,
|
||||
})
|
||||
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 data = await response.json()
|
||||
return {
|
||||
status: 'healthy',
|
||||
objectData: {
|
||||
type: 'png',
|
||||
data: data.imageBase64,
|
||||
},
|
||||
message: {
|
||||
type: 'message',
|
||||
message: data.result,
|
||||
time: 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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const openScad = {
|
||||
render,
|
||||
// more functions to come
|
||||
}
|
||||
|
||||
export default openScad
|
||||
55
app/web/src/helpers/cascadeController.js
Normal file
55
app/web/src/helpers/cascadeController.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { initialize } from 'src/cascade/js/MainPage/CascadeMain'
|
||||
import { monacoEditor } from 'src/cascade/js/MainPage/CascadeState'
|
||||
|
||||
class CascadeController {
|
||||
_hasInitialised = false
|
||||
incomingOnCodeChang = () => {}
|
||||
controllerOnCodeChange = (code) => {
|
||||
this.incomingOnCodeChang(code)
|
||||
}
|
||||
|
||||
initialise(onCodeChange, code) {
|
||||
const onInit = () => {
|
||||
const editor = monacoEditor
|
||||
editor.setValue(code)
|
||||
editor.evaluateCode(false)
|
||||
}
|
||||
// only inits on first call, after that it just updates the editor and revaluates code, maybe should rename?
|
||||
this.incomingOnCodeChang = onCodeChange
|
||||
if (!this._hasInitialised) {
|
||||
initialize(this.controllerOnCodeChange, code, onInit)
|
||||
this._hasInitialised = true
|
||||
return
|
||||
}
|
||||
onInit()
|
||||
}
|
||||
|
||||
capture(environment, width = 512, height = 384) {
|
||||
environment.camera.aspect = width / height
|
||||
environment.camera.updateProjectionMatrix()
|
||||
environment.renderer.setSize(width, height)
|
||||
environment.renderer.render(
|
||||
environment.scene,
|
||||
environment.camera,
|
||||
null,
|
||||
false
|
||||
)
|
||||
let imgBlob = new Promise((resolve, reject) => {
|
||||
environment.renderer.domElement.toBlob(
|
||||
(blob) => {
|
||||
blob.name = `part_capture-${Date.now()}`
|
||||
resolve(blob)
|
||||
},
|
||||
'image/jpeg',
|
||||
1
|
||||
)
|
||||
})
|
||||
|
||||
// Return to original dimensions
|
||||
environment.onWindowResize()
|
||||
|
||||
return imgBlob
|
||||
}
|
||||
}
|
||||
|
||||
export default new CascadeController()
|
||||
37
app/web/src/helpers/clipboard.js
Normal file
37
app/web/src/helpers/clipboard.js
Normal file
@@ -0,0 +1,37 @@
|
||||
function fallbackCopyTextToClipboard(text) {
|
||||
var textArea = document.createElement('textarea')
|
||||
textArea.value = text
|
||||
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.top = '0'
|
||||
textArea.style.left = '0'
|
||||
textArea.style.position = 'fixed'
|
||||
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
|
||||
try {
|
||||
var successful = document.execCommand('copy')
|
||||
var msg = successful ? 'successful' : 'unsuccessful'
|
||||
console.log('Fallback: Copying text command was ' + msg)
|
||||
} catch (err) {
|
||||
console.error('Fallback: Oops, unable to copy', err)
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea)
|
||||
}
|
||||
export function copyTextToClipboard(text) {
|
||||
if (!navigator.clipboard) {
|
||||
fallbackCopyTextToClipboard(text)
|
||||
return
|
||||
}
|
||||
navigator.clipboard.writeText(text).then(
|
||||
function () {
|
||||
console.log('Async: Copying to clipboard was successful!')
|
||||
},
|
||||
function (err) {
|
||||
console.error('Async: Could not copy text: ', err)
|
||||
}
|
||||
)
|
||||
}
|
||||
32
app/web/src/helpers/cloudinary.js
Normal file
32
app/web/src/helpers/cloudinary.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// TODO: create a tidy util for uploading to Cloudinary and returning the public ID
|
||||
import axios from 'axios'
|
||||
import { threejsViewport } from 'src/cascade/js/MainPage/CascadeState'
|
||||
import CascadeController from 'src/helpers/cascadeController'
|
||||
|
||||
const CLOUDINARY_UPLOAD_PRESET = 'CadHub_project_images'
|
||||
const CLOUDINARY_UPLOAD_URL = 'https://api.cloudinary.com/v1_1/irevdev/upload'
|
||||
|
||||
export async function uploadToCloudinary(imgBlob) {
|
||||
const imageData = new FormData()
|
||||
imageData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET)
|
||||
imageData.append('file', imgBlob)
|
||||
let upload = axios.post(CLOUDINARY_UPLOAD_URL, imageData)
|
||||
|
||||
try {
|
||||
const { data } = await upload
|
||||
if (data && data.public_id !== '') {
|
||||
return data
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('ERROR', e)
|
||||
}
|
||||
}
|
||||
|
||||
export const captureAndSaveViewport = async () => {
|
||||
// Get the canvas image as a Data URL
|
||||
const imgBlob = await CascadeController.capture(threejsViewport.environment)
|
||||
|
||||
// Upload the image to Cloudinary
|
||||
const { public_id: publicId } = await uploadToCloudinary(imgBlob)
|
||||
return { publicId, imgBlob }
|
||||
}
|
||||
13
app/web/src/helpers/emote.js
Normal file
13
app/web/src/helpers/emote.js
Normal file
@@ -0,0 +1,13 @@
|
||||
export const countEmotes = (reactions = []) => {
|
||||
// would be good to do this sever side
|
||||
// counting unique emojis, and limiting to the 5 largest
|
||||
const emoteCounts = {}
|
||||
reactions.forEach(({ emote }) => {
|
||||
emoteCounts[emote] = emoteCounts[emote] ? emoteCounts[emote] + 1 : 1
|
||||
})
|
||||
// TODO the sort is causing the emotes to jump around after the user clicks one, not ideal
|
||||
return Object.entries(emoteCounts)
|
||||
.map(([emoji, count]) => ({ emoji, count }))
|
||||
.sort((a, b) => a.count - b.count)
|
||||
.slice(-5)
|
||||
}
|
||||
168
app/web/src/helpers/hooks/useIdeState.js
Normal file
168
app/web/src/helpers/hooks/useIdeState.js
Normal file
@@ -0,0 +1,168 @@
|
||||
import { useReducer } from 'react'
|
||||
import { cadPackages } from 'src/helpers/cadPackages'
|
||||
|
||||
function withThunk(dispatch, getState) {
|
||||
return (actionOrThunk) =>
|
||||
typeof actionOrThunk === 'function'
|
||||
? actionOrThunk(dispatch, getState)
|
||||
: dispatch(actionOrThunk)
|
||||
}
|
||||
|
||||
const initCodeMap = {
|
||||
openScad: `
|
||||
color(c="DarkGoldenrod")rotate_extrude()translate([20,0])circle(d=30);
|
||||
donut();
|
||||
module donut() {
|
||||
for(i=[1:360]){
|
||||
rotate(i*13.751)stick(20,i*1.351);
|
||||
}
|
||||
}
|
||||
module stick(basewid, angl){
|
||||
translate([basewid,0,0])rotate([angl,angl,angl*2])color(c="hotpink")hull(){
|
||||
sphere(7);
|
||||
translate([0,0,10])sphere(9);
|
||||
}
|
||||
}`,
|
||||
cadQuery: `import cadquery as cq
|
||||
from cadquery import exporters
|
||||
|
||||
diam = 5.0
|
||||
|
||||
result = (cq.Workplane().circle(diam).extrude(20.0)
|
||||
.faces(">Z").workplane(invert=True).circle(1.05).cutBlind(8.0)
|
||||
.faces("<Z").workplane(invert=True).circle(0.8).cutBlind(12.0)
|
||||
.edges("%CIRCLE").chamfer(0.15))
|
||||
|
||||
# exporters.export(coupler, "/home/jwright/Downloads/coupler.stl", exporters.ExportTypes.STL)
|
||||
|
||||
show_object(result)
|
||||
`,
|
||||
}
|
||||
|
||||
export const codeStorageKey = 'Last-editor-code'
|
||||
let mutableState = null
|
||||
|
||||
export const useIdeState = () => {
|
||||
const code = localStorage.getItem(codeStorageKey) || initCodeMap.openscad
|
||||
const initialState = {
|
||||
ideType: 'INIT',
|
||||
consoleMessages: [
|
||||
{ type: 'message', message: 'Initialising', time: new Date() },
|
||||
],
|
||||
code,
|
||||
objectData: {
|
||||
type: 'stl',
|
||||
data: null,
|
||||
},
|
||||
layout: {
|
||||
direction: 'row',
|
||||
first: 'Editor',
|
||||
second: {
|
||||
direction: 'column',
|
||||
first: 'Viewer',
|
||||
second: 'Console',
|
||||
splitPercentage: 70,
|
||||
},
|
||||
},
|
||||
camera: {},
|
||||
viewerSize: { width: 0, height: 0 },
|
||||
isLoading: false,
|
||||
}
|
||||
const reducer = (state, { type, payload }) => {
|
||||
switch (type) {
|
||||
case 'initIde':
|
||||
return {
|
||||
...state,
|
||||
code: initCodeMap[payload.cadPackage] || initCodeMap.openscad,
|
||||
ideType: payload.cadPackage,
|
||||
}
|
||||
case 'updateCode':
|
||||
return { ...state, code: payload }
|
||||
case 'healthyRender':
|
||||
return {
|
||||
...state,
|
||||
objectData: {
|
||||
type: payload.objectData?.type,
|
||||
data: payload.objectData?.data,
|
||||
},
|
||||
consoleMessages: payload.message
|
||||
? [...state.consoleMessages, payload.message]
|
||||
: payload.message,
|
||||
isLoading: false,
|
||||
}
|
||||
case 'errorRender':
|
||||
return {
|
||||
...state,
|
||||
consoleMessages: payload.message
|
||||
? [...state.consoleMessages, payload.message]
|
||||
: payload.message,
|
||||
isLoading: false,
|
||||
}
|
||||
case 'setLayout':
|
||||
return {
|
||||
...state,
|
||||
layout: payload.message,
|
||||
}
|
||||
case 'updateCamera':
|
||||
return {
|
||||
...state,
|
||||
camera: payload.camera,
|
||||
}
|
||||
case 'updateViewerSize':
|
||||
return {
|
||||
...state,
|
||||
viewerSize: payload.viewerSize,
|
||||
}
|
||||
case 'setLoading':
|
||||
return {
|
||||
...state,
|
||||
isLoading: true,
|
||||
}
|
||||
case 'resetLoading':
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, initialState)
|
||||
mutableState = state
|
||||
const getState = () => mutableState
|
||||
return [state, withThunk(dispatch, getState)]
|
||||
}
|
||||
|
||||
export const requestRender = ({
|
||||
state,
|
||||
dispatch,
|
||||
code,
|
||||
camera,
|
||||
viewerSize,
|
||||
}) => {
|
||||
state.ideType !== 'INIT' &&
|
||||
!state.isLoading &&
|
||||
cadPackages[state.ideType]
|
||||
.render({
|
||||
code,
|
||||
settings: {
|
||||
camera,
|
||||
viewerSize,
|
||||
},
|
||||
})
|
||||
.then(({ objectData, message, status }) => {
|
||||
if (status === 'error') {
|
||||
dispatch({
|
||||
type: 'errorRender',
|
||||
payload: { message },
|
||||
})
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'healthyRender',
|
||||
payload: { objectData, message, lastRunCode: code },
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here
|
||||
}
|
||||
19
app/web/src/helpers/hooks/useKeyPress.js
Normal file
19
app/web/src/helpers/hooks/useKeyPress.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useRef, useEffect } from 'react'
|
||||
|
||||
const useKeyPress = (fn) => {
|
||||
const cb = useRef(fn)
|
||||
|
||||
useEffect(() => {
|
||||
cb.current = fn
|
||||
}, [fn])
|
||||
|
||||
useEffect(() => {
|
||||
const onUnload = cb.current
|
||||
|
||||
window.addEventListener('keydown', onUnload)
|
||||
|
||||
return () => window.removeEventListener('keydown', onUnload)
|
||||
}, [])
|
||||
}
|
||||
|
||||
export default useKeyPress
|
||||
22
app/web/src/helpers/hooks/useUser.js
Normal file
22
app/web/src/helpers/hooks/useUser.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useQuery } from '@redwoodjs/web'
|
||||
import { useAuth } from '@redwoodjs/auth'
|
||||
|
||||
const QUERY = gql`
|
||||
query FIND_USER_BY_ID($id: String!) {
|
||||
user: user(id: $id) {
|
||||
id
|
||||
image
|
||||
userName
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function () {
|
||||
const { currentUser } = useAuth()
|
||||
const { data, loading } = useQuery(QUERY, {
|
||||
skip: !currentUser?.sub,
|
||||
variables: { id: currentUser?.sub },
|
||||
})
|
||||
return { user: data?.user, loading }
|
||||
}
|
||||
15
app/web/src/helpers/subscribe.js
Normal file
15
app/web/src/helpers/subscribe.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export const subscribe = ({ email, addMessage }) => {
|
||||
// subscribe to mailchimp newsletter
|
||||
const path = window.location.hostname + window.location.pathname
|
||||
try {
|
||||
fetch(
|
||||
`https://kurthutten.us10.list-manage.com/subscribe/post-json?u=cbd8888e924bdd99d06c14fa5&id=6a765a8b3d&EMAIL=${email}&FNAME=Kurt&PATHNAME=${path}&c=__jp0`
|
||||
)
|
||||
} catch (e) {
|
||||
setTimeout(() => {
|
||||
addMessage('Problem subscribing to newsletter', {
|
||||
classes: 'bg-red-300 text-red-900',
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user