23 Commits

Author SHA1 Message Date
Kurt Hutten Irev-Dev
cb1e786305 update node version 2023-01-28 18:36:37 +11:00
Kurt Hutten Irev-Dev
8bc6674849 remove engines 2023-01-28 18:18:47 +11:00
Kurt Hutten Irev-Dev
cd7d618276 add change 2023-01-28 18:14:52 +11:00
Kurt Hutten
063e13ff4a fix db (#618) (#619) 2023-01-28 18:10:47 +11:00
Kurt Hutten
f61280ef00 Kurt 611 (#612)
* disable python exectution

* update prod url
2022-07-31 08:42:39 +10:00
Kurt Hutten IrevDev
8500d223d4 update 2022-06-06 18:47:04 +10:00
Kurt Hutten
b91723ced4 Update 2020-10-31-curated-code-cad.md 2022-04-16 19:58:38 +10:00
sgenoud
859e018251 Added replicad in the curated codeCAD (#605) 2022-04-16 19:56:54 +10:00
Todd Medema
f30eeb2b95 Part 1 of top bar UI refresh (#603)
* Hitting enter should rename project title

* Part 1 of topbar styling refresh
2022-01-25 07:46:00 +11:00
Scott Martin
0a6439161e Discord chat bot to announce projects (#590) (#600)
* Discord chat bot to announce projects (#590)

Add support for discord chat bot to announce when images are set, with instructions on configuring for dev. This uses the REST
API instead of a websocket connection, which is needed for serverless deployment.

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Remove discord.js dependency.

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2022-01-23 13:17:08 +11:00
Kurt Hutten
861b8374bf Kurt/revert discord to attempt deploy (#598)
* Force netlify build

builds are being canceled even though there are changes

* Revert "Upgrade packages (#594)"

This reverts commit 5531f2e0c1.

* Revert "Discord chat bot to announce projects (#590)"

This reverts commit 90fece9598.

* Revert "Force netlify build"

This reverts commit 315ebf0c59.

* Make sure project title is robust
2022-01-19 20:54:43 +11:00
Todd Medema
844a1f6961 Hitting enter should rename project title (#595) 2022-01-19 20:52:21 +11:00
Kurt Hutten
5531f2e0c1 Upgrade packages (#594)
Getting sick of dependabot
2022-01-18 06:39:01 +11:00
Kurt Hutten
c48afaf07b Fix stl download name (#593)
Was broken on the profile page, would set the name to undefined.stl
2022-01-18 06:16:55 +11:00
Scott Martin
90fece9598 Discord chat bot to announce projects (#590)
* Add support for discord chat bot to announce when images are set, with instructions on configuring for dev

* Tweak discord bot message.

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2022-01-14 07:11:30 +11:00
Frank Noirot
82dd3d2555 FEAT: Create basic model embed (#588)
* initial commit, issue with OpenSCAD embed viewing

* initial implementation

* Fix openscad size bug

* Add overlays to embed

* Remove console.log and reuse exact query

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2022-01-11 17:30:15 +11:00
Scott Martin
e7dec57644 Add "STL Download" to project profile page (#585)
* Moved EditorMenu/helpers.ts file to src/helpers. Reused STL download helper on a new button in the project profile page

* Tweak download STL style - flex-wrap the column and grow original "built with" content so the button is pushed write but remains responsive on smaller screens
2022-01-11 06:48:20 +11:00
Davor Hrg
35fcd55229 Update jsCadController.tsx (#583) 2022-01-09 06:33:13 +11:00
Davor Hrg
3fef6474d3 default values and float type (#582) 2022-01-09 06:32:32 +11:00
Kurt Hutten
cd3060b3c7 Update CONTRIBUTING.md
CC @Isaac-Tait
2021-12-17 05:57:31 +11:00
Kurt Hutten
cef1d34c6f Give more ram to openscad preview image container
Speeds up the preview images
2021-12-02 07:24:39 +11:00
Kurt Hutten
70d4c40eac Add message for curv static image 2021-11-30 17:04:02 +11:00
Lee
2dec867803 Initial work to support curv (#578)
* Initial work to support curv

* Correct the initial code file location

* Preview and stl mvp working

* Prepare changes for review and preview build

* Run curv inside of /tmp

When exporting an stl it writes temporary files which is not allowed
when deployed to aws unless it's in temp.

* Lock in specific curv commit for reproducible builds

see: https://discord.com/channels/412182089279209474/886321278821216277/912507472441401385

* Add curv to backend schema

* Frontend changes to accommodate curv deploy

* Use vcount instead of vsize, as it's independant of geometry size,

This is good for CadHub usecase where we don't know anything about the
user's project

* Final tweaks for deploy

virtual screen size does matter,and curv is a little more memory hungry
than the other functions

* Format project

Co-authored-by: lf94 <inbox@leefallat.ca>
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2021-11-30 15:24:24 +11:00
67 changed files with 1011 additions and 138 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.idea
.history
.DS_Store
.env
.netlify

View File

@@ -1,9 +1,10 @@
{
"cSpell.words": [
"Cadhub",
"cadquery",
"curv",
"Customizer",
"Hutten",
"cadquery",
"jscad",
"openscad",
"sendmail"

View File

@@ -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)

View File

@@ -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"

View File

@@ -1 +1 @@
lts/*
16

View File

@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "CadPackage" ADD VALUE 'curv';

View File

@@ -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 {

View File

@@ -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",

View File

@@ -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}`)
})

View File

@@ -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

View File

@@ -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 : '',
})
}

View File

@@ -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 }
// }
}

View File

@@ -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 = {

View 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" ]

View 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()),
}

View 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 }
}
}

View File

@@ -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"

View File

@@ -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,
})
}

View File

@@ -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 }
}

View File

@@ -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

View File

@@ -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 {

View 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,
});
}
}
}

View File

@@ -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>>) =>

View File

@@ -30,7 +30,6 @@
}
},
"engines": {
"node": ">=14.x <=16.x",
"yarn": ">=1.15"
},
"prisma": {

View File

@@ -17,7 +17,7 @@
'SENTRY_AUTH_TOKEN',
'SENTRY_ORG',
'SENTRY_PROJECT',
'EMAIL_PASSWORD'
'EMAIL_PASSWORD',
]
# experimentalFastRefresh = true # this seems to break cascadeStudio
[api]

View File

@@ -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

View File

@@ -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">

View File

@@ -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: '',

View File

@@ -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}
/>
)
}

View File

@@ -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)

View File

@@ -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'

View 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

View File

@@ -0,0 +1,6 @@
// Define your own mock data here:
export const standard = (/* vars, { ctx, req } */) => ({
ideProject: {
id: 42,
},
})

View File

@@ -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' }

View File

@@ -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()
})
})

View 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>
)
}

View 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

View File

@@ -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,

View File

@@ -18,6 +18,7 @@ const IdeEditor = ({ Loading }) => {
cadquery: 'python',
openscad: 'cpp',
jscad: 'javascript',
curv: 'python',
INIT: '',
}
const monaco = useMonaco()

View File

@@ -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>

View File

@@ -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

View File

@@ -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}
/>
)
}

View File

@@ -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}
/>

View File

@@ -5,6 +5,7 @@ interface EditToggleType {
onEdit?: React.MouseEventHandler
isEditing?: boolean
}
// small change
const EditToggle = ({
onEdit = () => {

View 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>
)
}

View File

@@ -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 = () => {

View File

@@ -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 ' +

View File

@@ -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>
)

View File

@@ -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"

View File

@@ -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

View File

@@ -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"

View File

@@ -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) => {

View 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'")
}

View 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{}

View 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)

View File

@@ -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: '',
}

View File

@@ -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: {

View File

@@ -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',

View File

@@ -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',

View File

@@ -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,

View File

@@ -144,3 +144,6 @@ label {
input.error, textarea.error {
border: 1px solid red;
}
a.underline-hovered:hover {
text-decoration: underline;
}

View File

@@ -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

View File

@@ -0,0 +1,11 @@
import { render } from '@redwoodjs/testing'
import EmbedProjectPage from './EmbedProjectPage'
describe('EmbedProjectPage', () => {
it('renders successfully', () => {
expect(() => {
render(<EmbedProjectPage />)
}).not.toThrow()
})
})

View File

@@ -0,0 +1,11 @@
import EmbedProjectCell from 'src/components/EmbedProjectCell'
const EmbedProjectPage = ({ userName, projectTitle }) => {
return (
<>
<EmbedProjectCell userName={userName} projectTitle={projectTitle} />
</>
)
}
export default EmbedProjectPage

View File

@@ -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"

View File

@@ -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~~

View 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.