2 Commits

Author SHA1 Message Date
Davor Hrg
143371166a step and decimal calculation proposal 2021-08-21 17:57:51 +02:00
Kurt Hutten
14600d3899 live customizer attempt 2021-08-21 11:07:09 +10:00
189 changed files with 14909 additions and 18109 deletions

View File

@@ -1,67 +0,0 @@
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 and `cd` in the app directory (the docs directory is for [learn.cadhub](https://learn.cadhub.xyz/))
```
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`
## 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)

View File

@@ -1,6 +1,4 @@
![Screen Recording 2021-09-21 at 8](https://user-images.githubusercontent.com/29681384/134154332-65491787-7b36-4ad9-ba7a-bac0f2874051.gif)
![scrch2](https://user-images.githubusercontent.com/29681384/134156021-6b55c301-a77a-4851-b67b-b656875123e5.jpg)
![CadHub banner](https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/gear%20donutbanner.png)
# [C a d H u b](https://cadhub.xyz)
@@ -8,10 +6,84 @@
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 [contributing.md](https://github.com/Irev-Dev/cadhub/blob/main/CONTRIBUTING.md).
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)
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), [Torsten](https://github.com/t-paul) and [Hrg](https://github.com/hrgdavor) are a major contributors. 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) is a major contributor. Plus a number smaller contributors.

4
app/.gitignore vendored
View File

@@ -1,7 +1,3 @@
dist
web/types/graphql.d.ts
api/types/graphql.d.ts
# Deployment
.serverless

1
app/api/.babelrc.js Normal file
View File

@@ -0,0 +1 @@
module.exports = { extends: "../babel.config.js" }

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,5 +0,0 @@
-- 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;

View File

@@ -1,56 +0,0 @@
-- 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";

View File

@@ -5,7 +5,7 @@ datasource db {
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "rhel-openssl-1.0.x"]
binaryTargets = "native"
}
// sqlLight does not suport enums so we can't use enums until we set up postgresql in dev mode
@@ -14,6 +14,10 @@ generator client {
// ADMIN
// }
// enum ProjectType {
// JSCAD
// }
model User {
id String @id @default(uuid())
userName String @unique // reffered to as userId in @relations
@@ -37,7 +41,7 @@ model User {
enum CadPackage {
openscad
cadquery
jscad // TODO #422, add jscad to db schema when were ready to enable saving of jscad projects
// jscad // TODO #422, add jscad to db schema when were ready to enable saving of jscad projects
}
model Project {
@@ -53,12 +57,9 @@ model Project {
deleted Boolean @default(false)
cadPackage CadPackage @default(openscad)
socialCard SocialCard?
forkedFromId String?
forkedFrom Project? @relation("Fork", fields: [forkedFromId], references: [id])
childForks Project[] @relation("Fork")
Comment Comment[]
Reaction ProjectReaction[]
Comment Comment[]
Reaction ProjectReaction[]
@@unique([title, userId])
}

122
app/api/db/seed.js Normal file
View File

@@ -0,0 +1,122 @@
/* 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()
})

View File

@@ -3,25 +3,13 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@redwoodjs/api": "^0.38.1",
"@redwoodjs/graphql-server": "^0.38.1",
"@redwoodjs/api": "^0.34.1",
"@sentry/node": "^6.5.1",
"axios": "^0.21.1",
"cloudinary": "^1.23.0",
"cors": "^2.8.5",
"express": "^4.17.1",
"human-id": "^2.0.1",
"middy": "^0.36.0",
"nanoid": "^3.1.20",
"nodemailer": "^6.6.2",
"serverless-binary-cors": "^0.0.1"
"nodemailer": "^6.6.2"
},
"devDependencies": {
"@netlify/zip-it-and-ship-it": "^4.30.0",
"@types/nodemailer": "^6.4.2",
"concurrently": "^6.0.0",
"nodemon": "^2.0.7",
"serverless-dotenv-plugin": "^3.10.0",
"serverless-plugin-git-variables": "^5.1.0"
"@types/nodemailer": "^6.4.2"
}
}

View File

@@ -1,18 +0,0 @@
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()
})
})

View File

@@ -1,22 +0,0 @@
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

View File

@@ -1,10 +0,0 @@
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')
})
})

View File

@@ -1,16 +0,0 @@
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

View File

@@ -1,12 +1,11 @@
# Serverless
We're using the serverless framework for deployment
We're using the serverless from work for deployment
```
yarn rw build api && sls deploy --stage <stagename>
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
@@ -15,19 +14,21 @@ 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.
Run
Then cd into this folder `cd api/src/docker` and:
```bash
yarn cad
docker-compose up --build
```
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 aws-emulate
yarn install
yarn 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).

View File

@@ -1,6 +1,7 @@
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())
@@ -15,16 +16,10 @@ const makeRequest = (route, port) => [
console.log(`making post request to ${port}, ${route}`)
try {
const { data } = await axios.post(invocationURL(port), {
body: Buffer.from(JSON.stringify(req.body)).toString('base64'),
body: JSON.stringify(req.body),
})
res.status(data.statusCode)
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'))
}
res.send(data.body)
} catch (e) {
res.status(500)
res.send()

View File

@@ -1,15 +1,14 @@
FROM continuumio/miniconda3
ENV PATH="/root/miniconda3/bin:${PATH}"
ARG PATH="/root/miniconda3/bin:${PATH}"
FROM public.ecr.aws/lts/ubuntu:20.04_stable
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update --fix-missing -qq
RUN apt-get update -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 api/src/docker/common/node14source_setup.sh /nodesource_setup.sh
ADD common/node14source_setup.sh /nodesource_setup.sh
RUN ["chmod", "+x", "/nodesource_setup.sh"]
RUN bash nodesource_setup.sh
RUN apt-get install -y nodejs
@@ -22,45 +21,32 @@ RUN apt-get update && \
cmake \
unzip \
automake autoconf libtool \
libcurl4-openssl-dev \
curl \
git
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 api/src/docker/common/aws-lambda-rie /usr/local/bin/aws-lambda-rie
ADD 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/
# aws-lambda-ric does not play nice with yarn, so installing it seperately,
# circle back to this later for a proper solution
COPY package*.json /var/task/
RUN npm install aws-lambda-ric@1.0.0
COPY cadquery/package*.json /var/task/
RUN npm install
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 echo "cadhub-concat-split" > /var/task/cadhub-concat-split
# using built javascript from dist
# run `yarn rw build` and $(npm bin)/zip-it-and-ship-it api/dist/functions/ api/dist/zipball before bulding this image
COPY api/dist/zipball/cadquery.zip /var/task/
# -n stops aws-lamda-ric from being overridden.
RUN unzip -n /var/task/cadquery.zip
COPY api/src/docker/common/entrypoint.sh /entrypoint.sh
RUN chmod +x cq-cli/cq-cli
COPY cadquery/*.js /var/task/
COPY common/*.js /var/common/
COPY common/entrypoint.sh /entrypoint.sh
RUN ["chmod", "+x", "/entrypoint.sh"]
ENTRYPOINT ["sh", "/entrypoint.sh"]
CMD [ "cadquery.stl" ]

View File

@@ -0,0 +1,57 @@
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()),
}

View File

@@ -1,21 +0,0 @@
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, consoleMessage, fullPath } = await runCQ({ file, settings })
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
})
}
export const stl = middy(loggerWrap(_stl)).use(cors())

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
{
"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"
}
}

View File

@@ -0,0 +1,26 @@
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 }
}
}

View File

@@ -1,60 +0,0 @@
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 = ''
try {
consoleMessage = await runCommand(command, 30000)
const params = JSON.parse(
await readFile(customizerPath, { encoding: 'ascii' })
)
await writeFiles(
[
{
file: JSON.stringify({
customizerParams: params,
consoleMessage,
type: 'stl',
}),
fileName: 'metadata.json',
},
],
tempFile
)
await runCommand(
`cat ${stlPath} /var/task/cadhub-concat-split /tmp/${tempFile}/metadata.json | gzip > ${fullPath}`,
15000,
true
)
return { consoleMessage, fullPath }
} catch (error) {
return { error: consoleMessage || error, fullPath }
}
}

View File

@@ -2,32 +2,29 @@ const { exec } = require('child_process')
const { promises } = require('fs')
const { writeFile } = promises
const { createHash } = require('crypto')
import { readFile } from 'fs/promises'
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) {
//
const CONSOLE_MESSAGE_KEY = 'console-message-b64'
function putConsoleMessageInMetadata(consoleMessage) {
return {
[CONSOLE_MESSAGE_KEY]: Buffer.from(consoleMessage, 'utf-8').toString(
'base64'
),
}
await Promise.all(
files.map(({ file, fileName }) =>
writeFile(`/tmp/${tempFile}/${fileName}`, file)
)
)
}
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)
return tempFile
}
export async function runCommand(
command,
timeout = 5000,
shouldRejectStdErr = false
): Promise<string> {
async function runCommand(command, timeout = 5000) {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
@@ -39,10 +36,6 @@ export async function runCommand(
}
if (stderr) {
console.log(`stderr: ${stderr}`)
if (shouldRejectStdErr) {
reject(stderr)
return
}
resolve(stderr)
return
}
@@ -61,8 +54,10 @@ function makeHash(script) {
async function checkIfAlreadyExists(params, s3) {
try {
await s3.headObject(params).promise()
return { isAlreadyInBucket: true }
const objectHead = await s3.headObject(params).promise()
const consoleMessage = getConsoleMessageFromMetadata(objectHead.Metadata)
console.log('consoleMessage', consoleMessage)
return { isAlreadyInBucket: true, consoleMessage }
} catch (e) {
console.log("couldn't find it", e)
return { isAlreadyInBucket: false }
@@ -89,7 +84,7 @@ function getObjectUrl(params, s3, tk) {
)
}
export function loggerWrap(handler) {
function loggerWrap(handler) {
return (req, _context, callback) => {
try {
return handler(req, _context, callback)
@@ -99,53 +94,72 @@ export function loggerWrap(handler) {
}
}
export async function storeAssetAndReturnUrl({
async function storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
}: {
error: string
callback: Function
fullPath: string
consoleMessage: string
key,
s3,
params,
tk,
}) {
if (error) {
const response = {
statusCode: 400,
body: Buffer.from(JSON.stringify({ error, fullPath })).toString('base64'),
isBase64Encoded: true,
body: JSON.stringify({ error, fullPath }),
}
callback(null, response)
return
} else {
console.log(`got result in route: ${consoleMessage}, file is: ${fullPath}`)
let buffer = ''
const { readFile } = require('fs/promises')
let buffer
try {
buffer = await readFile(fullPath, { encoding: 'base64' })
buffer = await readFile(`${fullPath}.gz`)
} catch (e) {
console.log('read file error', e)
const response = {
statusCode: 400,
body: Buffer.from(
JSON.stringify({ error: consoleMessage, fullPath })
).toString('base64'),
isBase64Encoded: true,
body: JSON.stringify({ error: consoleMessage, fullPath }),
}
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: buffer,
isBase64Encoded: true,
headers: {
'Content-Type': 'application/javascript',
'Content-Encoding': 'gzip',
},
body: JSON.stringify({
url,
consoleMessage,
}),
}
callback(null, response)
return
}
}
module.exports = {
runCommand,
makeFile,
makeHash,
checkIfAlreadyExists,
getObjectUrl,
loggerWrap,
storeAssetAndReturnUrl,
}

View File

@@ -2,14 +2,10 @@ services:
openscad-preview:
build:
context: ../../../
dockerfile: ./api/src/docker/openscad/Dockerfile
context: ./
dockerfile: ./openscad/Dockerfile
image: openscad
command: 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:
@@ -19,9 +15,6 @@ services:
openscad-stl:
image: openscad
volumes:
- ../dist/docker/openscad:/var/task/js/
- ../dist/docker/common:/var/task/common/
command: openscad.stl
ports:
- "5053:8080"
@@ -32,11 +25,8 @@ services:
cadquery-stl:
build:
context: ../../../
dockerfile: ./api/src/docker/cadquery/Dockerfile
volumes:
- ../dist/docker/cadquery:/var/task/js/
- ../dist/docker/common:/var/task/common/
context: ./
dockerfile: ./cadquery/Dockerfile
command: cadquery.stl
ports:
- 5060:8080

View File

@@ -3,18 +3,16 @@ 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 --fix-missing -qq
RUN apt-get update -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 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 add-apt-repository ppa:openscad/releases
RUN apt-get update -qq
RUN apt-get install -y openscad-nightly
RUN apt-get install -y -qq openscad
RUN apt-get install -y curl wget
# install node14, see comment at the to of node14source_setup.sh
ADD api/src/docker/common/node14source_setup.sh /nodesource_setup.sh
ADD common/node14source_setup.sh /nodesource_setup.sh
RUN ["chmod", "+x", "/nodesource_setup.sh"]
RUN bash nodesource_setup.sh
RUN apt-get install -y nodejs
@@ -32,15 +30,13 @@ 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 api/src/docker/common/aws-lambda-rie /usr/local/bin/aws-lambda-rie
ADD 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/
# aws-lambda-ric does not play nice with yarn, so installing it seperately,
# circle back to this later for a proper solution
COPY package*.json /var/task/
RUN npm install aws-lambda-ric@1.0.0
COPY openscad/package*.json /var/task/
RUN npm install
# Install OpenSCAD libraries
# It's experimental, so only adding latest Round-Anything for now
@@ -48,19 +44,12 @@ 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 api/src/docker/openscad/cadhubtheme.json /usr/share/openscad-nightly/color-schemes/render/
RUN echo "cadhub-concat-split" > /var/task/cadhub-concat-split
# using built javascript from dist
# run `yarn rw build` and $(npm bin)/zip-it-and-ship-it api/dist/functions/ api/dist/zipball before bulding this image
COPY api/dist/zipball/openscad.zip /var/task/
# -n stops aws-lamda-ric from being overridden.
RUN unzip -n /var/task/openscad.zip
COPY api/src/docker/common/entrypoint.sh /entrypoint.sh
COPY openscad/cadhubtheme.json /usr/share/openscad/color-schemes/render/
COPY openscad/*.js /var/task/
COPY common/*.js /var/common/
COPY common/entrypoint.sh /entrypoint.sh
RUN ["chmod", "+x", "/entrypoint.sh"]
ENTRYPOINT ["sh", "/entrypoint.sh"]
CMD [ "openscad.preview" ]
CMD [ "openscad.render" ]

View File

@@ -0,0 +1,126 @@
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()),
}

View File

@@ -1,44 +0,0 @@
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 } = await runScad({
file,
settings,
})
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
})
}
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 } = await stlExport({
file,
settings,
})
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
})
}
export const stl = middy(loggerWrap(_stl)).use(cors())
export const preview = middy(loggerWrap(_preview)).use(cors())

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
{
"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"
}
}

View File

@@ -0,0 +1,64 @@
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 }
}
}

View File

@@ -1,150 +0,0 @@
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
}> => {
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 }
} 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 }
} catch (error) {
return { error, fullPath }
}
}

1799
app/api/src/docker/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
{
"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"
}
}

View File

@@ -0,0 +1,166 @@
service: cad-lambdas
# app and org for use with dashboard.serverless.com
#app: your-app-name
#org: your-org-name
# plugins:
# - serverless-offline
# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
frameworkVersion: '2'
provider:
name: aws
lambdaHashingVersion: 20201221
ecr:
images:
# this image is built locally and push to ECR
openscadimage:
path: ./
file: ./openscad/Dockerfile
cadqueryimage:
path: ./
file: ./cadquery/Dockerfile
apiGateway:
metrics: true
# you can overwrite defaults here
# stage: dev
# region: us-east-1
# you can add statements to the Lambda function's IAM Role here
iam:
role:
statements:
- Effect: "Allow"
Action:
- "s3:GetObject"
Resource: "arn:aws:s3:::cad-preview-bucket-prod-001/*"
- Effect: "Allow"
Action:
- "s3:PutObject"
Resource: "arn:aws:s3:::cad-preview-bucket-prod-001/*"
# Dev bucket is cad-preview-bucket-dev-001/*"
# you can define service wide environment variables here
# environment:
# variable1: value1
functions:
openscadpreview:
image:
name: openscadimage
command:
- openscad.preview
entryPoint:
- '/entrypoint.sh'
events:
- http:
path: openscad/preview
method: post
cors: true
timeout: 25
environment:
BUCKET: cad-preview-bucket-prod-001
openscadstl:
image:
name: openscadimage
command:
- openscad.stl
entryPoint:
- '/entrypoint.sh'
events:
- http:
path: openscad/stl
method: post
cors: true
timeout: 30
environment:
BUCKET: cad-preview-bucket-prod-001
cadquerystl:
image:
name: cadqueryimage
command:
- cadquery.stl
entryPoint:
- '/entrypoint.sh'
events:
- http:
path: cadquery/stl
method: post
cors: true
timeout: 30
environment:
BUCKET: cad-preview-bucket-prod-001
# 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
# events:
# - httpApi:
# path: /users/create
# method: get
# - websocket: $connect
# - s3: ${env:BUCKET}
# - schedule: rate(10 minutes)
# - sns: greeter-topic
# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
# - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
# - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
# - iot:
# sql: "SELECT * FROM 'some_topic'"
# - cloudwatchEvent:
# event:
# source:
# - "aws.ec2"
# detail-type:
# - "EC2 Instance State-change Notification"
# detail:
# state:
# - pending
# - cloudwatchLog: '/aws/lambda/hello'
# - cognitoUserPool:
# pool: MyUserPool
# trigger: PreSignUp
# - alb:
# listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/
# priority: 1
# conditions:
# host: example.com
# path: /hello
# Define function environment variables here
# environment:
# variable2: value2
# you can add CloudFormation resource templates here
#resources:
# Resources:
# NewResource:
# Type: AWS::S3::Bucket
# Properties:
# BucketName: my-new-bucket
# Outputs:
# NewOutput:
# Description: "Description for the output"
# Value: "Some output value"
resources:
Resources:
GatewayResponseDefault4XX:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: DEFAULT_4XX
RestApiId:
Ref: 'ApiGatewayRestApi'
GatewayResponseDefault5XX:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: DEFAULT_5XX
RestApiId:
Ref: 'ApiGatewayRestApi'

View File

@@ -1,3 +0,0 @@
import { stl } from 'src/docker/cadquery/cadquery'
export { stl }

View File

@@ -1,40 +0,0 @@
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,
}),
}
}

View File

@@ -1,26 +1,23 @@
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
import {
createGraphQLHandler,
makeMergedSchema,
makeServices,
} from '@redwoodjs/api'
import { createSentryApolloPlugin } from 'src/lib/sentry'
import { logger } from 'src/lib/logger'
import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import schemas from 'src/graphql/**/*.{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,
directives,
sdls,
services,
schema: makeMergedSchema({
schemas,
services: makeServices({ services }),
}),
plugins: [createSentryApolloPlugin()],
cors: {
origin: '*',
credentials: true,
},
onException: () => {
// Disconnect from your database with an unhandled exception.
db.$disconnect()

View File

@@ -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 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.
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.
Hit me up anytime for questions or concerns.
Cheers,

View File

@@ -1,3 +0,0 @@
import { stl, preview } from 'src/docker/openscad/openscad'
export { stl, preview }

View File

@@ -11,10 +11,9 @@ export const schema = gql`
}
type Query {
projectReactions: [ProjectReaction!]! @skipAuth
projectReaction(id: String!): ProjectReaction @skipAuth
projectReactions: [ProjectReaction!]!
projectReaction(id: String!): ProjectReaction
projectReactionsByProjectId(projectId: String!): [ProjectReaction!]!
@skipAuth
}
input ToggleProjectReactionInput {
@@ -31,11 +30,10 @@ export const schema = gql`
type Mutation {
toggleProjectReaction(input: ToggleProjectReactionInput!): ProjectReaction!
@requireAuth
updateProjectReaction(
id: String!
input: UpdateProjectReactionInput!
): ProjectReaction! @requireAuth
deleteProjectReaction(id: String!): ProjectReaction! @requireAuth
): ProjectReaction!
deleteProjectReaction(id: String!): ProjectReaction!
}
`

View File

@@ -11,8 +11,8 @@ export const schema = gql`
}
type Query {
comments: [Comment!]! @skipAuth
comment(id: String!): Comment @skipAuth
comments: [Comment!]!
comment(id: String!): Comment
}
input CreateCommentInput {
@@ -28,9 +28,8 @@ export const schema = gql`
}
type Mutation {
createComment(input: CreateCommentInput!): Comment! @requireAuth
createComment(input: CreateCommentInput!): Comment!
updateComment(id: String!, input: UpdateCommentInput!): Comment!
@requireAuth
deleteComment(id: String!): Comment! @requireAuth
deleteComment(id: String!): Comment!
}
`

View File

@@ -15,6 +15,6 @@ export const schema = gql`
}
type Mutation {
sendAllUsersEmail(input: Email!): EmailResponse! @requireAuth
sendAllUsersEmail(input: Email!): EmailResponse!
}
`

View File

@@ -14,22 +14,17 @@ export const schema = gql`
socialCard: SocialCard
Comment: [Comment]!
Reaction(userId: String): [ProjectReaction]!
forkedFromId: String
forkedFrom: Project
childForks: [Project]!
}
enum CadPackage {
openscad
cadquery
jscad
}
type Query {
projects(userName: String): [Project!]! @skipAuth
project(id: String!): Project @skipAuth
projects(userName: String): [Project!]!
project(id: String!): Project
projectByUserAndTitle(userName: String!, projectTitle: String!): Project
@skipAuth
}
input CreateProjectInput {
@@ -41,12 +36,6 @@ export const schema = gql`
cadPackage: CadPackage!
}
input ForkProjectInput {
userId: String!
forkedFromId: String
code: String
}
input UpdateProjectInput {
title: String
description: String
@@ -56,15 +45,14 @@ export const schema = gql`
}
type Mutation {
createProject(input: CreateProjectInput!): Project! @requireAuth
forkProject(input: ForkProjectInput!): Project! @requireAuth
createProject(input: CreateProjectInput!): Project!
forkProject(input: CreateProjectInput!): Project!
updateProject(id: String!, input: UpdateProjectInput!): Project!
@requireAuth
updateProjectImages(
id: String!
mainImage64: String
socialCard64: String
): Project! @requireAuth
deleteProject(id: String!): Project! @requireAuth
): Project!
deleteProject(id: String!): Project!
}
`

View File

@@ -10,7 +10,7 @@ export const schema = gql`
}
type Query {
socialCards: [SocialCard!]! @skipAuth
socialCard(id: String!): SocialCard @skipAuth
socialCards: [SocialCard!]!
socialCard(id: String!): SocialCard
}
`

View File

@@ -10,8 +10,8 @@ export const schema = gql`
}
type Query {
subjectAccessRequests: [SubjectAccessRequest!]! @requireAuth
subjectAccessRequest(id: String!): SubjectAccessRequest @requireAuth
subjectAccessRequests: [SubjectAccessRequest!]!
subjectAccessRequest(id: String!): SubjectAccessRequest
}
input CreateSubjectAccessRequestInput {
@@ -29,11 +29,11 @@ export const schema = gql`
type Mutation {
createSubjectAccessRequest(
input: CreateSubjectAccessRequestInput!
): SubjectAccessRequest! @requireAuth
): SubjectAccessRequest!
updateSubjectAccessRequest(
id: String!
input: UpdateSubjectAccessRequestInput!
): SubjectAccessRequest! @requireAuth
deleteSubjectAccessRequest(id: String!): SubjectAccessRequest! @requireAuth
): SubjectAccessRequest!
deleteSubjectAccessRequest(id: String!): SubjectAccessRequest!
}
`

View File

@@ -16,9 +16,9 @@ export const schema = gql`
}
type Query {
users: [User!]! @requireAuth
user(id: String!): User @skipAuth
userName(userName: String!): User @skipAuth
users: [User!]!
user(id: String!): User
userName(userName: String!): User
}
input CreateUserInput {
@@ -38,10 +38,9 @@ export const schema = gql`
}
type Mutation {
createUser(input: CreateUserInput!): User! @requireAuth
updateUser(id: String!, input: UpdateUserInput!): User! @requireAuth
createUser(input: CreateUserInput!): User!
updateUser(id: String!, input: UpdateUserInput!): User!
updateUserByUserName(userName: String!, input: UpdateUserInput!): User!
@requireAuth
deleteUser(id: String!): User! @requireAuth
deleteUser(id: String!): User!
}
`

View File

@@ -1,5 +1,61 @@
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'
import { parseJWT } from '@redwoodjs/api'
// 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'
/**
* Use requireAuth in your services to check that a user is logged in,
@@ -41,24 +97,8 @@ import { parseJWT } from '@redwoodjs/api'
* }
* }
*/
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
export const getCurrentUser = async (decoded, { _token, _type }) => {
return { ...decoded, roles: parseJWT({ decoded }).roles }
}
/**

View File

@@ -1,4 +1,4 @@
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'
import { AuthenticationError, ForbiddenError } from '@redwoodjs/api'
import type { Project } from '@prisma/client'
import { db } from 'src/lib/db'

View File

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

View File

@@ -39,7 +39,7 @@ export const generateUniqueString = async (
}
export const generateUniqueStringWithoutSeed = async (
isUniqueCallback: (seed: string) => Promise<boolean>,
isUniqueCallback: (seed: string) => Promise<any>,
count = 0
) => {
const seed = humanId({

View File

@@ -1,4 +1,4 @@
import { UserInputError } from '@redwoodjs/graphql-server'
import { UserInputError } from '@redwoodjs/api'
import { requireAuth } from 'src/lib/auth'
import { requireOwnership } from 'src/lib/owner'

View File

@@ -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,6 +12,7 @@ import {
} from 'src/services/helpers'
import { requireAuth } from 'src/lib/auth'
import { requireOwnership, requireProjectOwnership } from 'src/lib/owner'
import { socialCard } from '../socialCards/socialCards'
export const projects = ({ userName }) => {
if (!userName) {
@@ -47,17 +48,15 @@ export const projectByUserAndTitle = async ({ userName, projectTitle }) => {
},
})
}
const isUniqueProjectTitle =
(userId: string) =>
async (seed: string): Promise<boolean> =>
!!(await db.project.findUnique({
where: {
title_userId: {
title: seed,
userId,
},
const isUniqueProjectTitle = (userId: string) => async (seed: string) =>
db.project.findUnique({
where: {
title_userId: {
title: seed,
userId,
},
}))
},
})
interface CreateProjectArgs {
input: Prisma.ProjectCreateArgs['data']
@@ -80,27 +79,13 @@ export const createProject = async ({ input }: CreateProjectArgs) => {
}
export const forkProject = async ({ input }) => {
requireAuth()
const projectData = await db.project.findUnique({
where: {
id: input.forkedFromId,
},
})
// Only difference between create and fork project is that fork project will generate a unique title
// (for the user) if there is a conflict
const isUniqueCallback = isUniqueProjectTitle(input.userId)
let title = projectData.title
title = await generateUniqueString(title, isUniqueCallback)
const { code, description, cadPackage } = projectData
const title = await generateUniqueString(input.title, isUniqueCallback)
// TODO change the description to `forked from userName/projectName ${rest of description}`
return db.project.create({
data: foreignKeyReplacement({
...input,
title,
code: input.code || code,
description,
cadPackage,
}),
data: foreignKeyReplacement({ ...input, title }),
})
}
@@ -118,19 +103,18 @@ 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)
}
@@ -258,29 +242,17 @@ export const updateProjectImages = async ({
export const deleteProject = async ({ id }: Prisma.ProjectWhereUniqueInput) => {
requireAuth()
await requireOwnership({ projectId: id })
const project = await db.project.findUnique({
return db.project.update({
data: {
deleted: true,
},
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 }) =>
db.project.findMany({ where: { forkedFromId: root.id } }),
user: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
db.user.findUnique({ where: { id: root.userId } }),
db.project.findUnique({ where: { id: root.id } }).user(),
socialCard: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
db.project.findUnique({ where: { id: root.id } }).socialCard(),
Comment: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>

View File

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

View File

@@ -1,30 +1,8 @@
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' })
@@ -47,51 +25,32 @@ export const createUser = ({ input }) => {
requireAuth({ role: 'admin' })
createUserInsecure({ input })
}
export const createUserInsecure = ({
input,
}: {
input: Prisma.UserUncheckedCreateInput
}) => {
if (typeof input.userName === 'string') {
input.userName = userNameVerification(input.userName)
}
nameVerification(input.name)
export const createUserInsecure = ({ input }) => {
return db.user.create({
data: input,
})
}
export const updateUser = ({
id,
input,
}: {
id: string
input: Prisma.UserUncheckedCreateInput
}) => {
export const updateUser = ({ id, input }) => {
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,
}: {
userName: string
input: Prisma.UserUncheckedCreateInput
}) => {
export const updateUserByUserName = async ({ userName, input }) => {
requireAuth()
await requireOwnership({ userName })
if (typeof input.userName === 'string') {
input.userName = userNameVerification(input.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 `
)
}
nameVerification(input.name)
const originalProject = await db.user.findUnique({ where: { userName } })
const imageToDestroy =
originalProject.image !== input.image && originalProject.image

View File

@@ -6,13 +6,9 @@
"web"
]
},
"scripts": {
"cad": "yarn rw build api && zip-it-and-ship-it api/dist/functions/ api/dist/zipball && docker-compose --file ./api/src/docker/docker-compose.yml up --build",
"cad-r": "yarn rw build api && zip-it-and-ship-it api/dist/functions/ api/dist/zipball && docker-compose --file ./api/src/docker/docker-compose.yml restart",
"aws-emulate": "nodemon ./api/src/docker/aws-emulator.js"
},
"scripts": {},
"devDependencies": {
"@redwoodjs/core": "^0.38.1"
"@redwoodjs/core": "^0.34.1"
},
"eslintConfig": {
"extends": "@redwoodjs/eslint-config",
@@ -30,10 +26,7 @@
}
},
"engines": {
"node": ">=14.x <=16.x",
"node": ">=14",
"yarn": ">=1.15"
},
"prisma": {
"seed": "yarn rw exec seed"
}
}
}

View File

@@ -7,19 +7,17 @@
[web]
port = 8910
title = 'CadHub'
# apiUrl = "/.netlify/functions"
apiUrl = "https://uk5gegwopd.execute-api.us-east-2.amazonaws.com/.netlify/functions"
apiProxyPath = "/.netlify/functions"
includeEnvironmentVariables = [
'GOOGLE_ANALYTICS_ID',
'CLOUDINARY_API_KEY',
# 'CLOUDINARY_API_SECRET',
'CLOUDINARY_API_SECRET',
'CAD_LAMBDA_BASE_URL',
'SENTRY_DSN',
'SENTRY_AUTH_TOKEN',
'SENTRY_ORG',
'SENTRY_PROJECT',
# 'EMAIL_PASSWORD'
'EMAIL_PASSWORD'
]
# experimentalFastRefresh = true # this seems to break cascadeStudio
[api]

View File

@@ -1,235 +0,0 @@
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();
}
`
}

View File

@@ -1,188 +0,0 @@
# See the full yml reference at https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml/
service: cadhubapi
# Uncomment org and app if you want to integrate your deployment with the Serverless dashboard. See https://www.serverless.com/framework/docs/dashboard/ for more details.
# org: your-org
# app: your-app
plugins:
- serverless-dotenv-plugin
- serverless-binary-cors
- serverless-plugin-git-variables
custom:
dotenv:
include:
- DATABASE_URL_PROD
- CLOUDINARY_API_KEY
- CLOUDINARY_API_SECRET
- EMAIL_PASSWORD
- SENTRY_DSN
# - # List the environment variables you want to include from your .env file here.
provider:
name: aws
lambdaHashingVersion: 20201221
runtime: nodejs14.x
region: us-east-2 # This is the AWS region where the service will be deployed.
httpApi: # HTTP API is used by default. To learn about the available options in API Gateway, see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html
cors: true
payload: '1.0'
stackTags: # Add CloudFormation stack tags here
source: serverless
name: Redwood Lambda API with HTTP API Gateway
tags: # Add service wide tags here
name: Redwood Lambda API with HTTP API Gateway
ecr:
images:
# this image is built locally and push to ECR
openscadimage:
path: ./
file: api/src/docker/openscad/Dockerfile
cadqueryimage:
path: ./
file: api/src/docker/cadquery/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 */* :(
- '*/*'
package:
individually: true
functions:
check-user-name:
description: check-user-name function deployed on AWS Lambda
package:
artifact: api/dist/zipball/check-user-name.zip # This is the default location of the zip file generated during the deploy command.
memorySize: 1024 # mb
timeout: 25 # seconds (max: 29)
tags: # Tags for this specific lambda function
endpoint: /.netlify/functions/check-user-name
# Uncomment this section to add environment variables either from the Serverless dotenv plugin or using Serverless params
environment:
SENTRY_DSN: ${env:SENTRY_DSN}
DATABASE_URL: ${env:DATABASE_URL_PROD}
COMMIT_REF: ${git:sha1}
CONTEXT: TODO
handler: check-user-name.handler
events:
- httpApi:
path: /.netlify/functions/check-user-name
method: GET
# cors: true
- httpApi:
path: /.netlify/functions/check-user-name
method: POST
# cors: true
graphql:
description: graphql function deployed on AWS Lambda
package:
artifact: api/dist/zipball/graphql.zip # This is the default location of the zip file generated during the deploy command.
memorySize: 1024 # mb
timeout: 25 # seconds (max: 29)
tags: # Tags for this specific lambda function
endpoint: /.netlify/functions/graphql
# Uncomment this section to add environment variables either from the Serverless dotenv plugin or using Serverless params
environment:
CLOUDINARY_API_KEY: ${env:CLOUDINARY_API_KEY}
CLOUDINARY_API_SECRET: ${env:CLOUDINARY_API_SECRET}
EMAIL_PASSWORD: ${env:EMAIL_PASSWORD}
SENTRY_DSN: ${env:SENTRY_DSN}
DATABASE_URL: ${env:DATABASE_URL_PROD}
COMMIT_REF: ${git:sha1}
CONTEXT: TODO
# YOUR_FIRST_ENV_VARIABLE: ${env:YOUR_FIRST_ENV_VARIABLE}
handler: graphql.handler
events:
- httpApi:
path: /.netlify/functions/graphql
method: GET
# cors: true
- httpApi:
path: /.netlify/functions/graphql
method: POST
# cors: true
# identity-signup: # this is netlify specific and is related to go true auth, so we'll continue having that deployed on netlify
# description: identity-signup function deployed on AWS Lambda
# package:
# artifact: api/dist/zipball/identity-signup.zip # This is the default location of the zip file generated during the deploy command.
# memorySize: 1024 # mb
# timeout: 25 # seconds (max: 29)
# tags: # Tags for this specific lambda function
# endpoint: /.netlify/functions/identity-signup
# # Uncomment this section to add environment variables either from the Serverless dotenv plugin or using Serverless params
# # environment:
# # YOUR_FIRST_ENV_VARIABLE: ${env:YOUR_FIRST_ENV_VARIABLE}
# handler: identity-signup.handler
# events:
# - httpApi:
# path: /.netlify/functions/identity-signup
# method: GET
# - httpApi:
# path: /.netlify/functions/identity-signup
# method: POST
openscadpreview:
image:
name: openscadimage
command:
- openscad.preview
entryPoint:
- '/entrypoint.sh'
events:
- http:
path: openscad/preview
method: post
cors: true
timeout: 25
openscadstl:
image:
name: openscadimage
command:
- openscad.stl
entryPoint:
- '/entrypoint.sh'
events:
- http:
path: openscad/stl
method: post
cors: true
timeout: 30
cadquerystl:
image:
name: cadqueryimage
command:
- cadquery.stl
entryPoint:
- '/entrypoint.sh'
events:
- http:
path: cadquery/stl
method: post
cors: true
timeout: 30
# this allows browsers to see error responses.
resources:
Resources:
GatewayResponseDefault4XX:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: DEFAULT_4XX
RestApiId:
Ref: 'ApiGatewayRestApi'
GatewayResponseDefault5XX:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: DEFAULT_5XX
RestApiId:
Ref: 'ApiGatewayRestApi'

View File

@@ -2,8 +2,7 @@ const path = require('path')
module.exports = {
plugins: [
require('postcss-import'),
require('tailwindcss')(path.resolve(__dirname, 'tailwind.config.js')),
require('tailwindcss')(path.resolve(__dirname, '../tailwind.config.js')),
require('autoprefixer'),
],
}

View File

@@ -2,12 +2,8 @@
module.exports = (config, { env }) => {
config.plugins.forEach((plugin) => {
if (plugin.constructor.name === 'HtmlWebpackPlugin') {
plugin.userOptions.favicon = './src/favicon.svg'
plugin.options.favicon = './src/favicon.svg'
}
})
config.module.rules.push({
test: /\.(md|jscad\.js|py|scad)$/i,
use: 'raw-loader',
});
return config
}

View File

@@ -1,10 +0,0 @@
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;
}

View File

@@ -13,25 +13,21 @@
]
},
"dependencies": {
"@headlessui/react": "^1.4.1",
"@heroicons/react": "^1.0.4",
"@headlessui/react": "^1.0.0",
"@material-ui/core": "^4.11.0",
"@monaco-editor/react": "^4.0.11",
"@react-three/drei": "^7.3.1",
"@react-three/fiber": "^7.0.5",
"@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",
"@redwoodjs/auth": "^0.34.1",
"@redwoodjs/forms": "^0.34.1",
"@redwoodjs/router": "^0.34.1",
"@redwoodjs/web": "^0.34.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",
@@ -42,24 +38,19 @@
"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-intersection-observer": "^8.32.1",
"react-mosaic-component": "^5.0.0",
"react-mosaic-component": "^4.1.1",
"react-tabs": "^3.2.2",
"rich-markdown-editor": "^11.0.2",
"styled-components": "^5.2.0",
"three": "^0.130.1",
"worker-loader": "^3.0.8"
"three": "^0.130.1"
},
"devDependencies": {
"@types/lodash": "^4.14.170",
"autoprefixer": "^10.3.1",
"autoprefixer": "^10.2.5",
"html-webpack-plugin": "^4.5.0",
"postcss": "^8.3.6",
"postcss-import": "^14.0.2",
"postcss-loader": "^6.1.1",
"raw-loader": "^4.0.2",
"tailwindcss": "^2.2.7"
"postcss": "^8.2.13",
"postcss-loader": "4.0.2",
"tailwindcss": "^2.1.2"
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -0,0 +1,623 @@
(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
});

Binary file not shown.

Binary file not shown.

View File

@@ -1,19 +1,15 @@
import { AuthProvider } from '@redwoodjs/auth'
import GoTrue from 'gotrue-js'
import { RedwoodProvider } from '@redwoodjs/web'
import FatalErrorBoundary from 'src/components/FatalErrorBoundary/FatalErrorBoundary'
import { FatalErrorBoundary } from '@redwoodjs/web'
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'
@@ -22,28 +18,13 @@ const goTrueClient = new GoTrue({
setCookie: true,
})
const theme = createTheme({
palette: {
type: 'dark',
primary: {
light: '#C99DFF',
main: '#A663FA',
dark: '#3B0480',
},
},
})
const App = () => (
<FatalErrorBoundary page={FatalErrorPage}>
<RedwoodProvider>
<AuthProvider client={goTrueClient} type="goTrue">
<RedwoodApolloProvider>
<ThemeProvider theme={theme}>
<Routes />
</ThemeProvider>
</RedwoodApolloProvider>
</AuthProvider>
</RedwoodProvider>
<AuthProvider client={goTrueClient} type="goTrue">
<RedwoodApolloProvider>
<Routes />
</RedwoodApolloProvider>
</AuthProvider>
</FatalErrorBoundary>
)

View File

@@ -35,7 +35,6 @@ 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" />
@@ -60,7 +59,7 @@ const Routes = () => {
<Private unauthenticated="home" role="admin">
<Route path="/admin/users" page={UsersPage} name="users" />
<Route path="/admin/projects" page={AdminProjectsPage} name="adminProjects" />
<Route path="/admin/projects" page={AdminProjectsPage} name="projects" />
<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" />

View File

@@ -1,75 +1,29 @@
export type CadPackageType = 'openscad' | 'cadquery' | 'jscad' | 'INIT'
interface CadPackageConfig {
label: string
buttonClasses: string
dotClasses: string
}
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',
},
INIT: {
label: '',
buttonClasses: '',
dotClasses: '',
},
}
import { ideTypeNameMap } from 'src/helpers/hooks/useIdeContext'
interface CadPackageProps {
cadPackage: CadPackageType
cadPackage: string
className?: string
dotClass?: string
onClick?: any
}
const CadPackage = ({
cadPackage,
className = '',
dotClass = 'w-5 h-5',
onClick,
}: CadPackageProps) => {
const cadPackageConfig = cadPackageConfigs[cadPackage]
const CadPackage = ({ cadPackage, className = '' }: CadPackageProps) => {
const cadName = ideTypeNameMap[cadPackage] || ''
const isOpenScad = cadPackage === 'openscad'
const isCadQuery = cadPackage === 'cadquery'
return (
<ButtonOrDiv
onClick={onClick}
<div
className={
`grid grid-flow-col-dense items-center gap-2 text-gray-100 bg-opacity-30
${cadPackageConfig?.buttonClasses} ` + 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
}
>
<div
className={`${cadPackageConfig?.dotClasses} ${dotClass} rounded-full`}
className={`${isOpenScad && 'bg-yellow-200'} ${
isCadQuery && 'bg-blue-800'
} w-5 h-5 rounded-full`}
/>
{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>
<div>{cadName}</div>
</div>
)
}

View File

@@ -1,265 +1,193 @@
import { useEffect, useState } from 'react'
import { useState } from 'react'
import { toast } from '@redwoodjs/web/toast'
import { toJpeg } from 'html-to-image'
import Popover from '@material-ui/core/Popover'
import Svg from 'src/components/Svg/Svg'
import Button from 'src/components/Button/Button'
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'
export const captureSize = { width: 500, height: 522 }
const anchorOrigin = {
vertical: 'bottom',
horizontal: 'center',
}
const transformOrigin = {
vertical: 'top',
horizontal: 'center',
}
const CaptureButtonViewer = ({
onInit,
onScadImage,
canvasRatio = 1,
}: {
onInit: (a: any) => void
onScadImage: (a: any) => void
canvasRatio: number
const CaptureButton = ({
canEdit,
TheButton,
shouldUpdateImage,
projectTitle,
userName,
}) => {
const { state } = useIdeContext()
const threeInstance = React.useRef(null)
const [dataType, dataTypeSetter] = useState(state?.objectData?.type)
const [artifact, artifactSetter] = useState(state?.objectData?.data)
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' &&
requestRenderStateless({
state,
camera,
viewerSize: {
width: threeInstance.current.size.width * canvasRatio,
height: threeInstance.current.size.height * canvasRatio,
},
viewAll: isFirstCameraChange,
})
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))
}
})
}
return (
<PureIdeViewer
scadRatio={canvasRatio}
dataType={dataType}
artifact={artifact}
onInit={getThreeInstance}
onCameraChange={onCameraChange}
isLoading={isLoading}
camera={camera}
isMinimal
/>
)
}
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>
)
}
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 [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 [partSnapShot64, partSnapShot64Setter] = React.useState('')
const [scadSnapShot64, scadSnapShot64Setter] = React.useState('')
const captureRef = React.useRef<HTMLDivElement>(null)
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)
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)
async function uploadAndUpdateImage() {
const upload = async () => {
const socialCard64 = toJpeg(ref.current, {
cacheBust: true,
quality: 0.7,
})
}
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,
// 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,
},
})
thunkDispatch({
type: 'switchEditorModel',
payload: state.editorTabs.length,
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 [{ 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)
}
}
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>
)}
</div>
)
}
export default CaptureButton

View File

@@ -1,6 +1,3 @@
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'
@@ -9,28 +6,16 @@ import {
CadhubStringParam,
CadhubBooleanParam,
CadhubNumberParam,
CadhubStringChoiceParam,
CadhubNumberChoiceParam,
} from './customizerConverter'
import { debounce } from 'lodash'
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 = {
@@ -44,20 +29,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 ${
isOpen ? 'h-full max-h-96' : ''
open ? 'h-2/3' : ''
}`}
>
<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={toggleOpen}>
<button className="px-2" onClick={() => setOpen(!open)}>
<Svg
name="chevron-down"
className={`h-8 w-8 ${!isOpen && 'transform rotate-180'}`}
className={`h-8 w-8 ${!open && 'transform rotate-180'}`}
/>
</button>
<div>Parameters</div>
</div>
{isOpen && (
{open && (
<>
<div className="flex items-center">
<div className="font-fira-sans text-sm mr-4">Auto Update</div>
@@ -77,17 +62,11 @@ 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
@@ -96,29 +75,18 @@ const Customizer = () => {
</>
)}
</div>
<div
className={`${isOpen ? 'h-full pb-32' : 'h-0'} overflow-y-auto px-12`}
>
<div className={`${open ? '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,
param.type == 'number' ? Number(value) : value
),
onChange: (value) => updateCustomizerParam(param.name, value),
}
if (
param.input === 'choice-string' ||
param.input === 'choice-number'
) {
return <ChoiceParam key={index} param={param} {...otherProps} />
} else if (param.input === 'default-string') {
if (param.type === 'string') {
return <StringParam key={index} param={param} {...otherProps} />
} else if (param.input === 'default-number') {
} else if (param.type === 'number') {
return <NumberParam key={index} param={param} {...otherProps} />
} else if (param.input === 'default-boolean') {
} else if (param.type === 'boolean') {
return <BooleanParam key={index} param={param} {...otherProps} />
}
return <div key={index}>{JSON.stringify(param)}</div>
@@ -143,7 +111,7 @@ function CustomizerParamBase({
return (
<li
className="grid items-center my-2"
style={{ gridTemplateColumns: 'auto 16rem' }}
style={{ gridTemplateColumns: 'auto 8rem' }}
>
<div className=" text-sm font-fira-sans">
<div className="font-bold text-base">{name}</div>
@@ -161,7 +129,7 @@ function BooleanParam({
}: {
param: CadhubBooleanParam
value: any
onChange: (value: any) => void
onChange: Function
}) {
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
@@ -191,7 +159,7 @@ function StringParam({
}: {
param: CadhubStringParam
value: any
onChange: (value: any) => void
onChange: Function
}) {
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
@@ -206,79 +174,6 @@ 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,
@@ -286,13 +181,24 @@ function NumberParam({
}: {
param: CadhubNumberParam
value: any
onChange: (value: any) => void
onChange: Function
}) {
const [isFocused, isFocusedSetter] = React.useState(false)
const [localValue, localValueSetter] = React.useState(value)
const [localValue, localValueSetter] = React.useState(0)
const [isLocked, isLockedSetter] = React.useState(false)
const [pixelsDragged, pixelsDraggedSetter] = React.useState(0)
const handleRender = useRender()
const liveRenderHandler = debounce((a) => handleRender(a), 250)
const step = param.step || 1
const live = false // TODO get from param
let decimal = 0
if('decimal' in param){
decimal = param.decimal
}else{
let str = String(step)
const idx = str.indexOf('.')
if(idx !== -1) decimal = str.length - idx - 1
}
const commitChange = () => {
let num = localValue
if (typeof param.step === 'number') {
@@ -304,7 +210,7 @@ function NumberParam({
if (typeof param.max === 'number') {
num = Math.min(param.max, num)
}
num = Number((num || 0).toFixed(2))
num = Number(num.toFixed(decimal))
localValueSetter(num)
onChange(num)
}
@@ -351,7 +257,9 @@ function NumberParam({
onMouseMove={({ movementX }) => {
if (isLocked && movementX) {
pixelsDraggedSetter(pixelsDragged + (movementX * step) / 8) // one step per 8 pixels
localValueSetter(Number(pixelsDragged.toFixed(2)))
const decimalFixed = Number((Math.round(pixelsDragged / step) * step).toFixed(decimal))
localValueSetter(decimalFixed)
if(live) liveRenderHandler({[param.name]: decimalFixed})
}
}}
>

View File

@@ -1,45 +1,24 @@
// 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
@@ -47,22 +26,95 @@ 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
| CadhubStringChoiceParam
| CadhubNumberChoiceParam
// 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 },
]

View File

@@ -21,7 +21,7 @@ const DelayedPingAnimation = ({
if (showLoading && isLoading)
return (
<div className="inset-0 absolute flex items-center justify-center pointer-events-none">
<div className="inset-0 absolute flex items-center justify-center">
<div className="h-16 w-16 bg-pink-600 rounded-full animate-ping"></div>
</div>
)

View File

@@ -24,7 +24,7 @@ const EditableProjectTitle = ({
const [newTitle, setNewTitle] = useState(projectTitle)
const inputRef = React.useRef(null)
const { updateProject } = useUpdateProject({
const { updateProject, loading, error } = useUpdateProject({
onCompleted: ({ updateProject }) => {
const routeVars = {
userName: updateProject.user.userName,

View File

@@ -1,53 +0,0 @@
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>
)
}

View File

@@ -1,68 +0,0 @@
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

View File

@@ -1,71 +0,0 @@
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>
)
}

View File

@@ -1,75 +1,133 @@
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">
{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>
<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>
<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 className="grid grid-flow-col-dense gap-6 px-5">
<FileDropdown
handleRender={onRender}
handleStlDownload={handleStlDownload}
/>
<button className="cursor-not-allowed" disabled>
Edit
</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>
</div>
<AllShortcutsModal />
</>
<CadPackage cadPackage={state.ideType} className="px-3" />
</div>
)
}
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 &amp; 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>
)
}

View File

@@ -3,7 +3,6 @@ import { fileSave } from 'browser-fs-access'
import { MeshBasicMaterial, Mesh, Scene } from 'three'
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'
import { requestRender, State } from 'src/helpers/hooks/useIdeState'
import { toast } from '@redwoodjs/web/toast'
export const PullTitleFromFirstLine = (code = '') => {
const firstLine = code.split('\n').filter(identity)[0] || ''
@@ -54,9 +53,6 @@ export const makeStlDownloadHandler =
extensions: ['.stl'],
})
}
toast(
"CadHub is a work in process and We're still working out kinks with the STL download."
)
if (geometry) {
if (
type === 'geometry' &&
@@ -64,8 +60,7 @@ export const makeStlDownloadHandler =
) {
saveFile(makeStlBlobFromGeo(geometry))
} else if (ideType == 'jscad') {
const clonedGeometry = geometry.map((mesh) => mesh.clone())
saveFile(makeStlBlobFromMesh(...clonedGeometry))
saveFile(makeStlBlobFromMesh(...geometry))
} else {
thunkDispatch((dispatch, getState) => {
const state = getState()
@@ -74,8 +69,12 @@ export const makeStlDownloadHandler =
requestRender({
state,
dispatch,
code: state.code,
viewerSize: state.viewerSize,
camera: state.camera,
quality: 'high',
specialCadProcess,
parameters: state.currentParameters,
}).then(
(result) => result && saveFile(makeStlBlobFromGeo(result.data))
)

View File

@@ -1,121 +0,0 @@
import React from 'react'
import { useRender } from 'src/components/IdeWrapper/useRender'
import { makeStlDownloadHandler, PullTitleFromFirstLine } from './helpers'
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>
}

View File

@@ -1,6 +1,5 @@
import { useState } from 'react'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { cadPackageConfigs } from 'src/components/CadPackage/CadPackage'
import { useIdeContext, ideTypeNameMap } from 'src/helpers/hooks/useIdeContext'
import OutBound from 'src/components/OutBound/OutBound'
import { prepareEncodedUrl, makeExternalUrl } from './helpers'
import { copyTextToClipboard } from 'src/helpers/clipboard'
@@ -12,11 +11,10 @@ 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 = cadPackageConfigs[state.ideType].label
const cadName = ideTypeNameMap[state.ideType]
const onPaste: React.ClipboardEventHandler<HTMLInputElement> = async ({
clipboardData,
@@ -55,7 +53,7 @@ const ExternalScript = () => {
}
return (
<div className="p-4">
<p className="text-sm pb-4 border-b border-gray-700">
<p className="text-sm pb-4">
Paste an external url containing a {cadName} script to generate a new
CadHub url for this resource.{' '}
<OutBound
@@ -68,9 +66,9 @@ const ExternalScript = () => {
</p>
{['INIT', 'ERROR'].includes(asyncState) && (
<>
<p className="mt-4">Paste url</p>
<p>Paste url</p>
<input
className="p-1 text-xs border border-ch-purple-450 w-full"
className="p-1 text-xs rounded border border-gray-700 w-full"
value={rawUrl}
onChange={onChange}
onPaste={onPaste}
@@ -93,23 +91,23 @@ const ExternalScript = () => {
<input
value={makeExternalUrl(rawUrl).replace(/^.+:\/\//g, '')}
readOnly
className="py-1 px-2 mt-4 text-xs border border-ch-purple-450 w-full"
className="p-1 mt-4 text-xs rounded-t border border-gray-700 w-full"
/>
<button
className="w-full bg-ch-purple-450 hover:bg-ch-purple-400 py-1 text-gray-300"
className="w-full bg-gray-700 py-1 rounded-b text-gray-300"
onClick={() => copyTextToClipboard(makeExternalUrl(rawUrl))}
>
Copy URL
</button>
<div className="flex flex-col gap-2 pt-2">
<button
className="bg-gray-500 hover:bg-gray-600 p-1 px-2 text-gray-200"
className="bg-gray-500 p-1 px-2 rounded text-gray-300"
onClick={onCopyRender}
>
Copy &amp; Render
</button>
<button
className="bg-gray-500 hover:bg-gray-600 p-1 px-2 text-gray-200"
className="bg-gray-500 p-1 px-2 rounded text-gray-300"
onClick={() => {
setAsyncState('INIT')
setRawUrl('')

View File

@@ -13,10 +13,10 @@ const FullScriptEncoding = () => {
<input
value={encodedLink.replace(/^.+:\/\//g, '')}
readOnly
className="py-1 px-2 mt-4 text-xs border border-ch-purple-450 w-full"
className="p-1 mt-4 text-xs rounded-t border border-gray-700 w-full"
/>
<button
className="w-full bg-ch-purple-450 hover:bg-ch-purple-400 py-1 text-gray-300"
className="w-full bg-gray-700 py-1 rounded-b text-gray-300"
onClick={() => copyTextToClipboard(encodedLink)}
>
Copy URL

View File

@@ -5,7 +5,6 @@ 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'
@@ -33,17 +32,13 @@ export function makeExternalUrl(resourceUrl: string): string {
}#${fetchText}=${prepareDecodedUrl(resourceUrl)}`
}
export function useIdeInit(
cadPackage: State['ideType'],
code = '',
viewerContext: State['viewerContext'] = 'ide'
) {
export function useIdeInit(cadPackage: string, code = '') {
const { thunkDispatch } = useIdeContext()
const handleRender = useRender()
useEffect(() => {
thunkDispatch({
type: 'initIde',
payload: { cadPackage, code, viewerContext },
payload: { cadPackage, code },
})
if (code) {
return

View File

@@ -2,34 +2,7 @@ import { FatalErrorBoundary as FatalErrorBoundaryBase } from '@redwoodjs/web'
import * as Sentry from '@sentry/browser'
class FatalErrorBoundary extends FatalErrorBoundaryBase {
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
*/
componentDidCatch(error, errorInfo) {
Sentry.withScope((scope) => {
scope.setExtras(errorInfo)
Sentry.captureException(error)

View File

@@ -3,7 +3,7 @@ import OutBound from 'src/components/OutBound'
const Footer = () => {
return (
<div className="bg-indigo-900 text-indigo-200 font-roboto text-sm">
<div className="bg-indigo-900 text-indigo-200 font-roboto mt-20 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

View File

@@ -0,0 +1,7 @@
import Footer from './Footer'
export const generated = () => {
return <Footer />
}
export default { title: 'Components/Footer' }

View File

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

View File

@@ -1,111 +0,0 @@
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>
))}
</>
)
}

View File

@@ -1,609 +0,0 @@
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.',
},
].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. Weve got a lot planned, and were 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>
)
}

View File

@@ -15,29 +15,14 @@ const IdeConsole = () => {
<div>
{state.consoleMessages?.map(({ type, message, time }, index) => (
<pre
className="font-mono text-sm text-gray-400"
className="font-mono text-sm text-gray-300"
key={`${message} ${index}`}
>
<div className="text-xs font-bold pt-2 text-ch-blue-400">
<div className="text-xs font-bold pt-2 text-ch-blue-600">
{time?.toLocaleString()}
</div>
<div className={(type === 'error' ? 'text-red-400' : '') + ' pl-4'}>
{(message || '').split('\n').map((line, index) => {
return (
<div key={index}>
{line.startsWith('ECHO:') ? (
<span className="text-xs">
ECHO:{' '}
<span className="text-purple-300 font-semibold text-base">
{line.slice(6)}
</span>
</span>
) : (
line
)}
</div>
)
})}
{message}
</div>
</pre>
))}

View File

@@ -8,7 +8,7 @@ import PanelToolbar from 'src/components/PanelToolbar/PanelToolbar'
import { use3dViewerResize } from 'src/helpers/hooks/use3dViewerResize'
const IdeEditor = lazy(() => import('src/components/IdeEditor/IdeEditor'))
import IdeViewer from 'src/components/IdeViewer/IdeViewer'
const IdeViewer = lazy(() => import('src/components/IdeViewer/IdeViewer'))
const SmallLoadingPing = (
<div className="bg-ch-gray-800 text-gray-200 font-ropa-sans relative w-full h-full flex justify-center items-center">
@@ -22,7 +22,7 @@ const SmallLoadingPing = (
)
export const BigLoadingPing = (
<div className="inset-0 absolute flex items-center justify-center bg-ch-gray-800 pointer-events-none">
<div className="inset-0 absolute flex items-center justify-center bg-ch-gray-800">
<div className="h-16 w-16 bg-pink-600 rounded-full animate-ping"></div>
</div>
)
@@ -33,45 +33,28 @@ const ELEMENT_MAP = {
<IdeEditor Loading={SmallLoadingPing} />
</Suspense>
),
Viewer: <IdeViewer />,
Viewer: (
<Suspense fallback={BigLoadingPing}>
<IdeViewer Loading={BigLoadingPing} />
</Suspense>
),
Console: <IdeConsole />,
}
const TOOLBAR_MAP = {
Editor: () => (
Editor: (
<div className="w-full">
<EditorMenu />
</div>
),
Viewer: (thunkDispatch) => (
Viewer: (
<div>
<PanelToolbar
panelName="Viewer"
onClick={() =>
thunkDispatch((dispatch) =>
dispatch({
type: 'settingsButtonClicked',
payload: ['Settings', 'viewer'],
})
)
}
/>
<PanelToolbar panelName="Viewer" />
</div>
),
Console: (thunkDispatch) => (
Console: (
<div>
<PanelToolbar
panelName="Console"
showTopGradient
onClick={() =>
thunkDispatch((dispatch) =>
dispatch({
type: 'settingsButtonClicked',
payload: ['Settings', 'console'],
})
)
}
/>
<PanelToolbar panelName="Console" showTopGradient />
</div>
),
}
@@ -91,7 +74,7 @@ const IdeContainer = () => {
return (
<MosaicWindow
path={path}
renderToolbar={() => TOOLBAR_MAP[id](thunkDispatch)}
renderToolbar={() => TOOLBAR_MAP[id]}
className={`${id.toLowerCase()} ${id.toLowerCase()}-tile`}
>
{id === 'Viewer' ? (
@@ -106,7 +89,7 @@ const IdeContainer = () => {
}}
value={state.layout}
onChange={(newLayout) =>
thunkDispatch({ type: 'setLayout', payload: newLayout })
thunkDispatch({ type: 'setLayout', payload: { message: newLayout } })
}
onRelease={handleViewerSizeUpdate}
/>

View File

@@ -1,11 +1,11 @@
import { useEffect, useRef, useState } from 'react'
import { useEffect, useState } from 'react'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { makeCodeStoreKey, requestRender } from 'src/helpers/hooks/useIdeState'
import Editor, { useMonaco } from '@monaco-editor/react'
import { theme } from 'src/../config/tailwind.config'
import { theme } from 'src/../tailwind.config'
import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode'
import type { CadPackageType } from 'src/components/CadPackage/CadPackage'
import EditorGuide from 'src/components/EditorGuide/EditorGuide'
import type { CadPackage as CadPackageType } from 'src/helpers/hooks/useIdeState'
import CadPackage from '../CadPackage/CadPackage'
const colors = theme.extend.colors
@@ -18,7 +18,6 @@ const IdeEditor = ({ Loading }) => {
cadquery: 'python',
openscad: 'cpp',
jscad: 'javascript',
INIT: '',
}
const monaco = useMonaco()
useEffect(() => {
@@ -60,78 +59,31 @@ const IdeEditor = ({ Loading }) => {
requestRender({
state,
dispatch,
code: state.code,
viewerSize: state.viewerSize,
camera: state.camera,
parameters: state.currentParameters,
})
})
localStorage.setItem(makeCodeStoreKey(state.ideType), state.code)
}
}
const currentTab = state.editorTabs[state.currentModel]
return (
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
className="h-full"
onKeyDown={handleSaveHotkey}
>
{state.editorTabs.length > 1 && (
<fieldset className="bg-ch-gray-700 text-ch-gray-300 flex m-0 p-0">
{state.editorTabs.map((model, i) => (
<label
key={model.type + '-' + i}
className={
'flex items-center gap-2 px-4 py-1 block m-0 select-none relative bg-ch-gray-600 ' +
(state.currentModel === i && 'bg-ch-gray-800')
}
>
{model.label}
<input
type="radio"
name="models"
className="sr-only absolute inset-0"
value={i}
checked={state.currentModel === i}
onChange={() =>
thunkDispatch({ type: 'switchEditorModel', payload: i })
}
/>
{model.type !== 'code' && (
<button
onClick={() =>
thunkDispatch({ type: 'removeEditorModel', payload: i })
}
className="block p-1 m-.5 hover:bg-ch-gray-550"
>
<svg viewBox="0 0 5 5" className="w-4 text-ch-gray-300">
<path
stroke="currentColor"
d="M 1 1 l 3 3 M 1 4 l 3 -3"
strokeLinecap="round"
strokeWidth=".5"
/>
</svg>
</button>
)}
</label>
))}
</fieldset>
)}
{currentTab.type === 'code' && (
<Editor
defaultValue={state.code}
value={state.code}
theme={theme}
loading={Loading}
// TODO #247 cpp seems better than js for the time being
defaultLanguage={ideTypeToLanguageMap[state.ideType] || 'cpp'}
language={ideTypeToLanguageMap[state.ideType] || 'cpp'}
onChange={handleCodeChange}
/>
)}
{currentTab.type === 'guide' && (
<div className="bg-ch-gray-800 h-full">
<EditorGuide content={currentTab.content} />
</div>
)}
{currentTab.type === 'component' && <currentTab.Component />}
<Editor
defaultValue={state.code}
value={state.code}
theme={theme}
loading={Loading}
// TODO #247 cpp seems better than js for the time being
defaultLanguage={ideTypeToLanguageMap[state.ideType] || 'cpp'}
language={ideTypeToLanguageMap[state.ideType] || 'cpp'}
onChange={handleCodeChange}
/>
</div>
)
}

View File

@@ -1,30 +1,16 @@
import { useAuth } from '@redwoodjs/auth'
import { useMutation } from '@redwoodjs/web'
import { Popover } from '@headlessui/react'
import { Link, navigate, routes } from '@redwoodjs/router'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'
import FullScriptEncoding from 'src/components/EncodedUrl/FullScriptEncoding'
import ExternalScript from 'src/components/EncodedUrl/ExternalScript'
import Svg from 'src/components/Svg/Svg'
import { toast } from '@redwoodjs/web/toast'
import CaptureButton from 'src/components/CaptureButton/CaptureButton'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import NavPlusButton from 'src/components/NavPlusButton'
import ProfileSlashLogin from 'src/components/ProfileSlashLogin'
import Gravatar from 'src/components/Gravatar/Gravatar'
import EditableProjectTitle from 'src/components/EditableProjecTitle/EditableProjecTitle'
const FORK_PROJECT_MUTATION = gql`
mutation ForkProjectMutation($input: ForkProjectInput!) {
forkProject(input: $input) {
id
title
user {
id
userName
}
}
}
`
import CaptureButton from 'src/components/CaptureButton/CaptureButton'
const TopButton = ({
onClick,
@@ -51,183 +37,150 @@ const TopButton = ({
)
}
export default function IdeHeader({
interface IdeHeaderProps {
handleRender: () => void
projectTitle?: string
projectOwner?: string
projectOwnerId?: string
projectOwnerImage?: string
projectId?: string
}
const IdeHeader = ({
handleRender,
context,
}: {
handleRender?: () => void
context: 'ide' | 'profile'
}) {
projectOwner,
projectTitle,
projectOwnerImage,
projectId,
projectOwnerId,
}: IdeHeaderProps) => {
const { currentUser } = useAuth()
const { project, state } = useIdeContext()
const hasUnsavedChanges =
project?.code && state?.code && project?.code !== state?.code
const isProfile = context === 'profile'
const { project } = useIdeContext()
const canEdit =
(currentUser && currentUser?.sub === project?.user?.id) ||
currentUser?.roles?.includes('admin')
const projectOwner = project?.user?.userName
const showForkMessage = !canEdit && hasUnsavedChanges && currentUser?.sub
const [createFork] = useMutation(FORK_PROJECT_MUTATION, {
onCompleted: ({ forkProject }) => {
const params = {
userName: forkProject?.user?.userName,
projectTitle: forkProject?.title,
}
navigate(!isProfile ? routes.ide(params) : routes.project(params))
},
})
const handleFork = () => {
const prom = createFork({
variables: {
input: {
userId: currentUser.sub,
forkedFromId: project?.id,
code: state?.code,
},
},
})
toast.promise(prom, {
loading: 'Forking...',
success: <b>Forked successfully!</b>,
error: <b>Problem forking.</b>,
})
}
(currentUser &&
currentUser?.sub === (project?.user?.id || projectOwnerId)) ||
currentUser?.roles.includes('admin')
const _projectId = projectId || project?.id
const _projectOwner = project?.user?.userName || projectOwner
return (
<>
<div className="flex justify-between flex-grow h-full">
<div className="flex h-full items-center text-gray-300">
{project?.id && (
<>
<span className="bg-ch-gray-700 h-full grid grid-flow-col-dense items-center gap-2 px-4">
<Gravatar image={project?.user?.image} className="w-10" />
<Link
to={routes.user({
userName: projectOwner,
})}
>
{projectOwner}
</Link>
</span>
<EditableProjectTitle
id={project?.id}
userName={projectOwner}
projectTitle={project?.title}
canEdit={canEdit}
shouldRouteToIde={!isProfile}
/>
</>
)}
</div>
<div className="grid grid-flow-col-dense gap-4 items-center mr-4">
{canEdit && !isProfile && (
<CaptureButton
TheButton={({ onClick }) => (
<TopButton
onClick={onClick}
name="Save Project Image"
className=" bg-ch-blue-650 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
>
<Svg name="camera" className="w-6 h-6 text-ch-blue-400" />
</TopButton>
)}
<div className="h-16 w-full bg-ch-gray-900 flex justify-between items-center text-lg">
{_projectId ? (
<div className="h-full text-gray-300 flex items-center">
<span className="bg-ch-gray-700 h-full grid grid-flow-col-dense items-center gap-2 px-4">
<Gravatar
image={project?.user?.image || projectOwnerImage}
className="w-10"
/>
)}
{!isProfile && (
<TopButton
className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
onClick={handleRender}
name={canEdit ? 'Save' : 'Preview'}
<Link
to={routes.user({
userName: _projectOwner,
})}
>
<Svg
name={canEdit ? 'floppy-disk' : 'photograph'}
className="w-6 h-6 text-ch-pink-500"
/>
</TopButton>
)}
{isProfile && (
<TopButton
className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
onClick={() =>
navigate(
routes.ide({
userName: projectOwner,
projectTitle: project.title,
})
)
}
name="Editor"
>
<Svg name="terminal" className="w-6 h-6 text-ch-pink-500" />
</TopButton>
)}
<Popover className="relative outline-none w-full h-full">
{({ open }) => {
return (
<>
<Popover.Button className="h-full outline-none">
<TopButton
Tag="div"
name="Share"
className=" bg-ch-purple-400 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
>
<Svg
name="share"
className="w-6 h-6 text-ch-purple-500 mt-1"
/>
</TopButton>
</Popover.Button>
{open && (
<Popover.Panel className="absolute z-10 mt-4 right-0">
<Tabs
className="bg-ch-purple-gray-200 rounded-md shadow-md overflow-hidden text-gray-700"
selectedTabClassName="bg-ch-gray-700 text-white"
>
<TabPanel>
<FullScriptEncoding />
</TabPanel>
<TabPanel>
<ExternalScript />
</TabPanel>
<TabList className="flex whitespace-nowrap text-gray-700 border-t border-gray-700">
<Tab className="p-3 px-5">encoded script</Tab>
<Tab className="p-3 px-5">external script</Tab>
</TabList>
</Tabs>
</Popover.Panel>
)}
</>
)
}}
</Popover>
{currentUser?.sub && (
<TopButton
onClick={handleFork}
name="Fork"
className={
showForkMessage
? ' bg-ch-blue-650 bg-opacity-80 hover:bg-opacity-100 text-ch-gray-300 border-blue-300 border-2'
: ' bg-ch-blue-650 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300'
}
>
<Svg name="fork-new" className="w-6 h-6 text-ch-blue-400" />
</TopButton>
)}
{showForkMessage && (
<div className="fixed bg-white w-60 h-10 top-16 right-24 z-10 rounded-md text-sm flex p-2 items-center">
<Svg
name="exclamation-circle"
className="w-8 h-8 mx-2 text-ch-blue-500"
/>{' '}
Fork to save your work
</div>
)}
{_projectOwner}
</Link>
</span>
<EditableProjectTitle
id={_projectId}
userName={_projectOwner}
projectTitle={project?.title || projectTitle}
canEdit={canEdit}
shouldRouteToIde={!projectTitle}
/>
</div>
) : (
<div />
)}
<div className="text-gray-200 grid grid-flow-col-dense gap-4 mr-4 items-center">
{canEdit && !projectTitle && (
<CaptureButton
canEdit={canEdit}
projectTitle={project?.title}
userName={project?.user?.userName}
shouldUpdateImage={!project?.mainImage}
TheButton={({ onClick }) => (
<TopButton
onClick={onClick}
name="Save Project Image"
className=" bg-ch-gray-300 bg-opacity-70 hover:bg-opacity-90 text-ch-gray-900"
>
<Svg name="camera" className="w-6 h-6" />
</TopButton>
)}
/>
)}
{!projectTitle && (
<TopButton
className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
onClick={handleRender}
name={canEdit ? 'Save' : 'Preview'}
>
<Svg
name={canEdit ? 'floppy-disk' : 'photograph'}
className="w-6 h-6 text-ch-pink-500"
/>
</TopButton>
)}
{projectTitle && (
<TopButton
className="bg-ch-pink-800 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
onClick={() =>
navigate(routes.ide({ userName: _projectOwner, projectTitle }))
}
name="Editor"
>
<Svg name="terminal" className="w-6 h-6 text-ch-pink-500" />
</TopButton>
)}
<Popover className="relative outline-none w-full h-full">
{({ open }) => {
return (
<>
<Popover.Button className="h-full w-full outline-none">
<TopButton
Tag="div"
name="Share"
className=" bg-ch-purple-400 bg-opacity-30 hover:bg-opacity-80 text-ch-gray-300"
>
<Svg
name="share"
className="w-6 h-6 text-ch-purple-500 mt-1"
/>
</TopButton>
</Popover.Button>
{open && (
<Popover.Panel className="absolute z-10 mt-4 right-0">
<Tabs
className="bg-gray-300 rounded-md shadow-md overflow-hidden text-gray-700"
selectedTabClassName="bg-gray-200"
>
<TabPanel>
<FullScriptEncoding />
</TabPanel>
<TabPanel>
<ExternalScript />
</TabPanel>
<TabList className="flex whitespace-nowrap text-gray-700 border-t border-gray-700">
<Tab className="p-3 px-5">encoded script</Tab>
<Tab className="p-3 px-5">external script</Tab>
</TabList>
</Tabs>
</Popover.Panel>
)}
</>
)
}}
</Popover>
{/* <TopButton>Fork</TopButton> */}
<div className="h-8 w-8 flex-shrink-0 rounded-full border-2 border-gray-200 flex items-center justify-center">
<NavPlusButton />
</div>
<ProfileSlashLogin />
</div>
</>
</div>
)
}
export default IdeHeader

View File

@@ -1,5 +1,7 @@
import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { navigate, routes } from '@redwoodjs/router'
import { QUERY as UsersProjectsQuery } from 'src/components/ProjectsOfUserCell'
import useUser from 'src/helpers/hooks/useUser'
import DevIdePage from 'src/pages/DevIdePage/DevIdePage'
@@ -50,6 +52,17 @@ export const UPDATE_PROJECT_MUTATION_IDE = gql`
}
}
`
export const FORK_PROJECT_MUTATION = gql`
mutation ForkProjectMutation($input: CreateProjectInput!) {
forkProject(input: $input) {
id
title
user {
userName
}
}
}
`
export const Loading = () => <div>Loading...</div>
@@ -77,5 +90,31 @@ export const Success = ({
},
}
)
const [forkProject] = useMutation(FORK_PROJECT_MUTATION, {
refetchQueries: [
{
query: UsersProjectsQuery,
variables: { userName: user?.userName },
},
],
onCompleted: ({ forkProject }) => {
navigate(
routes.ide({
userName: forkProject?.user?.userName,
projectTitle: forkProject?.title,
})
)
toast.success('Project Forked.')
},
})
const saveCode = async ({ input, id, isFork }: SaveCodeArgs) => {
if (!isFork) {
await updateProject({ variables: { id, input } })
refetch()
return
}
forkProject({ variables: { input } })
}
return <DevIdePage cadPackage={project?.cadPackage} project={project} />
}

View File

@@ -1,86 +1,22 @@
import { getActiveClasses } from 'get-active-classes'
import { Link, routes } from '@redwoodjs/router'
import Svg from 'src/components/Svg/Svg'
import {
sidebarTopConfig,
sidebarBottomConfig,
sidebarCombinedConfig,
} from './sidebarConfig'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
function TabToggle({ item, className = '', active, onChange, onClick }) {
return (
<label
className={getActiveClasses({
'bg-ch-pink-800 text-ch-pink-300 bg-opacity-30': active,
'text-ch-gray-550 cursor-not-allowed': item.disabled,
[`text-ch-gray-300 p-3 mb-1 flex justify-center ${className}`]: true,
})}
>
<input
name="sidebar-tabs"
type="radio"
disabled={item.disabled}
value={item.name}
onChange={onChange}
onClick={onClick}
className="sr-only"
/>
<Svg name={item.icon} className="w-8 mx-auto" />
</label>
)
}
const IdeSideBar = () => {
const { state, thunkDispatch } = useIdeContext()
function onTabClick(name) {
return function () {
thunkDispatch({ type: 'settingsButtonClicked', payload: [name] })
}
}
const selectedTab = React.useMemo(
() => sidebarCombinedConfig.find((item) => item.name === state.sideTray[0]),
[state.sideTray]
)
return (
<section className="flex h-full bg-ch-gray-900">
<fieldset className="h-full flex flex-col justify-between bg-ch-gray-700">
<div>
{sidebarTopConfig.map((topItem, i) => (
<TabToggle
item={topItem}
active={state.sideTray[0] === topItem.name}
onChange={() => onTabClick(topItem.name)}
onClick={onTabClick(topItem.name)}
key={'tab-' + i}
/>
))}
</div>
<div>
{sidebarBottomConfig.map((bottomItem, i) => (
<TabToggle
item={bottomItem}
active={state.sideTray[0] === bottomItem.name}
onChange={() => onTabClick(bottomItem.name)}
onClick={onTabClick(bottomItem.name)}
key={'tab-' + (sidebarTopConfig.length + i)}
/>
))}
</div>
</fieldset>
{selectedTab?.panel && (
<div
className="w-56 bg-ch-gray-900 text-ch-gray-300 border border-ch-pink-800 border-opacity-30"
style={{ height: 'calc(100% - 6px)', margin: '3px' }}
>
<h2 className="flex items-center h-9 px-4 bg-ch-pink-800 bg-opacity-30">
{selectedTab.name}
</h2>
{selectedTab.panel}
</div>
)}
</section>
<div className="h-full flex flex-col justify-between">
<div className="w-16 h-16 flex items-center justify-center bg-ch-gray-900">
<Link to={routes.home()}>
<Svg className="w-12" name="favicon" />
</Link>
</div>
<button
className="text-gray-300 p-2 pb-4 flex justify-center cursor-not-allowed"
aria-label="IDE settings"
disabled
>
<Svg name="big-gear" />
</button>
</div>
)
}

View File

@@ -1,239 +0,0 @@
import { ReactNode } from 'react'
import { SvgNames } from 'src/components/Svg/Svg'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { createRemoveUpdate, updateTree } from 'react-mosaic-component'
import type { MosaicPath } from 'react-mosaic-component'
import Toggle from 'src/components/Toggle'
import type { MosaicTree } from 'src/helpers/hooks/useIdeState'
interface SidebarConfigType {
name: string
icon: SvgNames
disabled: boolean
panel: ReactNode | null
}
const getPathById = (
tree: MosaicTree,
id: string,
path: MosaicPath = []
): MosaicPath => {
if (tree.first === id || tree.second === id) {
return [...path, tree.first === id ? 'first' : 'second']
}
if (typeof tree.first !== 'string' && typeof tree.second !== 'string') {
throw new Error('id not found')
}
try {
if (typeof tree.first !== 'string') {
return getPathById(tree.first, id, [...path, 'first'])
}
} catch (error) {
throw new Error('id not found')
}
try {
if (typeof tree.second !== 'string') {
return getPathById(tree.second, id, [...path, 'second'])
}
} catch (error) {
throw new Error('id not found')
}
throw new Error('id not found')
}
export const sidebarTopConfig: SidebarConfigType[] = [
{
name: 'Files',
icon: 'files',
disabled: false,
panel: (
<article className="px-2 py-4">
<p>
<em>Coming Soon</em>
</p>
<hr className="my-4" />
<p>
We're working on multi-file support in tandem with the GitHub
integration.
</p>
</article>
),
},
{
name: 'GitHub',
icon: 'github',
disabled: false,
panel: (
<article className="px-2 py-4">
<p>
<em>Coming Soon</em>
</p>
<hr className="my-4" />
<p>
This integration will allow you to sync a project with a GitHub repo
and push changes back to it as a commit!
</p>
</article>
),
},
{
name: 'Visibility',
icon: 'eye',
disabled: true,
panel: null,
},
]
const DiscordLink = () => (
<a
className="underline text-ch-pink-300"
href="https://discord.gg/SD7zFRNjGH"
target="_blank"
rel="noreferrer"
>
Discord
</a>
)
interface settingsConfig {
title: string
name: string
Content: React.FC
}
const settingsConfig: settingsConfig[] = [
{
title: 'Editor',
name: 'editor',
Content: () => (
<div className="p-2">
<p>
<em>Coming Soon</em>
</p>
<hr className="my-2" />
<p className="p-2">
We're building configuration settings for the Viewer pane now. Join us
on <DiscordLink /> if you want to lend a hand!
</p>
</div>
),
},
{
title: 'Viewer',
name: 'viewer',
Content: () => (
<div className="p-2">
<p>
<em>Coming Soon</em>
</p>
<hr className="my-2" />
<p className="p-2">
We're building configuration settings for the Viewer pane now. Join us
on <DiscordLink /> if you want to lend a hand!
</p>
</div>
),
},
{
title: 'Console',
name: 'console',
Content: () => {
const { state, thunkDispatch } = useIdeContext()
const consolePath = React.useMemo<MosaicPath | null>(() => {
try {
const path = getPathById(state.layout, 'Console')
return path
} catch (error) {
return null
}
}, [state.layout])
return (
<div className="p-2">
<li className="list-none select-none">
<label
className="grid items-center my-2 cursor-pointer"
style={{ gridTemplateColumns: '1fr auto' }}
>
<span>Visible</span>
<Toggle
offLabel="Hide"
onLabel="Show"
onChange={(newValue) => {
if (consolePath) {
const newTree = updateTree(state.layout, [
createRemoveUpdate(state.layout, consolePath),
])
thunkDispatch({ type: 'setLayout', payload: newTree })
} else {
// Split 'Viewer' panel to add console back in
const viewerPath = getPathById(state.layout, 'Viewer')
const newTree = { ...state.layout }
let temp = newTree
viewerPath.forEach((name) => {
if (newTree[name] === 'Viewer') {
newTree[name] = {
direction: 'column',
first: 'Viewer',
second: 'Console',
splitPercentage: 70,
}
return
}
temp = { ...newTree[name] }
})
thunkDispatch({ type: 'setLayout', payload: newTree })
}
}}
checked={!!consolePath}
/>
</label>
</li>
</div>
)
},
},
]
export const sidebarBottomConfig: SidebarConfigType[] = [
{
name: 'Settings',
icon: 'gear',
disabled: false,
panel: <SettingsMenu parentName="Settings" />,
},
]
export const sidebarCombinedConfig = [
...sidebarTopConfig,
...sidebarBottomConfig,
]
function SettingsMenu({ parentName }: { parentName: string }) {
const { state, thunkDispatch } = useIdeContext()
return (
<article className="">
{settingsConfig.map(({ name, title, Content }) => (
<details
key={'settings-tray-' + name}
open={state.sideTray.slice(-1)[0] === name}
>
<summary
className="px-2 py-2 bg-ch-pink-800 bg-opacity-10 my-px cursor-pointer"
onClick={(e) => {
e.preventDefault()
thunkDispatch((dispatch) =>
dispatch({
type: 'settingsButtonClicked',
payload: [parentName, name],
})
)
}}
>
{title}
</summary>
<Content />
</details>
))}
</article>
)
}

View File

@@ -1,104 +0,0 @@
import * as THREE from 'three'
import { useState } from 'react'
import { useThree } from '@react-three/fiber'
import { useTexture } from '@react-three/drei'
import { useEdgeSplit } from 'src/helpers/hooks/useEdgeSplit'
import texture from './dullFrontLitMetal.png'
import type { ArtifactTypes } from 'src/helpers/cadPackages/common'
const thresholdAngle = 12
export function Asset({
geometry: incomingGeo,
dataType,
controlsRef,
}: {
geometry: any // eslint-disable-line @typescript-eslint/no-explicit-any
dataType: 'INIT' | ArtifactTypes
controlsRef: React.MutableRefObject<any> // eslint-disable-line @typescript-eslint/no-explicit-any
}) {
const threeInstance = useThree()
const [initZoom, setInitZoom] = useState(true)
const mesh = useEdgeSplit((thresholdAngle * Math.PI) / 180, true, incomingGeo)
const edges = React.useMemo(
() =>
incomingGeo.length || dataType !== 'geometry'
? null
: new THREE.EdgesGeometry(incomingGeo, thresholdAngle),
[incomingGeo, dataType]
)
React.useEffect(() => {
const getBoundingSphere = () => {
if (dataType === 'geometry') {
if (!incomingGeo.boundingSphere && incomingGeo.computeBoundingSphere) {
incomingGeo.computeBoundingSphere()
}
return incomingGeo.boundingSphere
}
const group = new THREE.Group()
incomingGeo.forEach((mesh) => group.add(mesh))
const bbox = new THREE.Box3().setFromObject(group)
return bbox.getBoundingSphere(new THREE.Sphere())
}
const bSphere = getBoundingSphere()
const zoomToFit = () => {
const { center, radius } = bSphere
const { camera } = threeInstance
const offset = 3
controlsRef.current.reset()
controlsRef.current.target.copy(center)
camera.position.copy(
center
.clone()
.add(
new THREE.Vector3(
offset * radius,
-offset * radius,
offset * radius
)
)
)
camera.updateProjectionMatrix()
}
if (initZoom) {
if (!bSphere) return
zoomToFit()
setInitZoom(false)
}
}, [incomingGeo, dataType, controlsRef, initZoom, threeInstance])
const PrimitiveArray = React.useMemo(
() =>
dataType === 'primitive-array' &&
incomingGeo?.map((mesh) => mesh.clone()),
[dataType, incomingGeo]
)
const colorMap = useTexture(texture)
if (!incomingGeo) return null
if (PrimitiveArray)
return PrimitiveArray.map((mesh, index) => (
<primitive object={mesh} key={index} />
))
return (
<group dispose={null}>
<mesh ref={mesh} scale={[1, 1, 1]} geometry={incomingGeo}>
<meshPhysicalMaterial
envMapIntensity={0.1}
color="#F472B6"
map={colorMap}
clearcoat={0.1}
clearcoatRoughness={0.2}
roughness={10}
metalness={0.7}
smoothShading
/>
</mesh>
<lineSegments geometry={edges} renderOrder={100}>
<lineBasicMaterial color="#aaaaff" opacity={0.5} transparent />
</lineSegments>
</group>
)
}

View File

@@ -1,50 +1,278 @@
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import * as THREE from 'three'
import { useRef, useState, useEffect } from 'react'
import { Canvas, useThree } from '@react-three/fiber'
import {
PerspectiveCamera,
GizmoHelper,
GizmoViewport,
OrbitControls,
Environment,
useTexture,
} from '@react-three/drei'
import { useEdgeSplit } from 'src/helpers/hooks/useEdgeSplit'
import { Vector3 } from 'three'
import { requestRender } from 'src/helpers/hooks/useIdeState'
import { PureIdeViewer } from './PureIdeViewer'
import texture from './dullFrontLitMetal.png'
import Customizer from 'src/components/Customizer/Customizer'
import DelayedPingAnimation from 'src/components/DelayedPingAnimation/DelayedPingAnimation'
const IdeViewer = ({
handleOwnCamera = false,
}: {
handleOwnCamera?: boolean
}) => {
const thresholdAngle = 12
function Asset({ geometry: incomingGeo }) {
const mesh = useEdgeSplit((thresholdAngle * Math.PI) / 180, true, incomingGeo)
const edges = React.useMemo(
() =>
incomingGeo.length
? null
: new THREE.EdgesGeometry(incomingGeo, thresholdAngle),
[incomingGeo]
)
const colorMap = useTexture(texture)
if (!incomingGeo) return null
return (
<group dispose={null}>
<mesh ref={mesh} scale={[1, 1, 1]} geometry={incomingGeo}>
<meshPhysicalMaterial
envMapIntensity={2}
color="#F472B6"
map={colorMap}
clearcoat={0.1}
clearcoatRoughness={0.2}
roughness={10}
metalness={0.9}
smoothShading
/>
</mesh>
<lineSegments geometry={edges} renderOrder={100}>
<lineBasicMaterial color="#aaaaff" opacity={0.5} transparent />
</lineSegments>
</group>
)
}
let debounceTimeoutId
function Controls({ onCameraChange, onDragStart, onInit }) {
const controls = useRef<any>()
const threeInstance = useThree()
const { camera, gl } = threeInstance
useEffect(() => {
onInit(threeInstance)
// init camera position
camera.position.x = 200
camera.position.y = 140
camera.position.z = 20
camera.far = 10000
camera.fov = 22.5 // matches default openscad fov
camera.updateProjectionMatrix()
camera.rotation._order = 'ZYX'
const getRotations = (): number[] => {
const { x, y, z } = camera?.rotation || {}
return [x, y, z].map((rot) => (rot * 180) / Math.PI)
}
const getPositions = () => {
// Difficult to make this clean since I'm not sure why it works
// The OpenSCAD camera seems hard to work with but maybe it's just me
// this gives us a vector the same length as the camera.position
const cameraViewVector = new Vector3(0, 0, 1)
.applyQuaternion(camera.quaternion) // make unit vector of the camera
.multiplyScalar(camera.position.length()) // make it the same length as the position vector
// make a vector from the position vector to the cameraView vector
const head2Head = new Vector3().subVectors(
camera.position,
cameraViewVector
)
const { x, y, z } = head2Head.add(camera.position)
return {
position: { x, y, z },
dist: camera.position.length(),
}
}
if (controls.current) {
const dragCallback = () => {
clearTimeout(debounceTimeoutId)
debounceTimeoutId = setTimeout(() => {
const [x, y, z] = getRotations()
const { position, dist } = getPositions()
onCameraChange({
position,
rotation: { x, y, z },
dist,
})
}, 400)
}
const dragStart = () => {
onDragStart()
clearTimeout(debounceTimeoutId)
}
controls?.current?.addEventListener('end', dragCallback)
controls?.current?.addEventListener('start', dragStart)
const oldCurrent = controls.current
dragCallback()
return () => {
oldCurrent.removeEventListener('end', dragCallback)
oldCurrent.removeEventListener('start', dragStart)
}
}
}, [camera, controls])
return (
<OrbitControls makeDefault ref={controls} args={[camera, gl.domElement]} />
)
}
function Box(props) {
// This reference will give us direct access to the mesh
const mesh = useRef()
return (
<mesh {...props} ref={mesh} scale={[1, 1, 1]}>
<boxBufferGeometry args={props.size} />
<meshStandardMaterial color={props.color} />
</mesh>
)
}
function Sphere(props) {
const mesh = useRef()
return (
<mesh {...props} ref={mesh} scale={[1, 1, 1]}>
<sphereBufferGeometry args={[2, 30, 30]} />
<meshStandardMaterial color={props.color} />
</mesh>
)
}
const IdeViewer = ({ Loading }) => {
const { state, thunkDispatch } = useIdeContext()
const dataType = state.objectData?.type
const artifact = state.objectData?.data
const [isDragging, setIsDragging] = useState(false)
const [image, setImage] = useState()
const onInit = (threeInstance) => {
thunkDispatch({ type: 'setThreeInstance', payload: threeInstance })
}
const onCameraChange = (camera) => {
if (handleOwnCamera) {
return
}
thunkDispatch({
type: 'updateCamera',
payload: { camera },
})
thunkDispatch((dispatch, getState) => {
const state = getState()
if (['png', 'INIT'].includes(state?.objectData?.type)) {
dispatch({ type: 'setLoading' })
requestRender({
state,
dispatch,
camera,
viewAll: state?.objectData?.type === 'INIT',
})
}
})
}
useEffect(() => {
setImage(state.objectData?.type === 'png' && state.objectData?.data)
setIsDragging(false)
}, [state.objectData?.type, state.objectData?.data])
const R3FComponent = React.useMemo(
() => state.objectData?.type === 'r3f-component' && state.objectData?.data,
[state.objectData?.type, state.objectData?.data]
)
// the following are tailwind colors in hex, can't use these classes to color three.js meshes.
const pink400 = '#F472B6'
const indigo300 = '#A5B4FC'
const indigo900 = '#312E81'
const jscadLightIntensity =
state.objectData?.type === 'geometry' &&
state.objectData?.data &&
state.objectData?.data.length
? 0.5
: 1.2
return (
<PureIdeViewer
dataType={dataType}
artifact={artifact}
onInit={onInit}
onCameraChange={onCameraChange}
isLoading={state.isLoading}
camera={state?.camera}
/>
<div className="relative h-full bg-ch-gray-800">
{state.isLoading && Loading}
{image && (
<div
className={`absolute inset-0 transition-opacity duration-500 ${
isDragging ? 'opacity-25' : 'opacity-100'
}`}
>
<img
alt="code-cad preview"
id="special"
src={URL.createObjectURL(image)}
className="h-full w-full"
/>
</div>
)}
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
className={`opacity-0 absolute inset-0 transition-opacity duration-500 ${
!(isDragging || state.objectData?.type !== 'png')
? 'hover:opacity-50'
: 'opacity-100'
}`}
onMouseDown={() => setIsDragging(true)}
>
<Canvas linear={true} dpr={[1, 2]}>
<Controls
onDragStart={() => setIsDragging(true)}
onInit={onInit}
onCameraChange={(camera) => {
thunkDispatch({
type: 'updateCamera',
payload: { camera },
})
thunkDispatch((dispatch, getState) => {
const state = getState()
if (['png', 'INIT'].includes(state.objectData?.type)) {
dispatch({ type: 'setLoading' })
requestRender({
state,
dispatch,
code: state.code,
viewerSize: state.viewerSize,
camera,
parameters: state.currentParameters,
})
}
})
}}
/>
<PerspectiveCamera makeDefault up={[0, 0, 1]}>
<pointLight
position={[0, 0, 100]}
intensity={jscadLightIntensity}
/>
</PerspectiveCamera>
<ambientLight intensity={0.3} />
<Environment preset="warehouse" />
<pointLight
position={[-1000, -1000, -1000]}
color="#5555FF"
intensity={0.5}
/>
<pointLight
position={[-1000, 0, 1000]}
color="#5555FF"
intensity={0.5}
/>
<gridHelper
args={[200, 20, 0xff5555, 0x555555]}
material-opacity={0.2}
material-transparent
rotation-x={Math.PI / 2}
/>
<GizmoHelper alignment={'top-left'} margin={[80, 80]}>
<GizmoViewport
axisColors={['red', 'green', 'blue']}
labelColor="black"
/>
</GizmoHelper>
{state.objectData?.type === 'png' && (
<>
<Sphere position={[0, 0, 0]} color={pink400} />
<Box position={[0, 50, 0]} size={[1, 100, 1]} color={indigo900} />
<Box position={[0, 0, 50]} size={[1, 1, 100]} color={indigo300} />
<Box position={[50, 0, 0]} size={[100, 1, 1]} color={pink400} />
</>
)}
{state.objectData?.type === 'geometry' && state.objectData?.data && (
<Asset geometry={state.objectData?.data} />
)}
{R3FComponent && <R3FComponent />}
</Canvas>
</div>
<DelayedPingAnimation isLoading={state.isLoading} />
<Customizer />
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More