Compare commits
223 Commits
kurt/live-
...
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 | ||
|
|
e9859d85b8 | ||
|
|
a62c5bce03 | ||
|
|
149b2b6360 | ||
|
|
43fc897bf9 | ||
|
|
a909188f15 | ||
|
|
e8da05be8c | ||
|
|
dd0178d554 | ||
|
|
e3efb1a3dd | ||
|
|
cd90c3ce49 | ||
|
|
1ea4f9bdd5 | ||
|
|
e5f2552fc9 | ||
|
|
219f341972 | ||
|
|
e26beda598 | ||
|
|
c402b051e2 | ||
|
|
f7172be68b | ||
|
|
da0a4d6f1c | ||
|
|
0bc759cf9e | ||
|
|
a4cfc37576 | ||
|
|
bc1f12971d | ||
|
|
50cd44cd76 | ||
|
|
55917395b4 | ||
|
|
dc92920481 | ||
|
|
434eb0ef86 | ||
|
|
96ee9c4aa4 | ||
|
|
77014f0d36 | ||
|
|
3df903ffc6 | ||
|
|
68fa10437e | ||
|
|
342953b25f | ||
|
|
421ceee88d | ||
|
|
ae05a79e58 | ||
|
|
549217e953 | ||
|
|
4804c3bfe9 | ||
|
|
6c093e65bf | ||
|
|
4b9a8591ab | ||
|
|
9f769d6a61 | ||
|
|
32d6ef27ad | ||
|
|
c4c195074b | ||
|
|
3f6d919f22 | ||
|
|
aabe682782 | ||
|
|
5efaec4df0 | ||
|
|
6d0c832f6f | ||
|
|
66dc04d98e | ||
|
|
3aa3254e48 | ||
|
|
879f24b08b | ||
|
|
b80ea7f813 | ||
|
|
e9ad7180a7 | ||
|
|
0ce7ce4e76 | ||
|
|
088cfa4f2d | ||
|
|
ab92894a2d | ||
|
|
911744a071 | ||
|
|
d4bfcb4eb8 | ||
|
|
965e5b0f54 | ||
|
|
77799a5870 | ||
|
|
7540c908e7 | ||
|
|
dd152709ff | ||
|
|
2d7fb91f92 | ||
|
|
02463db741 | ||
|
|
38b905e180 | ||
|
|
cc50c984e4 | ||
|
|
9aee4ae725 | ||
|
|
6e45ce96d7 | ||
|
|
892b1d3809 | ||
|
|
83d327ad20 | ||
|
|
c77169cf21 | ||
|
|
0ba4ec4e21 | ||
|
|
06dbc35cf8 | ||
|
|
8170da854d | ||
|
|
d255a78cd1 | ||
|
|
c19658b7f8 | ||
|
|
335dac8677 | ||
|
|
18732e27fc | ||
|
|
64624b9c3e | ||
|
|
a89f2e7992 | ||
|
|
6d7f6fb4bf | ||
|
|
b621d78eb4 | ||
|
|
1dcae6057c | ||
|
|
648f174bd8 | ||
|
|
f20fe9a075 | ||
|
|
023b4862eb | ||
|
|
a2d278fa4d | ||
|
|
f6df9d1988 | ||
|
|
39ce35b219 | ||
|
|
33c08119ec | ||
|
|
1475fa24d1 | ||
|
|
348d2e0a01 | ||
|
|
65fc526220 | ||
|
|
4c4f5643f4 | ||
|
|
634304dfce | ||
|
|
70980afab0 | ||
|
|
bb4659a2dd | ||
|
|
59df7fdc25 | ||
|
|
2d4977ba8f | ||
|
|
b27bcd2d35 | ||
|
|
2f006d3e3b | ||
|
|
d71eec6a5e | ||
|
|
3a977ded02 | ||
|
|
b14587cdf1 | ||
|
|
5f8862b4d2 | ||
|
|
f3201cfd97 | ||
|
|
d94645d381 | ||
|
|
cd1cecd774 | ||
|
|
a87c1ae9f4 | ||
|
|
c271600432 | ||
|
|
1fb14db6f3 | ||
|
|
7b2be01430 | ||
|
|
9d9e3c4957 | ||
|
|
fc6cded59e | ||
|
|
2ec3a0b202 | ||
|
|
88326ed573 | ||
|
|
58fc8866f1 | ||
|
|
74a5f9bf2c | ||
|
|
690d45ff9a | ||
|
|
34757cf535 | ||
|
|
69c83d33b1 | ||
|
|
55d48057da | ||
|
|
e7031e9c0d | ||
|
|
e99f0c07ba | ||
|
|
e526fa812e | ||
|
|
3dbb963e4e | ||
|
|
126b60f5dd | ||
|
|
6a69a1c1bf | ||
|
|
750d10c01d | ||
|
|
a51991ef0d | ||
|
|
011baad9d0 | ||
|
|
ec9f9d241e | ||
|
|
b8fa22eede | ||
|
|
a8c05a3d27 | ||
|
|
39f9a02c0a | ||
|
|
e47ad59003 | ||
|
|
0d7e958505 | ||
|
|
9812db5cd6 | ||
|
|
12ab456446 | ||
|
|
206ec7fdab | ||
|
|
da557a5c16 | ||
|
|
fba971b419 | ||
|
|
2e2e7be633 | ||
|
|
8a54a88b0a | ||
|
|
5d128c6cbd | ||
|
|
b09733175e | ||
|
|
d3d4b5a632 | ||
|
|
5b85eec64c | ||
|
|
0cf599bbe2 | ||
|
|
3f1947a4d9 | ||
|
|
3e26e3d420 | ||
|
|
51bc32aad0 | ||
|
|
70cbe9d11e | ||
|
|
c95bfc400b | ||
|
|
678754d251 | ||
|
|
58b618cf5f | ||
|
|
22da074965 | ||
|
|
9ae1cd4aff | ||
|
|
9887eb4804 | ||
|
|
7f4eb85106 | ||
|
|
896baf08d1 | ||
|
|
e1d429877c | ||
|
|
b9f3955767 | ||
|
|
f9a43e53e2 | ||
|
|
442da1ffc6 | ||
|
|
57970465b1 | ||
|
|
d203fe7e57 | ||
|
|
abdebfccad | ||
|
|
eb238b6902 | ||
|
|
edfde1aa9f | ||
|
|
912135877c | ||
|
|
597bf89135 | ||
|
|
e4c95cb396 | ||
|
|
867bc0ca29 | ||
|
|
35198b6cc3 | ||
|
|
5b2ebac15e | ||
|
|
4a3144d360 | ||
|
|
25bee7ab95 | ||
|
|
1c13a38ccb | ||
|
|
bbf2a2eb55 | ||
|
|
01a28f4d53 | ||
|
|
f5113da9c2 | ||
|
|
db9270b7ce | ||
|
|
a4a92c18cb | ||
|
|
eb5d5616bb | ||
|
|
04261355b7 | ||
|
|
0bb106028b | ||
|
|
431cd2e867 | ||
|
|
cdbf6ed6b4 | ||
|
|
87f132a684 | ||
|
|
5d79efbf15 | ||
|
|
118c68c9da | ||
|
|
8ee4c112cf | ||
|
|
2bc4d904c6 | ||
|
|
a690265f70 | ||
|
|
2b4bc7aa43 | ||
|
|
9041301642 | ||
|
|
9f088ba463 | ||
|
|
e7b9059958 | ||
|
|
e407a3c002 | ||
|
|
6be2ced06f | ||
|
|
95bdb570f2 | ||
|
|
9aa686b4a4 | ||
|
|
b4cdd3e1ef | ||
|
|
96fa776bd9 | ||
|
|
aa43a848a1 | ||
|
|
335a1abf41 |
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"
|
||||
|
||||
88
CONTRIBUTING.md
Normal file
88
CONTRIBUTING.md
Normal file
@@ -0,0 +1,88 @@
|
||||
Hello 👋
|
||||
|
||||
|
||||
Really happy you're checking out how to contribute.
|
||||
Here you'll find a break down of the tech we're using,
|
||||
|
||||
If you'd like to get involved one of the best ways is to drop by the [discord](https://discord.gg/SD7zFRNjGH), say hi and let us know you're interested in contributing. All are welcome.
|
||||
|
||||
## Tech used
|
||||
|
||||
### Redwood
|
||||
CadHub is a [RedWood app](https://redwoodjs.com/). Simplistically this means it's a React frontend, using a serverless graphQL backend with Prisma.
|
||||
We are also using [Tailwind](https://tailwindcss.com/) to style the app.
|
||||
To learn more about Redwood, here are some useful links:
|
||||
- [Tutorial](https://redwoodjs.com/tutorial/welcome-to-redwood): getting started and complete overview guide.
|
||||
- [Docs](https://redwoodjs.com/docs/introduction): using the Redwood Router, handling assets and files, list of command-line tools, and more.
|
||||
- [Redwood Community](https://community.redwoodjs.com): get help, share tips and tricks, and collaborate on everything about RedwoodJS.
|
||||
|
||||
### Cad Packages
|
||||
Because Each CadPackage is it's own beast we opted to use Docker in order to give us lots of flexibility for setting up the environment to run there packages. The containers are run using AWS's container lambda and deployed using the serverless framework (JSCAD is an exception since it runs client-side). See [our docs](https://learn.cadhub.xyz/docs/general-cadhub/integrations) for more information of how this is setup.
|
||||
|
||||
## Getting your dev environment setup
|
||||
|
||||
|
||||
Clone the repo, then `cd` in the repo and app directory (the docs directory is for [learn.cadhub](https://learn.cadhub.xyz/))
|
||||
```
|
||||
cd cadhub/app
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
```terminal
|
||||
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 (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
|
||||
```
|
||||
|
||||
p.s. `yarn rw prisma studio` spins up an app to inspect the db
|
||||
|
||||
### Fire up dev
|
||||
```terminal
|
||||
yarn rw dev
|
||||
```
|
||||
|
||||
Your browser should open automatically to `http://localhost:8910` to see the web app. Lambda functions run on `http://localhost:8911` and are also proxied to `http://localhost:8910/.redwood/functions/*`.
|
||||
|
||||
If you want to access the websight on your phone use `yarn redwood dev --fwd="--host <ip-address-on-your-network-i.e.-192.168.0.5>"`
|
||||
|
||||
you can sign in to the following accounts locally
|
||||
|
||||
localUser1@kurthutten.com: `abc123`
|
||||
|
||||
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)
|
||||
|
||||
## Docs
|
||||
Docs are hosted at [learn.cadhub.xyz](http://learn.cadhub.xyz/). It includes a OpenSCAD tutorial at this point, and more is coming. The docs can be found in this repo at [docs](https://github.com/Irev-Dev/cadhub/tree/main/docs)
|
||||
84
README.md
84
README.md
@@ -1,4 +1,6 @@
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
# [C a d H u b](https://cadhub.xyz)
|
||||
|
||||
@@ -6,84 +8,10 @@
|
||||
|
||||
Let's help Code-CAD reach its [full potential!](https://cadhub.xyz) We're making a ~~cad~~hub for the Code-CAD community, think of it as model-repository crossed with a live editor. We have integrations in progress for [OpenSCAD](https://cadhub.xyz/dev-ide/openscad) and [CadQuery](https://cadhub.xyz/dev-ide/cadquery) with [more coming soon](https://github.com/Irev-Dev/curated-code-cad).
|
||||
|
||||
If you want to be involved in anyway, checkout the [Road Map](https://github.com/Irev-Dev/cadhub/discussions/212) and get in touch via, [twitter](https://twitter.com/IrevDev), [discord](https://discord.gg/SD7zFRNjGH) or [discussions](https://github.com/Irev-Dev/cadhub/discussions).
|
||||
|
||||
<img src="https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/fullcadhubshot.jpg">
|
||||
<img src="https://raw.githubusercontent.com/Irev-Dev/cadhub/main/docs/static/img/blog/curated-code-cad/CadHubSS.jpg">
|
||||
|
||||
## Getting your dev environment setup
|
||||
|
||||
|
||||
```terminal
|
||||
git clone git@github.com:Irev-Dev/cadhub.git
|
||||
# or
|
||||
git clone https://github.com/Irev-Dev/cadhub.git
|
||||
```
|
||||
|
||||
cd in the app directory
|
||||
```
|
||||
cd app
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
```terminal
|
||||
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
|
||||
``` terminal
|
||||
yarn rw prisma migrate dev
|
||||
yarn rw prisma db seed
|
||||
```
|
||||
|
||||
p.s. `yarn rw prisma studio` spins up an app to inspect the db
|
||||
|
||||
### Fire up dev
|
||||
```terminal
|
||||
yarn rw dev
|
||||
```
|
||||
|
||||
Your browser should open automatically to `http://localhost:8910` to see the web app. Lambda functions run on `http://localhost:8911` and are also proxied to `http://localhost:8910/.redwood/functions/*`.
|
||||
|
||||
If you want to access the websight on your phone use `yarn redwood dev --fwd="--host <ip-address-on-your-network-i.e.-192.168.0.5">"`
|
||||
|
||||
you can sign in to the following accounts locally
|
||||
|
||||
localUser1@kurthutten.com: `abc123`
|
||||
|
||||
localUser2@kurthutten.com: `abc123`
|
||||
|
||||
localAdmin@kurthutten.com: `abc123`
|
||||
|
||||
You may need to register a account depending on what issue you are trying to tackle, This can be done by clicking the login button on the top right. This will open up netlify's idenitiy modal asking for the websites url, since it will notice you developing locally. Enter `https://cadhub.xyz/` than use you email, verify your email and you should be set.
|
||||
(some routes are protected, but permissions is a big area that needs a lot of work in the near future, so it's in a very incomplete state atm)
|
||||
|
||||
### Note:
|
||||
We're using [RedwoodJS](https://redwoodjs.com/), this is perhaps unwise since they haven't reached 1.0 yet, however with their aim to release 1.0 by the end of the year, it shouldn't be too difficult to port changes over the coming months.
|
||||
If you not familiar with Redwood, never fear the main bit of tech it uses is React, Graphql(apollo) and serverless/lamdas, depending on what part of the app you want to help with, so long as you know you way around these bits of tech you should be fine with some light referencing of the RedWood docs
|
||||
|
||||
### Extra Redwood docs, i.e. getting familiar with the frame work.
|
||||
- [Tutorial](https://redwoodjs.com/tutorial/welcome-to-redwood): getting started and complete overview guide.
|
||||
- [Docs](https://redwoodjs.com/docs/introduction): using the Redwood Router, handling assets and files, list of command-line tools, and more.
|
||||
- [Redwood Community](https://community.redwoodjs.com): get help, share tips and tricks, and collaborate on everything about RedwoodJS.
|
||||
|
||||
## Styles
|
||||
|
||||
We're using tailwind utility classes so please try and use them as much as possible. Again if you not familiar, the [tailwind search](https://tailwindcss.com/) is fantastic, so searching for the css property you want to use will lead you to the correct class 99% of the time.
|
||||
|
||||
## Designs
|
||||
|
||||
In progress, though can be [seen on Figma](https://www.figma.com/file/VUh53RdncjZ7NuFYj0RGB9/CadHub?node-id=0%3A1)
|
||||
|
||||
## Integrations
|
||||
The OpenSCAD and CadQuery integrations work by leveraging each of their cli tools in a docker image. It's currently deployed to AWS and can be found [here](https://github.com/Irev-Dev/cadhub/tree/main/app/api/src/docker).
|
||||
|
||||
## Docs
|
||||
Docs are hosted at [learn.cadhub.xyz](http://learn.cadhub.xyz/). It includes a OpenSCAD tutorial at this point, and more is coming. The docs can be found in this repo at [docs](https://github.com/Irev-Dev/cadhub/tree/main/docs)
|
||||
If you want to be involved in anyway, checkout the [contributing.md](https://github.com/Irev-Dev/cadhub/blob/main/CONTRIBUTING.md).
|
||||
|
||||
you might also be interested in the [Road Map](https://github.com/Irev-Dev/cadhub/discussions/212) and getting in touch via, [twitter](https://twitter.com/IrevDev), [discord](https://discord.gg/SD7zFRNjGH) or [discussions](https://github.com/Irev-Dev/cadhub/discussions).
|
||||
|
||||
## Who is CadHub
|
||||
|
||||
[Kurt](https://github.com/Irev-Dev) and [Frank](https://github.com/franknoirot) make up the Core-team and [Jeremy](https://github.com/jmwright) is a major contributor. Plus a number smaller contributors.
|
||||
[Kurt](https://github.com/Irev-Dev) and [Frank](https://github.com/franknoirot) make up the Core-team and [Jeremy](https://github.com/jmwright), [Torsten](https://github.com/t-paul) and [Hrg](https://github.com/hrgdavor) are a major contributors. Plus a number smaller contributors.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = { extends: "../babel.config.js" }
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "CadPackage" ADD VALUE 'jscad';
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Project" ADD COLUMN "forkedFromId" TEXT;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Project" ADD FOREIGN KEY ("forkedFromId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
56
app/api/db/migrations/20211113002346_prisma_v3/migration.sql
Normal file
56
app/api/db/migrations/20211113002346_prisma_v3/migration.sql
Normal file
@@ -0,0 +1,56 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Comment" DROP CONSTRAINT "Comment_projectId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Comment" DROP CONSTRAINT "Comment_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Project" DROP CONSTRAINT "Project_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "ProjectReaction" DROP CONSTRAINT "ProjectReaction_projectId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "ProjectReaction" DROP CONSTRAINT "ProjectReaction_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "SocialCard" DROP CONSTRAINT "SocialCard_projectId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "SubjectAccessRequest" DROP CONSTRAINT "SubjectAccessRequest_userId_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Project" ADD CONSTRAINT "Project_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SocialCard" ADD CONSTRAINT "SocialCard_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProjectReaction" ADD CONSTRAINT "ProjectReaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProjectReaction" ADD CONSTRAINT "ProjectReaction_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SubjectAccessRequest" ADD CONSTRAINT "SubjectAccessRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "Project.title_userId_unique" RENAME TO "Project_title_userId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "ProjectReaction.emote_userId_projectId_unique" RENAME TO "ProjectReaction_emote_userId_projectId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "SocialCard.projectId_unique" RENAME TO "SocialCard_projectId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "User.email_unique" RENAME TO "User_email_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "User.userName_unique" RENAME TO "User_userName_key";
|
||||
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
|
||||
@@ -14,10 +14,6 @@ generator client {
|
||||
// ADMIN
|
||||
// }
|
||||
|
||||
// enum ProjectType {
|
||||
// JSCAD
|
||||
// }
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
userName String @unique // reffered to as userId in @relations
|
||||
@@ -41,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 {
|
||||
@@ -57,9 +54,12 @@ model Project {
|
||||
deleted Boolean @default(false)
|
||||
cadPackage CadPackage @default(openscad)
|
||||
socialCard SocialCard?
|
||||
forkedFromId String?
|
||||
forkedFrom Project? @relation("Fork", fields: [forkedFromId], references: [id])
|
||||
|
||||
Comment Comment[]
|
||||
Reaction ProjectReaction[]
|
||||
childForks Project[] @relation("Fork")
|
||||
Comment Comment[]
|
||||
Reaction ProjectReaction[]
|
||||
@@unique([title, userId])
|
||||
}
|
||||
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
const { PrismaClient } = require('@prisma/client')
|
||||
const dotenv = require('dotenv')
|
||||
|
||||
dotenv.config()
|
||||
const db = new PrismaClient()
|
||||
|
||||
async function main() {
|
||||
// Seed data is database data that needs to exist for your app to run.
|
||||
// Ideally this file should be idempotent: running it multiple times
|
||||
// will result in the same database state (usually by checking for the
|
||||
// existence of a record before trying to create it). For example:
|
||||
//
|
||||
// const existing = await db.user.findMany({ where: { email: 'admin@email.com' }})
|
||||
// if (!existing.length) {
|
||||
// await db.user.create({ data: { name: 'Admin', email: 'admin@email.com' }})
|
||||
// }
|
||||
const users = [
|
||||
{
|
||||
id: "a2b21ce1-ae57-43a2-b6a3-b6e542fd9e60",
|
||||
userName: "local-user-1",
|
||||
name: "local 1",
|
||||
email: "localUser1@kurthutten.com"
|
||||
},
|
||||
{
|
||||
id: "682ba807-d10e-4caf-bf28-74054e46c9ec",
|
||||
userName: "local-user-2",
|
||||
name: "local 2",
|
||||
email: "localUser2@kurthutten.com"
|
||||
},
|
||||
{
|
||||
id: "5cea3906-1e8e-4673-8f0d-89e6a963c096",
|
||||
userName: "local-admin-2",
|
||||
name: "local admin",
|
||||
email: "localAdmin@kurthutten.com"
|
||||
},
|
||||
]
|
||||
|
||||
let existing
|
||||
existing = await db.user.findMany({ where: { id: users[0].id }})
|
||||
if(!existing.length) {
|
||||
await db.user.create({
|
||||
data: users[0],
|
||||
})
|
||||
}
|
||||
existing = await db.user.findMany({ where: { id: users[1].id }})
|
||||
if(!existing.length) {
|
||||
await db.user.create({
|
||||
data: users[1],
|
||||
})
|
||||
}
|
||||
|
||||
const projects = [
|
||||
{
|
||||
title: 'demo-project1',
|
||||
description: '# can be markdown',
|
||||
mainImage: 'CadHub/kjdlgjnu0xmwksia7xox',
|
||||
user: {
|
||||
connect: {
|
||||
id: users[0].id,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'demo-project2',
|
||||
description: '## [hey](www.google.com)',
|
||||
user: {
|
||||
connect: {
|
||||
id: users[1].id,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
existing = await db.project.findMany({where: { title: projects[0].title}})
|
||||
if(!existing.length) {
|
||||
await db.project.create({
|
||||
data: projects[0],
|
||||
})
|
||||
}
|
||||
existing = await db.project.findMany({where: { title: projects[1].title}})
|
||||
if(!existing.length) {
|
||||
await db.project.create({
|
||||
data: projects[1],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
const aProject = await db.project.findUnique({where: {
|
||||
title_userId: {
|
||||
title: projects[0].title,
|
||||
userId: users[0].id,
|
||||
}
|
||||
}})
|
||||
await db.comment.create({
|
||||
data: {
|
||||
text: "nice project, I like it",
|
||||
userId: users[0].id,
|
||||
projectId: aProject.id,
|
||||
// user: {connect: { id: users[0].id}},
|
||||
// project: {connect: { id: aProject.id}},
|
||||
}
|
||||
})
|
||||
await db.projectReaction.create({
|
||||
data: {
|
||||
emote: "❤️",
|
||||
userId: users[0].id,
|
||||
projectId: aProject.id,
|
||||
// user: {connect: { id: users[0].id}},
|
||||
// project: {connect: { id: aProject.id}},
|
||||
}
|
||||
})
|
||||
|
||||
console.info('No data to seed. See api/prisma/seeds.js for info.')
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => console.error(e))
|
||||
.finally(async () => {
|
||||
await db.$disconnect()
|
||||
})
|
||||
@@ -3,13 +3,22 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@redwoodjs/api": "^0.34.1",
|
||||
"@redwoodjs/api": "^0.38.1",
|
||||
"@redwoodjs/graphql-server": "^0.38.1",
|
||||
"@sentry/node": "^6.5.1",
|
||||
"axios": "^0.25.0",
|
||||
"cloudinary": "^1.23.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"human-id": "^2.0.1",
|
||||
"nodemailer": "^6.6.2"
|
||||
"middy": "^0.36.0",
|
||||
"nanoid": "^3.1.20",
|
||||
"nodemailer": "^6.6.2",
|
||||
"serverless-binary-cors": "^0.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/nodemailer": "^6.4.2"
|
||||
"@types/nodemailer": "^6.4.2",
|
||||
"concurrently": "^6.0.0",
|
||||
"nodemon": "^2.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
18
app/api/src/directives/requireAuth/requireAuth.test.ts
Normal file
18
app/api/src/directives/requireAuth/requireAuth.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { mockRedwoodDirective, getDirectiveName } from '@redwoodjs/testing/api'
|
||||
|
||||
import requireAuth from './requireAuth'
|
||||
|
||||
describe('requireAuth directive', () => {
|
||||
it('declares the directive sdl as schema, with the correct name', () => {
|
||||
expect(requireAuth.schema).toBeTruthy()
|
||||
expect(getDirectiveName(requireAuth.schema)).toBe('requireAuth')
|
||||
})
|
||||
|
||||
it('requireAuth has stub implementation. Should not throw when current user', () => {
|
||||
// If you want to set values in context, pass it through e.g.
|
||||
// mockRedwoodDirective(requireAuth, { context: { currentUser: { id: 1, name: 'Lebron McGretzky' } }})
|
||||
const mockExecution = mockRedwoodDirective(requireAuth, { context: {} })
|
||||
|
||||
expect(mockExecution).not.toThrowError()
|
||||
})
|
||||
})
|
||||
22
app/api/src/directives/requireAuth/requireAuth.ts
Normal file
22
app/api/src/directives/requireAuth/requireAuth.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import { createValidatorDirective } from '@redwoodjs/graphql-server'
|
||||
|
||||
import { requireAuth as applicationRequireAuth } from 'src/lib/auth'
|
||||
|
||||
export const schema = gql`
|
||||
"""
|
||||
Use to check whether or not a user is authenticated and is associated
|
||||
with an optional set of roles.
|
||||
"""
|
||||
directive @requireAuth(roles: [String]) on FIELD_DEFINITION
|
||||
`
|
||||
|
||||
const validate = ({ directiveArgs }) => {
|
||||
const { roles } = directiveArgs
|
||||
applicationRequireAuth({ roles })
|
||||
}
|
||||
|
||||
const requireAuth = createValidatorDirective(schema, validate)
|
||||
|
||||
export default requireAuth
|
||||
10
app/api/src/directives/skipAuth/skipAuth.test.ts
Normal file
10
app/api/src/directives/skipAuth/skipAuth.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { getDirectiveName } from '@redwoodjs/testing/api'
|
||||
|
||||
import skipAuth from './skipAuth'
|
||||
|
||||
describe('skipAuth directive', () => {
|
||||
it('declares the directive sdl as schema, with the correct name', () => {
|
||||
expect(skipAuth.schema).toBeTruthy()
|
||||
expect(getDirectiveName(skipAuth.schema)).toBe('skipAuth')
|
||||
})
|
||||
})
|
||||
16
app/api/src/directives/skipAuth/skipAuth.ts
Normal file
16
app/api/src/directives/skipAuth/skipAuth.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import { createValidatorDirective } from '@redwoodjs/graphql-server'
|
||||
|
||||
export const schema = gql`
|
||||
"""
|
||||
Use to skip authentication checks and allow public access.
|
||||
"""
|
||||
directive @skipAuth on FIELD_DEFINITION
|
||||
`
|
||||
|
||||
const skipAuth = createValidatorDirective(schema, () => {
|
||||
return
|
||||
})
|
||||
|
||||
export default skipAuth
|
||||
@@ -1,11 +1,12 @@
|
||||
# Serverless
|
||||
|
||||
We're using the serverless from work for deployment
|
||||
We're using the serverless framework for deployment
|
||||
|
||||
```
|
||||
sls deploy --stage stagename
|
||||
yarn rw build api && sls deploy --stage <stagename>
|
||||
```
|
||||
But [Kurt Hutten](https://github.com/Irev-Dev) is the only one with credentials for deployment atm, though if you wanted to set your own account you could deploy to that if you wanted to test.
|
||||
Deploying has `yarn rw build` first because the image uses built js files
|
||||
|
||||
## Testing changes locally
|
||||
|
||||
@@ -14,21 +15,19 @@ You'll need to have Docker installed
|
||||
Because of the way the docker containers to be deployed as lambdas on aws are somewhat specialised for the purpose we're using `docker-compose` to spin one up for each function/endpoint. So we've added a aws-emulation layer
|
||||
|
||||
|
||||
The docker build relies on a git ignored file, the aws-lambda-rie. [Download it](https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v1.0/aws-lambda-rie), then put it into `app/api/src/docker/common/`. alternatively you can put this download into the DockerFiles by reading the instructions at around line 29 of the DockerFiles (`app/api/src/docker/openscad/Dockerfile` & `app/api/src/docker/cadquery/Dockerfile`). However this will mean slower build times as it will need download this 14mb file every build.
|
||||
The docker build relies on a git ignored file, the aws-lambda-rie. [Download it](https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v1.0/aws-lambda-rie), then put it into `app/api/src/docker/common/`. Alternatively you can put this download into the DockerFiles by reading the instructions at around line 29 of the DockerFiles (`app/api/src/docker/openscad/Dockerfile` & `app/api/src/docker/cadquery/Dockerfile`). However this will mean slower build times as it will need download this 14mb file every build.
|
||||
|
||||
you will also need to create a .env in `app/api/src/docker/.env` for the following env-vars `DEV_AWS_SECRET_ACCESS_KEY, DEV_AWS_ACCESS_KEY_ID and DEV_BUCKET`. Ask @irev-dev for credentials and he can sort you out.
|
||||
|
||||
Then cd into this folder `cd api/src/docker` and:
|
||||
Run
|
||||
|
||||
```bash
|
||||
docker-compose up --build
|
||||
yarn cad
|
||||
```
|
||||
The first time you run this, it has to build the main image it will take some time, but launching again will be quicker.
|
||||
|
||||
After which we'll also spin up a light express server to act as an emulator to transform some the request from the front end into how the lambda's expect them (This emulates the aws-api-gateway which changes tranforms the inbound requests somewhat).
|
||||
```
|
||||
yarn install
|
||||
yarn emulate
|
||||
yarn aws-emulate
|
||||
```
|
||||
You can now add CAD_LAMBDA_BASE_URL="http://localhost:8080" to you .env file and restart your main dev process (`yarn rw dev`) comment that line out if you want to go back to using the aws endpoint (and restart the dev process).
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const express = require('express')
|
||||
var cors = require('cors')
|
||||
const axios = require('axios')
|
||||
const { restart } = require('nodemon')
|
||||
const app = express()
|
||||
const port = 8080
|
||||
app.use(express.json())
|
||||
@@ -16,10 +15,16 @@ const makeRequest = (route, port) => [
|
||||
console.log(`making post request to ${port}, ${route}`)
|
||||
try {
|
||||
const { data } = await axios.post(invocationURL(port), {
|
||||
body: JSON.stringify(req.body),
|
||||
body: Buffer.from(JSON.stringify(req.body)).toString('base64'),
|
||||
})
|
||||
res.status(data.statusCode)
|
||||
res.send(data.body)
|
||||
res.setHeader('Content-Type', 'application/javascript')
|
||||
if (data.headers && data.headers['Content-Encoding'] === 'gzip') {
|
||||
res.setHeader('Content-Encoding', 'gzip')
|
||||
res.send(Buffer.from(data.body, 'base64'))
|
||||
} else {
|
||||
res.send(Buffer.from(data.body, 'base64'))
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500)
|
||||
res.send()
|
||||
@@ -29,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}`)
|
||||
})
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
FROM public.ecr.aws/lts/ubuntu:20.04_stable
|
||||
|
||||
FROM continuumio/miniconda3
|
||||
ENV PATH="/root/miniconda3/bin:${PATH}"
|
||||
ARG PATH="/root/miniconda3/bin:${PATH}"
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update -qq
|
||||
RUN apt-get update --fix-missing -qq
|
||||
RUN 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 install -y wget
|
||||
|
||||
# install node14, see comment at the to of node14source_setup.sh
|
||||
ADD common/node14source_setup.sh /nodesource_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
|
||||
@@ -21,32 +22,41 @@ RUN apt-get update && \
|
||||
cmake \
|
||||
unzip \
|
||||
automake autoconf libtool \
|
||||
libcurl4-openssl-dev
|
||||
libcurl4-openssl-dev \
|
||||
curl \
|
||||
git
|
||||
|
||||
# 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 common/aws-lambda-rie /usr/local/bin/aws-lambda-rie
|
||||
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 cadquery/package*.json /var/task/
|
||||
COPY package*.json /var/task/
|
||||
RUN npm install
|
||||
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
|
||||
|
||||
# Get a copy of cq-cli from GitHub
|
||||
RUN git clone https://github.com/CadQuery/cq-cli.git
|
||||
|
||||
# Get the distribution copy of cq-cli
|
||||
RUN apt-get install -y libglew2.1
|
||||
RUN wget https://github.com/CadQuery/cq-cli/releases/download/v2.2-beta.2/cq-cli-Linux-x86_64.zip
|
||||
# Comment the entry above out and uncomment the one below to revert to the stable release
|
||||
# RUN wget https://github.com/CadQuery/cq-cli/releases/download/v2.1.0/cq-cli-Linux-x86_64.zip
|
||||
RUN unzip cq-cli-Linux-x86_64.zip
|
||||
|
||||
RUN chmod +x cq-cli/cq-cli
|
||||
RUN echo "cadhub-concat-split" > /var/task/cadhub-concat-split
|
||||
|
||||
COPY cadquery/*.js /var/task/
|
||||
COPY common/*.js /var/common/
|
||||
COPY common/entrypoint.sh /entrypoint.sh
|
||||
# using built javascript from dist
|
||||
# run `yarn rw build` before bulding this image
|
||||
COPY dist/docker/cadquery/*.js /var/task/js/
|
||||
COPY dist/docker/common/*.js /var/task/common/
|
||||
COPY src/docker/common/entrypoint.sh /entrypoint.sh
|
||||
RUN ["chmod", "+x", "/entrypoint.sh"]
|
||||
ENTRYPOINT ["sh", "/entrypoint.sh"]
|
||||
CMD [ "cadquery.stl" ]
|
||||
CMD [ "js/cadquery.stl" ]
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
const { runCQ } = require('./runCQ')
|
||||
const middy = require('middy')
|
||||
const { cors } = require('middy/middlewares')
|
||||
const AWS = require('aws-sdk')
|
||||
const tk = require('timekeeper')
|
||||
const {
|
||||
makeHash,
|
||||
checkIfAlreadyExists,
|
||||
getObjectUrl,
|
||||
loggerWrap,
|
||||
storeAssetAndReturnUrl,
|
||||
} = require('../common/utils')
|
||||
|
||||
const s3 = new AWS.S3()
|
||||
|
||||
const stl = async (req, _context, callback) => {
|
||||
_context.callbackWaitsForEmptyEventLoop = false
|
||||
const eventBody = req.body
|
||||
console.log('eventBody', eventBody)
|
||||
const key = `${makeHash(eventBody)}.stl`
|
||||
console.log('key', key)
|
||||
|
||||
const params = {
|
||||
Bucket: process.env.BUCKET,
|
||||
Key: key,
|
||||
}
|
||||
const previousAsset = await checkIfAlreadyExists(params, s3)
|
||||
if (previousAsset.isAlreadyInBucket) {
|
||||
console.log('already in bucket')
|
||||
const response = {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({
|
||||
url: getObjectUrl(params, s3, tk),
|
||||
consoleMessage: previousAsset.consoleMessage,
|
||||
}),
|
||||
}
|
||||
callback(null, response)
|
||||
return
|
||||
}
|
||||
|
||||
const { file, settings } = JSON.parse(eventBody)
|
||||
const { error, consoleMessage, fullPath } = await runCQ({ file, settings })
|
||||
await storeAssetAndReturnUrl({
|
||||
error,
|
||||
callback,
|
||||
fullPath,
|
||||
consoleMessage,
|
||||
key,
|
||||
s3,
|
||||
params,
|
||||
tk,
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
stl: middy(loggerWrap(stl)).use(cors()),
|
||||
}
|
||||
27
app/api/src/docker/cadquery/cadquery.ts
Normal file
27
app/api/src/docker/cadquery/cadquery.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { runCQ } from './runCQ'
|
||||
import middy from 'middy'
|
||||
import { cors } from 'middy/middlewares'
|
||||
import { loggerWrap, storeAssetAndReturnUrl } from '../common/utils'
|
||||
|
||||
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, fullPath } = await runCQ({
|
||||
file,
|
||||
settings,
|
||||
})
|
||||
await storeAssetAndReturnUrl({
|
||||
error,
|
||||
callback,
|
||||
fullPath,
|
||||
consoleMessage: '',
|
||||
tempFile : '',
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
stl: middy(loggerWrap(stl)).use(cors()),
|
||||
}
|
||||
1156
app/api/src/docker/cadquery/package-lock.json
generated
1156
app/api/src/docker/cadquery/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"name": "openscad-endpoint",
|
||||
"version": "0.0.1",
|
||||
"description": "endpoint for openscad",
|
||||
"main": "index.js",
|
||||
"author": "Kurt Hutten <kurt@kurthutten.com>",
|
||||
"license": "",
|
||||
"dependencies": {
|
||||
"aws-sdk": "^2.907.0",
|
||||
"cors": "^2.8.5",
|
||||
"middy": "^0.36.0",
|
||||
"nanoid": "^3.1.20",
|
||||
"timekeeper": "2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-lambda-ric": "^1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
const { makeFile, runCommand } = require('../common/utils')
|
||||
const { nanoid } = require('nanoid')
|
||||
|
||||
module.exports.runCQ = async ({
|
||||
file,
|
||||
settings: { deflection = 0.3 } = {},
|
||||
} = {}) => {
|
||||
const tempFile = await makeFile(file, '.py', nanoid)
|
||||
const fullPath = `/tmp/${tempFile}/output.stl`
|
||||
const command = [
|
||||
`cq-cli/cq-cli`,
|
||||
`--codec stl`,
|
||||
`--infile /tmp/${tempFile}/main.py`,
|
||||
`--outfile ${fullPath}`,
|
||||
`--outputopts "deflection:${deflection};angularDeflection:${deflection};"`,
|
||||
`&& gzip ${fullPath}`,
|
||||
].join(' ')
|
||||
console.log('command', command)
|
||||
|
||||
try {
|
||||
const consoleMessage = await runCommand(command, 30000)
|
||||
return { consoleMessage, fullPath }
|
||||
} catch (error) {
|
||||
return { error, fullPath }
|
||||
}
|
||||
}
|
||||
61
app/api/src/docker/cadquery/runCQ.ts
Normal file
61
app/api/src/docker/cadquery/runCQ.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { writeFiles, runCommand } from '../common/utils'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { readFile } from 'fs/promises'
|
||||
|
||||
export const runCQ = async ({
|
||||
file,
|
||||
settings: { deflection = 0.3, parameters } = {},
|
||||
} = {}) => {
|
||||
const tempFile = await writeFiles(
|
||||
[
|
||||
{ file, fileName: 'main.py' },
|
||||
{
|
||||
file: JSON.stringify(parameters),
|
||||
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 customizerPath = `/tmp/${tempFile}/customizer.json`
|
||||
const command = [
|
||||
`/var/task/cq-cli/cq-cli.py`,
|
||||
`--codec stl`,
|
||||
`--infile /tmp/${tempFile}/main.py`,
|
||||
`--outfile ${stlPath}`,
|
||||
`--outputopts "deflection:${deflection};angularDeflection:${deflection};"`,
|
||||
`--params /tmp/${tempFile}/params.json`,
|
||||
`--getparams ${customizerPath}`,
|
||||
].join(' ')
|
||||
console.log('command', command)
|
||||
let consoleMessage = ''
|
||||
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 }
|
||||
// }
|
||||
}
|
||||
@@ -2,29 +2,32 @@ const { exec } = require('child_process')
|
||||
const { promises } = require('fs')
|
||||
const { writeFile } = promises
|
||||
const { createHash } = require('crypto')
|
||||
import { readFile } from 'fs/promises'
|
||||
|
||||
const CONSOLE_MESSAGE_KEY = 'console-message-b64'
|
||||
function putConsoleMessageInMetadata(consoleMessage) {
|
||||
return {
|
||||
[CONSOLE_MESSAGE_KEY]: Buffer.from(consoleMessage, 'utf-8').toString(
|
||||
'base64'
|
||||
),
|
||||
export async function writeFiles(
|
||||
files: { file: string; fileName: string }[] = [],
|
||||
tempFile: string
|
||||
): Promise<string> {
|
||||
console.log(`file to write: ${files.length}`)
|
||||
|
||||
try {
|
||||
await runCommand(`mkdir /tmp/${tempFile}`)
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
function getConsoleMessageFromMetadata(metadata) {
|
||||
return Buffer.from(metadata[CONSOLE_MESSAGE_KEY], 'base64').toString('utf-8')
|
||||
}
|
||||
|
||||
async function makeFile(file, extension = '.scad', makeHash) {
|
||||
const tempFile = 'a' + makeHash() // 'a' ensure nothing funny happens if it start with a bad character like "-", maybe I should pick a safer id generator :shrug:
|
||||
console.log(`file to write: ${file}`)
|
||||
|
||||
await runCommand(`mkdir /tmp/${tempFile}`)
|
||||
await writeFile(`/tmp/${tempFile}/main${extension}`, file)
|
||||
await Promise.all(
|
||||
files.map(({ file, fileName }) =>
|
||||
writeFile(`/tmp/${tempFile}/${fileName}`, file)
|
||||
)
|
||||
)
|
||||
return tempFile
|
||||
}
|
||||
|
||||
async function runCommand(command, timeout = 5000) {
|
||||
export async function runCommand(
|
||||
command,
|
||||
timeout = 5000,
|
||||
shouldRejectStdErr = false
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
@@ -36,6 +39,10 @@ async function runCommand(command, timeout = 5000) {
|
||||
}
|
||||
if (stderr) {
|
||||
console.log(`stderr: ${stderr}`)
|
||||
if (shouldRejectStdErr) {
|
||||
reject(stderr)
|
||||
return
|
||||
}
|
||||
resolve(stderr)
|
||||
return
|
||||
}
|
||||
@@ -54,10 +61,8 @@ function makeHash(script) {
|
||||
|
||||
async function checkIfAlreadyExists(params, s3) {
|
||||
try {
|
||||
const objectHead = await s3.headObject(params).promise()
|
||||
const consoleMessage = getConsoleMessageFromMetadata(objectHead.Metadata)
|
||||
console.log('consoleMessage', consoleMessage)
|
||||
return { isAlreadyInBucket: true, consoleMessage }
|
||||
await s3.headObject(params).promise()
|
||||
return { isAlreadyInBucket: true }
|
||||
} catch (e) {
|
||||
console.log("couldn't find it", e)
|
||||
return { isAlreadyInBucket: false }
|
||||
@@ -84,7 +89,7 @@ function getObjectUrl(params, s3, tk) {
|
||||
)
|
||||
}
|
||||
|
||||
function loggerWrap(handler) {
|
||||
export function loggerWrap(handler) {
|
||||
return (req, _context, callback) => {
|
||||
try {
|
||||
return handler(req, _context, callback)
|
||||
@@ -94,72 +99,56 @@ function loggerWrap(handler) {
|
||||
}
|
||||
}
|
||||
|
||||
async function storeAssetAndReturnUrl({
|
||||
export async function storeAssetAndReturnUrl({
|
||||
error,
|
||||
callback,
|
||||
fullPath,
|
||||
consoleMessage,
|
||||
key,
|
||||
s3,
|
||||
params,
|
||||
tk,
|
||||
tempFile,
|
||||
}: {
|
||||
error: string
|
||||
callback: Function
|
||||
fullPath: string
|
||||
consoleMessage: string
|
||||
tempFile: string
|
||||
}) {
|
||||
if (error) {
|
||||
const response = {
|
||||
statusCode: 400,
|
||||
body: JSON.stringify({ error, fullPath }),
|
||||
body: Buffer.from(JSON.stringify({ error, fullPath })).toString('base64'),
|
||||
isBase64Encoded: true,
|
||||
}
|
||||
callback(null, response)
|
||||
return
|
||||
} else {
|
||||
console.log(`got result in route: ${consoleMessage}, file is: ${fullPath}`)
|
||||
const { readFile } = require('fs/promises')
|
||||
let buffer
|
||||
let buffer = ''
|
||||
|
||||
try {
|
||||
buffer = await readFile(`${fullPath}.gz`)
|
||||
buffer = await readFile(fullPath, { encoding: 'base64' })
|
||||
await runCommand(`rm -R /tmp/${tempFile}`)
|
||||
} catch (e) {
|
||||
console.log('read file error', e)
|
||||
const response = {
|
||||
statusCode: 400,
|
||||
body: JSON.stringify({ error: consoleMessage, fullPath }),
|
||||
body: Buffer.from(
|
||||
JSON.stringify({ error: consoleMessage, fullPath })
|
||||
).toString('base64'),
|
||||
isBase64Encoded: true,
|
||||
}
|
||||
callback(null, response)
|
||||
return
|
||||
}
|
||||
const FiveDays = 432000
|
||||
const storedRender = await s3
|
||||
.putObject({
|
||||
Bucket: process.env.BUCKET,
|
||||
Key: key,
|
||||
Body: buffer,
|
||||
CacheControl: `max-age=${FiveDays}`, // browser caching to stop downloads of the same part
|
||||
ContentType: 'text/stl',
|
||||
ContentEncoding: 'gzip',
|
||||
Metadata: putConsoleMessageInMetadata(consoleMessage),
|
||||
})
|
||||
.promise()
|
||||
console.log('stored object', storedRender)
|
||||
const url = getObjectUrl(params, s3, tk)
|
||||
console.log('url', url)
|
||||
const response = {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({
|
||||
url,
|
||||
consoleMessage,
|
||||
}),
|
||||
body: buffer,
|
||||
isBase64Encoded: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/javascript',
|
||||
'Content-Encoding': 'gzip',
|
||||
},
|
||||
}
|
||||
callback(null, response)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
runCommand,
|
||||
makeFile,
|
||||
makeHash,
|
||||
checkIfAlreadyExists,
|
||||
getObjectUrl,
|
||||
loggerWrap,
|
||||
storeAssetAndReturnUrl,
|
||||
}
|
||||
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 }
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,14 @@ services:
|
||||
|
||||
openscad-preview:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./openscad/Dockerfile
|
||||
context: ../../
|
||||
dockerfile: ./src/docker/openscad/Dockerfile
|
||||
image: openscad
|
||||
command: openscad.preview
|
||||
command: js/openscad.preview
|
||||
# Adding volumes so that the containers can be restarted for js only changes in local dev
|
||||
volumes:
|
||||
- ../../dist/docker/openscad:/var/task/js/
|
||||
- ../../dist/docker/common:/var/task/common/
|
||||
ports:
|
||||
- "5052:8080"
|
||||
environment:
|
||||
@@ -15,7 +19,10 @@ services:
|
||||
|
||||
openscad-stl:
|
||||
image: openscad
|
||||
command: openscad.stl
|
||||
volumes:
|
||||
- ../../dist/docker/openscad:/var/task/js/
|
||||
- ../../dist/docker/common:/var/task/common/
|
||||
command: js/openscad.stl
|
||||
ports:
|
||||
- "5053:8080"
|
||||
environment:
|
||||
@@ -25,9 +32,12 @@ services:
|
||||
|
||||
cadquery-stl:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./cadquery/Dockerfile
|
||||
command: cadquery.stl
|
||||
context: ../../
|
||||
dockerfile: ./src/docker/cadquery/Dockerfile
|
||||
volumes:
|
||||
- ../../dist/docker/cadquery:/var/task/js/
|
||||
- ../../dist/docker/common:/var/task/common/
|
||||
command: js/cadquery.stl
|
||||
ports:
|
||||
- 5060:8080
|
||||
environment:
|
||||
@@ -35,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"
|
||||
|
||||
|
||||
@@ -3,16 +3,18 @@ FROM public.ecr.aws/lts/ubuntu:20.04_stable
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
## install things needed to run openscad (xvfb is an important one)
|
||||
RUN apt-get update -qq
|
||||
RUN apt-get update --fix-missing -qq
|
||||
# double check this below, I'm not sure we need inkscape etc
|
||||
RUN apt-get -y -qq install software-properties-common dirmngr apt-transport-https lsb-release ca-certificates xvfb imagemagick unzip inkscape
|
||||
RUN add-apt-repository ppa:openscad/releases
|
||||
RUN apt-get update -qq
|
||||
RUN apt-get install -y -qq openscad
|
||||
RUN apt-get install -y curl wget
|
||||
RUN touch /etc/apt/sources.list.d/openscad.list
|
||||
RUN echo "deb https://download.opensuse.org/repositories/home:/t-paul/xUbuntu_20.04/ ./" >> /etc/apt/sources.list.d/openscad.list
|
||||
RUN wget -qO - https://files.openscad.org/OBS-Repository-Key.pub | apt-key add -
|
||||
RUN apt-get update -qq
|
||||
RUN apt-get install -y openscad-nightly
|
||||
|
||||
# install node14, see comment at the to of node14source_setup.sh
|
||||
ADD common/node14source_setup.sh /nodesource_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
|
||||
@@ -30,13 +32,14 @@ RUN apt-get update && \
|
||||
# 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 common/aws-lambda-rie /usr/local/bin/aws-lambda-rie
|
||||
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 openscad/package*.json /var/task/
|
||||
COPY package*.json /var/task/
|
||||
RUN npm install
|
||||
RUN npm install aws-lambda-ric@1.0.0
|
||||
|
||||
# Install OpenSCAD libraries
|
||||
# It's experimental, so only adding latest Round-Anything for now
|
||||
@@ -44,12 +47,16 @@ RUN echo "OPENSCADPATH=/var/task/openscad" >>/etc/profile && \
|
||||
wget -P /var/task/openscad/ https://github.com/Irev-Dev/Round-Anything/archive/refs/tags/1.0.4.zip && \
|
||||
unzip /var/task/openscad/1.0.4
|
||||
# Add our own theming (based on DeepOcean with a different "background" and "opencsg-face-back")
|
||||
COPY openscad/cadhubtheme.json /usr/share/openscad/color-schemes/render/
|
||||
COPY src/docker/openscad/cadhubtheme.json /usr/share/openscad-nightly/color-schemes/render/
|
||||
|
||||
COPY openscad/*.js /var/task/
|
||||
COPY common/*.js /var/common/
|
||||
COPY common/entrypoint.sh /entrypoint.sh
|
||||
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/openscad/* /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 [ "openscad.render" ]
|
||||
CMD [ "js/openscad.render" ]
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
const { runScad, stlExport } = require('./runScad')
|
||||
const middy = require('middy')
|
||||
const { cors } = require('middy/middlewares')
|
||||
const AWS = require('aws-sdk')
|
||||
const tk = require('timekeeper')
|
||||
const {
|
||||
makeHash,
|
||||
checkIfAlreadyExists,
|
||||
getObjectUrl,
|
||||
loggerWrap,
|
||||
storeAssetAndReturnUrl,
|
||||
} = require('../common/utils')
|
||||
|
||||
const s3 = new AWS.S3()
|
||||
|
||||
const openScadStlKey = (eventBody) => {
|
||||
const { file } = JSON.parse(eventBody)
|
||||
return `${makeHash(JSON.stringify(file))}.stl`
|
||||
}
|
||||
|
||||
const preview = async (req, _context, callback) => {
|
||||
_context.callbackWaitsForEmptyEventLoop = false
|
||||
const eventBody = req.body
|
||||
console.log('eventBody', eventBody)
|
||||
const key = `${makeHash(eventBody)}.png`
|
||||
const stlKey = openScadStlKey(eventBody)
|
||||
|
||||
console.log('key', key)
|
||||
|
||||
const stlParams = {
|
||||
Bucket: process.env.BUCKET,
|
||||
Key: stlKey,
|
||||
}
|
||||
|
||||
const params = {
|
||||
Bucket: process.env.BUCKET,
|
||||
Key: key,
|
||||
}
|
||||
const [previousAssetStl, previousAssetPng] = await Promise.all([
|
||||
checkIfAlreadyExists(stlParams, s3),
|
||||
checkIfAlreadyExists(params, s3),
|
||||
])
|
||||
const type = previousAssetStl.isAlreadyInBucket ? 'stl' : 'png'
|
||||
const previousAsset = previousAssetStl.isAlreadyInBucket
|
||||
? previousAssetStl
|
||||
: previousAssetPng
|
||||
if (previousAsset.isAlreadyInBucket) {
|
||||
console.log('already in bucket')
|
||||
const response = {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({
|
||||
url: getObjectUrl(
|
||||
{
|
||||
Bucket: process.env.BUCKET,
|
||||
Key: previousAssetStl.isAlreadyInBucket ? stlKey : key,
|
||||
},
|
||||
s3,
|
||||
tk
|
||||
),
|
||||
consoleMessage:
|
||||
previousAsset.consoleMessage || previousAssetPng.consoleMessage,
|
||||
type,
|
||||
}),
|
||||
}
|
||||
callback(null, response)
|
||||
return
|
||||
}
|
||||
|
||||
const { file, settings } = JSON.parse(eventBody)
|
||||
const { error, consoleMessage, fullPath } = await runScad({ file, settings })
|
||||
await storeAssetAndReturnUrl({
|
||||
error,
|
||||
callback,
|
||||
fullPath,
|
||||
consoleMessage,
|
||||
key,
|
||||
s3,
|
||||
params,
|
||||
tk,
|
||||
})
|
||||
}
|
||||
|
||||
const stl = async (req, _context, callback) => {
|
||||
_context.callbackWaitsForEmptyEventLoop = false
|
||||
const eventBody = req.body
|
||||
console.log(eventBody, 'eventBody')
|
||||
const stlKey = openScadStlKey(eventBody)
|
||||
|
||||
console.log('key', stlKey)
|
||||
|
||||
const params = {
|
||||
Bucket: process.env.BUCKET,
|
||||
Key: stlKey,
|
||||
}
|
||||
console.log('original params', params)
|
||||
const previousAsset = await checkIfAlreadyExists(params, s3)
|
||||
if (previousAsset.isAlreadyInBucket) {
|
||||
console.log('already in bucket')
|
||||
const response = {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({
|
||||
url: getObjectUrl({ ...params }, s3, tk),
|
||||
consoleMessage: previousAsset.consoleMessage,
|
||||
}),
|
||||
}
|
||||
callback(null, response)
|
||||
return
|
||||
}
|
||||
const { file } = JSON.parse(eventBody)
|
||||
const { error, consoleMessage, fullPath } = await stlExport({ file })
|
||||
await storeAssetAndReturnUrl({
|
||||
error,
|
||||
callback,
|
||||
fullPath,
|
||||
consoleMessage,
|
||||
key: stlKey,
|
||||
s3,
|
||||
params,
|
||||
tk,
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
stl: middy(stl).use(cors()),
|
||||
preview: middy(loggerWrap(preview)).use(cors()),
|
||||
}
|
||||
48
app/api/src/docker/openscad/openscad.ts
Normal file
48
app/api/src/docker/openscad/openscad.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { runScad, stlExport } from './runScad'
|
||||
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 runScad({
|
||||
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()),
|
||||
}
|
||||
1156
app/api/src/docker/openscad/package-lock.json
generated
1156
app/api/src/docker/openscad/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"name": "openscad-endpoint",
|
||||
"version": "0.0.1",
|
||||
"description": "endpoint for openscad",
|
||||
"main": "index.js",
|
||||
"author": "Kurt Hutten <kurt@kurthutten.com>",
|
||||
"license": "",
|
||||
"dependencies": {
|
||||
"aws-sdk": "^2.907.0",
|
||||
"cors": "^2.8.5",
|
||||
"middy": "^0.36.0",
|
||||
"nanoid": "^3.1.20",
|
||||
"timekeeper": "2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-lambda-ric": "^1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
const { makeFile, runCommand } = require('../common/utils')
|
||||
const { nanoid } = require('nanoid')
|
||||
|
||||
const OPENSCAD_COMMON = `xvfb-run --auto-servernum --server-args "-screen 0 1024x768x24" openscad`
|
||||
|
||||
/** Removes our generated/hash filename with just "main.scad", so that it's a nice message in the IDE */
|
||||
const cleanOpenScadError = (error) =>
|
||||
error.replace(/["|']\/tmp\/.+\/main.scad["|']/g, "'main.scad'")
|
||||
|
||||
module.exports.runScad = async ({
|
||||
file,
|
||||
settings: {
|
||||
size: { x = 500, y = 500 } = {},
|
||||
camera: {
|
||||
position = { x: 40, y: 40, z: 40 },
|
||||
rotation = { x: 55, y: 0, z: 25 },
|
||||
dist = 200,
|
||||
} = {},
|
||||
} = {}, // TODO add view settings
|
||||
} = {}) => {
|
||||
const tempFile = await makeFile(file, '.scad', nanoid)
|
||||
const { x: rx, y: ry, z: rz } = rotation
|
||||
const { x: px, y: py, z: pz } = position
|
||||
const cameraArg = `--camera=${px},${py},${pz},${rx},${ry},${rz},${dist}`
|
||||
const fullPath = `/tmp/${tempFile}/output.png`
|
||||
const command = [
|
||||
OPENSCAD_COMMON,
|
||||
`-o ${fullPath}`,
|
||||
cameraArg,
|
||||
`--imgsize=${x},${y}`,
|
||||
`--colorscheme CadHub`,
|
||||
`/tmp/${tempFile}/main.scad`,
|
||||
`&& gzip ${fullPath}`,
|
||||
].join(' ')
|
||||
console.log('command', command)
|
||||
|
||||
try {
|
||||
const consoleMessage = await runCommand(command, 15000)
|
||||
return { consoleMessage, fullPath }
|
||||
} catch (dirtyError) {
|
||||
const error = cleanOpenScadError(dirtyError)
|
||||
return { error }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.stlExport = async ({ file } = {}) => {
|
||||
const tempFile = await makeFile(file, '.scad', nanoid)
|
||||
const fullPath = `/tmp/${tempFile}/output.stl`
|
||||
const command = [
|
||||
OPENSCAD_COMMON,
|
||||
`--export-format=binstl`,
|
||||
`-o ${fullPath}`,
|
||||
`/tmp/${tempFile}/main.scad`,
|
||||
`&& gzip ${fullPath}`,
|
||||
].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)
|
||||
return { consoleMessage, fullPath }
|
||||
} catch (error) {
|
||||
return { error, fullPath }
|
||||
}
|
||||
}
|
||||
151
app/api/src/docker/openscad/runScad.ts
Normal file
151
app/api/src/docker/openscad/runScad.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { writeFiles, runCommand } from '../common/utils'
|
||||
import { nanoid } from 'nanoid'
|
||||
const { readFile } = require('fs/promises')
|
||||
|
||||
const OPENSCAD_COMMON = `xvfb-run --auto-servernum --server-args "-screen 0 1024x768x24" openscad-nightly`
|
||||
|
||||
/** Removes our generated/hash filename with just "main.scad", so that it's a nice message in the IDE */
|
||||
const cleanOpenScadError = (error) =>
|
||||
error.replace(/["|']\/tmp\/.+\/main.scad["|']/g, "'main.scad'")
|
||||
|
||||
export const runScad = async ({
|
||||
file,
|
||||
settings: {
|
||||
viewAll = false,
|
||||
size: { x = 500, y = 500 } = {},
|
||||
parameters,
|
||||
camera: {
|
||||
position = { x: 40, y: 40, z: 40 },
|
||||
rotation = { x: 55, y: 0, z: 25 },
|
||||
dist = 200,
|
||||
} = {},
|
||||
} = {}, // TODO add view settings
|
||||
} = {}): Promise<{
|
||||
error?: string
|
||||
consoleMessage?: string
|
||||
fullPath?: string
|
||||
customizerPath?: string
|
||||
tempFile?: string
|
||||
}> => {
|
||||
const tempFile = await writeFiles(
|
||||
[
|
||||
{ file, fileName: 'main.scad' },
|
||||
{
|
||||
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 { x: rx, y: ry, z: rz } = rotation
|
||||
const { x: px, y: py, z: pz } = position
|
||||
const cameraArg = `--camera=${px},${py},${pz},${rx},${ry},${rz},${dist}`
|
||||
const fullPath = `/tmp/${tempFile}/output.gz`
|
||||
const imPath = `/tmp/${tempFile}/output.png`
|
||||
const customizerPath = `/tmp/${tempFile}/customizer.param`
|
||||
const summaryPath = `/tmp/${tempFile}/summary.json` // contains camera info
|
||||
const command = [
|
||||
OPENSCAD_COMMON,
|
||||
`-o ${customizerPath}`,
|
||||
`-o ${imPath}`,
|
||||
`--summary camera --summary-file ${summaryPath}`,
|
||||
viewAll ? '--viewall' : '',
|
||||
`-p /tmp/${tempFile}/params.json -P default`,
|
||||
cameraArg,
|
||||
`--imgsize=${x},${y}`,
|
||||
`--colorscheme CadHub`,
|
||||
`/tmp/${tempFile}/main.scad`,
|
||||
].join(' ')
|
||||
console.log('command', command)
|
||||
|
||||
try {
|
||||
const consoleMessage = await runCommand(command, 15000)
|
||||
const files: string[] = await Promise.all(
|
||||
[customizerPath, summaryPath].map((path) =>
|
||||
readFile(path, { encoding: 'ascii' })
|
||||
)
|
||||
)
|
||||
const [params, cameraInfo] = files.map((fileStr: string) =>
|
||||
JSON.parse(fileStr)
|
||||
)
|
||||
await writeFiles(
|
||||
[
|
||||
{
|
||||
file: JSON.stringify({
|
||||
cameraInfo: viewAll ? cameraInfo.camera : undefined,
|
||||
customizerParams: params.parameters,
|
||||
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: cleanOpenScadError(dirtyError) }
|
||||
}
|
||||
}
|
||||
|
||||
export const stlExport = async ({ file, settings: { parameters } } = {}) => {
|
||||
const tempFile = await writeFiles(
|
||||
[
|
||||
{ file, fileName: 'main.scad' },
|
||||
{
|
||||
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 customizerPath = `/tmp/${tempFile}/customizer.param`
|
||||
const command = [
|
||||
OPENSCAD_COMMON,
|
||||
// `--export-format=binstl`,
|
||||
`-o ${customizerPath}`,
|
||||
`-o ${stlPath}`,
|
||||
`-p /tmp/${tempFile}/params.json -P default`,
|
||||
`/tmp/${tempFile}/main.scad`,
|
||||
].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)
|
||||
const params = JSON.parse(
|
||||
await readFile(customizerPath, { encoding: 'ascii' })
|
||||
).parameters
|
||||
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
|
||||
)
|
||||
return { consoleMessage, fullPath, customizerPath, tempFile }
|
||||
} catch (error) {
|
||||
return { error, fullPath }
|
||||
}
|
||||
}
|
||||
1799
app/api/src/docker/package-lock.json
generated
1799
app/api/src/docker/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "aws-emulator",
|
||||
"version": "1.0.0",
|
||||
"description": "thin layer so that we can use docker lambdas locally",
|
||||
"scripts": {
|
||||
"lambdas": "docker-compose up --build",
|
||||
"emulate": "nodemon ./aws-emulator.js",
|
||||
"watch": "concurrently \"yarn lambdas\" \"yarn emulate\""
|
||||
},
|
||||
"main": "aws-emulator.js",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^6.0.0",
|
||||
"nodemon": "^2.0.7"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@ service: cad-lambdas
|
||||
#app: your-app-name
|
||||
#org: your-org-name
|
||||
|
||||
# plugins:
|
||||
plugins:
|
||||
- serverless-binary-cors
|
||||
# - serverless-offline
|
||||
|
||||
# You can pin your service to only deploy with a specific Serverless version
|
||||
@@ -17,13 +18,21 @@ provider:
|
||||
images:
|
||||
# this image is built locally and push to ECR
|
||||
openscadimage:
|
||||
path: ./
|
||||
file: ./openscad/Dockerfile
|
||||
path: ../../
|
||||
file: ./src/docker/openscad/Dockerfile
|
||||
cadqueryimage:
|
||||
path: ./
|
||||
file: ./cadquery/Dockerfile
|
||||
path: ../../
|
||||
file: ./src/docker/cadquery/Dockerfile
|
||||
curvimage:
|
||||
path: ../../
|
||||
file: ./src/docker/curv/Dockerfile
|
||||
apiGateway:
|
||||
metrics: true
|
||||
binaryMediaTypes:
|
||||
# we need to allow binary types to be able to send back images and stls, but it would be better to be more specific
|
||||
# ie image/png etc. as */* treats everything as binary including the json body as the input the lambdas
|
||||
# which mean we need to decode the input bode from base64, but the images break with anything other than */* :(
|
||||
- '*/*'
|
||||
|
||||
# you can overwrite defaults here
|
||||
# stage: dev
|
||||
@@ -52,7 +61,7 @@ functions:
|
||||
image:
|
||||
name: openscadimage
|
||||
command:
|
||||
- openscad.preview
|
||||
- js/openscad.preview
|
||||
entryPoint:
|
||||
- '/entrypoint.sh'
|
||||
events:
|
||||
@@ -61,13 +70,14 @@ functions:
|
||||
method: post
|
||||
cors: true
|
||||
timeout: 25
|
||||
memorySize: 2048
|
||||
environment:
|
||||
BUCKET: cad-preview-bucket-prod-001
|
||||
openscadstl:
|
||||
image:
|
||||
name: openscadimage
|
||||
command:
|
||||
- openscad.stl
|
||||
- js/openscad.stl
|
||||
entryPoint:
|
||||
- '/entrypoint.sh'
|
||||
events:
|
||||
@@ -78,11 +88,12 @@ functions:
|
||||
timeout: 30
|
||||
environment:
|
||||
BUCKET: cad-preview-bucket-prod-001
|
||||
|
||||
cadquerystl:
|
||||
image:
|
||||
name: cadqueryimage
|
||||
command:
|
||||
- cadquery.stl
|
||||
- js/cadquery.stl
|
||||
entryPoint:
|
||||
- '/entrypoint.sh'
|
||||
events:
|
||||
@@ -93,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
|
||||
|
||||
40
app/api/src/functions/check-user-name/check-user-name.ts
Normal file
40
app/api/src/functions/check-user-name/check-user-name.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { APIGatewayEvent /*, Context*/ } from 'aws-lambda'
|
||||
import { logger } from 'src/lib/logger'
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
/**
|
||||
* The handler function is your code that processes http request events.
|
||||
* You can use return and throw to send a response or error, respectively.
|
||||
*
|
||||
* Important: When deployed, a custom serverless function is an open API endpoint and
|
||||
* is your responsibility to secure appropriately.
|
||||
*
|
||||
* @see {@link https://redwoodjs.com/docs/serverless-functions#security-considerations|Serverless Function Considerations}
|
||||
* in the RedwoodJS documentation for more information.
|
||||
*
|
||||
* @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent
|
||||
* @typedef { import('aws-lambda').Context } Context
|
||||
* @param { APIGatewayEvent } event - an object which contains information from the invoker.
|
||||
* @param { Context } context - contains information about the invocation,
|
||||
* function, and execution environment.
|
||||
*/
|
||||
export const handler = async (event: APIGatewayEvent /*context: Context*/) => {
|
||||
logger.info('Invoked checkUserName function')
|
||||
const userName = event.queryStringParameters.username
|
||||
let isUserNameAvailable = false
|
||||
try {
|
||||
const user = await db.user.findUnique({ where: { userName } })
|
||||
isUserNameAvailable = !user
|
||||
} catch (error) {
|
||||
isUserNameAvailable = false
|
||||
}
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
isUserNameAvailable,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,22 @@
|
||||
import {
|
||||
createGraphQLHandler,
|
||||
makeMergedSchema,
|
||||
makeServices,
|
||||
} from '@redwoodjs/api'
|
||||
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
|
||||
import { createSentryApolloPlugin } from 'src/lib/sentry'
|
||||
import { logger } from 'src/lib/logger'
|
||||
|
||||
import schemas from 'src/graphql/**/*.{js,ts}'
|
||||
import directives from 'src/directives/**/*.{js,ts}'
|
||||
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
|
||||
import services from 'src/services/**/*.{js,ts}'
|
||||
|
||||
import { getCurrentUser } from 'src/lib/auth'
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
export const handler = createGraphQLHandler({
|
||||
loggerConfig: { logger, options: {} },
|
||||
getCurrentUser,
|
||||
schema: makeMergedSchema({
|
||||
schemas,
|
||||
services: makeServices({ services }),
|
||||
}),
|
||||
directives,
|
||||
sdls,
|
||||
services,
|
||||
plugins: [createSentryApolloPlugin()],
|
||||
|
||||
onException: () => {
|
||||
// Disconnect from your database with an unhandled exception.
|
||||
db.$disconnect()
|
||||
@@ -96,7 +96,7 @@ const unWrappedHandler = async (req, _context) => {
|
||||
I started CadHub because I wanted a community hub for people who like CodeCAD as much of I do, you should know that the development of CadHub is very much a community effort as well and if you want get involved the discord is the best place to start https://discord.gg/SD7zFRNjGH.
|
||||
Long term I hope that CadHub will help push CodeCad as a paradigm forward, as there are clear benefits such as: CI/CD for parts, GIT based workflow and CodeCAD parts are normally much more robust to changes to parametric variables because the author can add logic to accommodate big changes where as GUI-CAD usually relies on blackbox heuristics and is more brittle. Sorry I'm getting into the weeds, if you want to read more on the paradigm see our blog https://learn.cadhub.xyz/.
|
||||
|
||||
One very easy way to help out is to simply add any OpenSCAD or CadQuery models you have to the website, building out the library of parts atm is very important.
|
||||
One very easy way to help out is to give the repo a star (https://github.com/Irev-Dev/cadhub), or simply add any OpenSCAD or CadQuery models you have to the website, building out the library of parts atm is very important.
|
||||
|
||||
Hit me up anytime for questions or concerns.
|
||||
Cheers,
|
||||
|
||||
@@ -11,9 +11,10 @@ export const schema = gql`
|
||||
}
|
||||
|
||||
type Query {
|
||||
projectReactions: [ProjectReaction!]!
|
||||
projectReaction(id: String!): ProjectReaction
|
||||
projectReactions: [ProjectReaction!]! @skipAuth
|
||||
projectReaction(id: String!): ProjectReaction @skipAuth
|
||||
projectReactionsByProjectId(projectId: String!): [ProjectReaction!]!
|
||||
@skipAuth
|
||||
}
|
||||
|
||||
input ToggleProjectReactionInput {
|
||||
@@ -30,10 +31,11 @@ export const schema = gql`
|
||||
|
||||
type Mutation {
|
||||
toggleProjectReaction(input: ToggleProjectReactionInput!): ProjectReaction!
|
||||
@requireAuth
|
||||
updateProjectReaction(
|
||||
id: String!
|
||||
input: UpdateProjectReactionInput!
|
||||
): ProjectReaction!
|
||||
deleteProjectReaction(id: String!): ProjectReaction!
|
||||
): ProjectReaction! @requireAuth
|
||||
deleteProjectReaction(id: String!): ProjectReaction! @requireAuth
|
||||
}
|
||||
`
|
||||
|
||||
@@ -11,8 +11,8 @@ export const schema = gql`
|
||||
}
|
||||
|
||||
type Query {
|
||||
comments: [Comment!]!
|
||||
comment(id: String!): Comment
|
||||
comments: [Comment!]! @skipAuth
|
||||
comment(id: String!): Comment @skipAuth
|
||||
}
|
||||
|
||||
input CreateCommentInput {
|
||||
@@ -28,8 +28,9 @@ export const schema = gql`
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createComment(input: CreateCommentInput!): Comment!
|
||||
createComment(input: CreateCommentInput!): Comment! @requireAuth
|
||||
updateComment(id: String!, input: UpdateCommentInput!): Comment!
|
||||
deleteComment(id: String!): Comment!
|
||||
@requireAuth
|
||||
deleteComment(id: String!): Comment! @requireAuth
|
||||
}
|
||||
`
|
||||
|
||||
@@ -15,6 +15,6 @@ export const schema = gql`
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
sendAllUsersEmail(input: Email!): EmailResponse!
|
||||
sendAllUsersEmail(input: Email!): EmailResponse! @requireAuth
|
||||
}
|
||||
`
|
||||
|
||||
@@ -14,17 +14,24 @@ export const schema = gql`
|
||||
socialCard: SocialCard
|
||||
Comment: [Comment]!
|
||||
Reaction(userId: String): [ProjectReaction]!
|
||||
forkedFromId: String
|
||||
forkedFrom: Project
|
||||
childForks: [Project]!
|
||||
}
|
||||
|
||||
# should match enum in api/db/schema.prisma
|
||||
enum CadPackage {
|
||||
openscad
|
||||
cadquery
|
||||
jscad
|
||||
curv
|
||||
}
|
||||
|
||||
type Query {
|
||||
projects(userName: String): [Project!]!
|
||||
project(id: String!): Project
|
||||
projects(userName: String): [Project!]! @skipAuth
|
||||
project(id: String!): Project @skipAuth
|
||||
projectByUserAndTitle(userName: String!, projectTitle: String!): Project
|
||||
@skipAuth
|
||||
}
|
||||
|
||||
input CreateProjectInput {
|
||||
@@ -36,6 +43,12 @@ export const schema = gql`
|
||||
cadPackage: CadPackage!
|
||||
}
|
||||
|
||||
input ForkProjectInput {
|
||||
userId: String!
|
||||
forkedFromId: String
|
||||
code: String
|
||||
}
|
||||
|
||||
input UpdateProjectInput {
|
||||
title: String
|
||||
description: String
|
||||
@@ -45,14 +58,15 @@ export const schema = gql`
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createProject(input: CreateProjectInput!): Project!
|
||||
forkProject(input: CreateProjectInput!): Project!
|
||||
createProject(input: CreateProjectInput!): Project! @requireAuth
|
||||
forkProject(input: ForkProjectInput!): Project! @requireAuth
|
||||
updateProject(id: String!, input: UpdateProjectInput!): Project!
|
||||
@requireAuth
|
||||
updateProjectImages(
|
||||
id: String!
|
||||
mainImage64: String
|
||||
socialCard64: String
|
||||
): Project!
|
||||
deleteProject(id: String!): Project!
|
||||
): Project! @requireAuth
|
||||
deleteProject(id: String!): Project! @requireAuth
|
||||
}
|
||||
`
|
||||
|
||||
@@ -10,7 +10,7 @@ export const schema = gql`
|
||||
}
|
||||
|
||||
type Query {
|
||||
socialCards: [SocialCard!]!
|
||||
socialCard(id: String!): SocialCard
|
||||
socialCards: [SocialCard!]! @skipAuth
|
||||
socialCard(id: String!): SocialCard @skipAuth
|
||||
}
|
||||
`
|
||||
|
||||
@@ -10,8 +10,8 @@ export const schema = gql`
|
||||
}
|
||||
|
||||
type Query {
|
||||
subjectAccessRequests: [SubjectAccessRequest!]!
|
||||
subjectAccessRequest(id: String!): SubjectAccessRequest
|
||||
subjectAccessRequests: [SubjectAccessRequest!]! @requireAuth
|
||||
subjectAccessRequest(id: String!): SubjectAccessRequest @requireAuth
|
||||
}
|
||||
|
||||
input CreateSubjectAccessRequestInput {
|
||||
@@ -29,11 +29,11 @@ export const schema = gql`
|
||||
type Mutation {
|
||||
createSubjectAccessRequest(
|
||||
input: CreateSubjectAccessRequestInput!
|
||||
): SubjectAccessRequest!
|
||||
): SubjectAccessRequest! @requireAuth
|
||||
updateSubjectAccessRequest(
|
||||
id: String!
|
||||
input: UpdateSubjectAccessRequestInput!
|
||||
): SubjectAccessRequest!
|
||||
deleteSubjectAccessRequest(id: String!): SubjectAccessRequest!
|
||||
): SubjectAccessRequest! @requireAuth
|
||||
deleteSubjectAccessRequest(id: String!): SubjectAccessRequest! @requireAuth
|
||||
}
|
||||
`
|
||||
|
||||
@@ -16,9 +16,9 @@ export const schema = gql`
|
||||
}
|
||||
|
||||
type Query {
|
||||
users: [User!]!
|
||||
user(id: String!): User
|
||||
userName(userName: String!): User
|
||||
users: [User!]! @requireAuth
|
||||
user(id: String!): User @skipAuth
|
||||
userName(userName: String!): User @skipAuth
|
||||
}
|
||||
|
||||
input CreateUserInput {
|
||||
@@ -38,9 +38,10 @@ export const schema = gql`
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createUser(input: CreateUserInput!): User!
|
||||
updateUser(id: String!, input: UpdateUserInput!): User!
|
||||
createUser(input: CreateUserInput!): User! @requireAuth
|
||||
updateUser(id: String!, input: UpdateUserInput!): User! @requireAuth
|
||||
updateUserByUserName(userName: String!, input: UpdateUserInput!): User!
|
||||
deleteUser(id: String!): User!
|
||||
@requireAuth
|
||||
deleteUser(id: String!): User! @requireAuth
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,61 +1,5 @@
|
||||
// Define what you want `currentUser` to return throughout your app. For example,
|
||||
// to return a real user from your database, you could do something like:
|
||||
//
|
||||
// export const getCurrentUser = async ({ email }) => {
|
||||
// return await db.user.findUnique({ where: { email } })
|
||||
// }
|
||||
//
|
||||
// If you want to enforce role-based access ...
|
||||
//
|
||||
// You'll need to set the currentUser's roles attributes to the
|
||||
// collection of roles as defined by your app.
|
||||
//
|
||||
// This allows requireAuth() on the api side and hasRole() in the useAuth() hook on the web side
|
||||
// to check if the user is assigned a given role or not.
|
||||
//
|
||||
// How you set the currentUser's roles depends on your auth provider and its implementation.
|
||||
//
|
||||
// For example, your decoded JWT may store `roles` in it namespaced `app_metadata`:
|
||||
//
|
||||
// {
|
||||
// 'https://example.com/app_metadata': { authorization: { roles: ['admin'] } },
|
||||
// 'https://example.com/user_metadata': {},
|
||||
// iss: 'https://app.us.auth0.com/',
|
||||
// sub: 'email|1234',
|
||||
// aud: [
|
||||
// 'https://example.com',
|
||||
// 'https://app.us.auth0.com/userinfo'
|
||||
// ],
|
||||
// iat: 1596481520,
|
||||
// exp: 1596567920,
|
||||
// azp: '1l0w6JXXXXL880T',
|
||||
// scope: 'openid profile email'
|
||||
// }
|
||||
//
|
||||
// The parseJWT utility will extract the roles from decoded token.
|
||||
//
|
||||
// The app_medata claim may or may not be namespaced based on the auth provider.
|
||||
// Note: Auth0 requires namespacing custom JWT claims
|
||||
//
|
||||
// Some providers, such as with Auth0, will set roles an authorization
|
||||
// attribute in app_metadata (namespaced or not):
|
||||
//
|
||||
// 'app_metadata': { authorization: { roles: ['publisher'] } }
|
||||
// 'https://example.com/app_metadata': { authorization: { roles: ['publisher'] } }
|
||||
//
|
||||
// Other providers may include roles simply within app_metadata:
|
||||
//
|
||||
// 'app_metadata': { roles: ['author'] }
|
||||
// 'https://example.com/app_metadata': { roles: ['author'] }
|
||||
//
|
||||
// And yet other may define roles as a custom claim at the root of the decoded token:
|
||||
//
|
||||
// roles: ['admin']
|
||||
//
|
||||
// The function `getCurrentUser` should return the user information
|
||||
// together with a collection of roles to check for role assignment:
|
||||
|
||||
import { AuthenticationError, ForbiddenError, parseJWT } from '@redwoodjs/api'
|
||||
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'
|
||||
import { parseJWT } from '@redwoodjs/api'
|
||||
|
||||
/**
|
||||
* Use requireAuth in your services to check that a user is logged in,
|
||||
@@ -97,8 +41,24 @@ import { AuthenticationError, ForbiddenError, parseJWT } from '@redwoodjs/api'
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export const getCurrentUser = async (decoded, { _token, _type }) => {
|
||||
return { ...decoded, roles: parseJWT({ decoded }).roles }
|
||||
export const getCurrentUser = async (
|
||||
decoded,
|
||||
{ _token, _type },
|
||||
{ _event, _context }
|
||||
) => {
|
||||
if (!decoded) {
|
||||
// if no decoded, then never set currentUser
|
||||
return null
|
||||
}
|
||||
|
||||
const { roles } = parseJWT({ decoded }) // extract and check roles separately
|
||||
|
||||
if (roles) {
|
||||
return { ...decoded, roles }
|
||||
}
|
||||
|
||||
return { ...decoded } // only return when certain you have
|
||||
// the currentUser properties
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AuthenticationError, ForbiddenError } from '@redwoodjs/api'
|
||||
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'
|
||||
import type { Project } from '@prisma/client'
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Config, ApolloError } from '@redwoodjs/graphql-server'
|
||||
import * as Sentry from '@sentry/node'
|
||||
import { context, Config, ApolloError } from '@redwoodjs/api'
|
||||
|
||||
let sentryInitialized = false
|
||||
if (process.env.SENTRY_DSN && !sentryInitialized) {
|
||||
|
||||
@@ -39,7 +39,7 @@ export const generateUniqueString = async (
|
||||
}
|
||||
|
||||
export const generateUniqueStringWithoutSeed = async (
|
||||
isUniqueCallback: (seed: string) => Promise<any>,
|
||||
isUniqueCallback: (seed: string) => Promise<boolean>,
|
||||
count = 0
|
||||
) => {
|
||||
const seed = humanId({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UserInputError } from '@redwoodjs/api'
|
||||
import { UserInputError } from '@redwoodjs/graphql-server'
|
||||
|
||||
import { requireAuth } from 'src/lib/auth'
|
||||
import { requireOwnership } from 'src/lib/owner'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ResolverArgs } from '@redwoodjs/graphql-server'
|
||||
import type { Prisma, Project as ProjectType } from '@prisma/client'
|
||||
import type { ResolverArgs } from '@redwoodjs/api'
|
||||
import { uploadImage, makeSocialPublicIdServer } from 'src/lib/cloudinary'
|
||||
|
||||
import { db } from 'src/lib/db'
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
} from 'src/services/helpers'
|
||||
import { requireAuth } from 'src/lib/auth'
|
||||
import { requireOwnership, requireProjectOwnership } from 'src/lib/owner'
|
||||
import { socialCard } from '../socialCards/socialCards'
|
||||
import { sendDiscordMessage } from 'src/lib/discord'
|
||||
|
||||
|
||||
export const projects = ({ userName }) => {
|
||||
if (!userName) {
|
||||
@@ -48,15 +49,17 @@ export const projectByUserAndTitle = async ({ userName, projectTitle }) => {
|
||||
},
|
||||
})
|
||||
}
|
||||
const isUniqueProjectTitle = (userId: string) => async (seed: string) =>
|
||||
db.project.findUnique({
|
||||
where: {
|
||||
title_userId: {
|
||||
title: seed,
|
||||
userId,
|
||||
const isUniqueProjectTitle =
|
||||
(userId: string) =>
|
||||
async (seed: string): Promise<boolean> =>
|
||||
!!(await db.project.findUnique({
|
||||
where: {
|
||||
title_userId: {
|
||||
title: seed,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
|
||||
interface CreateProjectArgs {
|
||||
input: Prisma.ProjectCreateArgs['data']
|
||||
@@ -79,13 +82,27 @@ export const createProject = async ({ input }: CreateProjectArgs) => {
|
||||
}
|
||||
|
||||
export const forkProject = async ({ input }) => {
|
||||
// Only difference between create and fork project is that fork project will generate a unique title
|
||||
// (for the user) if there is a conflict
|
||||
requireAuth()
|
||||
const projectData = await db.project.findUnique({
|
||||
where: {
|
||||
id: input.forkedFromId,
|
||||
},
|
||||
})
|
||||
const isUniqueCallback = isUniqueProjectTitle(input.userId)
|
||||
const title = await generateUniqueString(input.title, isUniqueCallback)
|
||||
// TODO change the description to `forked from userName/projectName ${rest of description}`
|
||||
let title = projectData.title
|
||||
|
||||
title = await generateUniqueString(title, isUniqueCallback)
|
||||
|
||||
const { code, description, cadPackage } = projectData
|
||||
|
||||
return db.project.create({
|
||||
data: foreignKeyReplacement({ ...input, title }),
|
||||
data: foreignKeyReplacement({
|
||||
...input,
|
||||
title,
|
||||
code: input.code || code,
|
||||
description,
|
||||
cadPackage,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -103,18 +120,19 @@ export const updateProject = async ({ id, input }: UpdateProjectArgs) => {
|
||||
const descriptionChange =
|
||||
input.description && input.description !== oldProject.description
|
||||
if (titleChange || descriptionChange) {
|
||||
const socialCard = await db.socialCard.findUnique({where: {projectId}})
|
||||
if (socialCard) {
|
||||
return db.socialCard.update({
|
||||
data: { outOfDate: true },
|
||||
where: { id: socialCard.id },
|
||||
})
|
||||
}
|
||||
const socialCard = await db.socialCard.findUnique({
|
||||
where: { projectId },
|
||||
})
|
||||
if (socialCard) {
|
||||
return db.socialCard.update({
|
||||
data: { outOfDate: true },
|
||||
where: { id: socialCard.id },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
requireAuth()
|
||||
const originalProject = await requireProjectOwnership({ projectId: id })
|
||||
console.log('yooooo', originalProject)
|
||||
if (input.title) {
|
||||
input.title = enforceAlphaNumeric(input.title)
|
||||
}
|
||||
@@ -227,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
|
||||
}
|
||||
|
||||
@@ -242,17 +272,32 @@ export const updateProjectImages = async ({
|
||||
export const deleteProject = async ({ id }: Prisma.ProjectWhereUniqueInput) => {
|
||||
requireAuth()
|
||||
await requireOwnership({ projectId: id })
|
||||
return db.project.update({
|
||||
data: {
|
||||
deleted: true,
|
||||
},
|
||||
const project = await db.project.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
const childrenDeletePromises = [
|
||||
db.comment.deleteMany({ where: { projectId: project.id } }),
|
||||
db.projectReaction.deleteMany({ where: { projectId: project.id } }),
|
||||
db.socialCard.deleteMany({ where: { projectId: project.id } }),
|
||||
]
|
||||
await Promise.all(childrenDeletePromises)
|
||||
await db.project.delete({
|
||||
where: { id },
|
||||
})
|
||||
return project
|
||||
}
|
||||
|
||||
export const Project = {
|
||||
forkedFrom: (_obj, { root }) =>
|
||||
root.forkedFromId &&
|
||||
db.project.findUnique({ where: { id: root.forkedFromId } }),
|
||||
childForks: (_obj, { root }) => {
|
||||
console.log(' ')
|
||||
return []
|
||||
},
|
||||
// db.project.findMany({ where: { forkedFromId: root.id } }),
|
||||
user: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
||||
db.project.findUnique({ where: { id: root.id } }).user(),
|
||||
db.user.findUnique({ where: { id: root.userId } }),
|
||||
socialCard: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
||||
db.project.findUnique({ where: { id: root.id } }).socialCard(),
|
||||
Comment: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ResolverArgs, BeforeResolverSpecType } from '@redwoodjs/graphql-server'
|
||||
import type { Prisma } from '@prisma/client'
|
||||
import type { ResolverArgs, BeforeResolverSpecType } from '@redwoodjs/api'
|
||||
|
||||
import { db } from 'src/lib/db'
|
||||
import { requireAuth } from 'src/lib/auth'
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
import { UserInputError, ForbiddenError } from '@redwoodjs/graphql-server'
|
||||
import { db } from 'src/lib/db'
|
||||
import { requireAuth } from 'src/lib/auth'
|
||||
import { requireOwnership } from 'src/lib/owner'
|
||||
import { UserInputError } from '@redwoodjs/api'
|
||||
import { enforceAlphaNumeric, destroyImage } from 'src/services/helpers'
|
||||
import type { Prisma } from '@prisma/client'
|
||||
|
||||
function userNameVerification(userName: string): string {
|
||||
if (userName.length < 5) {
|
||||
throw new ForbiddenError('userName too short')
|
||||
}
|
||||
if (userName && ['new', 'edit', 'update'].includes(userName)) {
|
||||
//TODO complete this and use a regexp so that it's not case sensitive, don't want someone with the userName eDiT
|
||||
throw new UserInputError(
|
||||
`You've tried to used a protected word as you userName, try something other than `
|
||||
)
|
||||
}
|
||||
if (userName) {
|
||||
return enforceAlphaNumeric(userName)
|
||||
}
|
||||
}
|
||||
|
||||
function nameVerification(name: string) {
|
||||
if (typeof name === 'string' && name.length < 3) {
|
||||
throw new ForbiddenError('name too short')
|
||||
}
|
||||
}
|
||||
|
||||
export const users = () => {
|
||||
requireAuth({ role: 'admin' })
|
||||
@@ -25,32 +47,51 @@ export const createUser = ({ input }) => {
|
||||
requireAuth({ role: 'admin' })
|
||||
createUserInsecure({ input })
|
||||
}
|
||||
export const createUserInsecure = ({ input }) => {
|
||||
export const createUserInsecure = ({
|
||||
input,
|
||||
}: {
|
||||
input: Prisma.UserUncheckedCreateInput
|
||||
}) => {
|
||||
if (typeof input.userName === 'string') {
|
||||
input.userName = userNameVerification(input.userName)
|
||||
}
|
||||
nameVerification(input.name)
|
||||
return db.user.create({
|
||||
data: input,
|
||||
})
|
||||
}
|
||||
|
||||
export const updateUser = ({ id, input }) => {
|
||||
export const updateUser = ({
|
||||
id,
|
||||
input,
|
||||
}: {
|
||||
id: string
|
||||
input: Prisma.UserUncheckedCreateInput
|
||||
}) => {
|
||||
requireAuth()
|
||||
if (typeof input.userName === 'string') {
|
||||
input.userName = userNameVerification(input.userName)
|
||||
}
|
||||
nameVerification(input.name)
|
||||
return db.user.update({
|
||||
data: input,
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const updateUserByUserName = async ({ userName, input }) => {
|
||||
export const updateUserByUserName = async ({
|
||||
userName,
|
||||
input,
|
||||
}: {
|
||||
userName: string
|
||||
input: Prisma.UserUncheckedCreateInput
|
||||
}) => {
|
||||
requireAuth()
|
||||
await requireOwnership({ userName })
|
||||
if (input.userName) {
|
||||
input.userName = enforceAlphaNumeric(input.userName)
|
||||
}
|
||||
if (input.userName && ['new', 'edit', 'update'].includes(input.userName)) {
|
||||
//TODO complete this and use a regexp so that it's not case sensitive, don't want someone with the userName eDiT
|
||||
throw new UserInputError(
|
||||
`You've tried to used a protected word as you userName, try something other than `
|
||||
)
|
||||
if (typeof input.userName === 'string') {
|
||||
input.userName = userNameVerification(input.userName)
|
||||
}
|
||||
nameVerification(input.name)
|
||||
const originalProject = await db.user.findUnique({ where: { userName } })
|
||||
const imageToDestroy =
|
||||
originalProject.image !== input.image && originalProject.image
|
||||
|
||||
@@ -6,9 +6,13 @@
|
||||
"web"
|
||||
]
|
||||
},
|
||||
"scripts": {},
|
||||
"scripts": {
|
||||
"cad": "yarn rw build api && docker-compose --file ./api/src/docker/docker-compose.yml up --build",
|
||||
"cad-r": "yarn rw build api && docker-compose --file ./api/src/docker/docker-compose.yml restart",
|
||||
"aws-emulate": "nodemon ./api/src/docker/aws-emulator.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redwoodjs/core": "^0.34.1"
|
||||
"@redwoodjs/core": "^0.38.1"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@redwoodjs/eslint-config",
|
||||
@@ -26,7 +30,9 @@
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"yarn": ">=1.15"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "yarn rw exec seed"
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
[web]
|
||||
port = 8910
|
||||
apiProxyPath = "/.netlify/functions"
|
||||
apiUrl = "/.netlify/functions"
|
||||
includeEnvironmentVariables = [
|
||||
'GOOGLE_ANALYTICS_ID',
|
||||
'CLOUDINARY_API_KEY',
|
||||
@@ -17,7 +17,7 @@
|
||||
'SENTRY_AUTH_TOKEN',
|
||||
'SENTRY_ORG',
|
||||
'SENTRY_PROJECT',
|
||||
'EMAIL_PASSWORD'
|
||||
'EMAIL_PASSWORD',
|
||||
]
|
||||
# experimentalFastRefresh = true # this seems to break cascadeStudio
|
||||
[api]
|
||||
|
||||
235
app/scripts/seed.ts
Normal file
235
app/scripts/seed.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import type { Prisma } from '@prisma/client'
|
||||
|
||||
import { db } from '$api/src/lib/db'
|
||||
|
||||
export default async () => {
|
||||
try {
|
||||
const users = [
|
||||
{
|
||||
id: "a2b21ce1-ae57-43a2-b6a3-b6e542fd9e60",
|
||||
userName: "local-user-1",
|
||||
name: "local 1",
|
||||
email: "localUser1@kurthutten.com"
|
||||
},
|
||||
{
|
||||
id: "682ba807-d10e-4caf-bf28-74054e46c9ec",
|
||||
userName: "local-user-2",
|
||||
name: "local 2",
|
||||
email: "localUser2@kurthutten.com"
|
||||
},
|
||||
{
|
||||
id: "5cea3906-1e8e-4673-8f0d-89e6a963c096",
|
||||
userName: "local-admin-2",
|
||||
name: "local admin",
|
||||
email: "localAdmin@kurthutten.com"
|
||||
},
|
||||
]
|
||||
|
||||
let existing
|
||||
existing = await db.user.findMany({ where: { id: users[0].id }})
|
||||
if(!existing.length) {
|
||||
await db.user.create({
|
||||
data: users[0],
|
||||
})
|
||||
}
|
||||
existing = await db.user.findMany({ where: { id: users[1].id }})
|
||||
if(!existing.length) {
|
||||
await db.user.create({
|
||||
data: users[1],
|
||||
})
|
||||
}
|
||||
|
||||
const projects = [
|
||||
{
|
||||
title: 'demo-project1',
|
||||
description: '# can be markdown',
|
||||
mainImage: 'CadHub/kjdlgjnu0xmwksia7xox',
|
||||
code: getOpenScadHingeCode(),
|
||||
cadPackage: 'openscad',
|
||||
user: {
|
||||
connect: {
|
||||
id: users[0].id,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'demo-project2',
|
||||
description: '## [hey](www.google.com)',
|
||||
user: {
|
||||
connect: {
|
||||
id: users[1].id,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
existing = await db.project.findMany({where: { title: projects[0].title}})
|
||||
if(!existing.length) {
|
||||
await db.project.create({
|
||||
data: projects[0],
|
||||
})
|
||||
}
|
||||
existing = await db.project.findMany({where: { title: projects[1].title}})
|
||||
if(!existing.length) {
|
||||
const result = await db.project.create({
|
||||
data: projects[1],
|
||||
})
|
||||
|
||||
await db.project.create({
|
||||
data: {
|
||||
...projects[1],
|
||||
title: `${projects[1].title}-fork`,
|
||||
forkedFrom: {
|
||||
connect: {
|
||||
id: result.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
const aProject = await db.project.findUnique({where: {
|
||||
title_userId: {
|
||||
title: projects[0].title,
|
||||
userId: users[0].id,
|
||||
}
|
||||
}})
|
||||
await db.comment.create({
|
||||
data: {
|
||||
text: "nice project, I like it",
|
||||
userId: users[0].id,
|
||||
projectId: aProject.id,
|
||||
// user: {connect: { id: users[0].id}},
|
||||
// project: {connect: { id: aProject.id}},
|
||||
}
|
||||
})
|
||||
await db.projectReaction.create({
|
||||
data: {
|
||||
emote: "❤️",
|
||||
userId: users[0].id,
|
||||
projectId: aProject.id,
|
||||
// user: {connect: { id: users[0].id}},
|
||||
// project: {connect: { id: aProject.id}},
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Please define your seed data.')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getOpenScadHingeCode () {
|
||||
return `
|
||||
baseWidth=15; // [0.1:0.1:50]
|
||||
|
||||
hingeLength=30; // [0.1:0.1:50]
|
||||
|
||||
// Hole mant mounting holes per half.
|
||||
mountingHoleCount=3; // [1:20]
|
||||
|
||||
baseThickness=3; // [0.1:0.1:20]
|
||||
|
||||
pivotRadius=5; // [0.1:0.1:20]
|
||||
|
||||
// Pin that the hinge pivots on.
|
||||
pinRadius=2; // [0.1:0.1:20]
|
||||
|
||||
mountingHoleRadius=1.5; // [0.1:0.1:10]
|
||||
|
||||
// How far away the hole is from the egde.
|
||||
mountingHoleEdgeOffset=4; // [0:50]
|
||||
|
||||
// Depending on the accuracy of your printer this may need to be increased in order for print in place to work.
|
||||
clearance=0.2; // [0.05:0.01:1]
|
||||
|
||||
// Radius difference in the ivot taper to stop the hinge from falling apart. Should be increased with large clearance values.
|
||||
pinTaper=0.25; // [0.1:0.1:2]
|
||||
|
||||
// calculated values
|
||||
hingeHalfExtrudeLength=hingeLength/2-clearance/2;
|
||||
mountingHoleMoveIncrement=(hingeLength-2*mountingHoleEdgeOffset)/
|
||||
(mountingHoleCount-1);
|
||||
|
||||
module costomizerEnd() {}
|
||||
$fn=30;
|
||||
tiny=0.005;
|
||||
// modules
|
||||
module hingeBaseProfile() {
|
||||
translate([pivotRadius,0,0]){
|
||||
square([baseWidth,baseThickness]);
|
||||
}
|
||||
}
|
||||
|
||||
module hingeBodyHalf() {
|
||||
difference() {
|
||||
union() {
|
||||
linear_extrude(hingeHalfExtrudeLength){
|
||||
offset(1)offset(-2)offset(1){
|
||||
translate([0,pivotRadius,0]){
|
||||
circle(pivotRadius);
|
||||
}
|
||||
square([pivotRadius,pivotRadius]);
|
||||
hingeBaseProfile();
|
||||
}
|
||||
}
|
||||
linear_extrude(hingeLength){
|
||||
offset(1)offset(-1)hingeBaseProfile();
|
||||
}
|
||||
}
|
||||
plateHoles();
|
||||
}
|
||||
}
|
||||
|
||||
module pin(rotateY, radiusOffset) {
|
||||
translate([0,pivotRadius,hingeHalfExtrudeLength+tiny]){
|
||||
rotate([0,rotateY,0]) {
|
||||
cylinder(
|
||||
h=hingeLength/2+clearance/2,
|
||||
r1=pinRadius+radiusOffset,
|
||||
r2=pinRadius+pinTaper+radiusOffset
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module hingeHalfFemale() {
|
||||
difference() {
|
||||
hingeBodyHalf();
|
||||
pin(rotateY=180, radiusOffset=clearance);
|
||||
}
|
||||
}
|
||||
|
||||
module hingeHalfMale() {
|
||||
translate([0,0,hingeLength]) {
|
||||
rotate([0,180,0]) {
|
||||
hingeBodyHalf();
|
||||
pin(rotateY=0, radiusOffset=0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module plateHoles() {
|
||||
for(i=[0:mountingHoleCount-1]){
|
||||
translate([
|
||||
baseWidth/2+pivotRadius,
|
||||
-baseThickness,
|
||||
i*mountingHoleMoveIncrement+mountingHoleEdgeOffset
|
||||
]){
|
||||
rotate([-90,0,0]){
|
||||
cylinder(r=mountingHoleRadius,h=baseThickness*4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// using high-level modules
|
||||
translate([0,0,-15]) {
|
||||
hingeHalfFemale();
|
||||
hingeHalfMale();
|
||||
}
|
||||
`
|
||||
}
|
||||
@@ -2,7 +2,8 @@ const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('tailwindcss')(path.resolve(__dirname, '../tailwind.config.js')),
|
||||
require('postcss-import'),
|
||||
require('tailwindcss')(path.resolve(__dirname, 'tailwind.config.js')),
|
||||
require('autoprefixer'),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
purge: ['./src/**/*.html', './src/**/*.js', './src/**/*.ts', './src/**/*.tsx'],
|
||||
purge: ['src/**/*.{js,jsx,ts,tsx}'],
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
theme: {
|
||||
extend: {
|
||||
@@ -13,27 +13,41 @@ module.exports = {
|
||||
borderRadius: {
|
||||
half: '50%',
|
||||
},
|
||||
boxShadow: {
|
||||
ch: '0 4px 4px 0 rgba(0, 0, 0, 0.25), 0 4px 4px 0 rgba(13, 13, 19, 0.15)',
|
||||
},
|
||||
colors: {
|
||||
'ch-gray': {
|
||||
900: '#0D0D13',
|
||||
800: '#1A1A1D',
|
||||
750: '#222222',
|
||||
760: '#232532',
|
||||
710: '#2B303C', // TODO: Use HSL so I stop adding grays to fix the warm/cool problem
|
||||
700: '#2A3038',
|
||||
600: '#3B3E4B',
|
||||
550: '#63636A',
|
||||
500: '#9F9FB4',
|
||||
400: '#A4A4B0',
|
||||
300: '#CFCFD8',
|
||||
},
|
||||
'ch-purple': {
|
||||
400: '#3B0480',
|
||||
450: '#671BC6',
|
||||
500: '#8732F2',
|
||||
600: '#A663FA',
|
||||
200: '#C99DFF',
|
||||
},
|
||||
'ch-purple-gray': {
|
||||
200: '#DBDBEC',
|
||||
},
|
||||
'ch-blue': {
|
||||
600: '#79B2F8',
|
||||
500: '5098F1',
|
||||
300: '#08466F'
|
||||
700: '#08466F',
|
||||
650: '#0958BA',
|
||||
640: '#0A57B5',
|
||||
630: '#3285EB',
|
||||
500: '#5098F1',
|
||||
400: '#79B2F8',
|
||||
300: '#9BC8FF',
|
||||
},
|
||||
'ch-pink': {
|
||||
800: '#93064F',
|
||||
@@ -45,14 +59,17 @@ module.exports = {
|
||||
grab: 'grab'
|
||||
},
|
||||
fontFamily: {
|
||||
'ropa-sans': ['Ropa Sans', 'Arial', 'sans-serif'],
|
||||
'ropa-sans': ['"Ropa Sans"', 'Arial', 'sans-serif'],
|
||||
roboto: ['Roboto', 'Arial', 'sans-serif'],
|
||||
'fira-code': ['Fira Code', 'monospace'],
|
||||
'fira-sans': ['Fira Sans', 'sans-seri'],
|
||||
'fira-code': ['"Fira Code"', 'monospace'],
|
||||
'fira-sans': ['"Fira Sans"', 'sans-serif'],
|
||||
},
|
||||
gridAutoColumns: {
|
||||
'preview-layout': 'minmax(30rem, 1fr) minmax(auto, 2fr)',
|
||||
},
|
||||
gridTemplateColumns: {
|
||||
'profile-layout': 'minmax(32rem, 1fr) 2fr',
|
||||
},
|
||||
keyframes: {
|
||||
'bounce-sm': {
|
||||
'0%, 100%': {
|
||||
@@ -81,6 +98,9 @@ module.exports = {
|
||||
minHeight: {
|
||||
md: '28rem',
|
||||
},
|
||||
outline: {
|
||||
gray: ['2px solid #3B3E4B', '8px'],
|
||||
},
|
||||
skew: {
|
||||
'-20': '-20deg',
|
||||
},
|
||||
@@ -2,8 +2,12 @@
|
||||
module.exports = (config, { env }) => {
|
||||
config.plugins.forEach((plugin) => {
|
||||
if (plugin.constructor.name === 'HtmlWebpackPlugin') {
|
||||
plugin.options.favicon = './src/favicon.svg'
|
||||
plugin.userOptions.favicon = './src/favicon.svg'
|
||||
}
|
||||
})
|
||||
config.module.rules.push({
|
||||
test: /\.(md|jscad\.js|py|scad|curv)$/i,
|
||||
use: 'raw-loader',
|
||||
});
|
||||
return config
|
||||
}
|
||||
|
||||
10
app/web/config/worker-loader.d.ts
vendored
Normal file
10
app/web/config/worker-loader.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
declare module "worker-loader!*" {
|
||||
// You need to change `Worker`, if you specified a different value for the `workerType` option
|
||||
class WebpackWorker extends Worker {
|
||||
constructor();
|
||||
}
|
||||
|
||||
// Uncomment this if you set the `esModule` option to `false`
|
||||
// export = WebpackWorker;
|
||||
export default WebpackWorker;
|
||||
}
|
||||
@@ -13,21 +13,25 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.0.0",
|
||||
"@headlessui/react": "^1.4.1",
|
||||
"@heroicons/react": "^1.0.4",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@monaco-editor/react": "^4.0.11",
|
||||
"@react-three/drei": "^7.3.1",
|
||||
"@react-three/fiber": "^7.0.5",
|
||||
"@redwoodjs/auth": "^0.34.1",
|
||||
"@redwoodjs/forms": "^0.34.1",
|
||||
"@redwoodjs/router": "^0.34.1",
|
||||
"@redwoodjs/web": "^0.34.1",
|
||||
"@react-three/postprocessing": "^2.0.5",
|
||||
"@redwoodjs/auth": "^0.38.1",
|
||||
"@redwoodjs/forms": "^0.38.1",
|
||||
"@redwoodjs/router": "^0.38.1",
|
||||
"@redwoodjs/web": "^0.38.1",
|
||||
"@sentry/browser": "^6.5.1",
|
||||
"@tailwindcss/aspect-ratio": "0.2.1",
|
||||
"axios": "^0.21.1",
|
||||
"browser-fs-access": "^0.17.2",
|
||||
"cloudinary-react": "^1.6.7",
|
||||
"get-active-classes": "^0.0.11",
|
||||
"gotrue-js": "^0.9.27",
|
||||
"hotkeys-js": "^3.8.7",
|
||||
"html-to-image": "^1.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"netlify-identity-widget": "^1.9.1",
|
||||
@@ -38,19 +42,24 @@
|
||||
"react-dropzone": "^11.2.1",
|
||||
"react-ga": "^3.3.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hotkeys-hook": "^3.4.0",
|
||||
"react-image-crop": "^8.6.6",
|
||||
"react-mosaic-component": "^4.1.1",
|
||||
"react-intersection-observer": "^8.32.1",
|
||||
"react-mosaic-component": "^5.0.0",
|
||||
"react-tabs": "^3.2.2",
|
||||
"rich-markdown-editor": "^11.0.2",
|
||||
"styled-components": "^5.2.0",
|
||||
"three": "^0.130.1"
|
||||
"three": "^0.130.1",
|
||||
"worker-loader": "^3.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.170",
|
||||
"autoprefixer": "^10.2.5",
|
||||
"autoprefixer": "^10.3.1",
|
||||
"html-webpack-plugin": "^4.5.0",
|
||||
"postcss": "^8.2.13",
|
||||
"postcss-loader": "4.0.2",
|
||||
"tailwindcss": "^2.1.2"
|
||||
"postcss": "^8.3.6",
|
||||
"postcss-import": "^14.0.2",
|
||||
"postcss-loader": "^6.1.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"tailwindcss": "^2.2.7"
|
||||
}
|
||||
}
|
||||
BIN
app/web/public/default-social-image.jpg
Normal file
BIN
app/web/public/default-social-image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
@@ -1,623 +0,0 @@
|
||||
(function(f) {
|
||||
if (typeof exports === "object" && typeof module !== "undefined") {
|
||||
module.exports = f()
|
||||
} else if (typeof define === "function" && define.amd) {
|
||||
define([], f)
|
||||
} else {
|
||||
var g;
|
||||
if (typeof window !== "undefined") {
|
||||
g = window
|
||||
} else if (typeof global !== "undefined") {
|
||||
g = global
|
||||
} else if (typeof self !== "undefined") {
|
||||
g = self
|
||||
} else {
|
||||
g = this
|
||||
}
|
||||
g.jscadWorker = f()
|
||||
}
|
||||
})(function() {
|
||||
// multi purpose module
|
||||
|
||||
|
||||
|
||||
const setPoints = (points, p, i)=>{
|
||||
points[i++] = p[0]
|
||||
points[i++] = p[1]
|
||||
points[i++] = p[2] || 0
|
||||
}
|
||||
|
||||
function CSG2Vertices(csg){
|
||||
let idx = 0
|
||||
|
||||
let vLen = 0, iLen = 0
|
||||
for (let poly of csg.polygons){
|
||||
let len = poly.vertices.length
|
||||
vLen += len *3
|
||||
iLen += 3 * (len-2)
|
||||
}
|
||||
const vertices = new Float32Array(vLen)
|
||||
const indices = vLen > 65535 ? new Uint32Array(iLen) : new Uint16Array(iLen)
|
||||
|
||||
let vertOffset = 0
|
||||
let indOffset = 0
|
||||
let posOffset = 0
|
||||
let first = 0
|
||||
for (let poly of csg.polygons){
|
||||
let arr = poly.vertices
|
||||
let len = arr.length
|
||||
first = posOffset
|
||||
vertices.set(arr[0], vertOffset)
|
||||
vertOffset +=3
|
||||
vertices.set(arr[1], vertOffset)
|
||||
vertOffset +=3
|
||||
posOffset +=2
|
||||
for(let i=2; i<len; i++){
|
||||
vertices.set(arr[i], vertOffset)
|
||||
|
||||
indices[indOffset++] = first
|
||||
indices[indOffset++] = first + i -1
|
||||
indices[indOffset++] = first + i
|
||||
|
||||
vertOffset += 3
|
||||
posOffset += 1
|
||||
}
|
||||
}
|
||||
return {vertices, indices, type:'mesh'}
|
||||
}
|
||||
|
||||
|
||||
function CSG2LineVertices(csg){
|
||||
let vLen = csg.points.length * 3
|
||||
if(csg.isClosed) vLen += 3
|
||||
|
||||
var vertices = new Float32Array(vLen)
|
||||
|
||||
|
||||
csg.points.forEach((p,idx)=>setPoints(vertices, p, idx * 3 ))
|
||||
|
||||
if(csg.isClosed){
|
||||
setPoints(vertices, csg.points[0], vertices.length - 3 )
|
||||
}
|
||||
return {vertices, type:'line'}
|
||||
}
|
||||
|
||||
function CSG2LineSegmentsVertices(csg){
|
||||
let vLen = csg.sides.length * 6
|
||||
|
||||
var vertices = new Float32Array(vLen)
|
||||
csg.sides.forEach((side,idx)=>{
|
||||
let i = idx * 6
|
||||
setPoints(vertices, side[0], i)
|
||||
setPoints(vertices, side[1], i+3)
|
||||
})
|
||||
return {vertices, type:'lines'}
|
||||
|
||||
}
|
||||
|
||||
function CSGCached(func, data, cacheKey, transferable){
|
||||
cacheKey = cacheKey || data
|
||||
|
||||
let geo = CSGToBuffers.cache.get(cacheKey)
|
||||
if(geo) return geo
|
||||
|
||||
geo = func(data)
|
||||
|
||||
// fill transferable array for postMessage optimization
|
||||
if(transferable){
|
||||
const {vertices, indices} = geo
|
||||
transferable.push(vertices)
|
||||
if(indices) transferable.push(indices)
|
||||
}
|
||||
|
||||
CSGToBuffers.cache.set(cacheKey, geo)
|
||||
return geo
|
||||
}
|
||||
|
||||
function CSGToBuffers(csg, transferable){
|
||||
let obj
|
||||
|
||||
if(csg.polygons) obj = CSGCached(CSG2Vertices,csg,csg.polygons, transferable)
|
||||
if(csg.sides && !csg.points) obj = CSGCached(CSG2LineSegmentsVertices,csg,csg.sides, transferable)
|
||||
if(csg.points) obj = CSGCached(CSG2LineVertices,csg,csg.points, transferable)
|
||||
|
||||
return obj
|
||||
}
|
||||
CSGToBuffers.clearCache = ()=>{CSGToBuffers.cache = new WeakMap()}
|
||||
CSGToBuffers.clearCache()
|
||||
|
||||
|
||||
|
||||
|
||||
let workerBaseURI
|
||||
|
||||
function require(url){
|
||||
url = require.alias[url] || url
|
||||
if(url[0] != '/' && url.substr(0,2) != './' && url.substr(0,4) != 'http') url = 'https://unpkg.com/'+url
|
||||
let exports=require.cache[url]; //get from cache
|
||||
if (!exports) { //not cached
|
||||
let module = requireModule(url)
|
||||
require.cache[url] = exports = module.exports; //cache obj exported by module
|
||||
}
|
||||
return exports; //require returns object exported by module
|
||||
}
|
||||
|
||||
function requireFile(url){
|
||||
try{
|
||||
let X=new XMLHttpRequest();
|
||||
X.open("GET", new URL(url,workerBaseURI), 0); // sync
|
||||
X.send();
|
||||
if (X.status && X.status !== 200) throw new Error(X.statusText);
|
||||
return X.responseText;
|
||||
}catch(e){
|
||||
console.log('problem loading url ',url,'base',workerBaseURI,' error:',e.message)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
function requireModule(url, source){
|
||||
try {
|
||||
const exports={};
|
||||
if(!source) source = requireFile(url)
|
||||
const module = { id: url, uri: url, exports:exports, source }; //according to node.js modules
|
||||
// fix, add comment to show source on Chrome Dev Tools
|
||||
source="//@ sourceURL="+url+"\n" + source;
|
||||
//------
|
||||
const anonFn = new Function("require", "exports", "module", source); //create a Fn with module code, and 3 params: require, exports & module
|
||||
anonFn(require, exports, module); // call the Fn, Execute the module
|
||||
return module
|
||||
} catch (err) {
|
||||
console.error("Error loading module "+url, err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
require.cache = {}
|
||||
require.alias = {}
|
||||
|
||||
|
||||
const initCanvas = (canvas, callback)=>{
|
||||
|
||||
// convert HTML events (mouse movement) to viewer changes
|
||||
let lastX = 0
|
||||
let lastY = 0
|
||||
|
||||
let pointerDown = false
|
||||
|
||||
const moveHandler = (ev) => {
|
||||
if(!pointerDown) return
|
||||
const cmd = {
|
||||
worker: 'render',
|
||||
dx: lastX - ev.pageX,
|
||||
dy: ev.pageY - lastY
|
||||
}
|
||||
|
||||
const shiftKey = (ev.shiftKey === true) || (ev.touches && ev.touches.length > 2)
|
||||
cmd.action = shiftKey ? 'pan':'rotate'
|
||||
callback(cmd)
|
||||
|
||||
lastX = ev.pageX
|
||||
lastY = ev.pageY
|
||||
|
||||
ev.preventDefault()
|
||||
}
|
||||
const downHandler = (ev) => {
|
||||
pointerDown = true
|
||||
lastX = ev.pageX
|
||||
lastY = ev.pageY
|
||||
canvas.setPointerCapture(ev.pointerId)
|
||||
ev.preventDefault()
|
||||
}
|
||||
|
||||
const upHandler = (ev) => {
|
||||
pointerDown = false
|
||||
canvas.releasePointerCapture(ev.pointerId)
|
||||
ev.preventDefault()
|
||||
}
|
||||
|
||||
const wheelHandler = (ev) => {
|
||||
callback({action:'zoom', dy:ev.deltaY, worker: 'render'})
|
||||
ev.preventDefault()
|
||||
}
|
||||
|
||||
canvas.onpointermove = moveHandler
|
||||
canvas.onpointerdown = downHandler
|
||||
canvas.onpointerup = upHandler
|
||||
canvas.onwheel = wheelHandler
|
||||
}
|
||||
|
||||
const cmdHandler = (handlers)=>(cmd)=>{
|
||||
const fn = handlers[cmd.action]
|
||||
if (!fn) throw new Error('no handler for type: ' + cmd.action)
|
||||
fn(cmd);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const makeScriptWorker = ({callback, convertToSolids})=>{
|
||||
let workerBaseURI, onInit
|
||||
|
||||
function runMain(params={}){
|
||||
let time = Date.now()
|
||||
let solids
|
||||
let transfer = []
|
||||
try{
|
||||
solids = main(params)
|
||||
}catch(e){
|
||||
callback({action:'entities', worker:'render', error:e.message, stack:e.stack.toString()}, transfer)
|
||||
return
|
||||
}
|
||||
let solidsTime = Date.now() - time
|
||||
scriptStats = `generate solids ${solidsTime}ms`
|
||||
|
||||
if(convertToSolids === 'buffers'){
|
||||
CSGToBuffers.clearCache()
|
||||
entities = solids.map((csg)=>{
|
||||
let obj = CSGToBuffers(csg, transfer)
|
||||
obj.color = csg.color
|
||||
obj.transforms = csg.transforms
|
||||
return obj
|
||||
})
|
||||
}else if(convertToSolids === 'regl'){
|
||||
const { entitiesFromSolids } = require('@jscad/regl-renderer')
|
||||
time = Date.now()
|
||||
entities = entitiesFromSolids({}, solids)
|
||||
scriptStats += ` convert to entities ${Date.now()-time}ms`
|
||||
}else{
|
||||
entities = solids
|
||||
}
|
||||
callback({action:'entities', worker:'render', entities, scriptStats}, transfer)
|
||||
}
|
||||
|
||||
let initialized = false
|
||||
const handlers = {
|
||||
runScript: ({script,url, params={}})=>{
|
||||
if(!initialized){
|
||||
onInit = ()=>handlers.runScript({script,url, params})
|
||||
}
|
||||
let script_module
|
||||
try{
|
||||
script_module = requireModule(url,script)
|
||||
}catch(e){
|
||||
callback({action:'entities', worker:'render', error:e.message, stack:e.stack.toString()})
|
||||
return
|
||||
}
|
||||
main = script_module.exports.main
|
||||
let gp = script_module.exports.getParameterDefinitions
|
||||
if(gp){
|
||||
callback({action:'parameterDefinitions', worker:'main', data:gp()})
|
||||
}
|
||||
runMain(params)
|
||||
},
|
||||
updateParams: ({params={}})=>{
|
||||
runMain(params)
|
||||
},
|
||||
init: (params)=>{
|
||||
let {baseURI, alias=[]} = params
|
||||
if(!baseURI && typeof document != 'undefined' && document.baseURI){
|
||||
baseURI = document.baseURI
|
||||
}
|
||||
|
||||
if(baseURI) workerBaseURI = baseURI.toString()
|
||||
|
||||
alias.forEach(arr=>{
|
||||
let [orig, ...aliases] = arr
|
||||
aliases.forEach(a=>{
|
||||
require.alias[a] = orig
|
||||
if(a.toLowerCase().substr(-3)!=='.js') require.alias[a+'.js'] = orig
|
||||
})
|
||||
})
|
||||
initialized = true
|
||||
if(onInit) onInit()
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
// called from outside to pass mesasges into worker
|
||||
postMessage: cmdHandler(handlers),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/** Make render worker */
|
||||
|
||||
const makeRenderWorker = ()=>{
|
||||
let perspectiveCamera
|
||||
const state = {}
|
||||
|
||||
const rotateSpeed = 0.002
|
||||
const panSpeed = 1
|
||||
const zoomSpeed = 0.08
|
||||
let rotateDelta = [0, 0]
|
||||
let panDelta = [0, 0]
|
||||
let zoomDelta = 0
|
||||
let updateRender = true
|
||||
let orbitControls, renderOptions, gridOptions, axisOptions, renderer
|
||||
|
||||
let entities = []
|
||||
|
||||
function createContext (canvas, contextAttributes) {
|
||||
function get (type) {
|
||||
try {
|
||||
return {gl:canvas.getContext(type, contextAttributes), type}
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return (
|
||||
get('webgl2') ||
|
||||
get('webgl') ||
|
||||
get('experimental-webgl') ||
|
||||
get('webgl-experimental')
|
||||
)
|
||||
}
|
||||
|
||||
const startRenderer = ({canvas, cameraPosition, cameraTarget, axis={}, grid={}})=>{
|
||||
const { prepareRender, drawCommands, cameras, controls } = require('@jscad/regl-renderer')
|
||||
|
||||
perspectiveCamera = cameras.perspective
|
||||
orbitControls = controls.orbit
|
||||
|
||||
state.canvas = canvas
|
||||
state.camera = Object.assign({}, perspectiveCamera.defaults)
|
||||
if(cameraPosition) state.camera.position = cameraPosition
|
||||
if(cameraTarget) state.camera.target = cameraTarget
|
||||
|
||||
resize({ width:canvas.width, height:canvas.height })
|
||||
|
||||
state.controls = orbitControls.defaults
|
||||
|
||||
const {gl, type} = createContext(canvas)
|
||||
// prepare the renderer
|
||||
const setupOptions = {
|
||||
glOptions: {gl}
|
||||
}
|
||||
if(type == 'webgl'){
|
||||
setupOptions.glOptions.optionalExtensions = ['oes_element_index_uint']
|
||||
}
|
||||
renderer = prepareRender(setupOptions)
|
||||
|
||||
gridOptions = {
|
||||
visuals: {
|
||||
drawCmd: 'drawGrid',
|
||||
show: grid.show || grid.show === undefined ,
|
||||
color: grid.color || [0, 0, 0, 1],
|
||||
subColor: grid.subColor || [0, 0, 1, 0.5],
|
||||
fadeOut: false,
|
||||
transparent: true
|
||||
},
|
||||
size: grid.size || [200, 200],
|
||||
ticks: grid.ticks || [10, 1]
|
||||
}
|
||||
|
||||
axisOptions = {
|
||||
visuals: {
|
||||
drawCmd: 'drawAxis',
|
||||
show: axis.show || axis.show === undefined
|
||||
},
|
||||
size: axis.size || 100,
|
||||
}
|
||||
|
||||
// assemble the options for rendering
|
||||
renderOptions = {
|
||||
camera: state.camera,
|
||||
drawCommands: {
|
||||
drawAxis: drawCommands.drawAxis,
|
||||
drawGrid: drawCommands.drawGrid,
|
||||
drawLines: drawCommands.drawLines,
|
||||
drawMesh: drawCommands.drawMesh
|
||||
},
|
||||
// define the visual content
|
||||
entities: [
|
||||
gridOptions,
|
||||
axisOptions,
|
||||
...entities
|
||||
]
|
||||
}
|
||||
// the heart of rendering, as themes, controls, etc change
|
||||
|
||||
updateView()
|
||||
}
|
||||
|
||||
let renderTimer
|
||||
const tmFunc = typeof requestAnimationFrame === 'undefined' ? setTimeout : requestAnimationFrame
|
||||
|
||||
function updateView(delay=8){
|
||||
if(renderTimer || !renderer) return
|
||||
renderTimer = tmFunc(updateAndRender,delay)
|
||||
}
|
||||
|
||||
const doRotatePanZoom = () => {
|
||||
|
||||
if (rotateDelta[0] || rotateDelta[1]) {
|
||||
const updated = orbitControls.rotate({ controls: state.controls, camera: state.camera, speed: rotateSpeed }, rotateDelta)
|
||||
state.controls = { ...state.controls, ...updated.controls }
|
||||
rotateDelta = [0, 0]
|
||||
}
|
||||
|
||||
if (panDelta[0] || panDelta[1]) {
|
||||
const updated = orbitControls.pan({ controls:state.controls, camera:state.camera, speed: panSpeed }, panDelta)
|
||||
state.controls = { ...state.controls, ...updated.controls }
|
||||
panDelta = [0, 0]
|
||||
state.camera.position = updated.camera.position
|
||||
state.camera.target = updated.camera.target
|
||||
}
|
||||
|
||||
if (zoomDelta) {
|
||||
const updated = orbitControls.zoom({ controls:state.controls, camera:state.camera, speed: zoomSpeed }, zoomDelta)
|
||||
state.controls = { ...state.controls, ...updated.controls }
|
||||
zoomDelta = 0
|
||||
}
|
||||
}
|
||||
|
||||
const updateAndRender = (timestamp) => {
|
||||
renderTimer = null
|
||||
doRotatePanZoom()
|
||||
|
||||
const updates = orbitControls.update({ controls: state.controls, camera: state.camera })
|
||||
state.controls = { ...state.controls, ...updates.controls }
|
||||
if(state.controls.changed) updateView(16) // for elasticity in rotate / zoom
|
||||
|
||||
state.camera.position = updates.camera.position
|
||||
perspectiveCamera.update(state.camera)
|
||||
renderOptions.entities = [
|
||||
gridOptions,
|
||||
axisOptions,
|
||||
...entities
|
||||
]
|
||||
let time = Date.now()
|
||||
renderer(renderOptions)
|
||||
if(updateRender){
|
||||
updateRender = '';
|
||||
}
|
||||
}
|
||||
|
||||
function resize({width,height}){
|
||||
state.canvas.width = width
|
||||
state.canvas.height = height
|
||||
perspectiveCamera.setProjection(state.camera, state.camera, { width, height })
|
||||
perspectiveCamera.update(state.camera, state.camera)
|
||||
updateView()
|
||||
}
|
||||
|
||||
const handlers = {
|
||||
pan: ({dx,dy})=>{
|
||||
panDelta[0] += dx
|
||||
panDelta[1] += dy
|
||||
updateView()
|
||||
},
|
||||
rotate: ({dx,dy})=>{
|
||||
rotateDelta[0] -= dx
|
||||
rotateDelta[1] -= dy
|
||||
updateView()
|
||||
},
|
||||
zoom: ({dy})=>{
|
||||
zoomDelta += dy
|
||||
updateView()
|
||||
},
|
||||
resize,
|
||||
entities: (params)=>{
|
||||
entities = params.entities
|
||||
updateRender = params.scriptStats
|
||||
updateView()
|
||||
},
|
||||
init: (params)=>{
|
||||
if(params.canvas) startRenderer(params)
|
||||
initialized = true
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
// called from outside to pass mesasges into worker
|
||||
postMessage: cmdHandler(handlers),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return (params)=>{
|
||||
let { canvas, baseURI=(typeof document === 'undefined') ? '':document.location.toString(), scope='main', renderInWorker, render, callback=()=>{}, scriptUrl='demo-worker.js', alias, convertToSolids=false } = params
|
||||
// by default 'render' messages go outside of this instance (result of modeling)
|
||||
let sendToRender = callback
|
||||
let scriptWorker, renderWorker
|
||||
workerBaseURI = baseURI
|
||||
|
||||
const sendCmd = (params, transfer)=>{
|
||||
if(params.worker === 'render')
|
||||
sendToRender(params, transfer)
|
||||
else if(params.worker === 'script')
|
||||
scriptWorker.postMessage(params, transfer)
|
||||
else{
|
||||
// parameter definitions will arrive from scriptWorker
|
||||
callback(params, transfer)
|
||||
}
|
||||
}
|
||||
|
||||
const updateSize = function({width,height}){
|
||||
sendCmd({ action:'resize', worker:'render', width: canvas.offsetWidth, height: canvas.offsetHeight})
|
||||
}
|
||||
|
||||
|
||||
renderInWorker = !!(canvas && renderInWorker && canvas.transferControlToOffscreen)
|
||||
const makeRenderWorkerHere = (scope === 'main' && canvas && !renderInWorker) || (scope === 'worker' && render)
|
||||
// worker is in current thread
|
||||
if(makeRenderWorkerHere){
|
||||
renderWorker = makeRenderWorker({callback:sendCmd})
|
||||
sendToRender = (params, transfer)=>renderWorker.postMessage(params, transfer)
|
||||
}
|
||||
|
||||
if(scope === 'main'){
|
||||
// let extraScript = renderInWorker ? `,'https://unpkg.com/@jscad/regl-renderer'`:''
|
||||
let script =`let baseURI = '${baseURI}'
|
||||
importScripts(new URL('${scriptUrl}',baseURI))
|
||||
let worker = jscadWorker({
|
||||
baseURI: baseURI,
|
||||
convertToSolids: ${convertToSolids},
|
||||
scope:'worker',
|
||||
callback:(params)=>self.postMessage(params),
|
||||
render:${renderInWorker}
|
||||
})
|
||||
self.addEventListener('message', (e)=>worker.postMessage(e.data))
|
||||
`
|
||||
let blob = new Blob([script],{type: 'text/javascript'})
|
||||
scriptWorker = new Worker(window.URL.createObjectURL(blob))
|
||||
scriptWorker.addEventListener('message',(e)=>sendCmd(e.data))
|
||||
scriptWorker.postMessage({action:'init', baseURI, alias})
|
||||
if(renderInWorker) renderWorker = scriptWorker
|
||||
|
||||
if(canvas){
|
||||
initCanvas(canvas, sendCmd)
|
||||
window.addEventListener('resize',updateSize)
|
||||
}
|
||||
}else{
|
||||
scriptWorker = makeScriptWorker({callback:sendCmd, convertToSolids})
|
||||
callback({action:'workerInit',worker:'main'})
|
||||
}
|
||||
|
||||
if(canvas){
|
||||
// redirect 'render' messages to renderWorker
|
||||
sendToRender = (params, transfer)=>renderWorker.postMessage(params, transfer)
|
||||
let width = canvas.width = canvas.clientWidth
|
||||
let height = canvas.height = canvas.clientHeight
|
||||
if(scope == 'main'){
|
||||
const offscreen = renderInWorker ? canvas.transferControlToOffscreen() : canvas
|
||||
renderWorker.postMessage({action:'init', worker:'render', canvas:offscreen, width, height}, [offscreen])
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
updateSize,
|
||||
updateParams:({params={}})=>sendCmd({ action:'updateParams', worker:'script', params}),
|
||||
runScript: ({script,url=''})=>sendCmd({ action:'runScript', worker:'script', script, url}),
|
||||
postMessage: sendCmd,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// multi purpose module
|
||||
});
|
||||
BIN
app/web/public/hinge.stl
Normal file
BIN
app/web/public/hinge.stl
Normal file
Binary file not shown.
BIN
app/web/public/pumpjack.stl
Normal file
BIN
app/web/public/pumpjack.stl
Normal file
Binary file not shown.
@@ -1,15 +1,19 @@
|
||||
import { AuthProvider } from '@redwoodjs/auth'
|
||||
import GoTrue from 'gotrue-js'
|
||||
|
||||
import { FatalErrorBoundary } from '@redwoodjs/web'
|
||||
import { RedwoodProvider } from '@redwoodjs/web'
|
||||
import FatalErrorBoundary from 'src/components/FatalErrorBoundary/FatalErrorBoundary'
|
||||
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
|
||||
import FatalErrorPage from 'src/pages/FatalErrorPage'
|
||||
import { createTheme } from '@material-ui/core/styles'
|
||||
import { ThemeProvider } from '@material-ui/styles'
|
||||
import ReactGA from 'react-ga'
|
||||
|
||||
ReactGA.initialize(process.env.GOOGLE_ANALYTICS_ID)
|
||||
|
||||
import Routes from 'src/Routes'
|
||||
|
||||
import './font-imports.css'
|
||||
import './scaffold.css'
|
||||
import './index.css'
|
||||
|
||||
@@ -18,13 +22,28 @@ const goTrueClient = new GoTrue({
|
||||
setCookie: true,
|
||||
})
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
type: 'dark',
|
||||
primary: {
|
||||
light: '#C99DFF',
|
||||
main: '#A663FA',
|
||||
dark: '#3B0480',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const App = () => (
|
||||
<FatalErrorBoundary page={FatalErrorPage}>
|
||||
<AuthProvider client={goTrueClient} type="goTrue">
|
||||
<RedwoodApolloProvider>
|
||||
<Routes />
|
||||
</RedwoodApolloProvider>
|
||||
</AuthProvider>
|
||||
<RedwoodProvider>
|
||||
<AuthProvider client={goTrueClient} type="goTrue">
|
||||
<RedwoodApolloProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Routes />
|
||||
</ThemeProvider>
|
||||
</RedwoodApolloProvider>
|
||||
</AuthProvider>
|
||||
</RedwoodProvider>
|
||||
</FatalErrorBoundary>
|
||||
)
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ const Routes = () => {
|
||||
)
|
||||
return (
|
||||
<Router>
|
||||
<Route path="/projects" page={ProjectsPage} name="projects" />
|
||||
<Route path="/dev-ide/{cadPackage}" page={DevIdePage} name="devIde" />
|
||||
<Route path="/policies/privacy-policy" page={PrivacyPolicyPage} name="privacyPolicy" />
|
||||
<Route path="/policies/code-of-conduct" page={CodeOfConductPage} name="codeOfConduct" />
|
||||
@@ -55,11 +56,12 @@ 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">
|
||||
<Route path="/admin/users" page={UsersPage} name="users" />
|
||||
<Route path="/admin/projects" page={AdminProjectsPage} name="projects" />
|
||||
<Route path="/admin/projects" page={AdminProjectsPage} name="adminProjects" />
|
||||
<Route path="/admin/subject-access-requests/{id}/edit" page={EditSubjectAccessRequestPage} name="editSubjectAccessRequest" />
|
||||
<Route path="/admin/subject-access-requests/{id}" page={SubjectAccessRequestPage} name="subjectAccessRequest" />
|
||||
<Route path="/admin/subject-access-requests" page={SubjectAccessRequestsPage} name="subjectAccessRequests" />
|
||||
|
||||
@@ -1,29 +1,80 @@
|
||||
import { ideTypeNameMap } from 'src/helpers/hooks/useIdeContext'
|
||||
export type CadPackageType = 'openscad' | 'cadquery' | 'jscad' | 'curv' | 'INIT'
|
||||
|
||||
interface CadPackageProps {
|
||||
cadPackage: string
|
||||
className?: string
|
||||
interface CadPackageConfig {
|
||||
label: string
|
||||
buttonClasses: string
|
||||
dotClasses: string
|
||||
}
|
||||
|
||||
const CadPackage = ({ cadPackage, className = '' }: CadPackageProps) => {
|
||||
const cadName = ideTypeNameMap[cadPackage] || ''
|
||||
const isOpenScad = cadPackage === 'openscad'
|
||||
const isCadQuery = cadPackage === 'cadquery'
|
||||
export const cadPackageConfigs: { [key in CadPackageType]: CadPackageConfig } =
|
||||
{
|
||||
openscad: {
|
||||
label: 'OpenSCAD',
|
||||
buttonClasses: 'bg-yellow-800',
|
||||
dotClasses: 'bg-yellow-200',
|
||||
},
|
||||
cadquery: {
|
||||
label: 'CadQuery',
|
||||
buttonClasses: 'bg-ch-blue-700',
|
||||
dotClasses: 'bg-blue-800',
|
||||
},
|
||||
jscad: {
|
||||
label: 'JSCAD',
|
||||
buttonClasses: 'bg-ch-purple-500',
|
||||
dotClasses: 'bg-yellow-300',
|
||||
},
|
||||
curv: {
|
||||
label: 'Curv',
|
||||
buttonClasses: 'bg-blue-600',
|
||||
dotClasses: 'bg-green-500',
|
||||
},
|
||||
INIT: {
|
||||
label: '',
|
||||
buttonClasses: '',
|
||||
dotClasses: '',
|
||||
},
|
||||
}
|
||||
|
||||
interface CadPackageProps {
|
||||
cadPackage: CadPackageType
|
||||
className?: string
|
||||
dotClass?: string
|
||||
onClick?: any
|
||||
}
|
||||
|
||||
const CadPackage = ({
|
||||
cadPackage,
|
||||
className = '',
|
||||
dotClass = 'w-5 h-5',
|
||||
onClick,
|
||||
}: CadPackageProps) => {
|
||||
const cadPackageConfig = cadPackageConfigs[cadPackage]
|
||||
|
||||
return (
|
||||
<div
|
||||
<ButtonOrDiv
|
||||
onClick={onClick}
|
||||
className={
|
||||
`grid grid-flow-col-dense items-center gap-2 cursor-default text-gray-100 ${
|
||||
isOpenScad && 'bg-yellow-800'
|
||||
} ${isCadQuery && 'bg-ch-blue-300'} bg-opacity-30 ` + className
|
||||
`grid grid-flow-col-dense items-center gap-2 text-gray-100 bg-opacity-30
|
||||
${cadPackageConfig?.buttonClasses} ` + className
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`${isOpenScad && 'bg-yellow-200'} ${
|
||||
isCadQuery && 'bg-blue-800'
|
||||
} w-5 h-5 rounded-full`}
|
||||
className={`${cadPackageConfig?.dotClasses} ${dotClass} rounded-full`}
|
||||
/>
|
||||
<div>{cadName}</div>
|
||||
</div>
|
||||
{cadPackageConfig?.label}
|
||||
</ButtonOrDiv>
|
||||
)
|
||||
}
|
||||
|
||||
// Returns a proper button if an onClick handler is passed in, or a div
|
||||
// if the element is meant to be a simple badge
|
||||
function ButtonOrDiv({ onClick, className, children }) {
|
||||
return onClick ? (
|
||||
<button className={className + ' hover:bg-opacity-80'} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
) : (
|
||||
<div className={className}>{children}</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,193 +1,267 @@
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from '@redwoodjs/web/toast'
|
||||
import Popover from '@material-ui/core/Popover'
|
||||
import Svg from 'src/components/Svg/Svg'
|
||||
import Button from 'src/components/Button/Button'
|
||||
import { toJpeg } from 'html-to-image'
|
||||
|
||||
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||
import { canvasToBlob, blobTo64 } from 'src/helpers/canvasToBlob'
|
||||
import { useUpdateProjectImages } from 'src/helpers/hooks/useUpdateProjectImages'
|
||||
|
||||
import { requestRenderStateless } from 'src/helpers/hooks/useIdeState'
|
||||
import { PureIdeViewer } from 'src/components/IdeViewer/PureIdeViewer'
|
||||
import { State } from 'src/helpers/hooks/useIdeState'
|
||||
import SocialCardCell from 'src/components/SocialCardCell/SocialCardCell'
|
||||
import { toJpeg } from 'html-to-image'
|
||||
|
||||
const anchorOrigin = {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
}
|
||||
const transformOrigin = {
|
||||
vertical: 'top',
|
||||
horizontal: 'center',
|
||||
}
|
||||
export const captureSize = { width: 500, height: 522 }
|
||||
|
||||
const CaptureButton = ({
|
||||
canEdit,
|
||||
TheButton,
|
||||
shouldUpdateImage,
|
||||
projectTitle,
|
||||
userName,
|
||||
const CaptureButtonViewer = ({
|
||||
onInit,
|
||||
onScadImage,
|
||||
canvasRatio = 1,
|
||||
}: {
|
||||
onInit: (a: any) => void
|
||||
onScadImage: (a: any) => void
|
||||
canvasRatio: number
|
||||
}) => {
|
||||
const [captureState, setCaptureState] = useState<any>({})
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [whichPopup, setWhichPopup] = useState(null)
|
||||
const { state, project } = useIdeContext()
|
||||
const ref = React.useRef<HTMLDivElement>(null)
|
||||
const { updateProjectImages } = useUpdateProjectImages({})
|
||||
|
||||
const onCapture = async () => {
|
||||
const threeInstance = state.threeInstance
|
||||
const isOpenScadImage = state?.objectData?.type === 'png'
|
||||
let imgBlob
|
||||
let image64
|
||||
if (!isOpenScadImage) {
|
||||
imgBlob = canvasToBlob(threeInstance, { width: 500, height: 375 })
|
||||
image64 = blobTo64(
|
||||
await canvasToBlob(threeInstance, { width: 500, height: 522 })
|
||||
)
|
||||
} else {
|
||||
imgBlob = state.objectData.data
|
||||
image64 = blobTo64(state.objectData.data)
|
||||
}
|
||||
const config = {
|
||||
image: await imgBlob,
|
||||
currImage: project?.mainImage,
|
||||
imageObjectURL: window.URL.createObjectURL(await imgBlob),
|
||||
callback: uploadAndUpdateImage,
|
||||
cloudinaryImgURL: '',
|
||||
updated: false,
|
||||
image64: await image64,
|
||||
}
|
||||
setCaptureState(config)
|
||||
|
||||
async function uploadAndUpdateImage() {
|
||||
const upload = async () => {
|
||||
const socialCard64 = toJpeg(ref.current, {
|
||||
cacheBust: true,
|
||||
quality: 0.7,
|
||||
})
|
||||
|
||||
// uploading in two separate mutations because of the 100kb limit of the lambda functions
|
||||
const imageUploadPromise1 = updateProjectImages({
|
||||
variables: {
|
||||
id: project?.id,
|
||||
mainImage64: await config.image64,
|
||||
},
|
||||
})
|
||||
const imageUploadPromise2 = updateProjectImages({
|
||||
variables: {
|
||||
id: project?.id,
|
||||
socialCard64: await socialCard64,
|
||||
},
|
||||
})
|
||||
return Promise.all([imageUploadPromise2, imageUploadPromise1])
|
||||
}
|
||||
const promise = upload()
|
||||
toast.promise(promise, {
|
||||
loading: 'Saving Image/s',
|
||||
success: <b>Image/s saved!</b>,
|
||||
error: <b>Problem saving.</b>,
|
||||
const { state } = useIdeContext()
|
||||
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) => {
|
||||
threeInstance.current = _threeInstance
|
||||
onInit(_threeInstance)
|
||||
}
|
||||
const onCameraChange = (camera, isFirstCameraChange) => {
|
||||
const renderPromise =
|
||||
(state.ideType === 'openscad' || state.ideType === 'curv') &&
|
||||
requestRenderStateless({
|
||||
state,
|
||||
camera,
|
||||
viewerSize: {
|
||||
width: threeInstance.current.size.width * canvasRatio,
|
||||
height: threeInstance.current.size.height * canvasRatio,
|
||||
},
|
||||
viewAll: isFirstCameraChange,
|
||||
})
|
||||
const [{ data }] = await promise
|
||||
return data?.updateProjectImages?.mainImage
|
||||
}
|
||||
|
||||
// if there isn't a screenshot saved yet, just go ahead and save right away
|
||||
if (shouldUpdateImage) {
|
||||
config.cloudinaryImgURL = await uploadAndUpdateImage()
|
||||
config.updated = true
|
||||
setCaptureState(config)
|
||||
if (!renderPromise) {
|
||||
return
|
||||
}
|
||||
isLoadingSetter(true)
|
||||
renderPromise.then(async ({ objectData, camera }) => {
|
||||
if (camera?.isScadUpdate) {
|
||||
cameraSetter(camera)
|
||||
}
|
||||
isLoadingSetter(false)
|
||||
dataTypeSetter(objectData?.type)
|
||||
artifactSetter(objectData?.data)
|
||||
if (objectData?.type === 'png') {
|
||||
onScadImage(await blobTo64(objectData?.data))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleClick = ({ event, whichPopup }) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
setWhichPopup(whichPopup)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
setWhichPopup(null)
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{canEdit && (
|
||||
<div>
|
||||
<TheButton
|
||||
onClick={async (event) => {
|
||||
handleClick({ event, whichPopup: 'capture' })
|
||||
onCapture()
|
||||
}}
|
||||
/>
|
||||
<Popover
|
||||
id={'capture-popover'}
|
||||
open={whichPopup === 'capture'}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={anchorOrigin}
|
||||
transformOrigin={transformOrigin}
|
||||
className="material-ui-overrides transform translate-y-4"
|
||||
>
|
||||
<div className="text-sm p-4 text-gray-500">
|
||||
{!captureState ? (
|
||||
'Loading...'
|
||||
) : (
|
||||
<div className="">
|
||||
<div className="text-lg">Thumbnail</div>
|
||||
<div
|
||||
className="rounded"
|
||||
style={{ width: 'fit-content', overflow: 'hidden' }}
|
||||
>
|
||||
<img src={captureState.imageObjectURL} className="w-32" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-lg mt-4">Social Media Card</div>
|
||||
<div className="rounded-lg shadow-md overflow-hidden">
|
||||
<div
|
||||
className="transform scale-50 origin-top-left"
|
||||
style={{ width: '600px', height: '315px' }}
|
||||
>
|
||||
<div style={{ width: '1200px', height: '630px' }} ref={ref}>
|
||||
<SocialCardCell
|
||||
userName={userName}
|
||||
projectTitle={projectTitle}
|
||||
image64={captureState.image64}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 text-indigo-800">
|
||||
{captureState.currImage && !captureState.updated ? (
|
||||
<Button
|
||||
iconName="refresh"
|
||||
className="shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-200 text-indigo-100 text-opacity-100 bg-opacity-80"
|
||||
shouldAnimateHover
|
||||
onClick={async () => {
|
||||
const cloudinaryImg = await captureState.callback()
|
||||
setCaptureState({
|
||||
...captureState,
|
||||
currImage: cloudinaryImg,
|
||||
updated: true,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Update Project Images
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex justify-center mb-4">
|
||||
<Svg
|
||||
name="checkmark"
|
||||
className="mr-2 w-6 text-indigo-600"
|
||||
/>{' '}
|
||||
Project Images Updated
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
<PureIdeViewer
|
||||
scadRatio={canvasRatio}
|
||||
dataType={dataType}
|
||||
artifact={artifact}
|
||||
onInit={getThreeInstance}
|
||||
onCameraChange={onCameraChange}
|
||||
isLoading={isLoading}
|
||||
camera={camera}
|
||||
isMinimal
|
||||
ideType={ideType}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabContent() {
|
||||
return (
|
||||
<div className="bg-ch-gray-800 h-full overflow-y-auto px-8 pb-16">
|
||||
<IsolatedCanvas
|
||||
size={{ width: 500, height: 375 }}
|
||||
uploadKey="mainImage64"
|
||||
RenderComponent={ThumbnailViewer}
|
||||
/>
|
||||
<IsolatedCanvas
|
||||
canvasRatio={2}
|
||||
size={captureSize}
|
||||
uploadKey="socialCard64"
|
||||
RenderComponent={SocialCardLiveViewer}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CaptureButton
|
||||
function SocialCardLiveViewer({
|
||||
forwardRef,
|
||||
onUpload,
|
||||
children,
|
||||
partSnapShot64,
|
||||
}) {
|
||||
const { project } = useIdeContext()
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-2xl text-ch-gray-300 pt-4">Set social Image</h3>
|
||||
<div className="flex py-4">
|
||||
<div className="rounded-md shadow-ch border border-gray-400 overflow-hidden">
|
||||
<div
|
||||
className="transform scale-50 origin-top-left"
|
||||
style={{ width: '600px', height: '315px' }}
|
||||
>
|
||||
<div style={{ width: '1200px', height: '630px' }} ref={forwardRef}>
|
||||
<SocialCardCell
|
||||
userName={project.user.userName}
|
||||
projectTitle={project.title}
|
||||
image64={partSnapShot64}
|
||||
>
|
||||
{children}
|
||||
</SocialCardCell>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button className="bg-gray-200 p-2 rounded-sm" onClick={onUpload}>
|
||||
save image
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ThumbnailViewer({ forwardRef, onUpload, children, partSnapShot64 }) {
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-2xl text-ch-gray-300 pt-4">Set thumbnail</h3>
|
||||
<div
|
||||
style={{ width: '500px', height: '375px' }}
|
||||
className="rounded-md shadow-ch border border-gray-400 overflow-hidden my-4"
|
||||
>
|
||||
<div className="h-full w-full relative" ref={forwardRef}>
|
||||
{children}
|
||||
{partSnapShot64 && (
|
||||
<img src={partSnapShot64} className="absolute inset-0" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button className="bg-gray-200 p-2 rounded-sm" onClick={onUpload}>
|
||||
save thumbnail
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function IsolatedCanvas({
|
||||
RenderComponent,
|
||||
canvasRatio = 1,
|
||||
size,
|
||||
uploadKey,
|
||||
}: {
|
||||
canvasRatio?: number
|
||||
uploadKey: 'socialCard64' | 'mainImage64'
|
||||
size: {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
RenderComponent: React.FC<{
|
||||
forwardRef: React.Ref<any>
|
||||
children: React.ReactNode
|
||||
partSnapShot64: string
|
||||
onUpload: (a: any) => void
|
||||
}>
|
||||
}) {
|
||||
const { project } = useIdeContext()
|
||||
const { updateProjectImages } = useUpdateProjectImages({})
|
||||
const [partSnapShot64, partSnapShot64Setter] = React.useState('')
|
||||
const [scadSnapShot64, scadSnapShot64Setter] = React.useState('')
|
||||
|
||||
const captureRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const threeInstance = React.useRef(null)
|
||||
const onInit = (_threeInstance) => (threeInstance.current = _threeInstance)
|
||||
const upload = async () => {
|
||||
const uploadPromise = new Promise((resolve, reject) => {
|
||||
const asyncHelper = async () => {
|
||||
if (!scadSnapShot64) {
|
||||
partSnapShot64Setter(
|
||||
await blobTo64(await canvasToBlob(threeInstance.current, size))
|
||||
)
|
||||
} else {
|
||||
partSnapShot64Setter(scadSnapShot64)
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
const capturedImage = await toJpeg(captureRef.current, {
|
||||
cacheBust: true,
|
||||
quality: 0.7,
|
||||
})
|
||||
await updateProjectImages({
|
||||
variables: {
|
||||
id: project?.id,
|
||||
[uploadKey]: capturedImage,
|
||||
},
|
||||
})
|
||||
partSnapShot64Setter('')
|
||||
resolve(capturedImage)
|
||||
})
|
||||
}
|
||||
asyncHelper()
|
||||
})
|
||||
toast.promise(uploadPromise, {
|
||||
loading: 'Saving Image',
|
||||
success: (finalImg: string) => (
|
||||
<div className="flex flex-col items-center">
|
||||
<b className="py-2">Image saved!</b>
|
||||
<img src={finalImg} />
|
||||
</div>
|
||||
),
|
||||
error: <b>Problem saving.</b>,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RenderComponent
|
||||
forwardRef={captureRef}
|
||||
onUpload={upload}
|
||||
partSnapShot64={partSnapShot64}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${size.width * canvasRatio}px`,
|
||||
height: `${size.height * canvasRatio}px`,
|
||||
}}
|
||||
>
|
||||
<CaptureButtonViewer
|
||||
onInit={onInit}
|
||||
onScadImage={scadSnapShot64Setter}
|
||||
canvasRatio={canvasRatio}
|
||||
/>
|
||||
</div>
|
||||
</RenderComponent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function CaptureButton({ TheButton }) {
|
||||
const { state, thunkDispatch } = useIdeContext()
|
||||
|
||||
return (
|
||||
<TheButton
|
||||
onClick={() => {
|
||||
thunkDispatch({
|
||||
type: 'addEditorModel',
|
||||
payload: {
|
||||
type: 'component',
|
||||
label: 'Social Media Card',
|
||||
Component: TabContent,
|
||||
},
|
||||
})
|
||||
thunkDispatch({
|
||||
type: 'switchEditorModel',
|
||||
payload: state.editorTabs.length,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { Listbox, Transition } from '@headlessui/react'
|
||||
import { CheckIcon, SelectorIcon } from '@heroicons/react/solid'
|
||||
|
||||
import { useRender } from 'src/components/IdeWrapper/useRender'
|
||||
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||
import { Switch } from '@headlessui/react'
|
||||
@@ -6,15 +9,28 @@ import {
|
||||
CadhubStringParam,
|
||||
CadhubBooleanParam,
|
||||
CadhubNumberParam,
|
||||
CadhubStringChoiceParam,
|
||||
CadhubNumberChoiceParam,
|
||||
} from './customizerConverter'
|
||||
|
||||
const Customizer = () => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [shouldLiveUpdate, setShouldLiveUpdate] = React.useState(false)
|
||||
const { state, thunkDispatch } = useIdeContext()
|
||||
const isOpen = state.isCustomizerOpen
|
||||
const customizerParams = state?.customizerParams
|
||||
const currentParameters = state?.currentParameters || {}
|
||||
const handleRender = useRender()
|
||||
const toggleOpen = () => {
|
||||
thunkDispatch({ type: 'setCustomizerOpenState', payload: !isOpen })
|
||||
if (state.viewerContext === 'ide') {
|
||||
// don't re-render on open/close in the project profile
|
||||
setTimeout(() => handleRender())
|
||||
}
|
||||
}
|
||||
const handleReset = () => {
|
||||
thunkDispatch({ type: 'resetCustomizer' })
|
||||
setTimeout(() => handleRender(true))
|
||||
}
|
||||
|
||||
const updateCustomizerParam = (paramName: string, paramValue: any) => {
|
||||
const payload = {
|
||||
@@ -28,20 +44,20 @@ const Customizer = () => {
|
||||
return (
|
||||
<div
|
||||
className={`absolute inset-x-0 bottom-0 bg-ch-gray-600 bg-opacity-60 text-ch-gray-300 text-lg font-fira-sans ${
|
||||
open ? 'h-2/3' : ''
|
||||
isOpen ? 'h-full max-h-96' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between px-6 py-2 items-center">
|
||||
<div className="grid grid-flow-col-dense gap-6 items-center">
|
||||
<button className="px-2" onClick={() => setOpen(!open)}>
|
||||
<button className="px-2" onClick={toggleOpen}>
|
||||
<Svg
|
||||
name="chevron-down"
|
||||
className={`h-8 w-8 ${!open && 'transform rotate-180'}`}
|
||||
className={`h-8 w-8 ${!isOpen && 'transform rotate-180'}`}
|
||||
/>
|
||||
</button>
|
||||
<div>Parameters</div>
|
||||
</div>
|
||||
{open && (
|
||||
{isOpen && (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<div className="font-fira-sans text-sm mr-4">Auto Update</div>
|
||||
@@ -61,11 +77,17 @@ const Customizer = () => {
|
||||
} inline-block w-4 h-4 transform bg-white rounded-full`}
|
||||
/>
|
||||
</Switch>
|
||||
<button
|
||||
className="px-4 py-1 rounded bg-ch-gray-300 text-ch-gray-600 mr-2"
|
||||
onClick={handleReset}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
className={`px-4 py-1 rounded bg-ch-gray-300 text-ch-gray-800 ${
|
||||
shouldLiveUpdate && 'bg-opacity-30 cursor-default'
|
||||
}`}
|
||||
onClick={handleRender}
|
||||
onClick={() => handleRender()}
|
||||
disabled={shouldLiveUpdate}
|
||||
>
|
||||
Update
|
||||
@@ -74,18 +96,29 @@ const Customizer = () => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${open ? 'h-full pb-32' : 'h-0'} overflow-y-auto px-12`}>
|
||||
<div
|
||||
className={`${isOpen ? 'h-full pb-32' : 'h-0'} overflow-y-auto px-12`}
|
||||
>
|
||||
<div>
|
||||
{customizerParams.map((param, index) => {
|
||||
const otherProps = {
|
||||
value: currentParameters[param.name],
|
||||
onChange: (value) => updateCustomizerParam(param.name, value),
|
||||
onChange: (value) =>
|
||||
updateCustomizerParam(
|
||||
param.name,
|
||||
param.type == 'number' ? Number(value) : value
|
||||
),
|
||||
}
|
||||
if (param.type === 'string') {
|
||||
if (
|
||||
param.input === 'choice-string' ||
|
||||
param.input === 'choice-number'
|
||||
) {
|
||||
return <ChoiceParam key={index} param={param} {...otherProps} />
|
||||
} else if (param.input === 'default-string') {
|
||||
return <StringParam key={index} param={param} {...otherProps} />
|
||||
} else if (param.type === 'number') {
|
||||
} else if (param.input === 'default-number') {
|
||||
return <NumberParam key={index} param={param} {...otherProps} />
|
||||
} else if (param.type === 'boolean') {
|
||||
} else if (param.input === 'default-boolean') {
|
||||
return <BooleanParam key={index} param={param} {...otherProps} />
|
||||
}
|
||||
return <div key={index}>{JSON.stringify(param)}</div>
|
||||
@@ -110,7 +143,7 @@ function CustomizerParamBase({
|
||||
return (
|
||||
<li
|
||||
className="grid items-center my-2"
|
||||
style={{ gridTemplateColumns: 'auto 8rem' }}
|
||||
style={{ gridTemplateColumns: 'auto 16rem' }}
|
||||
>
|
||||
<div className=" text-sm font-fira-sans">
|
||||
<div className="font-bold text-base">{name}</div>
|
||||
@@ -128,7 +161,7 @@ function BooleanParam({
|
||||
}: {
|
||||
param: CadhubBooleanParam
|
||||
value: any
|
||||
onChange: Function
|
||||
onChange: (value: any) => void
|
||||
}) {
|
||||
return (
|
||||
<CustomizerParamBase name={param.name} caption={param.caption}>
|
||||
@@ -158,7 +191,7 @@ function StringParam({
|
||||
}: {
|
||||
param: CadhubStringParam
|
||||
value: any
|
||||
onChange: Function
|
||||
onChange: (value: any) => void
|
||||
}) {
|
||||
return (
|
||||
<CustomizerParamBase name={param.name} caption={param.caption}>
|
||||
@@ -173,6 +206,79 @@ function StringParam({
|
||||
)
|
||||
}
|
||||
|
||||
function ChoiceParam({
|
||||
param,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
param: CadhubStringChoiceParam | CadhubNumberChoiceParam
|
||||
value: any
|
||||
onChange: (value: any) => void
|
||||
}) {
|
||||
return (
|
||||
<CustomizerParamBase name={param.name} caption={param.caption}>
|
||||
<Listbox value={value} onChange={onChange}>
|
||||
<div className="relative mt-1">
|
||||
<Listbox.Button className="relative w-full h-8 text-left cursor-default focus:outline-none focus-visible:ring-2 focus-visible:ring-opacity-75 focus-visible:ring-white focus-visible:ring-offset-orange-300 focus-visible:ring-offset-2 focus-visible:border-indigo-500 sm:text-sm border border-ch-gray-300 px-2 text-sm">
|
||||
<span className="block truncate">{value}</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-1 pointer-events-none">
|
||||
<SelectorIcon
|
||||
className="w-5 h-5 text-gray-300"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute w-full py-1 mt-1 bg-ch-gray-600 bg-opacity-80 overflow-auto text-base rounded-sm shadow-lg max-h-60 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
{param.options.map((option, optionIdx) => (
|
||||
<Listbox.Option
|
||||
key={optionIdx}
|
||||
className={({ active }) =>
|
||||
`${
|
||||
active
|
||||
? 'text-ch-blue-400 bg-ch-gray-700'
|
||||
: 'text-ch-gray-300'
|
||||
}
|
||||
cursor-default select-none relative py-2 pl-10 pr-4`
|
||||
}
|
||||
value={option.value}
|
||||
>
|
||||
{({ selected, active }) => (
|
||||
<>
|
||||
<span
|
||||
className={`${
|
||||
selected ? 'font-medium' : 'font-normal'
|
||||
} block truncate`}
|
||||
>
|
||||
{option.name}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span
|
||||
className={`${
|
||||
active ? 'text-ch-blue-400' : 'text-ch-gray-300'
|
||||
}
|
||||
absolute inset-y-0 left-0 flex items-center pl-3`}
|
||||
>
|
||||
<CheckIcon className="w-5 h-5" aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</CustomizerParamBase>
|
||||
)
|
||||
}
|
||||
|
||||
function NumberParam({
|
||||
param,
|
||||
value,
|
||||
@@ -180,10 +286,10 @@ function NumberParam({
|
||||
}: {
|
||||
param: CadhubNumberParam
|
||||
value: any
|
||||
onChange: Function
|
||||
onChange: (value: any) => void
|
||||
}) {
|
||||
const [isFocused, isFocusedSetter] = React.useState(false)
|
||||
const [localValue, localValueSetter] = React.useState(0)
|
||||
const [localValue, localValueSetter] = React.useState(value)
|
||||
const [isLocked, isLockedSetter] = React.useState(false)
|
||||
const [pixelsDragged, pixelsDraggedSetter] = React.useState(0)
|
||||
const step = param.step || 1
|
||||
@@ -198,7 +304,7 @@ function NumberParam({
|
||||
if (typeof param.max === 'number') {
|
||||
num = Math.min(param.max, num)
|
||||
}
|
||||
num = Number(num.toFixed(2))
|
||||
num = Number((num || 0).toFixed(2))
|
||||
localValueSetter(num)
|
||||
onChange(num)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,45 @@
|
||||
// CadHub
|
||||
|
||||
type CadhubTypeNames = 'number' | 'string' | 'boolean'
|
||||
type CadhubInputNames =
|
||||
| 'default-number'
|
||||
| 'default-string'
|
||||
| 'default-boolean'
|
||||
| 'choice-string'
|
||||
| 'choice-number'
|
||||
|
||||
export interface CadhubStringOption {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface CadhubNumberOption {
|
||||
name: string
|
||||
value: number
|
||||
}
|
||||
|
||||
interface CadhubParamBase {
|
||||
type: CadhubTypeNames
|
||||
caption: string
|
||||
name: string
|
||||
input: CadhubInputNames
|
||||
}
|
||||
|
||||
export interface CadhubStringParam extends CadhubParamBase {
|
||||
type: 'string'
|
||||
input: 'default-string'
|
||||
initial: string
|
||||
placeholder?: string
|
||||
maxLength?: number
|
||||
}
|
||||
export interface CadhubBooleanParam extends CadhubParamBase {
|
||||
type: 'boolean'
|
||||
input: 'default-boolean'
|
||||
initial?: boolean
|
||||
}
|
||||
export interface CadhubNumberParam extends CadhubParamBase {
|
||||
type: 'number'
|
||||
input: 'default-number'
|
||||
initial: number
|
||||
min?: number
|
||||
max?: number
|
||||
@@ -26,95 +47,22 @@ export interface CadhubNumberParam extends CadhubParamBase {
|
||||
decimal?: number
|
||||
}
|
||||
|
||||
export interface CadhubStringChoiceParam extends CadhubParamBase {
|
||||
type: 'string'
|
||||
input: 'choice-string'
|
||||
initial: string
|
||||
options: Array<CadhubStringOption>
|
||||
}
|
||||
export interface CadhubNumberChoiceParam extends CadhubParamBase {
|
||||
type: 'number'
|
||||
input: 'choice-number'
|
||||
initial: number
|
||||
options: Array<CadhubNumberOption>
|
||||
}
|
||||
|
||||
export type CadhubParams =
|
||||
| CadhubStringParam
|
||||
| CadhubBooleanParam
|
||||
| CadhubNumberParam
|
||||
|
||||
// OpenSCAD
|
||||
const openscadValues = `
|
||||
// slider widget for number with max. value
|
||||
sliderWithMax =34; // [50]
|
||||
|
||||
// slider widget for number in range
|
||||
sliderWithRange =34; // [10:100]
|
||||
|
||||
//step slider for number
|
||||
stepSlider=2; //[0:5:100]
|
||||
|
||||
// slider widget for number in range
|
||||
sliderCentered =0; // [-10:0.1:10]
|
||||
|
||||
// spinbox with step size 1
|
||||
Spinbox= 5;
|
||||
|
||||
// Text box for string
|
||||
String="hello";
|
||||
|
||||
// Text box for string with length 8
|
||||
String2="length"; //8
|
||||
|
||||
//description
|
||||
Variable = true;
|
||||
`
|
||||
|
||||
const openscadConverted: CadhubParams[] = [
|
||||
{
|
||||
type: 'number',
|
||||
name: 'sliderWithMax',
|
||||
caption: 'slider widget for number with max. value',
|
||||
initial: 34,
|
||||
step: 1,
|
||||
max: 50,
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'sliderWithRange',
|
||||
caption: 'slider widget for number in range',
|
||||
initial: 34,
|
||||
step: 1,
|
||||
min: 10,
|
||||
max: 100,
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'stepSlider',
|
||||
caption: 'step slider for number',
|
||||
initial: 2,
|
||||
step: 5,
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'sliderCentered',
|
||||
caption: 'slider widget for number in range',
|
||||
initial: 0,
|
||||
step: 0.1,
|
||||
min: -10,
|
||||
max: 10,
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'Spinbox',
|
||||
caption: 'spinbox with step size 1',
|
||||
initial: 5,
|
||||
step: 1,
|
||||
},
|
||||
|
||||
{
|
||||
type: 'string',
|
||||
name: 'String',
|
||||
caption: 'Text box for string',
|
||||
initial: 'hello',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'String2',
|
||||
caption: 'Text box for string with length 8',
|
||||
initial: 'length',
|
||||
maxLength: 8,
|
||||
},
|
||||
|
||||
{ type: 'boolean', name: 'Variable', caption: 'description', initial: true },
|
||||
]
|
||||
| CadhubStringChoiceParam
|
||||
| CadhubNumberChoiceParam
|
||||
|
||||
@@ -21,7 +21,7 @@ const DelayedPingAnimation = ({
|
||||
|
||||
if (showLoading && isLoading)
|
||||
return (
|
||||
<div className="inset-0 absolute flex items-center justify-center">
|
||||
<div className="inset-0 absolute flex items-center justify-center pointer-events-none">
|
||||
<div className="h-16 w-16 bg-pink-600 rounded-full animate-ping"></div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ const EditableProjectTitle = ({
|
||||
const [newTitle, setNewTitle] = useState(projectTitle)
|
||||
const inputRef = React.useRef(null)
|
||||
|
||||
const { updateProject, loading, error } = useUpdateProject({
|
||||
const { updateProject } = useUpdateProject({
|
||||
onCompleted: ({ updateProject }) => {
|
||||
const routeVars = {
|
||||
userName: updateProject.user.userName,
|
||||
@@ -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)
|
||||
|
||||
53
app/web/src/components/EditorGuide/EditorGuide.tsx
Normal file
53
app/web/src/components/EditorGuide/EditorGuide.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useMarkdownMetaData } from 'src/helpers/hooks/useMarkdownMetaData'
|
||||
import Editor from 'rich-markdown-editor'
|
||||
import { useRef } from 'react'
|
||||
import KeyValue from 'src/components/KeyValue/KeyValue'
|
||||
|
||||
export default function EditorGuide({ content }) {
|
||||
const [rawMetadata, metadata] = useMarkdownMetaData(content)
|
||||
|
||||
const processedContent = rawMetadata
|
||||
? content.replace(rawMetadata[0], '')
|
||||
: content
|
||||
const ref = useRef(null)
|
||||
|
||||
return (
|
||||
<div className="markdown-overrides py-6 px-8">
|
||||
{metadata && (
|
||||
<>
|
||||
<h1 className="my-4">{metadata.title}</h1>
|
||||
<section className="grid grid-cols-3 my-6 gap-y-4">
|
||||
{Object.entries(metadata)
|
||||
.filter(([key]) => key !== 'title')
|
||||
.map(([key, value], i) => (
|
||||
<KeyValue keyName={key.replace(/"/g, '')} key={key + '-' + i}>
|
||||
<LinkOrParagraph>{value}</LinkOrParagraph>
|
||||
</KeyValue>
|
||||
))}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
<Editor
|
||||
ref={ref}
|
||||
readOnly={true}
|
||||
defaultValue={processedContent}
|
||||
value={processedContent}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LinkOrParagraph({ children }) {
|
||||
const markdownUrlExpression =
|
||||
/\[(.*)\]\((https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})\)/i
|
||||
const matches = children.match(markdownUrlExpression)
|
||||
|
||||
return matches === null ? (
|
||||
<p>{children}</p>
|
||||
) : (
|
||||
<a href={matches[2]} rel="noopener noreferrer" target="_blank">
|
||||
{matches[1]}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
68
app/web/src/components/EditorMenu/AllShortcutsModal.tsx
Normal file
68
app/web/src/components/EditorMenu/AllShortcutsModal.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Dialog from '@material-ui/core/Dialog'
|
||||
import { editorMenuConfig } from './menuConfig'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
transform: `translate3d(0,0,50px)`,
|
||||
},
|
||||
})
|
||||
|
||||
interface ShortcutsModalContextType {
|
||||
open: boolean
|
||||
toggleOpen: () => any
|
||||
}
|
||||
|
||||
export const ShortcutsModalContext = createContext<ShortcutsModalContextType>({
|
||||
open: false,
|
||||
toggleOpen: () => {},
|
||||
})
|
||||
|
||||
export function useShortcutsModalContext() {
|
||||
return useContext(ShortcutsModalContext)
|
||||
}
|
||||
|
||||
const AllShortcutsModal = () => {
|
||||
const classes = useStyles()
|
||||
const { open, toggleOpen } = useShortcutsModalContext()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => toggleOpen()}
|
||||
className={classes.root + ' bg-transparent'}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="bg-ch-gray-700 font-fira-sans shadow-lg text-ch-gray-300 p-4">
|
||||
<h2 className="text-2xl mb-4">All Shortcuts</h2>
|
||||
{editorMenuConfig
|
||||
.filter((menu) => menu.items.length)
|
||||
.map((menu) => (
|
||||
<section key={'allshortcuts-' + menu.name} className="my-6">
|
||||
<h3 className="text-xl border-b-2 pb-2 mb-2">{menu.label}</h3>
|
||||
{menu.items.map((item) => (
|
||||
<div
|
||||
className="flex gap-16 justify-between"
|
||||
key={'allshortcuts-' + menu.name + '-' + item.label}
|
||||
>
|
||||
<p>{item.label}</p>
|
||||
<span className="text-right font-fira-code text-ch-gray-400">
|
||||
{item.shortcutLabel}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AllShortcutsModal
|
||||
71
app/web/src/components/EditorMenu/Dropdowns.tsx
Normal file
71
app/web/src/components/EditorMenu/Dropdowns.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Menu } from '@headlessui/react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
||||
export function DropdownItem({ config, state, thunkDispatch }) {
|
||||
useHotkeys(config.shortcut, handleClick)
|
||||
|
||||
function handleClick(e) {
|
||||
e.preventDefault()
|
||||
config.callback(e, { state, thunkDispatch })
|
||||
}
|
||||
return (
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={`${
|
||||
active && 'bg-gray-600'
|
||||
} px-2 py-1 flex justify-between`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{config.label}
|
||||
{config.shortcutLabel && (
|
||||
<span className="text-gray-400 pl-6 text-right">
|
||||
{config.shortcutLabel}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export function Dropdown({
|
||||
label,
|
||||
disabled,
|
||||
children,
|
||||
}: {
|
||||
label: string
|
||||
disabled: boolean
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<Menu>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Menu.Button
|
||||
className={
|
||||
'text-gray-100' +
|
||||
(disabled ? ' text-gray-400 cursor-not-allowed' : '')
|
||||
}
|
||||
disabled={disabled}
|
||||
>
|
||||
{label}
|
||||
</Menu.Button>
|
||||
{children && (
|
||||
<Menu.Items
|
||||
static
|
||||
className={
|
||||
(open ? '' : 'hidden ') +
|
||||
'absolute flex flex-col mt-4 bg-ch-gray-760 rounded text-gray-100 overflow-hidden whitespace-nowrap border border-ch-gray-700'
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Menu.Items>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,133 +1,75 @@
|
||||
import { Menu } from '@headlessui/react'
|
||||
|
||||
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||
import Svg from 'src/components/Svg/Svg'
|
||||
import { useRender } from 'src/components/IdeWrapper/useRender'
|
||||
import { makeStlDownloadHandler, PullTitleFromFirstLine } from './helpers'
|
||||
import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode'
|
||||
import CadPackage from 'src/components/CadPackage/CadPackage'
|
||||
import { editorMenuConfig } from './menuConfig'
|
||||
import AllShortcutsModal from './AllShortcutsModal'
|
||||
import { Dropdown } from './Dropdowns'
|
||||
|
||||
const EditorMenu = () => {
|
||||
const handleRender = useRender()
|
||||
const saveCode = useSaveCode()
|
||||
const { state, thunkDispatch } = useIdeContext()
|
||||
const onRender = () => {
|
||||
handleRender()
|
||||
saveCode({ code: state.code })
|
||||
}
|
||||
const handleStlDownload = makeStlDownloadHandler({
|
||||
type: state.objectData?.type,
|
||||
ideType: state.ideType,
|
||||
geometry: state.objectData?.data,
|
||||
quality: state.objectData?.quality,
|
||||
fileName: PullTitleFromFirstLine(state.code || ''),
|
||||
thunkDispatch,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex justify-between bg-ch-gray-760 text-gray-100">
|
||||
<div className="flex items-center h-9 w-full cursor-grab">
|
||||
<div className=" text-ch-gray-760 bg-ch-gray-300 cursor-grab px-2 h-full flex items-center">
|
||||
<Svg name="drag-grid" className="w-4 p-px" />
|
||||
</div>
|
||||
<div className="grid grid-flow-col-dense gap-6 px-5">
|
||||
<FileDropdown
|
||||
handleRender={onRender}
|
||||
handleStlDownload={handleStlDownload}
|
||||
/>
|
||||
<button className="cursor-not-allowed" disabled>
|
||||
Edit
|
||||
<>
|
||||
<div className="flex justify-between bg-ch-gray-760 text-gray-100">
|
||||
<div className="flex items-center h-9 w-full cursor-grab">
|
||||
<div className=" text-ch-gray-760 bg-ch-gray-300 cursor-grab px-2 h-full flex items-center">
|
||||
<Svg name="drag-grid" className="w-4 p-px" />
|
||||
</div>
|
||||
<div className="grid grid-flow-col-dense gap-6 px-5">
|
||||
{editorMenuConfig.map((menu) => (
|
||||
<Dropdown
|
||||
label={menu.label}
|
||||
disabled={menu.disabled}
|
||||
key={menu.label + '-dropdown'}
|
||||
>
|
||||
{menu.items.map((itemConfig) => (
|
||||
<itemConfig.Component
|
||||
state={state}
|
||||
thunkDispatch={thunkDispatch}
|
||||
config={itemConfig}
|
||||
key={menu.label + '-' + itemConfig.label}
|
||||
/>
|
||||
))}
|
||||
</Dropdown>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
className="text-ch-gray-300 h-full"
|
||||
aria-label="editor settings"
|
||||
onClick={() =>
|
||||
thunkDispatch((dispatch) =>
|
||||
dispatch({
|
||||
type: 'settingsButtonClicked',
|
||||
payload: ['Settings', 'editor'],
|
||||
})
|
||||
)
|
||||
}
|
||||
>
|
||||
<Svg name="gear" className="w-6 p-px" />
|
||||
</button>
|
||||
<ViewDropdown
|
||||
handleLayoutReset={() => thunkDispatch({ type: 'resetLayout' })}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="text-ch-gray-300 h-full cursor-not-allowed"
|
||||
aria-label="editor settings"
|
||||
disabled
|
||||
>
|
||||
<Svg name="gear" className="w-6 p-px" />
|
||||
</button>
|
||||
<CadPackage
|
||||
cadPackage={state.ideType}
|
||||
className="px-3"
|
||||
onClick={() => {
|
||||
thunkDispatch({
|
||||
type: 'addEditorModel',
|
||||
payload: {
|
||||
type: 'guide',
|
||||
label: 'Guide',
|
||||
content: state.ideGuide,
|
||||
},
|
||||
})
|
||||
thunkDispatch({
|
||||
type: 'switchEditorModel',
|
||||
payload: state.editorTabs.length,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<CadPackage cadPackage={state.ideType} className="px-3" />
|
||||
</div>
|
||||
<AllShortcutsModal />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditorMenu
|
||||
|
||||
function FileDropdown({ handleRender, handleStlDownload }) {
|
||||
return (
|
||||
<Dropdown name="File">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={`${active && 'bg-gray-600'} px-2 py-1`}
|
||||
onClick={handleRender}
|
||||
>
|
||||
Save & Render{' '}
|
||||
<span className="text-gray-400 pl-4">
|
||||
{/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? (
|
||||
<>
|
||||
<Svg
|
||||
name="mac-cmd-key"
|
||||
className="h-3 w-3 inline-block text-left"
|
||||
/>
|
||||
S
|
||||
</>
|
||||
) : (
|
||||
'Ctrl S'
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={`${active && 'bg-gray-600'} px-2 py-1 text-left`}
|
||||
onClick={handleStlDownload}
|
||||
>
|
||||
Download STL
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
function ViewDropdown({ handleLayoutReset }) {
|
||||
return (
|
||||
<Dropdown name="View">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={`${active && 'bg-gray-600'} px-2 py-1`}
|
||||
onClick={handleLayoutReset}
|
||||
>
|
||||
Reset layout
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
function Dropdown({
|
||||
name,
|
||||
children,
|
||||
}: {
|
||||
name: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<Menu>
|
||||
<Menu.Button className="text-gray-100">{name}</Menu.Button>
|
||||
<Menu.Items className="absolute flex flex-col mt-4 bg-ch-gray-760 rounded text-gray-100 overflow-hidden whitespace-nowrap border border-ch-gray-700">
|
||||
{children}
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
121
app/web/src/components/EditorMenu/menuConfig.tsx
Normal file
121
app/web/src/components/EditorMenu/menuConfig.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React from 'react'
|
||||
import { useRender } from 'src/components/IdeWrapper/useRender'
|
||||
import { makeStlDownloadHandler, PullTitleFromFirstLine } from 'src/helpers/download_stl'
|
||||
import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode'
|
||||
import { DropdownItem } from './Dropdowns'
|
||||
import { useShortcutsModalContext } from './AllShortcutsModal'
|
||||
import type { State } from 'src/helpers/hooks/useIdeState'
|
||||
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||
|
||||
export function cmdOrCtrl() {
|
||||
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? '⌘' : 'Ctrl'
|
||||
}
|
||||
|
||||
const fileMenuConfig: EditorMenuConfig = {
|
||||
name: 'file',
|
||||
label: 'File',
|
||||
disabled: false,
|
||||
items: [
|
||||
{
|
||||
label: 'Save & Render',
|
||||
shortcut: 'ctrl+s, command+s',
|
||||
shortcutLabel: cmdOrCtrl() + ' S',
|
||||
Component: (props) => {
|
||||
const { state, config } = props
|
||||
const handleRender = useRender()
|
||||
const saveCode = useSaveCode()
|
||||
function onRender(e) {
|
||||
e.preventDefault()
|
||||
handleRender()
|
||||
saveCode({ code: state.code })
|
||||
}
|
||||
|
||||
config.callback = onRender
|
||||
|
||||
return <DropdownItem {...props} />
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Download STL',
|
||||
shortcut: 'ctrl+shift+d, command+shift+d',
|
||||
shortcutLabel: cmdOrCtrl() + ' Shift D',
|
||||
Component: (props) => {
|
||||
const { state, thunkDispatch, config } = props
|
||||
const { project } = useIdeContext()
|
||||
const handleStlDownload = makeStlDownloadHandler({
|
||||
type: state.objectData?.type,
|
||||
ideType: state.ideType,
|
||||
geometry: state.objectData?.data,
|
||||
quality: state.objectData?.quality,
|
||||
fileName: project
|
||||
? `${project.title}.stl`
|
||||
: PullTitleFromFirstLine(state.code || ''),
|
||||
thunkDispatch,
|
||||
})
|
||||
|
||||
config.callback = handleStlDownload
|
||||
|
||||
return <DropdownItem {...props} />
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const editMenuConfig: EditorMenuConfig = {
|
||||
name: 'edit',
|
||||
label: 'Edit',
|
||||
disabled: true,
|
||||
items: [],
|
||||
}
|
||||
|
||||
const viewMenuConfig: EditorMenuConfig = {
|
||||
name: 'view',
|
||||
label: 'View',
|
||||
disabled: false,
|
||||
items: [
|
||||
{
|
||||
label: 'Reset layout',
|
||||
shortcut: 'ctrl+shift+r',
|
||||
shortcutLabel: 'Ctrl Shift R',
|
||||
Component: (props) => {
|
||||
const { config, thunkDispatch } = props
|
||||
config.callback = () => thunkDispatch({ type: 'resetLayout' })
|
||||
return <DropdownItem {...props} />
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'All shortcuts',
|
||||
shortcut: 'ctrl+shift+/',
|
||||
shortcutLabel: 'Ctrl Shift /',
|
||||
Component: (props) => {
|
||||
const { config } = props
|
||||
const { toggleOpen } = useShortcutsModalContext()
|
||||
config.callback = toggleOpen
|
||||
return <DropdownItem {...props} />
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const editorMenuConfig = [fileMenuConfig, editMenuConfig, viewMenuConfig]
|
||||
|
||||
interface EditorMenuItemConfigBase {
|
||||
label: string
|
||||
shortcut: string
|
||||
shortcutLabel: React.ReactElement | string
|
||||
callback?: (...a: any[]) => void
|
||||
}
|
||||
export interface EditorMenuItemConfig extends EditorMenuItemConfigBase {
|
||||
Component: React.FC<{
|
||||
config: EditorMenuItemConfigBase
|
||||
state: State
|
||||
thunkDispatch: any
|
||||
}>
|
||||
}
|
||||
|
||||
export interface EditorMenuConfig {
|
||||
name: string
|
||||
label: string
|
||||
disabled: boolean
|
||||
items: Array<EditorMenuItemConfig>
|
||||
}
|
||||
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
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { useIdeContext, ideTypeNameMap } from 'src/helpers/hooks/useIdeContext'
|
||||
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||
import { cadPackageConfigs } from 'src/components/CadPackage/CadPackage'
|
||||
import OutBound from 'src/components/OutBound/OutBound'
|
||||
import { prepareEncodedUrl, makeExternalUrl } from './helpers'
|
||||
import { copyTextToClipboard } from 'src/helpers/clipboard'
|
||||
@@ -11,10 +12,11 @@ const ExternalScript = () => {
|
||||
const handleRender = useRender()
|
||||
const [rawUrl, setRawUrl] = useState('')
|
||||
const [script, setScript] = useState('')
|
||||
const [asyncState, setAsyncState] =
|
||||
useState<'INIT' | 'SUCCESS' | 'ERROR' | 'LOADING'>('INIT')
|
||||
const [asyncState, setAsyncState] = useState<
|
||||
'INIT' | 'SUCCESS' | 'ERROR' | 'LOADING'
|
||||
>('INIT')
|
||||
|
||||
const cadName = ideTypeNameMap[state.ideType]
|
||||
const cadName = cadPackageConfigs[state.ideType].label
|
||||
|
||||
const onPaste: React.ClipboardEventHandler<HTMLInputElement> = async ({
|
||||
clipboardData,
|
||||
@@ -53,7 +55,7 @@ const ExternalScript = () => {
|
||||
}
|
||||
return (
|
||||
<div className="p-4">
|
||||
<p className="text-sm pb-4">
|
||||
<p className="text-sm pb-4 border-b border-gray-700">
|
||||
Paste an external url containing a {cadName} script to generate a new
|
||||
CadHub url for this resource.{' '}
|
||||
<OutBound
|
||||
@@ -66,9 +68,9 @@ const ExternalScript = () => {
|
||||
</p>
|
||||
{['INIT', 'ERROR'].includes(asyncState) && (
|
||||
<>
|
||||
<p>Paste url</p>
|
||||
<p className="mt-4">Paste url</p>
|
||||
<input
|
||||
className="p-1 text-xs rounded border border-gray-700 w-full"
|
||||
className="p-1 text-xs border border-ch-purple-450 w-full"
|
||||
value={rawUrl}
|
||||
onChange={onChange}
|
||||
onPaste={onPaste}
|
||||
@@ -91,23 +93,23 @@ const ExternalScript = () => {
|
||||
<input
|
||||
value={makeExternalUrl(rawUrl).replace(/^.+:\/\//g, '')}
|
||||
readOnly
|
||||
className="p-1 mt-4 text-xs rounded-t border border-gray-700 w-full"
|
||||
className="py-1 px-2 mt-4 text-xs border border-ch-purple-450 w-full"
|
||||
/>
|
||||
<button
|
||||
className="w-full bg-gray-700 py-1 rounded-b text-gray-300"
|
||||
className="w-full bg-ch-purple-450 hover:bg-ch-purple-400 py-1 text-gray-300"
|
||||
onClick={() => copyTextToClipboard(makeExternalUrl(rawUrl))}
|
||||
>
|
||||
Copy URL
|
||||
</button>
|
||||
<div className="flex flex-col gap-2 pt-2">
|
||||
<button
|
||||
className="bg-gray-500 p-1 px-2 rounded text-gray-300"
|
||||
className="bg-gray-500 hover:bg-gray-600 p-1 px-2 text-gray-200"
|
||||
onClick={onCopyRender}
|
||||
>
|
||||
Copy & Render
|
||||
</button>
|
||||
<button
|
||||
className="bg-gray-500 p-1 px-2 rounded text-gray-300"
|
||||
className="bg-gray-500 hover:bg-gray-600 p-1 px-2 text-gray-200"
|
||||
onClick={() => {
|
||||
setAsyncState('INIT')
|
||||
setRawUrl('')
|
||||
|
||||
@@ -13,10 +13,10 @@ const FullScriptEncoding = () => {
|
||||
<input
|
||||
value={encodedLink.replace(/^.+:\/\//g, '')}
|
||||
readOnly
|
||||
className="p-1 mt-4 text-xs rounded-t border border-gray-700 w-full"
|
||||
className="py-1 px-2 mt-4 text-xs border border-ch-purple-450 w-full"
|
||||
/>
|
||||
<button
|
||||
className="w-full bg-gray-700 py-1 rounded-b text-gray-300"
|
||||
className="w-full bg-ch-purple-450 hover:bg-ch-purple-400 py-1 text-gray-300"
|
||||
onClick={() => copyTextToClipboard(encodedLink)}
|
||||
>
|
||||
Copy URL
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||
import { useRender } from 'src/components/IdeWrapper/useRender'
|
||||
import { encode, decode } from 'src/helpers/compress'
|
||||
import { isBrowser } from '@redwoodjs/prerender/browserUtils'
|
||||
import type { State } from 'src/helpers/hooks/useIdeState'
|
||||
|
||||
const scriptKey = 'encoded_script'
|
||||
const scriptKeyV2 = 'encoded_script_v2'
|
||||
@@ -32,13 +33,17 @@ export function makeExternalUrl(resourceUrl: string): string {
|
||||
}#${fetchText}=${prepareDecodedUrl(resourceUrl)}`
|
||||
}
|
||||
|
||||
export function useIdeInit(cadPackage: string, code = '') {
|
||||
export function useIdeInit(
|
||||
cadPackage: State['ideType'],
|
||||
code = '',
|
||||
viewerContext: State['viewerContext'] = 'ide'
|
||||
) {
|
||||
const { thunkDispatch } = useIdeContext()
|
||||
const handleRender = useRender()
|
||||
useEffect(() => {
|
||||
thunkDispatch({
|
||||
type: 'initIde',
|
||||
payload: { cadPackage, code },
|
||||
payload: { cadPackage, code, viewerContext },
|
||||
})
|
||||
if (code) {
|
||||
return
|
||||
|
||||
@@ -2,7 +2,34 @@ import { FatalErrorBoundary as FatalErrorBoundaryBase } from '@redwoodjs/web'
|
||||
import * as Sentry from '@sentry/browser'
|
||||
|
||||
class FatalErrorBoundary extends FatalErrorBoundaryBase {
|
||||
componentDidCatch(error, errorInfo) {
|
||||
async componentDidCatch(error, errorInfo) {
|
||||
// debug netlify prerender code below
|
||||
// const div = document.createElement('div')
|
||||
// div.innerHTML = JSON.stringify(error)
|
||||
// document.body.append(div)
|
||||
|
||||
/* More debug explanation.
|
||||
If there's an error in netlify's prerendering service,
|
||||
we don't have access to the log so we have to spin it up locally to check.
|
||||
This can be with the following commands
|
||||
```
|
||||
$ git clone https://github.com/netlify/prerender.git
|
||||
$ cd prerender
|
||||
```
|
||||
comment out the lines `server.use(require("./lib/plugins/basicAuth"));` and `server.use(require("./lib/plugins/s3HtmlCache"));` in `server.js`
|
||||
then
|
||||
```
|
||||
$ npm install
|
||||
$ npm start
|
||||
```
|
||||
This will spin up the service on port 3000, prerendering can than be tested with
|
||||
http://localhost:3000/https://cadhub.xyz
|
||||
or
|
||||
http://localhost:3000/http://localhost:8910/
|
||||
where the second url is the route you want to test.
|
||||
However we don't have access to the console since it's run by a separate chrome instance,
|
||||
so instead errors are put into the DOM
|
||||
*/
|
||||
Sentry.withScope((scope) => {
|
||||
scope.setExtras(errorInfo)
|
||||
Sentry.captureException(error)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import Footer from './Footer'
|
||||
|
||||
export const generated = () => {
|
||||
return <Footer />
|
||||
}
|
||||
|
||||
export default { title: 'Components/Footer' }
|
||||
@@ -3,7 +3,7 @@ import OutBound from 'src/components/OutBound'
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<div className="bg-indigo-900 text-indigo-200 font-roboto mt-20 text-sm">
|
||||
<div className="bg-indigo-900 text-indigo-200 font-roboto text-sm">
|
||||
<div className="flex h-16 md:justify-end items-center mx-2 md:mx-16 flex-wrap">
|
||||
<OutBound className="mr-8" to="https://github.com/Irev-Dev/cadhub">
|
||||
Github
|
||||
111
app/web/src/components/Hero/AssetWithGooey.tsx
Normal file
111
app/web/src/components/Hero/AssetWithGooey.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { useRef, useMemo } from 'react'
|
||||
import * as THREE from 'three'
|
||||
import { useLoader, useThree, useFrame } from '@react-three/fiber'
|
||||
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
|
||||
import { useEdgeSplit } from 'src/helpers/hooks/useEdgeSplit'
|
||||
import texture from 'src/components/IdeViewer/dullFrontLitMetal.png'
|
||||
import { MeshDistortMaterial, Sphere, useTexture } from '@react-three/drei'
|
||||
|
||||
const thresholdAngle = 10
|
||||
export default function AssetWithGooey({
|
||||
assetUrl,
|
||||
scale,
|
||||
}: {
|
||||
assetUrl: string
|
||||
scale: number
|
||||
}) {
|
||||
const geo = useLoader(STLLoader, assetUrl)
|
||||
const edgeRef = useRef(null)
|
||||
const coffeeRef = useRef(null)
|
||||
const mesh = useEdgeSplit((thresholdAngle * Math.PI) / 180, true, geo)
|
||||
const colorMap = useTexture(texture)
|
||||
const edges = React.useMemo(() => new THREE.EdgesGeometry(geo, 12), [geo])
|
||||
const position = [0, 0, 5]
|
||||
const scaleArr = Array.from({ length: 3 }).map(() => scale)
|
||||
const { mouse } = useThree()
|
||||
const [rEuler, rQuaternion] = useMemo(
|
||||
() => [new THREE.Euler(), new THREE.Quaternion()],
|
||||
[]
|
||||
)
|
||||
useFrame((state, delta) => {
|
||||
if (edgeRef.current) {
|
||||
edgeRef.current.rotation.y += 0.01
|
||||
}
|
||||
if (coffeeRef.current) {
|
||||
rEuler.set((-mouse.y * Math.PI) / 4, (mouse.x * Math.PI) / 2, 0)
|
||||
coffeeRef.current.quaternion.slerp(rQuaternion.setFromEuler(rEuler), 0.1)
|
||||
}
|
||||
})
|
||||
return (
|
||||
<group dispose={null} ref={edgeRef} position={position}>
|
||||
<group ref={coffeeRef}>
|
||||
<mesh ref={mesh} scale={scaleArr} geometry={geo}>
|
||||
<meshPhysicalMaterial
|
||||
color="#FF6EBD"
|
||||
map={colorMap}
|
||||
clearcoat={0.5}
|
||||
clearcoatRoughness={0.01}
|
||||
roughness={0}
|
||||
metalness={0.7}
|
||||
smoothShading
|
||||
/>
|
||||
</mesh>
|
||||
<lineSegments scale={scale} geometry={edges} renderOrder={100}>
|
||||
<lineBasicMaterial color="#aaaaff" />
|
||||
</lineSegments>
|
||||
</group>
|
||||
<ambientLight intensity={2} />
|
||||
<Gooey />
|
||||
<ambientLight intensity={1.8} />
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
function randomSign(num: number): number {
|
||||
return Math.random() > 0.5 ? num : -num
|
||||
}
|
||||
|
||||
function Gooey() {
|
||||
const blobsData = useMemo(() => {
|
||||
const firstSet = Array.from({ length: 5 }).map((_, index) => {
|
||||
const dist = Math.random() * 3 + 2.5
|
||||
const x = randomSign(Math.random() * dist)
|
||||
const y = randomSign(Math.sqrt(dist * dist - x * x))
|
||||
const z = randomSign(Math.random() * 2)
|
||||
const position: [number, number, number] = [x, z, y]
|
||||
const size = Math.random() * 0.8 + 0.1
|
||||
const distort = size > 0.1 ? Math.random() * 0.6 * size + 0.2 : 0
|
||||
const speed = size > 0.1 ? (Math.random() * 0.8) / size / size + 0.1 : 0
|
||||
return { position, size, distort, speed }
|
||||
})
|
||||
const secondSet = Array.from({ length: 5 }).map((_, index) => {
|
||||
const dist = Math.random() * 3 + 1.5
|
||||
const x = randomSign(Math.random() * dist)
|
||||
const y = randomSign(Math.sqrt(dist * dist - x * x))
|
||||
const z = randomSign(Math.random() * 2)
|
||||
const position: [number, number, number] = [x, z, y]
|
||||
const size = Math.random() * 0.2 + 0.05
|
||||
const distort = size > 0.1 ? Math.random() * 0.8 * size + 0.2 : 0
|
||||
const speed = size > 0.1 ? (Math.random() * 0.5) / size / size + 0.1 : 0
|
||||
return { position, size, distort, speed }
|
||||
})
|
||||
return [...firstSet, ...secondSet]
|
||||
}, [])
|
||||
return (
|
||||
<>
|
||||
{blobsData.map(({ position, size, distort, speed }, index) => (
|
||||
<Sphere key={index} visible position={position} args={[size, 16, 200]}>
|
||||
<MeshDistortMaterial
|
||||
color="#173E6F"
|
||||
attach="material"
|
||||
distort={distort} // Strength, 0 disables the effect (default=1)
|
||||
speed={speed} // Speed (default=1)
|
||||
roughness={0.2}
|
||||
opacity={0.6}
|
||||
transparent
|
||||
/>
|
||||
</Sphere>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
613
app/web/src/components/Hero/Hero.tsx
Normal file
613
app/web/src/components/Hero/Hero.tsx
Normal file
@@ -0,0 +1,613 @@
|
||||
import { Canvas, useLoader, useFrame } from '@react-three/fiber'
|
||||
import { Suspense } from 'react'
|
||||
import { Html, Stats } from '@react-three/drei'
|
||||
import CadPackage, {
|
||||
CadPackageType,
|
||||
} from 'src/components/CadPackage/CadPackage'
|
||||
import { navigate, routes, Link } from '@redwoodjs/router'
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
|
||||
import Svg, { SvgNames } from 'src/components/Svg/Svg'
|
||||
import Gravatar from 'src/components/Gravatar/Gravatar'
|
||||
import ProjectsCell from 'src/components/ProjectsCell'
|
||||
import OutBound from 'src/components/OutBound/OutBound'
|
||||
import { DynamicProjectButton } from 'src/components/NavPlusButton/NavPlusButton'
|
||||
import FatalErrorBoundary from 'src/components/FatalErrorBoundary/FatalErrorBoundary'
|
||||
|
||||
// dynamic import to enable pre-render iof the homepage
|
||||
const AssetWithGooey = React.lazy(
|
||||
() => import('src/components/Hero/AssetWithGooey')
|
||||
)
|
||||
const cqCode = `module beam(r1, r2, shr, msr){
|
||||
/* The walking beam acts as a class I lever transferring the
|
||||
* movement from the pitmans arms to the horse head. */
|
||||
|
||||
H = 12; // Height
|
||||
W = 10; // Width
|
||||
e = 10; // Total added extension
|
||||
|
||||
difference(){
|
||||
union(){
|
||||
translate([(r2-r1)/2,0,H/2]) // Walking beam body
|
||||
cube([r1+r2+e, W, H], center = true);
|
||||
|
||||
rotate([90, 0, 0]) // Fulcrum or pivoting point
|
||||
cylinder(r = 2*msr, h = W, center = true);
|
||||
}
|
||||
|
||||
rotate([90,0,0]) // Pivoting point hole
|
||||
cylinder(r = msr, h = W+1, center = true);
|
||||
|
||||
translate([r2,0,H/2]) // Equalizer mounting screw hole
|
||||
cylinder(r = shr, h = H+1, center = true);
|
||||
`.split('\n')
|
||||
|
||||
const scadCode = `hingeHalfExtrudeLength=hingeLength/2-clearance/2;
|
||||
mountingHoleMoveIncrement=(hingeLength-2*mountingHoleEdgeOffset)/
|
||||
(mountingHoleCount-1);
|
||||
|
||||
module costomizerEnd() {}
|
||||
$fn=30;
|
||||
tiny=0.005;
|
||||
// modules
|
||||
module hingeBaseProfile() {
|
||||
translate([pivotRadius,0,0]){
|
||||
square([baseWidth,baseThickness]);
|
||||
}
|
||||
}
|
||||
|
||||
module hingeBodyHalf() {
|
||||
difference() {
|
||||
union() {
|
||||
linear_extrude(hingeHalfExtrudeLength){
|
||||
offset(1)offset(-2)offset(1){
|
||||
translate([0,pivotRadius,0]){
|
||||
circle(pivotRadius);
|
||||
}
|
||||
square([pivotRadius,pivotRadius]);
|
||||
hingeBaseProfile();
|
||||
}
|
||||
}
|
||||
linear_extrude(hingeLength){
|
||||
offset(1)offset(-1)hingeBaseProfile();
|
||||
}
|
||||
}
|
||||
plateHoles();
|
||||
}
|
||||
}`.split('\n')
|
||||
|
||||
export const Hero = () => {
|
||||
return (
|
||||
<div className="bg-ch-gray-800">
|
||||
<div className="relative h-0 w-0">
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<clipPath
|
||||
id="code-blob-clip-path"
|
||||
clipPathUnits="objectBoundingBox"
|
||||
transform="scale(0.0038 0.0056)"
|
||||
>
|
||||
<path
|
||||
d="M68.5 169.159C13.3 167.159 5.69181e-05 144.659 0 71.1594C3.99994 13.1594 50.9244 -14.591 121.5 7.65941C223 39.6594 266 25.1594 263.5 113.659C261.634 179.701 191.5 173.616 68.5 169.159Z"
|
||||
fill="#C4C4C4"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="grid lg:grid-cols-5 max-w-8xl mx-auto">
|
||||
<div className="relative row-start-2 col-start-1 h-full lg:row-start-1 lg:col-span-3 lg:col-start-1 z-10">
|
||||
<div
|
||||
className="absolute inset-0 my-20 mx-10 lg:mr-40 bg-gradient-to-tr from-pink-400 to-blue-600 opacity-40 overflow-hidden"
|
||||
style={{ clipPath: 'url(#code-blob-clip-path)' }}
|
||||
>
|
||||
<pre className="lg:ml-20 mt-12 text-blue-100 font-fira-code">
|
||||
{cqCode.map((line, index) => (
|
||||
<div key={index}>
|
||||
<span className="w-12 pr-6 text-blue-200 text-opacity-50 inline-block text-right">
|
||||
{index + 1}
|
||||
</span>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
<ModelSection assetUrl="/pumpjack.stl" scale={0.04} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-end justify-center row-start-2 col-start-1 pt-96 pr-12 pl-6 pb-24 lg:col-span-3 lg:col-start-1 lg:row-start-1 lg:pt-0 pointer-events-none">
|
||||
<Link
|
||||
to={routes.project({
|
||||
userName: 'matiasmiche',
|
||||
projectTitle: 'oil-pumpjack',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className="grid grid-flow-col gap-2 sm:gap-4 items-center bg-ch-gray-760 bg-opacity-95 text-ch-gray-300 rounded-md p-2 font-fira-sans relative z-10 shadow-ch pointer-events-auto"
|
||||
style={{
|
||||
transform: 'translate3d(3vw, -100px, 0.3px) scale(0.7)',
|
||||
transformOrigin: 'top center',
|
||||
}}
|
||||
>
|
||||
<div className="pl-1 sm:pl-4">
|
||||
<Gravatar
|
||||
image="CadHub/jjze0hyqncxvkvsg4agz"
|
||||
className="w-12 h-12 mr-4"
|
||||
size={60}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl sm:text-3xl">Oil Pumpjack</div>
|
||||
<div>matiasmiche</div>
|
||||
</div>
|
||||
<div className="flex self-start">
|
||||
<CadPackage
|
||||
cadPackage="openscad"
|
||||
className="px-3 py-1 sm:text-xl rounded transform translate-x-4 sm:translate-x-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="col-start-1 px-4 py-32 lg:col-start-3 lg:row-start-1 lg:col-span-3 lg:pl-52">
|
||||
<div>
|
||||
<span
|
||||
className="text-7xl text-ch-blue-400 bg-ch-blue-640 bg-opacity-30 font-fira-code px-6 rounded-2xl shadow-ch"
|
||||
style={{
|
||||
boxShadow: 'inset 0 4px 4px 0 rgba(255,255,255, 0.06)',
|
||||
}}
|
||||
>
|
||||
Code
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-6xl font-fira-sans mt-8 text-ch-gray-300">
|
||||
is the future of CAD
|
||||
</div>
|
||||
<div className="text-2xl text-gray-600 mt-8 max-w-4xl">
|
||||
Designs backed by reliable, easy-to-write code open a world of new
|
||||
workflows and collaboration. We're building a place where you can
|
||||
build that future.
|
||||
</div>
|
||||
<OutlineButton
|
||||
color="pink"
|
||||
isLeft
|
||||
svgName="terminal"
|
||||
onClick={() =>
|
||||
navigate(routes.draftProject({ cadPackage: 'openscad' }))
|
||||
}
|
||||
>
|
||||
Start Hacking
|
||||
</OutlineButton>
|
||||
</div>
|
||||
</div>
|
||||
<ChooseYourCharacter />
|
||||
<Community />
|
||||
<div className="max-w-8xl mx-auto grid lg:grid-cols-5 py-16">
|
||||
<div className="row-start-2 col-start-1 lg:col-span-3 lg:col-start-3 lg:row-start-1 lg:-mx-10 h-full relative z-10">
|
||||
<div
|
||||
className="absolute inset-0 mb-24 mt-16 ml:10 mr:10 lg:ml-40 lg:mr-52 bg-gradient-to-tr from-pink-400 to-blue-600 opacity-30 overflow-hidden"
|
||||
style={{ clipPath: 'url(#code-blob-clip-path)' }}
|
||||
>
|
||||
<pre className="ml-10 mt-12 text-blue-100 text-xs font-fira-code">
|
||||
{scadCode.map((line, index) => (
|
||||
<div key={index}>
|
||||
<span className="w-12 pr-6 text-blue-200 text-opacity-50 inline-block text-right">
|
||||
{index + 1}
|
||||
</span>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
<ModelSection assetUrl="/hinge.stl" scale={0.12} />
|
||||
</div>
|
||||
|
||||
<div className="py-12 pb-32 ml-4 row-start-1 col-start-1 pr-12 pl-6 lg:py-32 lg:col-start-1 lg:col-span-3">
|
||||
<div className="text-4xl mb-6 text-ch-gray-300">Learn Code-CAD</div>
|
||||
|
||||
<p className="text-gray-600 max-w-lg">
|
||||
We want you to learn Code-CAD today so it can change the way you
|
||||
work tomorrow. Our community is writing tutorials to make this
|
||||
powerful paradigm more accessible to people new to code and CAD.
|
||||
</p>
|
||||
<OutBound
|
||||
to="https://learn.cadhub.xyz/docs/definitive-beginners/your-openscad-journey"
|
||||
className=""
|
||||
>
|
||||
<OutlineButton color="pink" isLeft svgName="terminal">
|
||||
Get Started with OpenSCAD
|
||||
</OutlineButton>
|
||||
</OutBound>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end justify-center row-start-2 col-start-1 pb-24 lg:row-start-1 lg:col-start-3 lg:col-span-3 pt-96 lg:pt-0 lg:pr-10 pointer-events-none">
|
||||
<Link
|
||||
to={routes.project({
|
||||
userName: 'irevdev',
|
||||
projectTitle: 'tutorial-hinge',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className="grid grid-flow-col sm:gap-2 items-center bg-ch-gray-760 bg-opacity-95 text-ch-gray-300 rounded-md py-2 pl-2 font-fira-sans relative z-10 shadow-ch pointer-events-auto"
|
||||
style={{
|
||||
transform: 'translate3d(-5vw, -100px, 0.3px) scale(0.7)',
|
||||
transformOrigin: 'top center',
|
||||
}}
|
||||
>
|
||||
<div className="pl-1 sm:pl-4">
|
||||
<Gravatar
|
||||
image="CadHub/xvrnxvarkv8tdzo4n65u"
|
||||
className="w-12 h-12 mr-4"
|
||||
size={60}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg sm:text-2xl w-28 sm:w-auto">
|
||||
Print in Place Hinge
|
||||
</div>
|
||||
<div>IrevDev</div>
|
||||
</div>
|
||||
<div className="flex self-start">
|
||||
<CadPackage
|
||||
cadPackage="openscad"
|
||||
className="px-3 py-1 sm:text-xl rounded transform translate-x-4 sm:translate-x-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Roadmap />
|
||||
<div className="h-3 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500" />
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const DisableRender = () => useFrame(() => null, 1000)
|
||||
|
||||
function ModelSection({
|
||||
assetUrl,
|
||||
scale,
|
||||
}: {
|
||||
assetUrl: string
|
||||
scale: number
|
||||
}) {
|
||||
const { ref, inView } = useInView()
|
||||
return (
|
||||
<div className="relative h-full">
|
||||
<FatalErrorBoundary
|
||||
page={() => (
|
||||
<div className="bg-gray-800 p-8 rounded-md text-ch-gray-300">
|
||||
something seams to have gone wrong here
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-0" ref={ref}>
|
||||
<Canvas
|
||||
linear
|
||||
dpr={[1, 2]}
|
||||
orthographic
|
||||
camera={{ zoom: 75, position: [0, 0, 500] }}
|
||||
>
|
||||
{!inView && <DisableRender />}
|
||||
<pointLight position={[2, 3, 5]} color="#FFFFFF" intensity={2} />
|
||||
<pointLight position={[2, 3, -5]} color="#FFFFFF" intensity={2} />
|
||||
<pointLight position={[-6, 3, -5]} color="#FFFFFF" intensity={2} />
|
||||
<pointLight position={[-6, 3, 5]} color="#FFFFFF" intensity={2} />
|
||||
|
||||
<pointLight position={[2, 1.5, 0]} color="#0000FF" intensity={2} />
|
||||
<pointLight position={[2, 1.5, 0]} color="#FF0000" intensity={2} />
|
||||
|
||||
<Suspense
|
||||
fallback={
|
||||
<Html center className="loading" children="Loading..." />
|
||||
}
|
||||
>
|
||||
<AssetWithGooey assetUrl={assetUrl} scale={scale} />
|
||||
</Suspense>
|
||||
|
||||
{/* uncomment for framerate and render time */}
|
||||
{/* <Stats showPanel={0} className="three-debug-panel-1" /> */}
|
||||
{/* <Stats showPanel={1} className="three-debug-panel-2" /> */}
|
||||
</Canvas>
|
||||
</div>
|
||||
</FatalErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ChooseYourCharacter() {
|
||||
return (
|
||||
<div className="text-ch-gray-300 grid lg:grid-cols-2 gap-12 font-fira-sans py-32 max-w-7xl mx-auto px-4">
|
||||
<div className="">
|
||||
<div className="text-4xl mb-6">Choose your character</div>
|
||||
<p className="text-gray-600 text-2xl">
|
||||
CadHub is the place you can try out Code-CAD packages to find the one
|
||||
that's right for you. Our dedicated community is making CAD easy to
|
||||
learn on the web. Try one of our three integrations today and keep an
|
||||
eye out for more.
|
||||
</p>
|
||||
</div>
|
||||
<ul className="flex-col flex justify-around items-center lg:items-start text-gray-600">
|
||||
{[
|
||||
{
|
||||
cadPackage: 'openscad',
|
||||
desc: 'A mature Code-CAD library focused on Constructed Solid Geometry (CSG) modeling with syntax like C++.',
|
||||
},
|
||||
{
|
||||
cadPackage: 'cadquery',
|
||||
desc: 'A Python-based library with support for CSG and sketch-based modeling and a clean-feeling API.',
|
||||
},
|
||||
{
|
||||
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,
|
||||
desc,
|
||||
}: {
|
||||
cadPackage: CadPackageType
|
||||
desc: string
|
||||
}) => (
|
||||
<li key={cadPackage} className="flex items-center">
|
||||
<div className="mr-4 sm:mr-12">
|
||||
<DynamicProjectButton ideType={cadPackage} className="">
|
||||
<CadPackage
|
||||
cadPackage={cadPackage}
|
||||
className="px-3 py-1 w-40 text-xl rounded"
|
||||
/>
|
||||
</DynamicProjectButton>
|
||||
</div>
|
||||
<p className="text-sm my-2 max-w-sm">{desc}</p>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Community() {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto py-40">
|
||||
<div className="text-ch-gray-300 grid lg:grid-cols-2 gap-8 font-fira-sans px-4 mb-6">
|
||||
<div className="text-4xl">Explore with our community</div>
|
||||
|
||||
<p className="text-gray-600 text-sm">
|
||||
CadHub is a social platform. You can ask users how they designed a
|
||||
part, fork their work to put your own spin on it, and find inspiration
|
||||
in abundance.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ProjectsCell shouldFilterProjectsWithoutImage projectLimit={8} />
|
||||
<div className="flex justify-end pr-4">
|
||||
<OutlineButton
|
||||
color="blue"
|
||||
svgName="arrow-right"
|
||||
onClick={() => navigate(routes.projects())}
|
||||
>
|
||||
See All Projects
|
||||
</OutlineButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function OutlineButton({
|
||||
color,
|
||||
svgName,
|
||||
isLeft = false,
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
color: 'blue' | 'pink' | 'purple'
|
||||
svgName: SvgNames
|
||||
isLeft?: boolean
|
||||
children: React.ReactNode
|
||||
onClick?: () => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`grid grid-flow-col-dense gap-4 items-center border px-4 py-1 rounded mt-6 relative z-10 ${
|
||||
color === 'pink' && 'border-ch-pink-500'
|
||||
} ${color === 'blue' && 'border-ch-blue-630'} ${
|
||||
color === 'purple' && 'border-ch-purple-500'
|
||||
}`}
|
||||
>
|
||||
{isLeft && (
|
||||
<Svg
|
||||
name={svgName}
|
||||
className={`${color === 'pink' && 'text-ch-pink-500'} ${
|
||||
color === 'blue' && 'text-ch-blue-300'
|
||||
} ${color === 'purple' && 'text-ch-purple-200'} w-6 h-6`}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={`text-2xl ${color === 'pink' && 'text-ch-pink-300'} ${
|
||||
color === 'blue' && 'text-ch-blue-300'
|
||||
} ${color === 'purple' && 'text-ch-purple-200'}`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
{!isLeft && (
|
||||
<Svg
|
||||
name={svgName}
|
||||
className={`${color === 'pink' && 'text-ch-pink-500'} ${
|
||||
color === 'blue' && 'text-ch-blue-300'
|
||||
} ${color === 'purple' && 'text-ch-purple-200'} w-6 h-6`}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function Roadmap() {
|
||||
const sections = [
|
||||
{
|
||||
title: 'Read our roadmap',
|
||||
desc: 'Version control with GitHub, multi-file projects, and team collaboration tools. We’ve got a lot planned, and we’re building it in the open.',
|
||||
buttonText: 'View on Github',
|
||||
color: 'purple',
|
||||
url: 'https://github.com/Irev-Dev/cadhub/discussions/212',
|
||||
},
|
||||
{
|
||||
title: 'Join our community',
|
||||
desc: 'CAD is ready to evolve. Join our Discord and opensource community on GitHub and build that future with us!',
|
||||
buttonText: 'Join the Discord',
|
||||
color: 'blue',
|
||||
url: 'https://discord.gg/SD7zFRNjGH',
|
||||
},
|
||||
]
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto grid md:grid-cols-2 py-32 mt-12">
|
||||
{sections.map(({ title, desc, buttonText, color, url }) => (
|
||||
<div className="ml-4 py-6" key={title}>
|
||||
<div className="text-4xl mb-6 text-ch-gray-300">{title}</div>
|
||||
<p className="text-gray-600 text-2xl max-w-lg">{desc}</p>
|
||||
<OutBound to={url} className="">
|
||||
<OutlineButton color={color} svgName="arrow-right">
|
||||
{buttonText}
|
||||
</OutlineButton>
|
||||
</OutBound>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Footer() {
|
||||
const section: {
|
||||
header: string
|
||||
links: { name: string; url: string }[]
|
||||
}[] = [
|
||||
{
|
||||
header: 'Community',
|
||||
links: [
|
||||
{
|
||||
name: 'Github',
|
||||
url: 'https://github.com/Irev-Dev/cadhub',
|
||||
},
|
||||
{
|
||||
name: 'Discord',
|
||||
url: 'https://discord.gg/SD7zFRNjGH',
|
||||
},
|
||||
{
|
||||
name: 'Newsletter',
|
||||
url: 'https://kurthutten.com/signup/',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'About',
|
||||
links: [
|
||||
{
|
||||
name: 'Road Map',
|
||||
url: 'https://github.com/Irev-Dev/cadhub/discussions/212',
|
||||
},
|
||||
{
|
||||
name: 'Code of Conduct',
|
||||
url: '/policies/code-of-conduct',
|
||||
},
|
||||
{
|
||||
name: 'Privacy Policy',
|
||||
url: '/policies/privacy-policy',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'Learn',
|
||||
links: [
|
||||
{
|
||||
name: 'Documentation',
|
||||
url: 'https://learn.cadhub.xyz/',
|
||||
},
|
||||
{
|
||||
name: 'Blog',
|
||||
url: 'https://learn.cadhub.xyz/blog',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'Integrations',
|
||||
links: [
|
||||
{
|
||||
name: 'OpenSCAD',
|
||||
url: 'https://openscad.org/',
|
||||
},
|
||||
{
|
||||
name: 'CadQuery',
|
||||
url: 'https://cadquery.readthedocs.io/en/latest/',
|
||||
},
|
||||
{
|
||||
name: 'JSCAD',
|
||||
url: 'https://github.com/jscad',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto py-16 px-4 grid">
|
||||
<div className="pl-20 lg:pl-0">
|
||||
<div className="flex items-center">
|
||||
<div className="rounded-full overflow-hidden">
|
||||
<Svg className="w-10 md:w-16" name="favicon" />
|
||||
</div>
|
||||
<div className="ml-2 md:ml-8 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>
|
||||
</div>
|
||||
<p className="text-gray-600 text-xl mt-12 max-w-xs">
|
||||
Built by{' '}
|
||||
<OutBound
|
||||
to="https://github.com/Irev-Dev/cadhub/graphs/contributors"
|
||||
className="font-bold"
|
||||
>
|
||||
22 contributors
|
||||
</OutBound>{' '}
|
||||
from around the world.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-4 gap-4 flex-grow pl-20 row-start-2 lg:col-start-2 lg:row-start-1 mt-20 lg:mt-0">
|
||||
{section.map(({ header, links }) => (
|
||||
<ul
|
||||
className="text-ch-gray-300 font-fira-sans pt-8 sm:pt-0"
|
||||
key={header}
|
||||
>
|
||||
<li className="text-xl font-bold">{header}</li>
|
||||
{links.map(({ name, url }) => (
|
||||
<li className="text-lg mt-6 font-light" key={url}>
|
||||
<a href={url}>{name}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user