Compare commits
23 Commits
kurt/serve
...
release
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb1e786305 | ||
|
|
8bc6674849 | ||
|
|
cd7d618276 | ||
|
|
063e13ff4a | ||
|
|
f61280ef00 | ||
|
|
8500d223d4 | ||
|
|
b91723ced4 | ||
|
|
859e018251 | ||
|
|
f30eeb2b95 | ||
|
|
0a6439161e | ||
|
|
861b8374bf | ||
|
|
844a1f6961 | ||
|
|
5531f2e0c1 | ||
|
|
c48afaf07b | ||
|
|
90fece9598 | ||
|
|
82dd3d2555 | ||
|
|
e7dec57644 | ||
|
|
35fcd55229 | ||
|
|
3fef6474d3 | ||
|
|
cd3060b3c7 | ||
|
|
cef1d34c6f | ||
|
|
70d4c40eac | ||
|
|
2dec867803 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
.idea
|
||||
.history
|
||||
.DS_Store
|
||||
.env
|
||||
.netlify
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"Cadhub",
|
||||
"cadquery",
|
||||
"curv",
|
||||
"Customizer",
|
||||
"Hutten",
|
||||
"cadquery",
|
||||
"jscad",
|
||||
"openscad",
|
||||
"sendmail"
|
||||
|
||||
@@ -22,9 +22,9 @@ Because Each CadPackage is it's own beast we opted to use Docker in order to giv
|
||||
## Getting your dev environment setup
|
||||
|
||||
|
||||
Clone the repo and `cd` in the app directory (the docs directory is for [learn.cadhub](https://learn.cadhub.xyz/))
|
||||
Clone the repo, then `cd` in the repo and app directory (the docs directory is for [learn.cadhub](https://learn.cadhub.xyz/))
|
||||
```
|
||||
cd app
|
||||
cd cadhub/app
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
@@ -34,7 +34,7 @@ yarn install
|
||||
|
||||
Setting up the db, you'll need to have a postgres installed locally, you can [follow this guide](https://redwoodjs.com/docs/local-postgres-setup).
|
||||
|
||||
Run the following
|
||||
Run the following (Note: these commands require the `DATABASE_URL` env variable to be set. if you see no result when you run `echo $DATABASE_URL`, you can set it with a command like `export DATABASE_URL=postgres://postgres:somepassword@localhost`)
|
||||
``` terminal
|
||||
yarn rw prisma migrate dev
|
||||
yarn rw prisma db seed
|
||||
@@ -59,6 +59,27 @@ localUser2@kurthutten.com: `abc123`
|
||||
|
||||
localAdmin@kurthutten.com: `abc123`
|
||||
|
||||
### Discord bot setup
|
||||
|
||||
To set up the discord bot to notify when users publish new content, we're using the [REST](https://discord.com/developers/docs/resources/channel#message-object) API directly, used more as a notification service rather than a bot since we are not listening to messages in the chat.
|
||||
|
||||
1. If you're setting up the bot in a dev environment, create a new discord server (the "plus" button on the left when logged into the Discord webpage). Make note of the name of the project.
|
||||
2. With [developer mode turned on](https://www.howtogeek.com/714348/how-to-enable-or-disable-developer-mode-on-discord/), right click the channel you wish the bot to announce on and select "Copy ID". Add this to `.env.defaults` as `DISCORD_CHANNEL_ID`.
|
||||
3. [create a new application](https://discord.com/developers/applications), or navigate to an existing one.
|
||||
4. Create a bot within that application. Copy the bot token and add it to `.env.defaults` as `DISCORD_TOKEN`.
|
||||
5. Go to the "URL Generator" under "OAuth2" and create a URL with scope "bot" and text permission "Send Messages".
|
||||
6. Copy the generated URL and open it in a new tab. Follow the instructions on the page to add the bot to your discord server.
|
||||
|
||||
When you next start CADHub, you should see in the logs `Discord: logged in as <bot name>` and you should see a startup message from the bot in the channel.
|
||||
|
||||
To send messages as the bot when things happen in the service, use the `sendChat` helper function:
|
||||
|
||||
```typescript
|
||||
import { sendDiscordMessage } from 'src/lib/discord'
|
||||
|
||||
sendDiscordMessage("hello world!")
|
||||
```
|
||||
|
||||
## Designs
|
||||
|
||||
In progress, though can be [seen on Figma](https://www.figma.com/file/VUh53RdncjZ7NuFYj0RGB9/CadHub?node-id=0%3A1)
|
||||
|
||||
@@ -18,9 +18,9 @@ CLOUDINARY_API_KEY=476712943135152
|
||||
# trace | info | debug | warn | error | silent
|
||||
# LOG_LEVEL=debug
|
||||
|
||||
|
||||
# EMAIL_PASSWORD=abc123
|
||||
|
||||
# DISCORD_TOKEN=abc123
|
||||
# DISCORD_CHANNEL_ID=12345
|
||||
|
||||
# CAD_LAMBDA_BASE_URL="http://localhost:8080"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
lts/*
|
||||
16
|
||||
|
||||
2
app/api/db/migrations/20211129205924_curv/migration.sql
Normal file
2
app/api/db/migrations/20211129205924_curv/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "CadPackage" ADD VALUE 'curv';
|
||||
@@ -5,7 +5,7 @@ datasource db {
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = "native"
|
||||
binaryTargets = ["native", "darwin-arm64", "darwin"]
|
||||
}
|
||||
|
||||
// sqlLight does not suport enums so we can't use enums until we set up postgresql in dev mode
|
||||
@@ -37,7 +37,8 @@ model User {
|
||||
enum CadPackage {
|
||||
openscad
|
||||
cadquery
|
||||
jscad // TODO #422, add jscad to db schema when were ready to enable saving of jscad projects
|
||||
jscad
|
||||
curv
|
||||
}
|
||||
|
||||
model Project {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"@redwoodjs/api": "^0.38.1",
|
||||
"@redwoodjs/graphql-server": "^0.38.1",
|
||||
"@sentry/node": "^6.5.1",
|
||||
"axios": "^0.21.1",
|
||||
"axios": "^0.25.0",
|
||||
"cloudinary": "^1.23.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
|
||||
@@ -34,8 +34,12 @@ const makeRequest = (route, port) => [
|
||||
|
||||
app.post(...makeRequest('/openscad/preview', 5052))
|
||||
app.post(...makeRequest('/openscad/stl', 5053))
|
||||
|
||||
app.post(...makeRequest('/cadquery/stl', 5060))
|
||||
|
||||
app.post(...makeRequest('/curv/preview', 5070))
|
||||
app.post(...makeRequest('/curv/stl', 5071))
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Example app listening at http://localhost:${port}`)
|
||||
})
|
||||
|
||||
@@ -41,8 +41,8 @@ RUN npm install aws-lambda-ric@1.0.0
|
||||
RUN conda --version
|
||||
|
||||
# Install CadQuery
|
||||
RUN conda install -c cadquery -c conda-forge cadquery=master ocp=7.5.2 python=3.8
|
||||
RUN conda info
|
||||
# RUN conda install -c cadquery -c conda-forge cadquery=master ocp=7.5.2 python=3.8
|
||||
# RUN conda info
|
||||
|
||||
# Get a copy of cq-cli from GitHub
|
||||
RUN git clone https://github.com/CadQuery/cq-cli.git
|
||||
|
||||
@@ -9,12 +9,16 @@ const stl = async (req, _context, callback) => {
|
||||
console.log('eventBody', eventBody)
|
||||
|
||||
const { file, settings } = JSON.parse(eventBody)
|
||||
const { error, consoleMessage, fullPath } = await runCQ({ file, settings })
|
||||
const { error, fullPath } = await runCQ({
|
||||
file,
|
||||
settings,
|
||||
})
|
||||
await storeAssetAndReturnUrl({
|
||||
error,
|
||||
callback,
|
||||
fullPath,
|
||||
consoleMessage,
|
||||
consoleMessage: '',
|
||||
tempFile : '',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -30,31 +30,32 @@ export const runCQ = async ({
|
||||
].join(' ')
|
||||
console.log('command', command)
|
||||
let consoleMessage = ''
|
||||
try {
|
||||
consoleMessage = await runCommand(command, 30000)
|
||||
const params = JSON.parse(
|
||||
await readFile(customizerPath, { encoding: 'ascii' })
|
||||
)
|
||||
await writeFiles(
|
||||
[
|
||||
{
|
||||
file: JSON.stringify({
|
||||
customizerParams: params,
|
||||
consoleMessage,
|
||||
type: 'stl',
|
||||
}),
|
||||
fileName: 'metadata.json',
|
||||
},
|
||||
],
|
||||
tempFile
|
||||
)
|
||||
await runCommand(
|
||||
`cat ${stlPath} /var/task/cadhub-concat-split /tmp/${tempFile}/metadata.json | gzip > ${fullPath}`,
|
||||
15000,
|
||||
true
|
||||
)
|
||||
return { consoleMessage, fullPath }
|
||||
} catch (error) {
|
||||
return { error: consoleMessage || error, fullPath }
|
||||
}
|
||||
return { error: 'python execution currently disabled, see: https://github.com/Irev-Dev/cadhub/issues/611', fullPath }
|
||||
// try {
|
||||
// consoleMessage = await runCommand(command, 30000)
|
||||
// const params = JSON.parse(
|
||||
// await readFile(customizerPath, { encoding: 'ascii' })
|
||||
// )
|
||||
// await writeFiles(
|
||||
// [
|
||||
// {
|
||||
// file: JSON.stringify({
|
||||
// customizerParams: params,
|
||||
// consoleMessage,
|
||||
// type: 'stl',
|
||||
// }),
|
||||
// fileName: 'metadata.json',
|
||||
// },
|
||||
// ],
|
||||
// tempFile
|
||||
// )
|
||||
// await runCommand(
|
||||
// `cat ${stlPath} /var/task/cadhub-concat-split /tmp/${tempFile}/metadata.json | gzip > ${fullPath}`,
|
||||
// 15000,
|
||||
// true
|
||||
// )
|
||||
// return { consoleMessage, fullPath, tempFile }
|
||||
// } catch (error) {
|
||||
// return { error: consoleMessage || error, fullPath }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -104,11 +104,13 @@ export async function storeAssetAndReturnUrl({
|
||||
callback,
|
||||
fullPath,
|
||||
consoleMessage,
|
||||
tempFile,
|
||||
}: {
|
||||
error: string
|
||||
callback: Function
|
||||
fullPath: string
|
||||
consoleMessage: string
|
||||
tempFile: string
|
||||
}) {
|
||||
if (error) {
|
||||
const response = {
|
||||
@@ -124,6 +126,7 @@ export async function storeAssetAndReturnUrl({
|
||||
|
||||
try {
|
||||
buffer = await readFile(fullPath, { encoding: 'base64' })
|
||||
await runCommand(`rm -R /tmp/${tempFile}`)
|
||||
} catch (e) {
|
||||
console.log('read file error', e)
|
||||
const response = {
|
||||
|
||||
67
app/api/src/docker/curv/Dockerfile
Normal file
67
app/api/src/docker/curv/Dockerfile
Normal file
@@ -0,0 +1,67 @@
|
||||
FROM public.ecr.aws/lts/ubuntu:20.04_stable
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
|
||||
RUN apt-get update --fix-missing -qq
|
||||
RUN apt-get update --fix-missing && apt-get -y -qq install software-properties-common dirmngr apt-transport-https lsb-release ca-certificates xvfb
|
||||
|
||||
RUN apt-get update -qq
|
||||
|
||||
RUN apt-get -y -qq install git \
|
||||
software-properties-common \
|
||||
xvfb unzip maim clang cmake \
|
||||
git-core libboost-all-dev \
|
||||
libopenexr-dev libtbb-dev \
|
||||
libglm-dev libpng-dev \
|
||||
libeigen3-dev dbus-x11 \
|
||||
libxcursor-dev libxinerama-dev \
|
||||
libxrandr-dev libglu1-mesa-dev \
|
||||
libgles2-mesa-dev libgl1-mesa-dev \
|
||||
libxi-dev
|
||||
|
||||
# Use commit to make sure build is reproduceable
|
||||
RUN git clone --recursive https://github.com/curv3d/curv && \
|
||||
cd curv && \
|
||||
git checkout b849eb57fba121f9f218dc065dc1f5ebc619836d && \
|
||||
make && make install
|
||||
|
||||
# install node14, see comment at the top of node14source_setup.sh
|
||||
ADD src/docker/common/node14source_setup.sh /nodesource_setup.sh
|
||||
RUN ["chmod", "+x", "/nodesource_setup.sh"]
|
||||
RUN bash nodesource_setup.sh
|
||||
RUN apt-get install -y nodejs
|
||||
|
||||
# Install aws-lambda-cpp build dependencies, this is for the post install script in aws-lambda-ric (in package.json)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
g++ \
|
||||
make \
|
||||
cmake \
|
||||
unzip \
|
||||
automake autoconf libtool \
|
||||
libcurl4-openssl-dev
|
||||
|
||||
# Add the lambda emulator for local dev, (see entrypoint.sh for where it's used),
|
||||
# I have the file locally (gitignored) to speed up build times (as it downloads everytime),
|
||||
# but you can use the http version of the below ADD command or download it yourself from that url.
|
||||
ADD src/docker/common/aws-lambda-rie /usr/local/bin/aws-lambda-rie
|
||||
# ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v1.0/aws-lambda-rie /usr/local/bin/aws-lambda-rie
|
||||
RUN ["chmod", "+x", "/usr/local/bin/aws-lambda-rie"]
|
||||
|
||||
WORKDIR /var/task/
|
||||
COPY package*.json /var/task/
|
||||
RUN npm install
|
||||
RUN npm install aws-lambda-ric@1.0.0
|
||||
|
||||
RUN echo "cadhub-concat-split" > /var/task/cadhub-concat-split
|
||||
|
||||
# using built javascript from dist
|
||||
# run `yarn rw build` before bulding this image
|
||||
COPY dist/docker/curv/* /var/task/js/
|
||||
COPY dist/docker/common/* /var/task/common/
|
||||
COPY src/docker/common/entrypoint.sh /entrypoint.sh
|
||||
RUN ["chmod", "+x", "/entrypoint.sh"]
|
||||
|
||||
ENTRYPOINT ["sh", "/entrypoint.sh"]
|
||||
CMD [ "js/curv.preview" ]
|
||||
48
app/api/src/docker/curv/curv.ts
Normal file
48
app/api/src/docker/curv/curv.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { runCurv, stlExport } from './runCurv'
|
||||
import middy from 'middy'
|
||||
import { cors } from 'middy/middlewares'
|
||||
import { loggerWrap, storeAssetAndReturnUrl } from '../common/utils'
|
||||
|
||||
const preview = async (req, _context, callback) => {
|
||||
_context.callbackWaitsForEmptyEventLoop = false
|
||||
const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
|
||||
console.log('eventBody', eventBody)
|
||||
|
||||
const { file, settings } = JSON.parse(eventBody)
|
||||
const { error, consoleMessage, fullPath, tempFile } = await runCurv({
|
||||
file,
|
||||
settings,
|
||||
})
|
||||
await storeAssetAndReturnUrl({
|
||||
error,
|
||||
callback,
|
||||
fullPath,
|
||||
consoleMessage,
|
||||
tempFile,
|
||||
})
|
||||
}
|
||||
|
||||
const stl = async (req, _context, callback) => {
|
||||
_context.callbackWaitsForEmptyEventLoop = false
|
||||
const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
|
||||
|
||||
console.log(eventBody, 'eventBody')
|
||||
|
||||
const { file, settings } = JSON.parse(eventBody)
|
||||
const { error, consoleMessage, fullPath, tempFile } = await stlExport({
|
||||
file,
|
||||
settings,
|
||||
})
|
||||
await storeAssetAndReturnUrl({
|
||||
error,
|
||||
callback,
|
||||
fullPath,
|
||||
consoleMessage,
|
||||
tempFile,
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
stl: middy(loggerWrap(stl)).use(cors()),
|
||||
preview: middy(loggerWrap(preview)).use(cors()),
|
||||
}
|
||||
114
app/api/src/docker/curv/runCurv.ts
Normal file
114
app/api/src/docker/curv/runCurv.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { writeFiles, runCommand } from '../common/utils'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
export const runCurv = async ({
|
||||
file,
|
||||
settings: { size: { x = 500, y = 500 } = {}, parameters } = {}, // TODO add view settings
|
||||
} = {}): Promise<{
|
||||
error?: string
|
||||
consoleMessage?: string
|
||||
fullPath?: string
|
||||
customizerPath?: string
|
||||
tempFile?: string
|
||||
}> => {
|
||||
const tempFile = await writeFiles(
|
||||
[
|
||||
{ file, fileName: 'main.curv' },
|
||||
{
|
||||
file: JSON.stringify({
|
||||
parameterSets: { default: parameters },
|
||||
fileFormatVersion: '1',
|
||||
}),
|
||||
fileName: 'params.json',
|
||||
},
|
||||
],
|
||||
'a' + nanoid() // 'a' ensure nothing funny happens if it start with a bad character like "-", maybe I should pick a safer id generator :shrug:
|
||||
)
|
||||
const fullPath = `/tmp/${tempFile}/output.gz`
|
||||
const imPath = `/tmp/${tempFile}/output.png`
|
||||
const customizerPath = `/tmp/${tempFile}/customizer.param`
|
||||
|
||||
const command = [
|
||||
'xvfb-run --auto-servernum --server-args "-screen 0 3840x2160x24" curv',
|
||||
`-o ${imPath}`,
|
||||
`-O xsize=${x}`,
|
||||
`-O ysize=${y}`,
|
||||
`-O bg=webRGB[26,26,29]`, // #1A1A1D
|
||||
`/tmp/${tempFile}/main.curv`,
|
||||
].join(' ')
|
||||
console.log('command', command)
|
||||
|
||||
try {
|
||||
const consoleMessage = await runCommand(command, 15000)
|
||||
await writeFiles(
|
||||
[
|
||||
{
|
||||
file: JSON.stringify({
|
||||
consoleMessage,
|
||||
type: 'png',
|
||||
}),
|
||||
fileName: 'metadata.json',
|
||||
},
|
||||
],
|
||||
tempFile
|
||||
)
|
||||
await runCommand(
|
||||
`cat ${imPath} /var/task/cadhub-concat-split /tmp/${tempFile}/metadata.json | gzip > ${fullPath}`,
|
||||
15000
|
||||
)
|
||||
return { consoleMessage, fullPath, customizerPath, tempFile }
|
||||
} catch (dirtyError) {
|
||||
return { error: dirtyError }
|
||||
}
|
||||
}
|
||||
|
||||
export const stlExport = async ({ file, settings: { parameters } } = {}) => {
|
||||
const tempFile = await writeFiles(
|
||||
[
|
||||
{ file, fileName: 'main.curv' },
|
||||
{
|
||||
file: JSON.stringify({
|
||||
parameterSets: { default: parameters },
|
||||
fileFormatVersion: '1',
|
||||
}),
|
||||
fileName: 'params.json',
|
||||
},
|
||||
],
|
||||
'a' + nanoid() // 'a' ensure nothing funny happens if it start with a bad character like "-", maybe I should pick a safer id generator :shrug:
|
||||
)
|
||||
const fullPath = `/tmp/${tempFile}/output.gz`
|
||||
const stlPath = `/tmp/${tempFile}/output.stl`
|
||||
const command = [
|
||||
'(cd /tmp && curv',
|
||||
'-o',
|
||||
stlPath,
|
||||
'-O jit',
|
||||
'-O vcount=350000',
|
||||
`/tmp/${tempFile}/main.curv`,
|
||||
')',
|
||||
].join(' ')
|
||||
|
||||
try {
|
||||
// lambda will time out before this, we might need to look at background jobs if we do git integration stl generation
|
||||
const consoleMessage = await runCommand(command, 60000)
|
||||
await writeFiles(
|
||||
[
|
||||
{
|
||||
file: JSON.stringify({
|
||||
consoleMessage,
|
||||
type: 'stl',
|
||||
}),
|
||||
fileName: 'metadata.json',
|
||||
},
|
||||
],
|
||||
tempFile
|
||||
)
|
||||
await runCommand(
|
||||
`cat ${stlPath} /var/task/cadhub-concat-split /tmp/${tempFile}/metadata.json | gzip > ${fullPath} && rm ${stlPath}`,
|
||||
15000
|
||||
)
|
||||
return { consoleMessage, fullPath, tempFile }
|
||||
} catch (error) {
|
||||
return { error, fullPath }
|
||||
}
|
||||
}
|
||||
@@ -45,3 +45,28 @@ services:
|
||||
AWS_ACCESS_KEY_ID: "${DEV_AWS_ACCESS_KEY_ID}"
|
||||
BUCKET: "${DEV_BUCKET}"
|
||||
|
||||
curv-preview:
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: ./src/docker/curv/Dockerfile
|
||||
image: curv
|
||||
command: js/curv.preview
|
||||
# Adding volumes so that the containers can be restarted for js only changes in local dev
|
||||
volumes:
|
||||
- ../../dist/docker/curv:/var/task/js/
|
||||
- ../../dist/docker/common:/var/task/common/
|
||||
ports:
|
||||
- "5070:8080"
|
||||
curv-stl:
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: ./src/docker/curv/Dockerfile
|
||||
image: curv
|
||||
command: js/curv.stl
|
||||
# Adding volumes so that the containers can be restarted for js only changes in local dev
|
||||
volumes:
|
||||
- ../../dist/docker/curv:/var/task/js/
|
||||
- ../../dist/docker/common:/var/task/common/
|
||||
ports:
|
||||
- "5071:8080"
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const preview = async (req, _context, callback) => {
|
||||
console.log('eventBody', eventBody)
|
||||
|
||||
const { file, settings } = JSON.parse(eventBody)
|
||||
const { error, consoleMessage, fullPath } = await runScad({
|
||||
const { error, consoleMessage, fullPath, tempFile } = await runScad({
|
||||
file,
|
||||
settings,
|
||||
})
|
||||
@@ -18,6 +18,7 @@ const preview = async (req, _context, callback) => {
|
||||
callback,
|
||||
fullPath,
|
||||
consoleMessage,
|
||||
tempFile,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,7 +29,7 @@ const stl = async (req, _context, callback) => {
|
||||
console.log(eventBody, 'eventBody')
|
||||
|
||||
const { file, settings } = JSON.parse(eventBody)
|
||||
const { error, consoleMessage, fullPath } = await stlExport({
|
||||
const { error, consoleMessage, fullPath, tempFile } = await stlExport({
|
||||
file,
|
||||
settings,
|
||||
})
|
||||
@@ -37,6 +38,7 @@ const stl = async (req, _context, callback) => {
|
||||
callback,
|
||||
fullPath,
|
||||
consoleMessage,
|
||||
tempFile,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ export const runScad = async ({
|
||||
consoleMessage?: string
|
||||
fullPath?: string
|
||||
customizerPath?: string
|
||||
tempFile?: string
|
||||
}> => {
|
||||
const tempFile = await writeFiles(
|
||||
[
|
||||
@@ -88,7 +89,7 @@ export const runScad = async ({
|
||||
`cat ${imPath} /var/task/cadhub-concat-split /tmp/${tempFile}/metadata.json | gzip > ${fullPath}`,
|
||||
15000
|
||||
)
|
||||
return { consoleMessage, fullPath, customizerPath }
|
||||
return { consoleMessage, fullPath, customizerPath, tempFile }
|
||||
} catch (dirtyError) {
|
||||
return { error: cleanOpenScadError(dirtyError) }
|
||||
}
|
||||
@@ -143,7 +144,7 @@ export const stlExport = async ({ file, settings: { parameters } } = {}) => {
|
||||
`cat ${stlPath} /var/task/cadhub-concat-split /tmp/${tempFile}/metadata.json | gzip > ${fullPath}`,
|
||||
15000
|
||||
)
|
||||
return { consoleMessage, fullPath, customizerPath }
|
||||
return { consoleMessage, fullPath, customizerPath, tempFile }
|
||||
} catch (error) {
|
||||
return { error, fullPath }
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ provider:
|
||||
cadqueryimage:
|
||||
path: ../../
|
||||
file: ./src/docker/cadquery/Dockerfile
|
||||
curvimage:
|
||||
path: ../../
|
||||
file: ./src/docker/curv/Dockerfile
|
||||
apiGateway:
|
||||
metrics: true
|
||||
binaryMediaTypes:
|
||||
@@ -67,6 +70,7 @@ functions:
|
||||
method: post
|
||||
cors: true
|
||||
timeout: 25
|
||||
memorySize: 2048
|
||||
environment:
|
||||
BUCKET: cad-preview-bucket-prod-001
|
||||
openscadstl:
|
||||
@@ -84,6 +88,7 @@ functions:
|
||||
timeout: 30
|
||||
environment:
|
||||
BUCKET: cad-preview-bucket-prod-001
|
||||
|
||||
cadquerystl:
|
||||
image:
|
||||
name: cadqueryimage
|
||||
@@ -99,6 +104,34 @@ functions:
|
||||
timeout: 30
|
||||
environment:
|
||||
BUCKET: cad-preview-bucket-prod-001
|
||||
|
||||
curvpreview:
|
||||
image:
|
||||
name: curvimage
|
||||
command:
|
||||
- js/curv.preview
|
||||
entryPoint:
|
||||
- '/entrypoint.sh'
|
||||
events:
|
||||
- http:
|
||||
path: curv/preview
|
||||
method: post
|
||||
cors: true
|
||||
timeout: 25
|
||||
memorySize: 3008
|
||||
curvstl:
|
||||
image:
|
||||
name: curvimage
|
||||
command:
|
||||
- js/curv.stl
|
||||
entryPoint:
|
||||
- '/entrypoint.sh'
|
||||
events:
|
||||
- http:
|
||||
path: curv/stl
|
||||
method: post
|
||||
cors: true
|
||||
timeout: 30
|
||||
# The following are a few example events you can configure
|
||||
# NOTE: Please make sure to change your handler code to work with those events
|
||||
# Check the event documentation for details
|
||||
|
||||
@@ -19,10 +19,12 @@ export const schema = gql`
|
||||
childForks: [Project]!
|
||||
}
|
||||
|
||||
# should match enum in api/db/schema.prisma
|
||||
enum CadPackage {
|
||||
openscad
|
||||
cadquery
|
||||
jscad
|
||||
curv
|
||||
}
|
||||
|
||||
type Query {
|
||||
|
||||
33
app/api/src/lib/discord.ts
Normal file
33
app/api/src/lib/discord.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import axios from 'axios'
|
||||
|
||||
let inst = null;
|
||||
if (!process.env.DISCORD_TOKEN || !process.env.DISCORD_CHANNEL_ID) {
|
||||
console.warn("Discord bot not configured - please set process.env.DISCORD_TOKEN and process.env.DISCORD_CHANNEL_ID to send discord chats");
|
||||
} else {
|
||||
inst = axios.create({
|
||||
baseURL: 'https://discord.com/api'
|
||||
});
|
||||
inst.defaults.headers.common['Authorization'] = `Bot ${process.env.DISCORD_TOKEN}`
|
||||
console.log(`Discord: using API token ${process.env.DISCORD_TOKEN}`);
|
||||
}
|
||||
|
||||
export async function sendDiscordMessage(text: string, url?: string) {
|
||||
if (!inst) {
|
||||
console.error(`Discord: not configured to send message ("${text}")`);
|
||||
} else {
|
||||
const API_URL = `/channels/${process.env.DISCORD_CHANNEL_ID}/messages`;
|
||||
if (url) {
|
||||
return inst.post(API_URL, { embeds: [{
|
||||
title: text,
|
||||
image: {
|
||||
url,
|
||||
},
|
||||
}] });
|
||||
} else {
|
||||
return inst.post(API_URL, {
|
||||
content: text,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
} from 'src/services/helpers'
|
||||
import { requireAuth } from 'src/lib/auth'
|
||||
import { requireOwnership, requireProjectOwnership } from 'src/lib/owner'
|
||||
import { sendDiscordMessage } from 'src/lib/discord'
|
||||
|
||||
|
||||
export const projects = ({ userName }) => {
|
||||
if (!userName) {
|
||||
@@ -243,7 +245,19 @@ export const updateProjectImages = async ({
|
||||
const [updatedProject] = await Promise.all([
|
||||
projectPromise,
|
||||
imageDestroyPromise,
|
||||
])
|
||||
]).then(async (result) => {
|
||||
const { userName } = await db.user.findUnique({
|
||||
where: { id: project.userId },
|
||||
})
|
||||
sendDiscordMessage([
|
||||
`${userName} just added an image to their ${project.cadPackage} project:`,
|
||||
` => ${project.title}`,
|
||||
``,
|
||||
`Check it out, leave a comment, make them feel welcome!`,
|
||||
`https://cadhub.xyz/u/${userName}/${project.title}`
|
||||
].join('\n'), `https://res.cloudinary.com/irevdev/image/upload/c_scale,w_700/v1/${mainImage}`)
|
||||
return result
|
||||
})
|
||||
return updatedProject
|
||||
}
|
||||
|
||||
@@ -277,8 +291,11 @@ export const Project = {
|
||||
forkedFrom: (_obj, { root }) =>
|
||||
root.forkedFromId &&
|
||||
db.project.findUnique({ where: { id: root.forkedFromId } }),
|
||||
childForks: (_obj, { root }) =>
|
||||
db.project.findMany({ where: { forkedFromId: root.id } }),
|
||||
childForks: (_obj, { root }) => {
|
||||
console.log(' ')
|
||||
return []
|
||||
},
|
||||
// db.project.findMany({ where: { forkedFromId: root.id } }),
|
||||
user: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
||||
db.user.findUnique({ where: { id: root.userId } }),
|
||||
socialCard: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.x <=16.x",
|
||||
"yarn": ">=1.15"
|
||||
},
|
||||
"prisma": {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
'SENTRY_AUTH_TOKEN',
|
||||
'SENTRY_ORG',
|
||||
'SENTRY_PROJECT',
|
||||
'EMAIL_PASSWORD'
|
||||
'EMAIL_PASSWORD',
|
||||
]
|
||||
# experimentalFastRefresh = true # this seems to break cascadeStudio
|
||||
[api]
|
||||
|
||||
@@ -6,7 +6,7 @@ module.exports = (config, { env }) => {
|
||||
}
|
||||
})
|
||||
config.module.rules.push({
|
||||
test: /\.(md|jscad\.js|py|scad)$/i,
|
||||
test: /\.(md|jscad\.js|py|scad|curv)$/i,
|
||||
use: 'raw-loader',
|
||||
});
|
||||
return config
|
||||
|
||||
@@ -56,6 +56,7 @@ const Routes = () => {
|
||||
<Route path="/u/{userName}" page={UserPage} name="user" />
|
||||
<Route path="/u/{userName}/{projectTitle}" page={ProjectPage} name="project" />
|
||||
<Route path="/u/{userName}/{projectTitle}/ide" page={IdeProjectPage} name="ide" />
|
||||
<Route path="/u/{userName}/{projectTitle}/embed" page={EmbedProjectPage} name="embed" />
|
||||
<Route path="/u/{userName}/{projectTitle}/social-card" page={SocialCardPage} name="socialCard" />
|
||||
|
||||
<Private unauthenticated="home" role="admin">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type CadPackageType = 'openscad' | 'cadquery' | 'jscad' | 'INIT'
|
||||
export type CadPackageType = 'openscad' | 'cadquery' | 'jscad' | 'curv' | 'INIT'
|
||||
|
||||
interface CadPackageConfig {
|
||||
label: string
|
||||
@@ -23,6 +23,11 @@ export const cadPackageConfigs: { [key in CadPackageType]: CadPackageConfig } =
|
||||
buttonClasses: 'bg-ch-purple-500',
|
||||
dotClasses: 'bg-yellow-300',
|
||||
},
|
||||
curv: {
|
||||
label: 'Curv',
|
||||
buttonClasses: 'bg-blue-600',
|
||||
dotClasses: 'bg-green-500',
|
||||
},
|
||||
INIT: {
|
||||
label: '',
|
||||
buttonClasses: '',
|
||||
|
||||
@@ -25,6 +25,7 @@ const CaptureButtonViewer = ({
|
||||
const threeInstance = React.useRef(null)
|
||||
const [dataType, dataTypeSetter] = useState(state?.objectData?.type)
|
||||
const [artifact, artifactSetter] = useState(state?.objectData?.data)
|
||||
const [ideType] = useState(state?.ideType)
|
||||
const [isLoading, isLoadingSetter] = useState(false)
|
||||
const [camera, cameraSetter] = useState<State['camera'] | null>(null)
|
||||
const getThreeInstance = (_threeInstance) => {
|
||||
@@ -33,7 +34,7 @@ const CaptureButtonViewer = ({
|
||||
}
|
||||
const onCameraChange = (camera, isFirstCameraChange) => {
|
||||
const renderPromise =
|
||||
state.ideType === 'openscad' &&
|
||||
(state.ideType === 'openscad' || state.ideType === 'curv') &&
|
||||
requestRenderStateless({
|
||||
state,
|
||||
camera,
|
||||
@@ -70,6 +71,7 @@ const CaptureButtonViewer = ({
|
||||
isLoading={isLoading}
|
||||
camera={camera}
|
||||
isMinimal
|
||||
ideType={ideType}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -42,18 +42,25 @@ const EditableProjectTitle = ({
|
||||
}
|
||||
setNewTitle(target.value.replace(/([^a-zA-Z\d_:])/g, '-').slice(0, 25))
|
||||
}
|
||||
const onKeyDown = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
updateProject({ variables: { id, input: { title: newTitle } } });
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{!inEditMode && (
|
||||
<>
|
||||
<Link
|
||||
/<Link
|
||||
className="underline-hovered"
|
||||
to={routes.project({
|
||||
userName,
|
||||
projectTitle,
|
||||
})}
|
||||
className="pl-4"
|
||||
>
|
||||
/{projectTitle}
|
||||
{projectTitle}
|
||||
</Link>
|
||||
{canEdit && (
|
||||
<button
|
||||
@@ -76,6 +83,7 @@ const EditableProjectTitle = ({
|
||||
value={newTitle}
|
||||
onChange={onTitleChange}
|
||||
ref={inputRef}
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={() =>
|
||||
setTimeout(() => {
|
||||
setInEditMode(false)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { useRender } from 'src/components/IdeWrapper/useRender'
|
||||
import { makeStlDownloadHandler, PullTitleFromFirstLine } from './helpers'
|
||||
import { makeStlDownloadHandler, PullTitleFromFirstLine } from 'src/helpers/download_stl'
|
||||
import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode'
|
||||
import { DropdownItem } from './Dropdowns'
|
||||
import { useShortcutsModalContext } from './AllShortcutsModal'
|
||||
|
||||
31
app/web/src/components/EmbedProject/EmbedProject.tsx
Normal file
31
app/web/src/components/EmbedProject/EmbedProject.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import Seo from 'src/components/Seo/Seo'
|
||||
import IdeViewer from 'src/components/IdeViewer/IdeViewer'
|
||||
import { useIdeState } from 'src/helpers/hooks/useIdeState'
|
||||
import type { Project } from 'src/components/EmbedProjectCell/EmbedProjectCell'
|
||||
import { IdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||
import { use3dViewerResize } from 'src/helpers/hooks/use3dViewerResize'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
|
||||
interface Props {
|
||||
project?: Project
|
||||
}
|
||||
|
||||
const EmbedProject = ({ project }: Props) => {
|
||||
const [state, thunkDispatch] = useIdeState()
|
||||
const { viewerDomRef, handleViewerSizeUpdate } = use3dViewerResize()
|
||||
|
||||
useEffect(() => {
|
||||
handleViewerSizeUpdate()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen" ref={viewerDomRef} >
|
||||
<IdeContext.Provider value={{ state, thunkDispatch, project }}>
|
||||
<IdeViewer />
|
||||
</IdeContext.Provider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmbedProject
|
||||
@@ -0,0 +1,6 @@
|
||||
// Define your own mock data here:
|
||||
export const standard = (/* vars, { ctx, req } */) => ({
|
||||
ideProject: {
|
||||
id: 42,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Loading, Empty, Success } from './EmbedProjectCell'
|
||||
import { standard } from './EmbedProjectCell.mock'
|
||||
|
||||
export const loading = () => {
|
||||
return Loading ? <Loading /> : null
|
||||
}
|
||||
|
||||
export const empty = () => {
|
||||
return Empty ? <Empty /> : null
|
||||
}
|
||||
|
||||
export const success = () => {
|
||||
return Success ? <Success {...standard()} /> : null
|
||||
}
|
||||
|
||||
export default { title: 'Cells/IdeProjectCell' }
|
||||
@@ -0,0 +1,21 @@
|
||||
import { render, screen } from '@redwoodjs/testing'
|
||||
import { Loading, Empty, Success } from './EmbedProjectCell'
|
||||
import { standard } from './EmbedProjectCell.mock'
|
||||
|
||||
describe('IdeProjectCell', () => {
|
||||
test('Loading renders successfully', () => {
|
||||
render(<Loading />)
|
||||
// Use screen.debug() to see output
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('Empty renders successfully', async () => {
|
||||
render(<Empty />)
|
||||
expect(screen.getByText('Empty')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('Success renders successfully', async () => {
|
||||
render(<Success ideProject={standard().ideProject} />)
|
||||
expect(screen.getByText(/42/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
47
app/web/src/components/EmbedProjectCell/EmbedProjectCell.tsx
Normal file
47
app/web/src/components/EmbedProjectCell/EmbedProjectCell.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useIdeState } from 'src/helpers/hooks/useIdeState'
|
||||
import { IdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||
import EmbedViewer from '../EmbedViewer/EmbedViewer'
|
||||
import { QUERY as IdeQuery } from 'src/components/IdeProjectCell'
|
||||
|
||||
export const QUERY = IdeQuery
|
||||
export interface Project {
|
||||
id: string
|
||||
title: string
|
||||
code: string
|
||||
description: string
|
||||
mainImage: string
|
||||
createdAt: string
|
||||
cadPackage: 'openscad' | 'cadquery'
|
||||
user: {
|
||||
id: string
|
||||
userName: string
|
||||
image: string
|
||||
}
|
||||
}
|
||||
|
||||
export const Loading = () => <div>Loading...</div>
|
||||
|
||||
export const Empty = () => <div>Project not found</div>
|
||||
|
||||
interface SaveCodeArgs {
|
||||
input: any
|
||||
id: string
|
||||
isFork: boolean
|
||||
}
|
||||
|
||||
export const Success = ({
|
||||
project,
|
||||
refetch,
|
||||
}: {
|
||||
project: Project
|
||||
refetch: any
|
||||
}) => {
|
||||
const [state, thunkDispatch] = useIdeState()
|
||||
|
||||
|
||||
return (
|
||||
<IdeContext.Provider value={{ state, thunkDispatch, project }}>
|
||||
<EmbedViewer project={project} />
|
||||
</IdeContext.Provider>
|
||||
)
|
||||
}
|
||||
34
app/web/src/components/EmbedViewer/EmbedViewer.tsx
Normal file
34
app/web/src/components/EmbedViewer/EmbedViewer.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useIdeInit } from 'src/components/EncodedUrl/helpers'
|
||||
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||
import IdeViewer from 'src/components/IdeViewer/IdeViewer'
|
||||
import { use3dViewerResize } from 'src/helpers/hooks/use3dViewerResize'
|
||||
import CadPackage from '../CadPackage/CadPackage'
|
||||
import LogoType from '../LogoType/LogoType'
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
|
||||
function EmbedViewer() {
|
||||
const { state, project } = useIdeContext()
|
||||
useIdeInit(project?.cadPackage, project?.code || state?.code, "viewer")
|
||||
const { viewerDomRef } = use3dViewerResize()
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col h-screen group" ref={viewerDomRef}>
|
||||
<IdeViewer isMinimal={true} />
|
||||
<div className="absolute top-5 left-5 text-ch-gray-300">
|
||||
<h1 className="mb-4 text-4xl font-normal capitalize ">
|
||||
{project?.title.replace(/-/g, ' ')}
|
||||
</h1>
|
||||
<h2 className="mb-2 transition-opacity duration-100 group-hover:opacity-0">by @{ project?.user?.userName }</h2>
|
||||
<h2 className="transition-opacity duration-100 group-hover:opacity-0">built with <div className="inline-block"><CadPackage cadPackage={project?.cadPackage} className="px-3 py-2"/></div></h2>
|
||||
</div>
|
||||
<div className="absolute grid items-center grid-flow-col-dense gap-2 bottom-5 right-5 text-ch-gray-300">
|
||||
View on <Link className="inline-block" to={routes.project({
|
||||
userName: project?.user?.userName,
|
||||
projectTitle: project?.title.toString(),
|
||||
})}><LogoType className="inline-block" wrappedInLink={true}/></Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmbedViewer
|
||||
@@ -348,6 +348,10 @@ function ChooseYourCharacter() {
|
||||
cadPackage: 'jscad',
|
||||
desc: 'A JavaScript Code-CAD library that will feel familiar to web developers, based on the same tech as OpenSCAD.',
|
||||
},
|
||||
{
|
||||
cadPackage: 'curv',
|
||||
desc: "Curv is a programming language for creating art using mathematics. It's a 2D and 3D geometric modelling tool.",
|
||||
},
|
||||
].map(
|
||||
({
|
||||
cadPackage,
|
||||
|
||||
@@ -18,6 +18,7 @@ const IdeEditor = ({ Loading }) => {
|
||||
cadquery: 'python',
|
||||
openscad: 'cpp',
|
||||
jscad: 'javascript',
|
||||
curv: 'python',
|
||||
INIT: '',
|
||||
}
|
||||
const monaco = useMonaco()
|
||||
|
||||
@@ -102,9 +102,9 @@ export default function IdeHeader({
|
||||
<div className="flex h-full items-center text-gray-300">
|
||||
{project?.id && (
|
||||
<>
|
||||
<span className="bg-ch-gray-700 h-full grid grid-flow-col-dense items-center gap-2 px-4">
|
||||
<Gravatar image={project?.user?.image} className="w-10" />
|
||||
<span className="h-full grid grid-flow-col-dense items-center gap-2 ml-4">
|
||||
<Link
|
||||
className="underline-hovered"
|
||||
to={routes.user({
|
||||
userName: projectOwner,
|
||||
})}
|
||||
@@ -138,19 +138,19 @@ export default function IdeHeader({
|
||||
)}
|
||||
{!isProfile && (
|
||||
<TopButton
|
||||
className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||
className="bg-ch-blue-650 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||
onClick={handleRender}
|
||||
name={canEdit ? 'Save' : 'Preview'}
|
||||
>
|
||||
<Svg
|
||||
name={canEdit ? 'floppy-disk' : 'photograph'}
|
||||
className="w-6 h-6 text-ch-pink-500"
|
||||
className="w-6 h-6 text-ch-blue-400"
|
||||
/>
|
||||
</TopButton>
|
||||
)}
|
||||
{isProfile && (
|
||||
<TopButton
|
||||
className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||
className="bg-ch-blue-650 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
routes.ide({
|
||||
@@ -161,7 +161,7 @@ export default function IdeHeader({
|
||||
}
|
||||
name="Editor"
|
||||
>
|
||||
<Svg name="terminal" className="w-6 h-6 text-ch-pink-500" />
|
||||
<Svg name="terminal" className="w-6 h-6 text-ch-blue-400" />
|
||||
</TopButton>
|
||||
)}
|
||||
<Popover className="relative outline-none w-full h-full">
|
||||
@@ -172,11 +172,11 @@ export default function IdeHeader({
|
||||
<TopButton
|
||||
Tag="div"
|
||||
name="Share"
|
||||
className=" bg-ch-purple-400 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||
className="bg-ch-blue-650 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
|
||||
>
|
||||
<Svg
|
||||
name="share"
|
||||
className="w-6 h-6 text-ch-purple-500 mt-1"
|
||||
className="w-6 h-6 text-ch-blue-400 mt-1"
|
||||
/>
|
||||
</TopButton>
|
||||
</Popover.Button>
|
||||
@@ -221,7 +221,7 @@ export default function IdeHeader({
|
||||
<div className="fixed bg-white w-60 h-10 top-16 right-24 z-10 rounded-md text-sm flex p-2 items-center">
|
||||
<Svg
|
||||
name="exclamation-circle"
|
||||
className="w-8 h-8 mx-2 text-ch-blue-500"
|
||||
className="w-8 h-8 mx-2 text-ch-blue-400"
|
||||
/>{' '}
|
||||
Fork to save your work
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface Project {
|
||||
code: string
|
||||
mainImage: string
|
||||
createdAt: string
|
||||
cadPackage: 'openscad' | 'cadquery'
|
||||
cadPackage: 'openscad' | 'cadquery' | 'jscad' | 'curv'
|
||||
user: {
|
||||
id: string
|
||||
userName: string
|
||||
|
||||
@@ -4,12 +4,15 @@ import { PureIdeViewer } from './PureIdeViewer'
|
||||
|
||||
const IdeViewer = ({
|
||||
handleOwnCamera = false,
|
||||
isMinimal = false,
|
||||
}: {
|
||||
handleOwnCamera?: boolean
|
||||
handleOwnCamera?: boolean,
|
||||
isMinimal?: boolean,
|
||||
}) => {
|
||||
const { state, thunkDispatch } = useIdeContext()
|
||||
const dataType = state.objectData?.type
|
||||
const artifact = state.objectData?.data
|
||||
const ideType = state.ideType
|
||||
|
||||
const onInit = (threeInstance) => {
|
||||
thunkDispatch({ type: 'setThreeInstance', payload: threeInstance })
|
||||
@@ -24,7 +27,12 @@ const IdeViewer = ({
|
||||
})
|
||||
thunkDispatch((dispatch, getState) => {
|
||||
const state = getState()
|
||||
if (['png', 'INIT'].includes(state?.objectData?.type)) {
|
||||
if (
|
||||
['png', 'INIT'].includes(state?.objectData?.type) &&
|
||||
(ideType === 'openscad' ||
|
||||
state?.objectData?.type === 'INIT' ||
|
||||
!state?.objectData?.type)
|
||||
) {
|
||||
dispatch({ type: 'setLoading' })
|
||||
requestRender({
|
||||
state,
|
||||
@@ -44,6 +52,8 @@ const IdeViewer = ({
|
||||
onCameraChange={onCameraChange}
|
||||
isLoading={state.isLoading}
|
||||
camera={state?.camera}
|
||||
ideType={ideType}
|
||||
isMinimal={isMinimal}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -169,6 +169,7 @@ export function PureIdeViewer({
|
||||
isMinimal = false,
|
||||
scadRatio = 1,
|
||||
camera,
|
||||
ideType,
|
||||
}: {
|
||||
dataType: 'INIT' | ArtifactTypes
|
||||
artifact: any
|
||||
@@ -178,6 +179,7 @@ export function PureIdeViewer({
|
||||
isMinimal?: boolean
|
||||
scadRatio?: number
|
||||
camera?: State['camera']
|
||||
ideType?: State['ideType']
|
||||
}) {
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [image, setImage] = useState()
|
||||
@@ -210,13 +212,15 @@ export function PureIdeViewer({
|
||||
alt="code-cad preview"
|
||||
id="special"
|
||||
src={URL.createObjectURL(image)}
|
||||
className="h-full w-full"
|
||||
className="w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
|
||||
className={`opacity-0 absolute inset-0 transition-opacity duration-500 ${
|
||||
!(isDragging || dataType !== 'png')
|
||||
ideType === 'curv' && dataType === 'png' // TODO hide axes while curve doesn't have a controllable camera
|
||||
? 'opacity-0'
|
||||
: !(isDragging || dataType !== 'png')
|
||||
? 'hover:opacity-50'
|
||||
: 'opacity-100'
|
||||
}`}
|
||||
@@ -226,7 +230,10 @@ export function PureIdeViewer({
|
||||
<Controls
|
||||
onDragStart={() => setIsDragging(true)}
|
||||
onInit={onInit}
|
||||
onCameraChange={onCameraChange}
|
||||
onCameraChange={(...args) => {
|
||||
onCameraChange(...args)
|
||||
setIsDragging(false)
|
||||
}}
|
||||
controlsRef={controlsRef}
|
||||
camera={camera}
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@ interface EditToggleType {
|
||||
onEdit?: React.MouseEventHandler
|
||||
isEditing?: boolean
|
||||
}
|
||||
// small change
|
||||
|
||||
const EditToggle = ({
|
||||
onEdit = () => {
|
||||
|
||||
43
app/web/src/components/LogoType/LogoType.js
Normal file
43
app/web/src/components/LogoType/LogoType.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import Tooltip from '@material-ui/core/Tooltip'
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
import Svg from 'src/components/Svg'
|
||||
|
||||
export default function LogoType({ className="", wrappedInLink=false }) {
|
||||
return (
|
||||
<ul className={"flex items-center " + className}>
|
||||
<li>
|
||||
{ (wrappedInLink
|
||||
? <Link to={routes.home()}>
|
||||
<div className="ml-2 overflow-hidden rounded-full">
|
||||
<Svg className="w-10" name="favicon" />
|
||||
</div>
|
||||
</Link>
|
||||
: <div>
|
||||
<div className="ml-2 overflow-hidden rounded-full">
|
||||
<Svg className="w-10" name="favicon" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
<Tooltip title="Very alpha, there's lots of work todo">
|
||||
<div className="flex ml-4">
|
||||
{/* Because of how specific these styles are to this heading/logo and it doesn't need to be replicated else where as well as it's very precise with the placement of "pre-alpha" I think it's appropriate. */}
|
||||
<h2
|
||||
className="py-1 text-2xl text-indigo-300 md:text-5xl font-ropa-sans md:tracking-wider"
|
||||
style={{ letterSpacing: '0.3em' }}
|
||||
>
|
||||
CadHub
|
||||
</h2>
|
||||
<div
|
||||
className="hidden text-sm font-bold text-pink-400 font-ropa-sans md:block"
|
||||
style={{ paddingBottom: '2rem', marginLeft: '-1.8rem' }}
|
||||
>
|
||||
pre-alpha
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
@@ -95,6 +95,13 @@ const menuOptions: {
|
||||
dotClasses: 'bg-yellow-300',
|
||||
ideType: 'jscad',
|
||||
},
|
||||
{
|
||||
name: 'Curv',
|
||||
sub: 'alpha ',
|
||||
bgClasses: 'bg-blue-600',
|
||||
dotClasses: 'bg-green-500',
|
||||
ideType: 'curv',
|
||||
},
|
||||
]
|
||||
|
||||
const NavPlusButton: React.FC = () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MouseEventHandler, useContext } from 'react'
|
||||
import { MosaicWindowContext } from 'react-mosaic-component'
|
||||
import Svg from 'src/components/Svg/Svg'
|
||||
import OpenscadStaticImageMessage from 'src/components/OpenscadStaticImageMessage/OpenscadStaticImageMessage'
|
||||
import StaticImageMessage from 'src/components/StaticImageMessage/StaticImageMessage'
|
||||
|
||||
const PanelToolbar = ({
|
||||
panelName,
|
||||
@@ -19,7 +19,7 @@ const PanelToolbar = ({
|
||||
<div className="absolute inset-x-0 top-0 h-10 bg-gradient-to-b from-ch-gray-800 to-transparent" />
|
||||
)}
|
||||
<div className="absolute top-0 right-0 flex items-center h-9">
|
||||
{panelName === 'Viewer' && <OpenscadStaticImageMessage />}
|
||||
{panelName === 'Viewer' && <StaticImageMessage />}
|
||||
<button
|
||||
className={
|
||||
'bg-ch-gray-760 text-ch-gray-300 px-3 rounded-bl-lg h-full ' +
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMutation } from '@redwoodjs/web'
|
||||
import { toast } from '@redwoodjs/web/toast'
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
import { useAuth } from '@redwoodjs/auth'
|
||||
import { makeStlDownloadHandler } from 'src/helpers/download_stl'
|
||||
import { useIdeState } from 'src/helpers/hooks/useIdeState'
|
||||
import { IdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||
import { CREATE_PROJECT_MUTATION } from 'src/components/NavPlusButton/NavPlusButton'
|
||||
@@ -192,6 +193,15 @@ export const Success = ({ userProject, refetch }) => {
|
||||
},
|
||||
})
|
||||
|
||||
const onStlDownload = makeStlDownloadHandler({
|
||||
type: state.objectData?.type,
|
||||
ideType: state.ideType,
|
||||
geometry: state.objectData?.data,
|
||||
quality: state.objectData?.quality,
|
||||
fileName: `${userProject.Project?.title }.stl`,
|
||||
thunkDispatch,
|
||||
})
|
||||
|
||||
return (
|
||||
<IdeContext.Provider
|
||||
value={{
|
||||
@@ -213,6 +223,7 @@ export const Success = ({ userProject, refetch }) => {
|
||||
onDelete={onDelete}
|
||||
onReaction={onReaction}
|
||||
onComment={onComment}
|
||||
onStlDownload={onStlDownload}
|
||||
/>
|
||||
</IdeContext.Provider>
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ import CadPackage from 'src/components/CadPackage/CadPackage'
|
||||
import Gravatar from 'src/components/Gravatar/Gravatar'
|
||||
import { useIdeInit } from 'src/components/EncodedUrl/helpers'
|
||||
import ProfileViewer from '../ProfileViewer/ProfileViewer'
|
||||
import OpenscadStaticImageMessage from 'src/components/OpenscadStaticImageMessage/OpenscadStaticImageMessage'
|
||||
import StaticImageMessage from 'src/components/StaticImageMessage/StaticImageMessage'
|
||||
import KeyValue from 'src/components/KeyValue/KeyValue'
|
||||
|
||||
const ProjectProfile = ({
|
||||
@@ -25,6 +25,7 @@ const ProjectProfile = ({
|
||||
onDelete,
|
||||
onReaction,
|
||||
onComment,
|
||||
onStlDownload,
|
||||
}) => {
|
||||
const [comment, setComment] = useState('')
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
@@ -81,7 +82,7 @@ const ProjectProfile = ({
|
||||
<div className="md:col-start-2 w-full min-h-md relative">
|
||||
<ProfileViewer />
|
||||
<div className="absolute right-0 top-0">
|
||||
<OpenscadStaticImageMessage />
|
||||
<StaticImageMessage />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -91,13 +92,26 @@ const ProjectProfile = ({
|
||||
<h3 className="text-5xl capitalize text-ch-gray-300">
|
||||
{project?.title.replace(/-/g, ' ')}
|
||||
</h3>
|
||||
<div className="flex items-center text-gray-100">
|
||||
<div className="flex items-center text-gray-100 flex-wrap">
|
||||
<div className="flex flex-grow items-center">
|
||||
<span className="pr-4">Built with</span>
|
||||
<CadPackage
|
||||
cadPackage={project?.cadPackage}
|
||||
className="px-3 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className={getActiveClasses(
|
||||
'ml-3 hover:bg-opacity-100 bg-ch-pink-800 bg-opacity-30 mt-4 mb-3 text-ch-gray-300',
|
||||
{ 'bg-indigo-200': currentUser }
|
||||
)}
|
||||
shouldAnimateHover
|
||||
iconName={'document-download'}
|
||||
onClick={onStlDownload}
|
||||
>
|
||||
Download STL
|
||||
</Button>
|
||||
</div>
|
||||
{(project?.description || hasPermissionToEdit) && (
|
||||
<KeyValue
|
||||
keyName="Description"
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import OutBound from 'src/components/OutBound/OutBound'
|
||||
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||
|
||||
const OpenscadStaticImageMessage = () => {
|
||||
const StaticImageMessage = () => {
|
||||
const { state } = useIdeContext()
|
||||
if (state.ideType !== 'openscad' || state.objectData?.type !== 'png') {
|
||||
if ((state.ideType !== 'openscad' && state.ideType !== 'curv') || state.objectData?.type !== 'png') {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
return state.ideType === 'openscad' ?
|
||||
<OutBound
|
||||
to="https://learn.cadhub.xyz/docs/general-cadhub/openscad-previews"
|
||||
className="text-ch-gray-300 border-ch-gray-300 rounded-md mr-12 px-2 py-1 text-xs"
|
||||
>
|
||||
Why reload each camera move?
|
||||
</OutBound>
|
||||
)
|
||||
: <div className="text-ch-gray-300 border-ch-gray-300 rounded-md mr-12 px-2 py-1 text-xs">
|
||||
Alpha Curv integration, no camera support currently.
|
||||
</div>
|
||||
}
|
||||
|
||||
export default OpenscadStaticImageMessage
|
||||
export default StaticImageMessage
|
||||
@@ -139,6 +139,21 @@ const Svg = ({
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
'document-download': (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={strokeWidth}
|
||||
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
'dots-vertical': (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Camera } from 'src/helpers/hooks/useIdeState'
|
||||
|
||||
export const lambdaBaseURL =
|
||||
process.env.CAD_LAMBDA_BASE_URL ||
|
||||
'https://oxt2p7ddgj.execute-api.us-east-1.amazonaws.com/prod'
|
||||
'https://2inlbple1b.execute-api.us-east-1.amazonaws.com/prod2/'
|
||||
|
||||
export const stlToGeometry = (url) =>
|
||||
new Promise((resolve, reject) => {
|
||||
|
||||
103
app/web/src/helpers/cadPackages/curv/curvController.ts
Normal file
103
app/web/src/helpers/cadPackages/curv/curvController.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
lambdaBaseURL,
|
||||
stlToGeometry,
|
||||
createHealthyResponse,
|
||||
createUnhealthyResponse,
|
||||
timeoutErrorMessage,
|
||||
RenderArgs,
|
||||
splitGziped,
|
||||
} from '../common'
|
||||
|
||||
export const render = async ({ code, settings }: RenderArgs) => {
|
||||
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,
|
||||
},
|
||||
file: code,
|
||||
})
|
||||
try {
|
||||
const response = await fetch(lambdaBaseURL + '/curv/preview', {
|
||||
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)
|
||||
}
|
||||
if (response.status === 502) {
|
||||
return createUnhealthyResponse(new Date(), timeoutErrorMessage)
|
||||
}
|
||||
const blob = await response.blob()
|
||||
const text = await new Response(blob).text()
|
||||
const { consoleMessage, type } = splitGziped(text)
|
||||
return createHealthyResponse({
|
||||
type: type !== 'stl' ? 'png' : 'geometry',
|
||||
data:
|
||||
type !== 'stl'
|
||||
? blob
|
||||
: await stlToGeometry(window.URL.createObjectURL(blob)),
|
||||
consoleMessage,
|
||||
date: new Date(),
|
||||
})
|
||||
} catch (e) {
|
||||
return createUnhealthyResponse(new Date())
|
||||
}
|
||||
}
|
||||
|
||||
export const stl = async ({ code /*settings*/ }: RenderArgs) => {
|
||||
const body = JSON.stringify({
|
||||
settings: {},
|
||||
file: code,
|
||||
})
|
||||
try {
|
||||
const response = await fetch(lambdaBaseURL + '/curv/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)
|
||||
}
|
||||
if (response.status === 502) {
|
||||
return createUnhealthyResponse(new Date(), timeoutErrorMessage)
|
||||
}
|
||||
const blob = await response.blob()
|
||||
const text = await new Response(blob).text()
|
||||
const { consoleMessage, type } = splitGziped(text)
|
||||
return createHealthyResponse({
|
||||
type: type !== 'stl' ? 'png' : 'geometry',
|
||||
data:
|
||||
type !== 'stl'
|
||||
? blob
|
||||
: await stlToGeometry(window.URL.createObjectURL(blob)),
|
||||
consoleMessage,
|
||||
date: new Date(),
|
||||
})
|
||||
} catch (e) {
|
||||
return createUnhealthyResponse(new Date())
|
||||
}
|
||||
}
|
||||
|
||||
const curv = {
|
||||
render,
|
||||
stl,
|
||||
}
|
||||
|
||||
export default curv
|
||||
|
||||
function cleanError(error) {
|
||||
return error.replace(/["|']\/tmp\/.+\/main.curv["|']/g, "'main.curv'")
|
||||
}
|
||||
10
app/web/src/helpers/cadPackages/curv/initialCode.curv
Normal file
10
app/web/src/helpers/cadPackages/curv/initialCode.curv
Normal file
@@ -0,0 +1,10 @@
|
||||
let
|
||||
N = 5;
|
||||
C = red;
|
||||
Twists = 6;
|
||||
in
|
||||
box [1,1,N]
|
||||
>> colour C
|
||||
>> twist (Twists*90*deg/N)
|
||||
>> rotate {angle: 90*deg, axis: Y_axis}
|
||||
>> bend{}
|
||||
15
app/web/src/helpers/cadPackages/curv/userGuide.md
Normal file
15
app/web/src/helpers/cadPackages/curv/userGuide.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: Curv
|
||||
Written with: [Domain-Specific Language](https://martinfowler.com/dsl.html)
|
||||
Kernal type: Signed distance functions
|
||||
Maintained by: [Doug Moen and contributors](https://github.com/curv/curv/graphs/contributors)
|
||||
Documentation: [curv3d.org](https://curv3d.org)
|
||||
---
|
||||
|
||||
Curv is a programming language for creating art using mathematics. It's a 2D and 3D geometric modelling tool that supports full colour, animation and 3D printing.
|
||||
|
||||
### [Examples](https://github.com/curv3d/curv/tree/master/examples)
|
||||
|
||||
- [Flog spiral](https://619b5e6c6689420008eedfe5--cadhubxyz.netlify.app/draft/curv#fetch_text_v1=https%3A%2F%2Fraw.githubusercontent.com%2Fcurv3d%2Fcurv%2Fmaster%2Fexamples%2Flog_spiral.curv)
|
||||
- [Shreks donut](https://619b5e6c6689420008eedfe5--cadhubxyz.netlify.app/draft/curv#fetch_text_v1=https%3A%2F%2Fraw.githubusercontent.com%2Fcurv3d%2Fcurv%2Fmaster%2Fexamples%2Fshreks_donut.curv)
|
||||
- [Wood grain](https://619b5e6c6689420008eedfe5--cadhubxyz.netlify.app/draft/curv#fetch_text_v1=https%3A%2F%2Fraw.githubusercontent.com%2Fcurv3d%2Fcurv%2Fmaster%2Fexamples%2Ffinial.curv)
|
||||
@@ -13,16 +13,22 @@ import jscad from './jsCad/jsCadController'
|
||||
import jsCadGuide from 'src/helpers/cadPackages/jsCad/userGuide.md'
|
||||
import jsCadInitialCode from 'src/helpers/cadPackages/jsCad/initialCode.jscad.js'
|
||||
|
||||
import curv from './curv/curvController'
|
||||
import curvGuide from 'src/helpers/cadPackages/curv/userGuide.md'
|
||||
import curvInitialCode from 'src/helpers/cadPackages/curv/initialCode.curv'
|
||||
|
||||
export const cadPackages: { [key in CadPackageType]: DefaultKernelExport } = {
|
||||
openscad,
|
||||
cadquery,
|
||||
jscad,
|
||||
curv,
|
||||
}
|
||||
|
||||
export const initGuideMap: { [key in CadPackageType]: string } = {
|
||||
openscad: openScadGuide,
|
||||
cadquery: cadQueryGuide,
|
||||
jscad: jsCadGuide,
|
||||
curv: curvGuide,
|
||||
INIT: '',
|
||||
}
|
||||
|
||||
@@ -30,5 +36,6 @@ export const initCodeMap: { [key in CadPackageType]: string } = {
|
||||
openscad: openScadInitialCode,
|
||||
cadquery: cadQueryInitialCode,
|
||||
jscad: jsCadInitialCode,
|
||||
curv: curvInitialCode,
|
||||
INIT: '',
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import TheWorker from 'worker-loader!./jscadWorker'
|
||||
|
||||
const materials = {
|
||||
mesh: {
|
||||
def: new MeshPhongMaterial({ color: 0x0084d1, flatShading: true }),
|
||||
def: new MeshPhongMaterial({ color: 0x13579d, flatShading: true }),
|
||||
material: (params) => new MeshPhongMaterial(params),
|
||||
},
|
||||
line: {
|
||||
|
||||
@@ -8,6 +8,7 @@ type JscadTypeNames =
|
||||
| 'group'
|
||||
| 'text'
|
||||
| 'int'
|
||||
| 'float'
|
||||
| 'number'
|
||||
| 'slider'
|
||||
| 'email'
|
||||
@@ -37,7 +38,7 @@ interface JscadTextParam extends JscadParamBase {
|
||||
maxLength: number
|
||||
}
|
||||
interface JscadIntNumberSliderParam extends JscadParamBase {
|
||||
type: 'int' | 'number' | 'slider'
|
||||
type: 'int' | 'number' | 'float' | 'slider'
|
||||
initial: number
|
||||
min?: number
|
||||
max?: number
|
||||
@@ -93,6 +94,7 @@ export function jsCadToCadhubParams(input: JsCadParams[]): CadhubParams[] {
|
||||
switch (param.type) {
|
||||
case 'slider':
|
||||
case 'number':
|
||||
case 'float':
|
||||
case 'int':
|
||||
return {
|
||||
type: 'number',
|
||||
|
||||
@@ -315,9 +315,12 @@ function parseDef(code, line) {
|
||||
}
|
||||
|
||||
const makeScriptWorker = ({ callback, convertToSolids }) => {
|
||||
let onInit, main, scriptStats, entities
|
||||
let onInit, main, scriptStats, entities, lastParamsDef
|
||||
|
||||
function runMain(params = {}) {
|
||||
if(lastParamsDef) lastParamsDef.forEach(def=>{
|
||||
if(!(def.name in params) && 'initial' in def) params[def.name] = def.initial
|
||||
})
|
||||
let time = Date.now()
|
||||
let solids
|
||||
const transfer = []
|
||||
@@ -397,10 +400,11 @@ const makeScriptWorker = ({ callback, convertToSolids }) => {
|
||||
if (idx === -1) {
|
||||
paramsDef.push(p)
|
||||
} else {
|
||||
paramsDef.splice(idx, 1, p)
|
||||
paramsDef[idx] = p
|
||||
}
|
||||
})
|
||||
}
|
||||
lastParamsDef = paramsDef
|
||||
callback({
|
||||
action: 'parameterDefinitions',
|
||||
worker: 'main',
|
||||
|
||||
@@ -69,7 +69,8 @@ export const makeStlDownloadHandler =
|
||||
} else {
|
||||
thunkDispatch((dispatch, getState) => {
|
||||
const state = getState()
|
||||
const specialCadProcess = ideType === 'openscad' && 'stl'
|
||||
const specialCadProcess =
|
||||
(ideType === 'openscad' || ideType === 'curv') && 'stl'
|
||||
dispatch({ type: 'setLoading' })
|
||||
requestRender({
|
||||
state,
|
||||
@@ -144,3 +144,6 @@ label {
|
||||
input.error, textarea.error {
|
||||
border: 1px solid red;
|
||||
}
|
||||
a.underline-hovered:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { useState, useEffect } from 'react'
|
||||
import { Link, routes, navigate } from '@redwoodjs/router'
|
||||
import { useAuth } from '@redwoodjs/auth'
|
||||
import { Toaster, toast } from '@redwoodjs/web/toast'
|
||||
import Tooltip from '@material-ui/core/Tooltip'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { getActiveClasses } from 'get-active-classes'
|
||||
import Footer from 'src/components/Footer'
|
||||
@@ -12,11 +11,11 @@ import NavPlusButton from 'src/components/NavPlusButton'
|
||||
import ReactGA from 'react-ga'
|
||||
import { isBrowser } from '@redwoodjs/prerender/browserUtils'
|
||||
|
||||
import Svg from 'src/components/Svg'
|
||||
import { ImageFallback } from 'src/components/ImageUploader'
|
||||
import useUser from 'src/helpers/hooks/useUser'
|
||||
import './MainLayout.css'
|
||||
import RecentProjectsCell from 'src/components/RecentProjectsCell'
|
||||
import LogoType from 'src/components/LogoType'
|
||||
|
||||
let previousSubmission = ''
|
||||
|
||||
@@ -72,39 +71,12 @@ const MainLayout = ({ children, shouldRemoveFooterInIde }) => {
|
||||
}, [hash, client])
|
||||
return (
|
||||
<div
|
||||
className="h-full flex flex-col ch-scrollbar overflow-y-scroll overflow-x-hidden"
|
||||
className="flex flex-col h-full overflow-x-hidden overflow-y-scroll ch-scrollbar"
|
||||
style={{ perspective: '1px', perspectiveOrigin: 'top center' }}
|
||||
>
|
||||
<header id="cadhub-main-header">
|
||||
<nav className="flex justify-between h-16 sm:px-4 bg-ch-gray-900">
|
||||
<ul className="flex items-center">
|
||||
<li>
|
||||
<Link to={routes.home()}>
|
||||
<div className="rounded-full overflow-hidden ml-2">
|
||||
<Svg className="w-10" name="favicon" />
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Tooltip title="Very alpha, there's lots of work todo">
|
||||
<div className="ml-4 flex">
|
||||
{/* Because of how specific these styles are to this heading/logo and it doesn't need to be replicated else where as well as it's very precise with the placement of "pre-alpha" I think it's appropriate. */}
|
||||
<h2
|
||||
className="text-indigo-300 text-2xl md:text-5xl font-ropa-sans py-1 md:tracking-wider"
|
||||
style={{ letterSpacing: '0.3em' }}
|
||||
>
|
||||
CadHub
|
||||
</h2>
|
||||
<div
|
||||
className="text-pink-400 text-sm font-bold font-ropa-sans hidden md:block"
|
||||
style={{ paddingBottom: '2rem', marginLeft: '-1.8rem' }}
|
||||
>
|
||||
pre-alpha
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</li>
|
||||
</ul>
|
||||
<LogoType />
|
||||
<ul className="flex items-center">
|
||||
<li
|
||||
className={getActiveClasses(
|
||||
@@ -114,29 +86,29 @@ const MainLayout = ({ children, shouldRemoveFooterInIde }) => {
|
||||
<NavPlusButton />
|
||||
</li>
|
||||
{isAuthenticated ? (
|
||||
<li className="h-10 w-10">
|
||||
<Popover className="relative outline-none w-full h-full">
|
||||
<li className="w-10 h-10">
|
||||
<Popover className="relative w-full h-full outline-none">
|
||||
<Popover.Button
|
||||
disabled={!isAuthenticated || !currentUser}
|
||||
className="h-full w-full outline-none border-ch-gray-400 border-2 rounded-full"
|
||||
className="w-full h-full border-2 rounded-full outline-none border-ch-gray-400"
|
||||
>
|
||||
{!loading && (
|
||||
<ImageFallback
|
||||
width={80}
|
||||
className="rounded-full object-cover"
|
||||
className="object-cover rounded-full"
|
||||
imageId={user?.image}
|
||||
/>
|
||||
)}
|
||||
</Popover.Button>
|
||||
{currentUser && (
|
||||
<Popover.Panel className="w-48 absolute z-10 right-0 bg-ch-gray-700 mt-4 px-3 py-2 rounded shadow-md overflow-hidden text-ch-gray-300">
|
||||
<Popover.Panel className="absolute right-0 z-10 w-48 px-3 py-2 mt-4 overflow-hidden rounded shadow-md bg-ch-gray-700 text-ch-gray-300">
|
||||
<Link to={routes.user({ userName: user?.userName })}>
|
||||
<p className="my-2 text-ch-blue-400 font-fira-code leading-4 text-sm">
|
||||
<p className="my-2 text-sm leading-4 text-ch-blue-400 font-fira-code">
|
||||
Hello {user?.name}
|
||||
</p>
|
||||
</Link>
|
||||
<Link
|
||||
className="my-2 block hover:text-ch-pink-300"
|
||||
className="block my-2 hover:text-ch-pink-300"
|
||||
to={routes.user({ userName: user?.userName })}
|
||||
>
|
||||
<div>View Your Profile</div>
|
||||
@@ -149,7 +121,7 @@ const MainLayout = ({ children, shouldRemoveFooterInIde }) => {
|
||||
Logout
|
||||
</a>
|
||||
<hr className="my-4" />
|
||||
<p className="text-ch-blue-400 font-fira-code leading-4 text-sm">
|
||||
<p className="text-sm leading-4 text-ch-blue-400 font-fira-code">
|
||||
Recent Projects
|
||||
</p>
|
||||
<RecentProjectsCell userName={user?.userName} />
|
||||
@@ -161,7 +133,7 @@ const MainLayout = ({ children, shouldRemoveFooterInIde }) => {
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
className="text-ch-gray-300 mr-1 sm:mr-2 px-2 sm:px-4 py-2 border-2 border-ch-gray-400 rounded-full hover:bg-ch-gray-600"
|
||||
className="px-2 py-2 mr-1 border-2 rounded-full text-ch-gray-300 sm:mr-2 sm:px-4 border-ch-gray-400 hover:bg-ch-gray-600"
|
||||
onClick={recordedLogin}
|
||||
>
|
||||
Sign In/Up
|
||||
|
||||
11
app/web/src/pages/EmbedProjectPage/EmbedProjectPage.test.tsx
Normal file
11
app/web/src/pages/EmbedProjectPage/EmbedProjectPage.test.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { render } from '@redwoodjs/testing'
|
||||
|
||||
import EmbedProjectPage from './EmbedProjectPage'
|
||||
|
||||
describe('EmbedProjectPage', () => {
|
||||
it('renders successfully', () => {
|
||||
expect(() => {
|
||||
render(<EmbedProjectPage />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
11
app/web/src/pages/EmbedProjectPage/EmbedProjectPage.tsx
Normal file
11
app/web/src/pages/EmbedProjectPage/EmbedProjectPage.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import EmbedProjectCell from 'src/components/EmbedProjectCell'
|
||||
|
||||
const EmbedProjectPage = ({ userName, projectTitle }) => {
|
||||
return (
|
||||
<>
|
||||
<EmbedProjectCell userName={userName} projectTitle={projectTitle} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmbedProjectPage
|
||||
@@ -5300,6 +5300,13 @@ axios@^0.21.1:
|
||||
dependencies:
|
||||
follow-redirects "^1.14.0"
|
||||
|
||||
axios@^0.25.0:
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a"
|
||||
integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==
|
||||
dependencies:
|
||||
follow-redirects "^1.14.7"
|
||||
|
||||
axobject-query@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
|
||||
@@ -9205,6 +9212,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.0:
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379"
|
||||
integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==
|
||||
|
||||
follow-redirects@^1.14.7:
|
||||
version "1.14.7"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
|
||||
integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
|
||||
|
||||
for-in@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
||||
|
||||
@@ -19,7 +19,7 @@ It's software that allows you to define 3D CAD models with code. It's a niche po
|
||||
|
||||
I recommend reading through the entire list below to see if one chimes with you and your needs, beyond that I can make the following recommendation and points:
|
||||
<!--truncate-->
|
||||
- My main recommendation is to use one of the packages that uses a B-rep kernal (and for opensounce tools that means OpenCascade, a mature C++ CAD library). Packages that do so are CadQuery, CascadeStudio, DeclaraCAD and pythonOCC. My reasons for recommending these are as follows:
|
||||
- My main recommendation is to use one of the packages that uses a B-rep kernal (and for opensource tools that means OpenCascade, a mature C++ CAD library). Packages that do so are CadQuery, CascadeStudio, DeclaraCAD and pythonOCC. My reasons for recommending these are as follows:
|
||||
- Most of Code-CAD tools are plagued with a CSG mindset (that is unions, subtractions and intersections of primitive shapes; cubes spheres etc). This is an inherently limited paradigm (one simple example of this is how internal fillets, which are important for reducing stress concentrations in parts, become very difficult). While CadQuery, CascadeStudio, DeclaraCAD and pythonOCC still offer CSG functionality, you're also able to move beyond.
|
||||
- OpenCascade uses a B-rep (boundary representation) kernel, In my opinion, this means you'll be learning a future-proof tool that won't limit the types of applications you can model for, which is likely the case for mesh kernels, which will cause trouble in for some applications like optics and injection moulding.
|
||||
|
||||
@@ -206,6 +206,20 @@ Python-based, Also uses [OpenCascade](https://github.com/tpaviot/oce).
|
||||
|
||||
Another project inspired by OpenSCAD. The author considers key differences to be procedural vs functional programming language style, (i.e variables can be modified) and the use of arbitrary precision arithmetic throughout (meaning there are no unexpected double/float rounding errors). There is a handy [feature matrix](https://github.com/GilesBathgate/RapCAD/blob/master/doc/feature_matrix.asciidoc) between RapCAD, OpenSCAD and ImplicitCad.
|
||||
|
||||
### [replicad](https://replicad.xyz)
|
||||
- [Repo](https://github.com/sgenoud/replicad)
|
||||
- [Community](https://github.com/sgenoud/replicad/discussions)
|
||||
- [Docs](https://replicad.xyz/docs/intro)
|
||||
- License: AGPL
|
||||
- [Online editor](https://studio.replicad.xyz/visualiser)
|
||||
|
||||
|
||||
A library to build browser based 3D models with code. Exposes an API inspired by
|
||||
[CadQuery](https://cadquery.readthedocs.io/en/latest/intro.html), written to
|
||||
run in the browser. Replicad is a javascript wrapper on top of
|
||||
[opencascade.js](https://github.com/donalffons/opencascade.js/)
|
||||
|
||||
|
||||
### [scad-clj](https://github.com/farrellm/scad-clj)
|
||||
- [Repo](https://github.com/farrellm/scad-clj)
|
||||
- ~~Community~~
|
||||
|
||||
43
docs/blog/2021-08-14-ux-studies-timeline.mdx
Normal file
43
docs/blog/2021-08-14-ux-studies-timeline.mdx
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
slug: ux-studies-intro
|
||||
title: "GUI-CAD UX studies: introduction"
|
||||
author: Frank Noirot
|
||||
author_title: CadHub Core Team
|
||||
author_url: https://github.com/franknoirot
|
||||
author_image_url: https://avatars.githubusercontent.com/u/23481541?v=4
|
||||
tags: []
|
||||
---
|
||||
|
||||
import Image from '@theme/IdealImage';
|
||||
|
||||
import ivanSutherland from '../static/img/blog/ux-case-studies-intro/ivan-sutherland-sketchpad.jpg';
|
||||
|
||||
I'm helping CadHub out by designing the interfaces for the [new editor](https://www.figma.com/file/VUh53RdncjZ7NuFYj0RGB9/CadHub?node-id=1114%3A1608), [project viewer](https://www.figma.com/file/VUh53RdncjZ7NuFYj0RGB9/?node-id=1046%3A0), and [more](https://www.figma.com/file/VUh53RdncjZ7NuFYj0RGB9/?node-id=1150%3A1618). Right now we're focused on getting the Code-CAD user experience perfected so that users can try out all the great Code-CAD packages out there in a simple and sharable way. But we think that the future of Code-CAD will pull UX lessons from traditional, GUI-based CAD systems. So I'll be taking a look at the history and UX of some of today's CAD tools to see how we might bring them along with the Code-CAD evolution.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
## Why GUIs aren't enough anymore
|
||||
|
||||
As others like [Jessie Frazelle](https://medium.com/embedded-ventures/mechanical-cad-yesterday-today-and-tomorrow-981cef7e06b1) have pointed out, the history of CAD software has been focused primarily on using software to emulate the frictionless user experiences of sketching and modelling by hand. That paradigm lead most of the major tools to build GUI-based systems, as they correctly assumed at the the time that the GUI offered an interface that could be understood by people in the industry. Decades have passed and the same assumption still forms the foundation of the paradigm, but could these assumptions have fundamentally changed?
|
||||
|
||||
<Image img={ivanSutherland} alt="Ivan Sutherland's Sketchpad program from the 1960's, a man using a pen-like tool on a screen to manipulate a 2D model, considered the first CAD program." className="mb-8 bg-contain rounded-md overflow-hidden max-w-lg mx-auto" />
|
||||
|
||||
It's hard to understate how much of a sea change web development has brought to technical culture. In the past decade the web technologies of HTML, CSS, and especially JavaScript have trained a large part of technical workers to think not in terms of software packages, but in terms of the technologies and languages that are used to construct them, because as a culture we have become accustomed to the idea that there is always an API powering whatever tool we're using. Technical users of course still want seamless GUI user experiences on platforms, but increasingly they also want the ability to get under the hood and use the APIs that power whatever tool or platform they're on. This trend is evident in the rise of API-first services like [Stripe](https://stripe.com) and monolith-fracturing trends like [JAMstack web development](https://jamstack.org).
|
||||
|
||||
With Code-CAD, we are putting a spotlight on this sea change in user expectations, and putting out a call to action for people to start creating experiences for this web-native, language-comfortable audience of CAD users. With CadHub, we're building a showcase for the great Code-CAD packages like [CadQuery](https://cadquery.readthedocs.io/en/latest/) and [OpenSCAD](https://openscad.org/) that have been under development by early adopters for years.
|
||||
|
||||
## A Gooey Hegelian Dialectic
|
||||
|
||||
Okay, so we in the Code-CAD community are a bunch of developers who want more interfaces from our CAD programs. We're comfortable with programming languages and APIs and we want access to them in addition to the GUIs that CAD has traditionally provided. That's all great, but then why isn't Code-CAD mainstream right now?
|
||||
|
||||
For one, the process of building a robust, text-first approach to so visual an activity is, as [Kurt has written about](/blog/right-level-of-abstraction) on this blog, incredibly difficult. But the Code-CAD community has been doing the monumental work of developing clean, expressive APIs for modelling. That important work is what we want to showcase and make more accessible with CadHub. But we think it's only half of the equation for creating the next step in CAD.
|
||||
|
||||
There are myriads of thorny user experience problems that have been solved by the dominant CAD packages of today. They are amazing pieces of software that users know and love. And all those clever UX solutions were created in the design space of GUIs. If Code-CAD as a paradigm is going to become the new normal for computer-aided design, we need to understand and address all the innovation that GUI-CAD has brought to design, and translate them into Code-CAD. We need to find a gooey-code synthesis. As previously stated, this new generation of users *still want seamless GUI experiences*. Code-CAD needs to provide a way of switching seamlessly between "Application Programming" and "Graphical User" interfaces.
|
||||
|
||||
## Already under construction
|
||||
|
||||
I'll try to explore that design space with a few brief case studies on UX that I love from existing CAD and 3D modelling software. I'm looking for key experiences that help empower designers, how they operate in the GUI-CAD paradigm, and how Code-CAD might provide code-based synonyms of these GUI experiences.
|
||||
|
||||
But I want to mention that work is already being done on this front. Jeremy Wright of the CadQuery team is building [Semblage](https://semblage.7bindustries.com/en/latest/), a GUI-code hybrid built with CadQuery and the Godot gaming engine. [BuildBee](https://makecode.buildbee.com/) lets users switch between Scratch-like block interface and JavaScript code for making models. Blender provides an [excellent Python API](https://docs.blender.org/api/current/index.html) for almost all of its incredible functionality, and there a dozen other projects pushing things forward while we look to the present and the past for more inspiration.
|
||||
|
||||
Our first stop will be the timeline feature of AutoDesk Fusion360, which is a clever way to make the order of operations in modelling intuitive. Stay tuned for this post and more in the coming weeks, check out our work on [GitHub](https://github.com/Irev-Dev/cadhub/discussions/404) and [Figma](https://www.figma.com/file/VUh53RdncjZ7NuFYj0RGB9/?node-id=633%3A0), sign up for [Kurt's newsletter](https://kurthutten.com/), and join [our Discord](https://discord.gg/sFYJyEJ6) to get plugged into our ongoing discussions about the future of Code-CAD.
|
||||
Reference in New Issue
Block a user