223 Commits

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

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

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

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

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

* Remove discord.js dependency.

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

builds are being canceled even though there are changes

* Revert "Upgrade packages (#594)"

This reverts commit 5531f2e0c1.

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

This reverts commit 90fece9598.

* Revert "Force netlify build"

This reverts commit 315ebf0c59.

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

* Tweak discord bot message.

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

* initial implementation

* Fix openscad size bug

* Add overlays to embed

* Remove console.log and reuse exact query

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

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

* Correct the initial code file location

* Preview and stl mvp working

* Prepare changes for review and preview build

* Run curv inside of /tmp

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

* Lock in specific curv commit for reproducible builds

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

* Add curv to backend schema

* Frontend changes to accommodate curv deploy

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

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

* Final tweaks for deploy

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

* Format project

Co-authored-by: lf94 <inbox@leefallat.ca>
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2021-11-30 15:24:24 +11:00
Kurt Hutten
e9859d85b8 Kurt/upgrade rw 38 (#575)
* Upgrade to rw 38

* Prisma migrate after V3 upgrade

* rw 0.38.1
2021-11-13 11:53:42 +11:00
Kurt Hutten
a62c5bce03 Update OpenSCAD userGuide 2021-11-12 07:29:44 +11:00
Kurt Hutten
149b2b6360 bounding sphere fix (#572) 2021-11-06 13:36:53 +11:00
Kurt Hutten
43fc897bf9 Zoom to fit for openscad (#569)
* Add viewall flag to openscad cli in prep for zoom to fit for scad previews

* Fix remaining issues with social image capture
2021-11-06 09:46:55 +11:00
Kurt Hutten
a909188f15 Increment contributor count 2021-10-24 07:12:04 +11:00
Jaakko Mäntylä
e8da05be8c Use project title as stl name (#570) 2021-10-24 07:09:46 +11:00
Kurt Hutten
dd0178d554 Add expressing intent blog post 2021-10-22 20:56:57 +11:00
Kurt Hutten
e3efb1a3dd Improve types in menuConfig 2021-10-21 07:08:50 +11:00
Kurt Hutten
cd90c3ce49 Update curated code cad 2021-10-20 15:31:08 +11:00
Kurt Hutten
1ea4f9bdd5 Remove the glitch effect on the home page. (#567)
I three people mention to me "what is getting all distorted. Obviously
it's not clear that it's a stylistic effect. plus the home page
animation is busy enough as it is.
2021-10-20 14:25:34 +11:00
Jay Clark
e5f2552fc9 Fix customizer input size (#565)
Signed-off-by: Jay Clark <jay@jayeclark.dev>
2021-10-20 14:19:39 +11:00
Kurt Hutten
219f341972 Kurt/rw 37 upgrade (#566)
* Update readme

* Upgrade redwood to 0.37.x
2021-10-20 14:10:19 +11:00
Kurt Hutten
e26beda598 Fix zoom to fit bug 2021-10-18 07:42:00 +11:00
Kurt Hutten
c402b051e2 Fix stl download bug 2021-10-18 07:25:38 +11:00
Lucas Barros
f7172be68b Pass current code as input for fork mutation (#563) 2021-10-17 11:41:26 +11:00
Kurt Hutten
da0a4d6f1c Add initial code to seed script 2021-10-17 10:23:49 +11:00
Kurt Hutten
0bc759cf9e Lint project 2021-10-17 05:42:25 +11:00
Leonel Jara
a4cfc37576 Add zoom to fit on first load (#561)
* get the bounding box of assets and get controls Ref

* Add zoom to fit on first load

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2021-10-17 05:40:58 +11:00
Kurt Hutten
bc1f12971d Make-cq-customizer-more-resilient (#560)
Was failing when initial values were missing
2021-10-16 03:59:19 +11:00
Kurt Hutten
50cd44cd76 Linting 2021-10-16 03:37:59 +11:00
Kurt Hutten
55917395b4 Refactor recent projects into it's own cell (#558) 2021-10-16 03:35:44 +11:00
Frank Noirot
dc92920481 Merge pull request #557 from Irev-Dev/franknoirot/add-recent-projects
Added recent projects list to logged-in nav
2021-10-15 09:24:28 -04:00
Kurt Hutten
434eb0ef86 Release CQ customizer (#559)
* Switched to Miniconda image

* Update cad endpoint url

and some minor tweaks

Co-authored-by: Jeremy Wright <wrightjmf@gmail.com>

Co-authored-by: Jeremy Wright <wrightjmf@gmail.com>
2021-10-15 18:06:31 +11:00
Jeremy Wright
96ee9c4aa4 Add CadQuery customizer (#547)
* Rough changes to make the CadQuery integration work with the customizer

* Tweak runCQ

* Switched to Anaconda

* Cleaned up code

* Update CadHub after anaconda

Related to #547

* Add final tweaks to CQ customizer

* Separated out customizer.json from params.json

* Changes after discussing CadHub integration

* linting runCQ

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2021-10-15 02:39:03 +11:00
Frank Johnson
77014f0d36 Added recent projects list to logged-in nav 2021-10-13 17:03:44 -04:00
Kurt Hutten
3df903ffc6 Linting 2021-10-13 20:22:49 +11:00
Kurt Hutten
68fa10437e Remove dead code 2021-10-13 20:15:37 +11:00
Kurt Hutten
342953b25f Update contributor count 2021-10-13 18:45:58 +11:00
Kurt Hutten
421ceee88d Format and tweak 554 2021-10-13 05:56:17 +11:00
Javier González Bodas
ae05a79e58 Add modal for alerting that is neccesary to fork the project. Remove unused import component (#554) 2021-10-13 05:55:23 +11:00
Davor Hrg
549217e953 Hide console option in view menu (#545)
* Update menuConfig.tsx

* Revert "Update menuConfig.tsx"

This reverts commit 7be28e2a76.

* second attempt

* Update mosaic tree to remove and add the console.

* Added Toggle UI component

* Remove console noise from Toggle component

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
Co-authored-by: Frank Johnson <frankjohnson1993@gmail.com>
2021-10-12 20:44:16 +11:00
Kurt Hutten
4804c3bfe9 Put social media save popover into editor tab (#541)
and make them live
2021-10-12 06:09:56 +11:00
Lucas Barros
6c093e65bf Add project fork to seed file (#552) 2021-10-12 05:54:08 +11:00
Kurt Hutten
4b9a8591ab Some changes to the side tray to help make writing an issue for #540 (#551) 2021-10-11 07:46:42 +11:00
Kurt Hutten
9f769d6a61 Improve preprender docs and format 2021-10-08 17:04:00 +11:00
Kurt Hutten
32d6ef27ad Attempt 2 at fixing prerendering error 2021-10-07 21:25:07 +11:00
Kurt Hutten
c4c195074b Update cad lambda docs 2021-10-07 20:22:51 +11:00
Kurt Hutten
3f6d919f22 Add jscad to metadata 2021-10-05 19:04:47 +11:00
Kurt Hutten
aabe682782 Fix netify's prerendering service (#546)
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.

textures was causing a issue on the home page resulting in "Fatal Error"
as the social preview text, not good.

As a bonus I thing I fix FE sentry logging too.
2021-10-05 18:52:24 +11:00
Kurt Hutten
5efaec4df0 Update contributor count
maybe this should be dynamic, but not too hard to update as well
2021-10-02 10:03:51 +10:00
Kurt Hutten
6d0c832f6f Add glitch to homepage (#542) 2021-10-01 21:48:13 +10:00
Kurt Hutten
66dc04d98e Add sign up toast message 2021-10-01 21:22:02 +10:00
Kurt Hutten
3aa3254e48 Add more verification to sign up 2021-09-30 20:28:13 +10:00
Kurt Hutten
879f24b08b Delete project properly as it's not causing problems with forking logic (#539) 2021-09-29 19:04:35 +10:00
Kurt Hutten
b80ea7f813 Make choose your character section only link to draft if not signed in 2021-09-29 18:03:00 +10:00
Kurt Hutten
e9ad7180a7 Fixing linting problem from running yarn rw lint (#537)
✖ 118 problems (65 errors, 53 warnings) currently
2021-09-29 17:35:07 +10:00
Kurt Hutten
0ce7ce4e76 fix homepage pre-rendering broken in #531 (#538)
import form project cell which in tern had dependencies that are not
pre render friendly was causing issues
2021-09-29 17:34:53 +10:00
Hendrie Bosch
088cfa4f2d Typo in integrations.mdx (#536) 2021-09-29 07:05:27 +10:00
Kurt Hutten
ab92894a2d Add id to project query 2021-09-29 05:10:39 +10:00
Frank Noirot
911744a071 Merge pull request #533 from Irev-Dev/frank/add-project-forking
Add project forking
2021-09-28 08:51:36 -04:00
Frank Noirot
d4bfcb4eb8 Merge pull request #534 from Irev-Dev/kurt/add-project-forking
Refactor IdeHeader to take middle buttons as children
2021-09-28 08:49:26 -04:00
Kurt Hutten
965e5b0f54 Update TopNav in UserProfile to remove unneeded props 2021-09-28 20:35:56 +10:00
Kurt Hutten
77799a5870 Refactor IdeHeader to take middle buttons as children 2021-09-28 20:26:15 +10:00
Frank Johnson
7540c908e7 Added link in ProjectProfile and fork count to ProjectCard 2021-09-28 06:18:41 +10:00
Kurt Hutten
dd152709ff Add forking graphQL resolvers 2021-09-28 06:18:41 +10:00
Frank Johnson
2d7fb91f92 added navigation to new project on fork 2021-09-28 06:18:41 +10:00
Kurt Hutten
02463db741 Start project fork feature
Updated schema, project service and UI
Still some polish to go.

Co-authored-by: Frank Noirot <franknoirot@users.noreply.github.com>
2021-09-28 06:18:41 +10:00
Kurt Hutten
38b905e180 Change how customizer params are applied (#529)
* Only send customizer params when it's open

* Add customizer reset button and have two modes of customizer vs not

depending of if the customizer is open.

* Remove re-render on customizer open/close in project profile
2021-09-28 06:05:22 +10:00
Kurt Hutten
cc50c984e4 Move create project into plus button (#531)
The draft page used to automatically create a new project and route
the user to the new project if the user was signed in. Problem arose
if the user use the back button as they would end up creating more
project. resolved my moving this logic into the plus button itself

Resolves #511
2021-09-27 20:43:30 +10:00
Kurt Hutten
9aee4ae725 Add comment 2021-09-26 21:05:35 +10:00
Kurt Hutten
6e45ce96d7 Update hero model (#532) 2021-09-26 05:39:29 +10:00
Kurt Hutten
892b1d3809 Patch for customizer bug causing CQ projects to fail 2021-09-26 05:01:37 +10:00
Kurt Hutten
83d327ad20 Z-index tweak for hinge 2021-09-25 19:51:09 +10:00
Kurt Hutten
c77169cf21 format project 2021-09-25 17:59:34 +10:00
Kurt Hutten
0ba4ec4e21 fix plus button z-index (#530)
The preserve-3d fix for the FF parallax broke the plus button as the
popover was always beneath the main body.

normal z-index fixes didn't apply since it was 3d z-index problem.
2021-09-25 17:25:08 +10:00
Kurt Hutten
06dbc35cf8 Fix JSCAD download again
Small problem where downloading the mesh would make it disappear
from the viewer. Fixed by cloning the geometry before downloading
2021-09-25 06:20:05 +10:00
Kurt Hutten
8170da854d Fix JSCAD download (#528) 2021-09-24 17:59:09 +10:00
Davor Hrg
d255a78cd1 fix parameters retention (#512) 2021-09-24 17:58:58 +10:00
Kurt Hutten
c19658b7f8 Enforce name and username with length 2021-09-24 05:26:51 +10:00
Frank Noirot
335dac8677 Show empty KeyValue's if editing (#526)
* Show empty KeyValue's if editing

* Add verification to name and user name, enforce length

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2021-09-23 18:51:44 +10:00
Kurt Hutten
18732e27fc Add parallax to floaty homepage cards (#524)
* Add parallax to floaty homepage cards

* Add firefox fix and restore model mouse tracking

* Add overflow-x hidden for safety
2021-09-23 18:16:41 +10:00
Kurt Hutten
64624b9c3e Add code backdrops to homepage (#522) 2021-09-23 18:14:06 +10:00
Kurt Hutten
a89f2e7992 Update README.md 2021-09-23 17:22:18 +10:00
Kurt Hutten
6d7f6fb4bf Create CONTRIBUTING.md 2021-09-23 17:20:32 +10:00
Kurt Hutten
b621d78eb4 Add integration info to docs 2021-09-23 17:13:43 +10:00
Kurt Hutten
1dcae6057c Update README.md 2021-09-22 07:58:07 +10:00
Davor Hrg
648f174bd8 Update README.md (#523) 2021-09-22 06:15:16 +10:00
Kurt Hutten
f20fe9a075 Add new design to readme 2021-09-21 20:37:54 +10:00
Kurt Hutten
023b4862eb rename scad again 2021-09-20 19:28:18 +10:00
Kurt Hutten
a2d278fa4d Rename scad 2021-09-20 19:27:47 +10:00
Kurt Hutten
f6df9d1988 upgrade rw + lint (#521)
* Various linting fixes

* Fix component name

* Upgrade to redwood 0.36.4
2021-09-20 19:08:03 +10:00
Kurt Hutten
39ce35b219 Merge pull request #519 from Irev-Dev/editor-tabs
Initial editor tabs implementation with CAD package guides
2021-09-20 17:55:30 +10:00
Kurt Hutten
33c08119ec format 2021-09-20 17:53:00 +10:00
Kurt Hutten
1475fa24d1 tweak uerGuide metadata 2021-09-20 17:52:19 +10:00
Frank Johnson
348d2e0a01 Made CadPackage component support button or div 2021-09-19 17:02:21 -04:00
Frank Johnson
65fc526220 fixed my broken merge with kurt's branch commit, updated OpenSCAD contributors 2021-09-19 14:10:23 -04:00
Frank Johnson
4c4f5643f4 Merge branch 'editor-tabs' of https://github.com/Irev-Dev/cadhub into editor-tabs 2021-09-19 11:07:32 -04:00
Frank Johnson
634304dfce Added cadPackage to ProjectsOfUser, other cleanup/linting 2021-09-19 11:06:45 -04:00
Frank Noirot
70980afab0 Update app/web/src/helpers/markdown.ts
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2021-09-19 09:48:17 -04:00
Kurt Hutten
bb4659a2dd Make raw-loader more specific and use .js extension for jscad
suggestion for #519
2021-09-19 20:13:28 +10:00
Frank Johnson
59df7fdc25 Added globals.d.ts 2021-09-19 01:07:46 -04:00
Frank Johnson
2d4977ba8f Fixed TS errors with file 2021-09-19 01:05:51 -04:00
Frank Johnson
b27bcd2d35 Completed initial CAD package guides, tweaked initial code import 2021-09-18 23:16:43 -04:00
Frank Johnson
2f006d3e3b Added test tabs, got closing and switching working 2021-09-18 19:54:54 -04:00
Kurt Hutten
d71eec6a5e Merge pull request #516 from Irev-Dev/kurt/user-profile-path-515
user profile patch
2021-09-18 17:54:14 +10:00
Kurt Hutten
3a977ded02 user profile patch
Resolved #515
2021-09-18 17:53:24 +10:00
Kurt Hutten
b14587cdf1 Merge pull request #514 from Irev-Dev/side-tray-styles-patch
side-tray-styles-patch
2021-09-18 17:33:41 +10:00
Kurt Hutten
5f8862b4d2 side-tray-styles-patch 2021-09-18 17:27:36 +10:00
Kurt Hutten
f3201cfd97 format 2021-09-18 16:47:17 +10:00
Kurt Hutten
d94645d381 Merge pull request #507 from Irev-Dev/sidebar-tray
Sidebar tray
2021-09-18 16:22:13 +10:00
Kurt Hutten
cd1cecd774 Remove double icon 2021-09-18 16:19:31 +10:00
Kurt Hutten
a87c1ae9f4 Merge branch 'main' into sidebar-tray 2021-09-18 15:41:52 +10:00
Kurt Hutten
c271600432 Merge pull request #510 from Irev-Dev/profile-page
Profile page redesign
2021-09-18 15:16:33 +10:00
Kurt Hutten
1fb14db6f3 Don't use https for social image 2021-09-15 19:47:15 +10:00
Frank Johnson
7b2be01430 Fix up KeyValue component, fix save issue of Bio, simplify UserProfile 2021-09-15 01:53:44 -04:00
Kurt Hutten
9d9e3c4957 Update homepage meta tag 2021-09-15 05:38:08 +10:00
Frank Johnson
fc6cded59e Merge issues trying to look at a stashed commit 2021-09-12 17:47:59 -04:00
Frank Johnson
2ec3a0b202 Sorted out using <details> element, got ancestor clicks closing out to their level 2021-09-12 17:13:30 -04:00
Frank Johnson
88326ed573 Style tweaks to ImageUploader no-image state 2021-09-12 13:40:47 -04:00
Frank Johnson
58fc8866f1 Minor tweak to Sign In/Up in IDE 2021-09-12 13:11:12 -04:00
Frank Johnson
74a5f9bf2c Linting fixes 2021-09-12 12:42:22 -04:00
Frank Johnson
690d45ff9a Merge branch 'main' of https://github.com/Irev-Dev/cadhub into profile-page 2021-09-12 12:41:27 -04:00
Frank Johnson
34757cf535 Finished fixing nav, tweaked KeyValue edit btn 2021-09-12 12:38:16 -04:00
Kurt Hutten
69c83d33b1 State controlled tray mvp 2021-09-12 19:54:31 +10:00
Frank Johnson
55d48057da Initial profile refactor of layout and config 2021-09-12 05:03:58 -04:00
Kurt Hutten
e7031e9c0d Merge branch 'main' into sidebar-tray 2021-09-12 17:25:52 +10:00
Kurt Hutten
e99f0c07ba Merge pull request #506 from Irev-Dev/kurt/three-perf-n-tweaks-rebase
Improve three scene performance and add JSCAD
2021-09-12 14:52:08 +10:00
Kurt Hutten
e526fa812e Improve three scene performance and add JSCAD
- smoothed follow mouse animation
- made mobile friendlier down to about 330px ish
- added default social image
- used smaller hero asset
2021-09-12 14:40:10 +10:00
Frank Johnson
3dbb963e4e Updated signed in user menu in IDE 2021-09-11 22:14:18 -04:00
Frank Johnson
126b60f5dd Style tweaks to signed-in user modal in top nav 2021-09-11 22:05:35 -04:00
Frank Johnson
6a69a1c1bf Updated styles on Pwd Recovery page 2021-09-11 21:19:34 -04:00
Kurt Hutten
750d10c01d Merge pull request #505 from Irev-Dev/popover-style-tweaks
Tweaked styles on  login modal, sign up button, and new project popover
2021-09-12 08:51:25 +10:00
Frank Johnson
a51991ef0d Tweaked styles on login modal, sign up button, and new project popover 2021-09-11 17:57:00 -04:00
Frank Johnson
011baad9d0 Broke out SettingsMenu as a standalone component 2021-09-11 13:49:26 -04:00
Kurt Hutten
ec9f9d241e Merge pull request #502 from Irev-Dev/kurt/homepage-redo-rebase
Redo homepage to @franknoirot 's new designs
2021-09-11 23:17:51 +10:00
Kurt Hutten
b8fa22eede Redo homepage to @franknoirot 's new designs
Not finished but enough for a mvp
designs; https://www.figma.com/file/VUh53RdncjZ7NuFYj0RGB9/CadHub?node-id=1652%3A4224
2021-09-11 23:15:28 +10:00
Frank Johnson
a8c05a3d27 Merge origin/main 2021-09-11 06:05:17 -04:00
Frank Johnson
39f9a02c0a Broke out into a config file 2021-09-11 05:57:00 -04:00
Frank Johnson
e47ad59003 Added Settings tray with initial sections as details elements 2021-09-11 05:43:29 -04:00
Frank Johnson
0d7e958505 got tsome dummy text in the Files and GitHub menus, improved styling 2021-09-11 05:02:15 -04:00
Frank Johnson
9812db5cd6 got working toggle tabs! I don't like HeadlessUI's Tabs, they don't appear to support programmatic opening 2021-09-11 04:34:13 -04:00
Kurt Hutten
12ab456446 format 2021-09-10 18:45:03 +10:00
Frank Johnson
206ec7fdab aupdated headless-ui, got started on naive implementation 2021-09-10 02:29:13 -04:00
Frank Noirot
da557a5c16 Merge pull request #496 from Irev-Dev/keyboard-shortcuts
Initial keyboard shortcuts configuration implementation
2021-09-09 18:12:59 -04:00
Frank Johnson
fba971b419 remove dependencies from /app/package.json 2021-09-09 07:24:53 -04:00
Kurt Hutten
2e2e7be633 Fix console error messages 2021-09-09 18:36:11 +10:00
Frank Noirot
8a54a88b0a Merge branch 'main' into keyboard-shortcuts 2021-09-08 17:52:58 -04:00
Frank Noirot
5d128c6cbd Merge pull request #497 from Irev-Dev/franknoirot/style-tweaks
minor style tweaks to editor
2021-09-08 17:23:29 -04:00
Frank Johnson
b09733175e removed unused lines in AllShortcutsModal.tsx 2021-09-08 11:46:55 -04:00
Frank Johnson
d3d4b5a632 Added AllShortcutsModal into View menu, fixed visual bug with border-radius 2021-09-08 11:35:17 -04:00
Frank Johnson
5b85eec64c removed big-gear 2021-09-08 10:44:44 -04:00
Frank Johnson
0cf599bbe2 Fixed NPM/Yarn mixup and ran linter, updated AllShortcutsModal shortcut 2021-09-08 10:36:21 -04:00
Kurt Hutten
3f1947a4d9 Merge pull request #495 from Irev-Dev/kurt/494-move-worker-into-webpack-build
Move worker into webpack build
2021-09-08 18:14:25 +10:00
Kurt Hutten
3e26e3d420 Fix pre-render fail 2021-09-08 17:44:31 +10:00
Frank Johnson
51bc32aad0 minor style tweaks to editor 2021-09-08 02:52:45 -04:00
Frank Johnson
70cbe9d11e found a solution for the menu items not rendering within HeadlessUI docs 2021-09-07 21:51:47 -04:00
Frank Johnson
c95bfc400b tweaked DropdownItem styling and removed dev process fluff 2021-09-07 21:25:54 -04:00
Frank Johnson
678754d251 Adding @irev-dev's solution to register all shortcuts 2021-09-07 21:19:29 -04:00
Kurt Hutten
58b618cf5f format jscad worker 2021-09-08 06:18:11 +10:00
Kurt Hutten
22da074965 Move worker into webpack build
The jscad worker code was hosted as a static asset, making it odd
javascript where we have to be conscious of what javascript features we
can use and if it will work on older browsers, plus it can't be
typescript like the rest of the codebase.

Since redwood 0.36 we using webpack 5 should make loading workers easy
https://webpack.js.org/guides/web-workers/
But I had trouble with this (see:
https://community.redwoodjs.com/t/has-anyone-tried-workers-with-webpack-5-rw0-36-x/2394)
and instead used the webpack 4 loader without any issues

This issue relates to #411 , and is a checklist item on #444
Resolves #494
2021-09-08 06:16:52 +10:00
Kurt Hutten
9ae1cd4aff Merge pull request #493 from Irev-Dev/kurt/492-project-card
Make new project card
2021-09-08 06:10:16 +10:00
Frank Johnson
9887eb4804 Another attempt using a component property within the config 2021-09-07 13:07:35 -04:00
Kurt Hutten
7f4eb85106 Make new project card
designs
https://www.figma.com/file/VUh53RdncjZ7NuFYj0RGB9/CadHub?node-id=1150%3A1619
Resolves #492
2021-09-07 20:05:51 +10:00
Frank Johnson
896baf08d1 added an All Shortcuts dialog 2021-09-06 14:20:46 -04:00
Frank Johnson
e1d429877c initial attempt at shortcut system 2021-09-06 13:01:38 -04:00
Kurt Hutten
b9f3955767 Merge pull request #491 from Irev-Dev/kurt/462
Make social card more robust
2021-09-06 20:49:12 +10:00
Kurt Hutten
f9a43e53e2 Make social card more robust
Resolves #462
2021-09-06 20:48:39 +10:00
Kurt Hutten
442da1ffc6 Merge pull request #490 from Irev-Dev/franknoirot/details-styles
style tweaks to description area within the Project details view.
2021-09-06 20:45:53 +10:00
Kurt Hutten
57970465b1 Update docs 2021-09-06 20:11:44 +10:00
Frank Johnson
d203fe7e57 style tweaks to description area within the Project details view. 2021-09-05 21:23:09 -04:00
Kurt Hutten
abdebfccad Fix typo 2021-09-06 08:06:35 +10:00
Kurt Hutten
eb238b6902 Add openscad fonts documentation. 2021-09-06 07:33:03 +10:00
Kurt Hutten
edfde1aa9f format project 2021-09-05 21:08:57 +10:00
Kurt Hutten
912135877c Merge pull request #489 from Irev-Dev/kurt/temp-fix-project-profile-load
Misc tweaks
2021-09-05 21:07:34 +10:00
Kurt Hutten
597bf89135 Add banner explaining static openscad images 2021-09-05 20:57:57 +10:00
Kurt Hutten
e4c95cb396 Allow click through of loading animation 2021-09-05 18:05:51 +10:00
Kurt Hutten
867bc0ca29 Fix left hand side background disappearing when loading project profile 2021-09-05 16:28:49 +10:00
Kurt Hutten
35198b6cc3 Merge branch 'main' of github.com:Irev-Dev/cadhub 2021-09-04 23:53:36 +10:00
Kurt Hutten
5b2ebac15e Merge pull request #485 from Irev-Dev/kurt/484-rebase
Remove s3
2021-09-04 23:53:21 +10:00
Kurt Hutten
4a3144d360 Remove s3
but also upgrade the cad lamdbas to use built javascript files,
allowing us to use typescript, and patching redwood
2021-09-04 23:52:44 +10:00
Kurt Hutten
25bee7ab95 Upgrade redwood patch to 0.36.3 2021-09-04 06:27:25 +10:00
Kurt Hutten
1c13a38ccb Fix font imports after tailwind upgrade 2021-09-03 07:02:19 +10:00
Kurt Hutten
bbf2a2eb55 Merge pull request #483 from Irev-Dev/kurt/482
Upgrade redwood to v0.36
2021-08-31 20:14:09 +10:00
Kurt Hutten
01a28f4d53 upgrade redwood to v 0.36 2021-08-31 20:12:18 +10:00
Kurt Hutten
f5113da9c2 Upgrade redwood to v 0.35 2021-08-31 18:35:51 +10:00
Kurt Hutten
db9270b7ce Fix doc build 2021-08-31 17:19:14 +10:00
Kurt Hutten
a4a92c18cb Merge pull request #480 from sgenoud/fix/url-for-rss
Fix generic URL for blog rss generation
2021-08-31 17:17:28 +10:00
Steve Genoud
eb5d5616bb Fix generic URL for blog rss generation 2021-08-30 08:30:37 +02:00
Kurt Hutten
04261355b7 Up date prod cad endpoint 2021-08-28 07:50:59 +10:00
Kurt Hutten
0bb106028b Merge pull request #477 from Irev-Dev/kurt/320-openscad-parms-demo-rebase
Initial support for OpenSCAD's customizer
2021-08-27 20:47:09 +10:00
Kurt Hutten
431cd2e867 Make sure number respects initial value 2021-08-27 20:21:43 +10:00
Kurt Hutten
cdbf6ed6b4 Fix select styling 2021-08-27 18:21:51 +10:00
Kurt Hutten
87f132a684 Add customizer support for OpenSCAD
This also includes sending metadata and part of the concatenated gzip,
not in the s3 metadata as that has a 2kb limit.

Resolves #320
2021-08-27 06:52:04 +10:00
Davor Hrg
5d79efbf15 choice input 2021-08-27 06:50:46 +10:00
Davor Hrg
118c68c9da types and converter for choice input 2021-08-27 06:49:48 +10:00
Kurt Hutten
8ee4c112cf Merge pull request #475 from Irev-Dev/simplify-jscad-default-script
simplify default jscad script
2021-08-25 05:25:38 +10:00
Davor Hrg
2bc4d904c6 simplify default jscad script 2021-08-24 17:03:41 +02:00
Kurt Hutten
a690265f70 Merge pull request #473 from Irev-Dev/customizer-size
1/3 for customizer is plenty
2021-08-24 18:37:22 +10:00
Kurt Hutten
2b4bc7aa43 Merge pull request #471 from Irev-Dev/jscad-new-default-script
better default script
2021-08-24 18:36:58 +10:00
Davor Hrg
9041301642 Update Customizer.tsx 2021-08-24 10:06:55 +02:00
Kurt Hutten
9f088ba463 Merge pull request #474 from Irev-Dev/franknoirot/style-tweaks
Share popover color tweaks
2021-08-24 17:17:22 +10:00
Frank Johnson
e7b9059958 realized I had missed the bottom border-radius on Encoded Script button! 2021-08-23 22:32:38 -04:00
Frank Johnson
e407a3c002 reverted deletion of .env.example 2021-08-23 22:23:25 -04:00
Frank Johnson
6be2ced06f style tweaks to make Share popover match Figma a bit closer. 2021-08-23 22:16:47 -04:00
Davor Hrg
95bdb570f2 1/3 for customizer is plenty 2021-08-23 14:02:27 +02:00
Davor Hrg
9aa686b4a4 better default script 2021-08-23 11:54:45 +02:00
Kurt Hutten
b4cdd3e1ef Tweak welcome message 2021-08-22 13:53:54 +10:00
Kurt Hutten
96fa776bd9 Merge pull request #470 from Irev-Dev/kurt/469
Highlight OpenSCAD echo
2021-08-22 13:40:49 +10:00
Kurt Hutten
aa43a848a1 Format project 2021-08-22 12:26:06 +10:00
Kurt Hutten
335a1abf41 Highlight OpenSCAD echo
As suggested by @OutwardBuckle in #464
2021-08-22 10:45:02 +10:00
205 changed files with 14966 additions and 14515 deletions

1
.gitignore vendored
View File

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

View File

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

88
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,88 @@
Hello 👋
Really happy you're checking out how to contribute.
Here you'll find a break down of the tech we're using,
If you'd like to get involved one of the best ways is to drop by the [discord](https://discord.gg/SD7zFRNjGH), say hi and let us know you're interested in contributing. All are welcome.
## Tech used
### Redwood
CadHub is a [RedWood app](https://redwoodjs.com/). Simplistically this means it's a React frontend, using a serverless graphQL backend with Prisma.
We are also using [Tailwind](https://tailwindcss.com/) to style the app.
To learn more about Redwood, here are some useful links:
- [Tutorial](https://redwoodjs.com/tutorial/welcome-to-redwood): getting started and complete overview guide.
- [Docs](https://redwoodjs.com/docs/introduction): using the Redwood Router, handling assets and files, list of command-line tools, and more.
- [Redwood Community](https://community.redwoodjs.com): get help, share tips and tricks, and collaborate on everything about RedwoodJS.
### Cad Packages
Because Each CadPackage is it's own beast we opted to use Docker in order to give us lots of flexibility for setting up the environment to run there packages. The containers are run using AWS's container lambda and deployed using the serverless framework (JSCAD is an exception since it runs client-side). See [our docs](https://learn.cadhub.xyz/docs/general-cadhub/integrations) for more information of how this is setup.
## Getting your dev environment setup
Clone the repo, then `cd` in the repo and app directory (the docs directory is for [learn.cadhub](https://learn.cadhub.xyz/))
```
cd cadhub/app
```
Install dependencies
```terminal
yarn install
```
Setting up the db, you'll need to have a postgres installed locally, you can [follow this guide](https://redwoodjs.com/docs/local-postgres-setup).
Run the following (Note: these commands require the `DATABASE_URL` env variable to be set. if you see no result when you run `echo $DATABASE_URL`, you can set it with a command like `export DATABASE_URL=postgres://postgres:somepassword@localhost`)
``` terminal
yarn rw prisma migrate dev
yarn rw prisma db seed
```
p.s. `yarn rw prisma studio` spins up an app to inspect the db
### Fire up dev
```terminal
yarn rw dev
```
Your browser should open automatically to `http://localhost:8910` to see the web app. Lambda functions run on `http://localhost:8911` and are also proxied to `http://localhost:8910/.redwood/functions/*`.
If you want to access the websight on your phone use `yarn redwood dev --fwd="--host <ip-address-on-your-network-i.e.-192.168.0.5>"`
you can sign in to the following accounts locally
localUser1@kurthutten.com: `abc123`
localUser2@kurthutten.com: `abc123`
localAdmin@kurthutten.com: `abc123`
### Discord bot setup
To set up the discord bot to notify when users publish new content, we're using the [REST](https://discord.com/developers/docs/resources/channel#message-object) API directly, used more as a notification service rather than a bot since we are not listening to messages in the chat.
1. If you're setting up the bot in a dev environment, create a new discord server (the "plus" button on the left when logged into the Discord webpage). Make note of the name of the project.
2. With [developer mode turned on](https://www.howtogeek.com/714348/how-to-enable-or-disable-developer-mode-on-discord/), right click the channel you wish the bot to announce on and select "Copy ID". Add this to `.env.defaults` as `DISCORD_CHANNEL_ID`.
3. [create a new application](https://discord.com/developers/applications), or navigate to an existing one.
4. Create a bot within that application. Copy the bot token and add it to `.env.defaults` as `DISCORD_TOKEN`.
5. Go to the "URL Generator" under "OAuth2" and create a URL with scope "bot" and text permission "Send Messages".
6. Copy the generated URL and open it in a new tab. Follow the instructions on the page to add the bot to your discord server.
When you next start CADHub, you should see in the logs `Discord: logged in as <bot name>` and you should see a startup message from the bot in the channel.
To send messages as the bot when things happen in the service, use the `sendChat` helper function:
```typescript
import { sendDiscordMessage } from 'src/lib/discord'
sendDiscordMessage("hello world!")
```
## Designs
In progress, though can be [seen on Figma](https://www.figma.com/file/VUh53RdncjZ7NuFYj0RGB9/CadHub?node-id=0%3A1)
## Docs
Docs are hosted at [learn.cadhub.xyz](http://learn.cadhub.xyz/). It includes a OpenSCAD tutorial at this point, and more is coming. The docs can be found in this repo at [docs](https://github.com/Irev-Dev/cadhub/tree/main/docs)

View File

@@ -1,4 +1,6 @@
![CadHub banner](https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/gear%20donutbanner.png)
![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)
# [C a d H u b](https://cadhub.xyz)
@@ -6,84 +8,10 @@
Let's help Code-CAD reach its [full potential!](https://cadhub.xyz) We're making a ~~cad~~hub for the Code-CAD community, think of it as model-repository crossed with a live editor. We have integrations in progress for [OpenSCAD](https://cadhub.xyz/dev-ide/openscad) and [CadQuery](https://cadhub.xyz/dev-ide/cadquery) with [more coming soon](https://github.com/Irev-Dev/curated-code-cad).
If you want to be involved in anyway, checkout the [Road Map](https://github.com/Irev-Dev/cadhub/discussions/212) and get in touch via, [twitter](https://twitter.com/IrevDev), [discord](https://discord.gg/SD7zFRNjGH) or [discussions](https://github.com/Irev-Dev/cadhub/discussions).
<img src="https://raw.githubusercontent.com/Irev-Dev/repo-images/main/images/fullcadhubshot.jpg">
<img src="https://raw.githubusercontent.com/Irev-Dev/cadhub/main/docs/static/img/blog/curated-code-cad/CadHubSS.jpg">
## Getting your dev environment setup
```terminal
git clone git@github.com:Irev-Dev/cadhub.git
# or
git clone https://github.com/Irev-Dev/cadhub.git
```
cd in the app directory
```
cd app
```
Install dependencies
```terminal
yarn install
```
Setting up the db, you'll need to have a postgres installed locally, you can [follow this guide](https://redwoodjs.com/docs/local-postgres-setup).
Run the following
``` terminal
yarn rw prisma migrate dev
yarn rw prisma db seed
```
p.s. `yarn rw prisma studio` spins up an app to inspect the db
### Fire up dev
```terminal
yarn rw dev
```
Your browser should open automatically to `http://localhost:8910` to see the web app. Lambda functions run on `http://localhost:8911` and are also proxied to `http://localhost:8910/.redwood/functions/*`.
If you want to access the websight on your phone use `yarn redwood dev --fwd="--host <ip-address-on-your-network-i.e.-192.168.0.5">"`
you can sign in to the following accounts locally
localUser1@kurthutten.com: `abc123`
localUser2@kurthutten.com: `abc123`
localAdmin@kurthutten.com: `abc123`
You may need to register a account depending on what issue you are trying to tackle, This can be done by clicking the login button on the top right. This will open up netlify's idenitiy modal asking for the websites url, since it will notice you developing locally. Enter `https://cadhub.xyz/` than use you email, verify your email and you should be set.
(some routes are protected, but permissions is a big area that needs a lot of work in the near future, so it's in a very incomplete state atm)
### Note:
We're using [RedwoodJS](https://redwoodjs.com/), this is perhaps unwise since they haven't reached 1.0 yet, however with their aim to release 1.0 by the end of the year, it shouldn't be too difficult to port changes over the coming months.
If you not familiar with Redwood, never fear the main bit of tech it uses is React, Graphql(apollo) and serverless/lamdas, depending on what part of the app you want to help with, so long as you know you way around these bits of tech you should be fine with some light referencing of the RedWood docs
### Extra Redwood docs, i.e. getting familiar with the frame work.
- [Tutorial](https://redwoodjs.com/tutorial/welcome-to-redwood): getting started and complete overview guide.
- [Docs](https://redwoodjs.com/docs/introduction): using the Redwood Router, handling assets and files, list of command-line tools, and more.
- [Redwood Community](https://community.redwoodjs.com): get help, share tips and tricks, and collaborate on everything about RedwoodJS.
## Styles
We're using tailwind utility classes so please try and use them as much as possible. Again if you not familiar, the [tailwind search](https://tailwindcss.com/) is fantastic, so searching for the css property you want to use will lead you to the correct class 99% of the time.
## Designs
In progress, though can be [seen on Figma](https://www.figma.com/file/VUh53RdncjZ7NuFYj0RGB9/CadHub?node-id=0%3A1)
## Integrations
The OpenSCAD and CadQuery integrations work by leveraging each of their cli tools in a docker image. It's currently deployed to AWS and can be found [here](https://github.com/Irev-Dev/cadhub/tree/main/app/api/src/docker).
## Docs
Docs are hosted at [learn.cadhub.xyz](http://learn.cadhub.xyz/). It includes a OpenSCAD tutorial at this point, and more is coming. The docs can be found in this repo at [docs](https://github.com/Irev-Dev/cadhub/tree/main/docs)
If you want to be involved in anyway, checkout the [contributing.md](https://github.com/Irev-Dev/cadhub/blob/main/CONTRIBUTING.md).
you might also be interested in the [Road Map](https://github.com/Irev-Dev/cadhub/discussions/212) and getting in touch via, [twitter](https://twitter.com/IrevDev), [discord](https://discord.gg/SD7zFRNjGH) or [discussions](https://github.com/Irev-Dev/cadhub/discussions).
## Who is CadHub
[Kurt](https://github.com/Irev-Dev) and [Frank](https://github.com/franknoirot) make up the Core-team and [Jeremy](https://github.com/jmwright) is a major contributor. Plus a number smaller contributors.
[Kurt](https://github.com/Irev-Dev) and [Frank](https://github.com/franknoirot) make up the Core-team and [Jeremy](https://github.com/jmwright), [Torsten](https://github.com/t-paul) and [Hrg](https://github.com/hrgdavor) are a major contributors. Plus a number smaller contributors.

View File

@@ -18,9 +18,9 @@ CLOUDINARY_API_KEY=476712943135152
# trace | info | debug | warn | error | silent
# LOG_LEVEL=debug
# EMAIL_PASSWORD=abc123
# DISCORD_TOKEN=abc123
# DISCORD_CHANNEL_ID=12345
# CAD_LAMBDA_BASE_URL="http://localhost:8080"

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "forkedFromId" TEXT;
-- AddForeignKey
ALTER TABLE "Project" ADD FOREIGN KEY ("forkedFromId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,56 @@
-- DropForeignKey
ALTER TABLE "Comment" DROP CONSTRAINT "Comment_projectId_fkey";
-- DropForeignKey
ALTER TABLE "Comment" DROP CONSTRAINT "Comment_userId_fkey";
-- DropForeignKey
ALTER TABLE "Project" DROP CONSTRAINT "Project_userId_fkey";
-- DropForeignKey
ALTER TABLE "ProjectReaction" DROP CONSTRAINT "ProjectReaction_projectId_fkey";
-- DropForeignKey
ALTER TABLE "ProjectReaction" DROP CONSTRAINT "ProjectReaction_userId_fkey";
-- DropForeignKey
ALTER TABLE "SocialCard" DROP CONSTRAINT "SocialCard_projectId_fkey";
-- DropForeignKey
ALTER TABLE "SubjectAccessRequest" DROP CONSTRAINT "SubjectAccessRequest_userId_fkey";
-- AddForeignKey
ALTER TABLE "Project" ADD CONSTRAINT "Project_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SocialCard" ADD CONSTRAINT "SocialCard_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProjectReaction" ADD CONSTRAINT "ProjectReaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ProjectReaction" ADD CONSTRAINT "ProjectReaction_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SubjectAccessRequest" ADD CONSTRAINT "SubjectAccessRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- RenameIndex
ALTER INDEX "Project.title_userId_unique" RENAME TO "Project_title_userId_key";
-- RenameIndex
ALTER INDEX "ProjectReaction.emote_userId_projectId_unique" RENAME TO "ProjectReaction_emote_userId_projectId_key";
-- RenameIndex
ALTER INDEX "SocialCard.projectId_unique" RENAME TO "SocialCard_projectId_key";
-- RenameIndex
ALTER INDEX "User.email_unique" RENAME TO "User_email_key";
-- RenameIndex
ALTER INDEX "User.userName_unique" RENAME TO "User_userName_key";

View File

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

View File

@@ -5,7 +5,7 @@ datasource db {
generator client {
provider = "prisma-client-js"
binaryTargets = "native"
binaryTargets = ["native", "darwin-arm64", "darwin"]
}
// sqlLight does not suport enums so we can't use enums until we set up postgresql in dev mode
@@ -14,10 +14,6 @@ generator client {
// ADMIN
// }
// enum ProjectType {
// JSCAD
// }
model User {
id String @id @default(uuid())
userName String @unique // reffered to as userId in @relations
@@ -41,7 +37,8 @@ model User {
enum CadPackage {
openscad
cadquery
// jscad // TODO #422, add jscad to db schema when were ready to enable saving of jscad projects
jscad
curv
}
model Project {
@@ -57,7 +54,10 @@ 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[]
@@unique([title, userId])

View File

@@ -1,122 +0,0 @@
/* eslint-disable no-console */
const { PrismaClient } = require('@prisma/client')
const dotenv = require('dotenv')
dotenv.config()
const db = new PrismaClient()
async function main() {
// Seed data is database data that needs to exist for your app to run.
// Ideally this file should be idempotent: running it multiple times
// will result in the same database state (usually by checking for the
// existence of a record before trying to create it). For example:
//
// const existing = await db.user.findMany({ where: { email: 'admin@email.com' }})
// if (!existing.length) {
// await db.user.create({ data: { name: 'Admin', email: 'admin@email.com' }})
// }
const users = [
{
id: "a2b21ce1-ae57-43a2-b6a3-b6e542fd9e60",
userName: "local-user-1",
name: "local 1",
email: "localUser1@kurthutten.com"
},
{
id: "682ba807-d10e-4caf-bf28-74054e46c9ec",
userName: "local-user-2",
name: "local 2",
email: "localUser2@kurthutten.com"
},
{
id: "5cea3906-1e8e-4673-8f0d-89e6a963c096",
userName: "local-admin-2",
name: "local admin",
email: "localAdmin@kurthutten.com"
},
]
let existing
existing = await db.user.findMany({ where: { id: users[0].id }})
if(!existing.length) {
await db.user.create({
data: users[0],
})
}
existing = await db.user.findMany({ where: { id: users[1].id }})
if(!existing.length) {
await db.user.create({
data: users[1],
})
}
const projects = [
{
title: 'demo-project1',
description: '# can be markdown',
mainImage: 'CadHub/kjdlgjnu0xmwksia7xox',
user: {
connect: {
id: users[0].id,
},
},
},
{
title: 'demo-project2',
description: '## [hey](www.google.com)',
user: {
connect: {
id: users[1].id,
},
},
},
]
existing = await db.project.findMany({where: { title: projects[0].title}})
if(!existing.length) {
await db.project.create({
data: projects[0],
})
}
existing = await db.project.findMany({where: { title: projects[1].title}})
if(!existing.length) {
await db.project.create({
data: projects[1],
})
}
const aProject = await db.project.findUnique({where: {
title_userId: {
title: projects[0].title,
userId: users[0].id,
}
}})
await db.comment.create({
data: {
text: "nice project, I like it",
userId: users[0].id,
projectId: aProject.id,
// user: {connect: { id: users[0].id}},
// project: {connect: { id: aProject.id}},
}
})
await db.projectReaction.create({
data: {
emote: "❤️",
userId: users[0].id,
projectId: aProject.id,
// user: {connect: { id: users[0].id}},
// project: {connect: { id: aProject.id}},
}
})
console.info('No data to seed. See api/prisma/seeds.js for info.')
}
main()
.catch((e) => console.error(e))
.finally(async () => {
await db.$disconnect()
})

View File

@@ -3,13 +3,22 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@redwoodjs/api": "^0.34.1",
"@redwoodjs/api": "^0.38.1",
"@redwoodjs/graphql-server": "^0.38.1",
"@sentry/node": "^6.5.1",
"axios": "^0.25.0",
"cloudinary": "^1.23.0",
"cors": "^2.8.5",
"express": "^4.17.1",
"human-id": "^2.0.1",
"nodemailer": "^6.6.2"
"middy": "^0.36.0",
"nanoid": "^3.1.20",
"nodemailer": "^6.6.2",
"serverless-binary-cors": "^0.0.1"
},
"devDependencies": {
"@types/nodemailer": "^6.4.2"
"@types/nodemailer": "^6.4.2",
"concurrently": "^6.0.0",
"nodemon": "^2.0.7"
}
}

View File

@@ -0,0 +1,18 @@
import { mockRedwoodDirective, getDirectiveName } from '@redwoodjs/testing/api'
import requireAuth from './requireAuth'
describe('requireAuth directive', () => {
it('declares the directive sdl as schema, with the correct name', () => {
expect(requireAuth.schema).toBeTruthy()
expect(getDirectiveName(requireAuth.schema)).toBe('requireAuth')
})
it('requireAuth has stub implementation. Should not throw when current user', () => {
// If you want to set values in context, pass it through e.g.
// mockRedwoodDirective(requireAuth, { context: { currentUser: { id: 1, name: 'Lebron McGretzky' } }})
const mockExecution = mockRedwoodDirective(requireAuth, { context: {} })
expect(mockExecution).not.toThrowError()
})
})

View File

@@ -0,0 +1,22 @@
import gql from 'graphql-tag'
import { createValidatorDirective } from '@redwoodjs/graphql-server'
import { requireAuth as applicationRequireAuth } from 'src/lib/auth'
export const schema = gql`
"""
Use to check whether or not a user is authenticated and is associated
with an optional set of roles.
"""
directive @requireAuth(roles: [String]) on FIELD_DEFINITION
`
const validate = ({ directiveArgs }) => {
const { roles } = directiveArgs
applicationRequireAuth({ roles })
}
const requireAuth = createValidatorDirective(schema, validate)
export default requireAuth

View File

@@ -0,0 +1,10 @@
import { getDirectiveName } from '@redwoodjs/testing/api'
import skipAuth from './skipAuth'
describe('skipAuth directive', () => {
it('declares the directive sdl as schema, with the correct name', () => {
expect(skipAuth.schema).toBeTruthy()
expect(getDirectiveName(skipAuth.schema)).toBe('skipAuth')
})
})

View File

@@ -0,0 +1,16 @@
import gql from 'graphql-tag'
import { createValidatorDirective } from '@redwoodjs/graphql-server'
export const schema = gql`
"""
Use to skip authentication checks and allow public access.
"""
directive @skipAuth on FIELD_DEFINITION
`
const skipAuth = createValidatorDirective(schema, () => {
return
})
export default skipAuth

View File

@@ -1,11 +1,12 @@
# Serverless
We're using the serverless from work for deployment
We're using the serverless framework for deployment
```
sls deploy --stage stagename
yarn rw build api && sls deploy --stage <stagename>
```
But [Kurt Hutten](https://github.com/Irev-Dev) is the only one with credentials for deployment atm, though if you wanted to set your own account you could deploy to that if you wanted to test.
Deploying has `yarn rw build` first because the image uses built js files
## Testing changes locally
@@ -14,21 +15,19 @@ You'll need to have Docker installed
Because of the way the docker containers to be deployed as lambdas on aws are somewhat specialised for the purpose we're using `docker-compose` to spin one up for each function/endpoint. So we've added a aws-emulation layer
The docker build relies on a git ignored file, the aws-lambda-rie. [Download it](https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v1.0/aws-lambda-rie), then put it into `app/api/src/docker/common/`. alternatively you can put this download into the DockerFiles by reading the instructions at around line 29 of the DockerFiles (`app/api/src/docker/openscad/Dockerfile` & `app/api/src/docker/cadquery/Dockerfile`). However this will mean slower build times as it will need download this 14mb file every build.
The docker build relies on a git ignored file, the aws-lambda-rie. [Download it](https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v1.0/aws-lambda-rie), then put it into `app/api/src/docker/common/`. Alternatively you can put this download into the DockerFiles by reading the instructions at around line 29 of the DockerFiles (`app/api/src/docker/openscad/Dockerfile` & `app/api/src/docker/cadquery/Dockerfile`). However this will mean slower build times as it will need download this 14mb file every build.
you will also need to create a .env in `app/api/src/docker/.env` for the following env-vars `DEV_AWS_SECRET_ACCESS_KEY, DEV_AWS_ACCESS_KEY_ID and DEV_BUCKET`. Ask @irev-dev for credentials and he can sort you out.
Then cd into this folder `cd api/src/docker` and:
Run
```bash
docker-compose up --build
yarn cad
```
The first time you run this, it has to build the main image it will take some time, but launching again will be quicker.
After which we'll also spin up a light express server to act as an emulator to transform some the request from the front end into how the lambda's expect them (This emulates the aws-api-gateway which changes tranforms the inbound requests somewhat).
```
yarn install
yarn emulate
yarn aws-emulate
```
You can now add CAD_LAMBDA_BASE_URL="http://localhost:8080" to you .env file and restart your main dev process (`yarn rw dev`) comment that line out if you want to go back to using the aws endpoint (and restart the dev process).

View File

@@ -1,7 +1,6 @@
const express = require('express')
var cors = require('cors')
const axios = require('axios')
const { restart } = require('nodemon')
const app = express()
const port = 8080
app.use(express.json())
@@ -16,10 +15,16 @@ const makeRequest = (route, port) => [
console.log(`making post request to ${port}, ${route}`)
try {
const { data } = await axios.post(invocationURL(port), {
body: JSON.stringify(req.body),
body: Buffer.from(JSON.stringify(req.body)).toString('base64'),
})
res.status(data.statusCode)
res.send(data.body)
res.setHeader('Content-Type', 'application/javascript')
if (data.headers && data.headers['Content-Encoding'] === 'gzip') {
res.setHeader('Content-Encoding', 'gzip')
res.send(Buffer.from(data.body, 'base64'))
} else {
res.send(Buffer.from(data.body, 'base64'))
}
} catch (e) {
res.status(500)
res.send()
@@ -29,8 +34,12 @@ const makeRequest = (route, port) => [
app.post(...makeRequest('/openscad/preview', 5052))
app.post(...makeRequest('/openscad/stl', 5053))
app.post(...makeRequest('/cadquery/stl', 5060))
app.post(...makeRequest('/curv/preview', 5070))
app.post(...makeRequest('/curv/stl', 5071))
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})

View File

@@ -1,14 +1,15 @@
FROM public.ecr.aws/lts/ubuntu:20.04_stable
FROM continuumio/miniconda3
ENV PATH="/root/miniconda3/bin:${PATH}"
ARG PATH="/root/miniconda3/bin:${PATH}"
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update -qq
RUN apt-get update --fix-missing -qq
RUN apt-get -y -qq install software-properties-common dirmngr apt-transport-https lsb-release ca-certificates xvfb
RUN apt-get update -qq
RUN apt-get install -y wget
# install node14, see comment at the to of node14source_setup.sh
ADD common/node14source_setup.sh /nodesource_setup.sh
ADD src/docker/common/node14source_setup.sh /nodesource_setup.sh
RUN ["chmod", "+x", "/nodesource_setup.sh"]
RUN bash nodesource_setup.sh
RUN apt-get install -y nodejs
@@ -21,32 +22,41 @@ RUN apt-get update && \
cmake \
unzip \
automake autoconf libtool \
libcurl4-openssl-dev
libcurl4-openssl-dev \
curl \
git
# Add the lambda emulator for local dev, (see entrypoint.sh for where it's used),
# I have the file locally (gitignored) to speed up build times (as it downloads everytime),
# but you can use the http version of the below ADD command or download it yourself from that url.
ADD common/aws-lambda-rie /usr/local/bin/aws-lambda-rie
ADD src/docker/common/aws-lambda-rie /usr/local/bin/aws-lambda-rie
# ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v1.0/aws-lambda-rie /usr/local/bin/aws-lambda-rie
RUN ["chmod", "+x", "/usr/local/bin/aws-lambda-rie"]
WORKDIR /var/task/
COPY cadquery/package*.json /var/task/
COPY package*.json /var/task/
RUN npm install
RUN npm install aws-lambda-ric@1.0.0
RUN conda --version
# Install CadQuery
# RUN conda install -c cadquery -c conda-forge cadquery=master ocp=7.5.2 python=3.8
# RUN conda info
# Get a copy of cq-cli from GitHub
RUN git clone https://github.com/CadQuery/cq-cli.git
# Get the distribution copy of cq-cli
RUN apt-get install -y libglew2.1
RUN wget https://github.com/CadQuery/cq-cli/releases/download/v2.2-beta.2/cq-cli-Linux-x86_64.zip
# Comment the entry above out and uncomment the one below to revert to the stable release
# RUN wget https://github.com/CadQuery/cq-cli/releases/download/v2.1.0/cq-cli-Linux-x86_64.zip
RUN unzip cq-cli-Linux-x86_64.zip
RUN chmod +x cq-cli/cq-cli
RUN echo "cadhub-concat-split" > /var/task/cadhub-concat-split
COPY cadquery/*.js /var/task/
COPY common/*.js /var/common/
COPY common/entrypoint.sh /entrypoint.sh
# using built javascript from dist
# run `yarn rw build` before bulding this image
COPY dist/docker/cadquery/*.js /var/task/js/
COPY dist/docker/common/*.js /var/task/common/
COPY src/docker/common/entrypoint.sh /entrypoint.sh
RUN ["chmod", "+x", "/entrypoint.sh"]
ENTRYPOINT ["sh", "/entrypoint.sh"]
CMD [ "cadquery.stl" ]
CMD [ "js/cadquery.stl" ]

View File

@@ -1,57 +0,0 @@
const { runCQ } = require('./runCQ')
const middy = require('middy')
const { cors } = require('middy/middlewares')
const AWS = require('aws-sdk')
const tk = require('timekeeper')
const {
makeHash,
checkIfAlreadyExists,
getObjectUrl,
loggerWrap,
storeAssetAndReturnUrl,
} = require('../common/utils')
const s3 = new AWS.S3()
const stl = async (req, _context, callback) => {
_context.callbackWaitsForEmptyEventLoop = false
const eventBody = req.body
console.log('eventBody', eventBody)
const key = `${makeHash(eventBody)}.stl`
console.log('key', key)
const params = {
Bucket: process.env.BUCKET,
Key: key,
}
const previousAsset = await checkIfAlreadyExists(params, s3)
if (previousAsset.isAlreadyInBucket) {
console.log('already in bucket')
const response = {
statusCode: 200,
body: JSON.stringify({
url: getObjectUrl(params, s3, tk),
consoleMessage: previousAsset.consoleMessage,
}),
}
callback(null, response)
return
}
const { file, settings } = JSON.parse(eventBody)
const { error, consoleMessage, fullPath } = await runCQ({ file, settings })
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
key,
s3,
params,
tk,
})
}
module.exports = {
stl: middy(loggerWrap(stl)).use(cors()),
}

View File

@@ -0,0 +1,27 @@
import { runCQ } from './runCQ'
import middy from 'middy'
import { cors } from 'middy/middlewares'
import { loggerWrap, storeAssetAndReturnUrl } from '../common/utils'
const stl = async (req, _context, callback) => {
_context.callbackWaitsForEmptyEventLoop = false
const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
console.log('eventBody', eventBody)
const { file, settings } = JSON.parse(eventBody)
const { error, fullPath } = await runCQ({
file,
settings,
})
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage: '',
tempFile : '',
})
}
module.exports = {
stl: middy(loggerWrap(stl)).use(cors()),
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
{
"name": "openscad-endpoint",
"version": "0.0.1",
"description": "endpoint for openscad",
"main": "index.js",
"author": "Kurt Hutten <kurt@kurthutten.com>",
"license": "",
"dependencies": {
"aws-sdk": "^2.907.0",
"cors": "^2.8.5",
"middy": "^0.36.0",
"nanoid": "^3.1.20",
"timekeeper": "2.2.0"
},
"devDependencies": {
"aws-lambda-ric": "^1.0.0"
}
}

View File

@@ -1,26 +0,0 @@
const { makeFile, runCommand } = require('../common/utils')
const { nanoid } = require('nanoid')
module.exports.runCQ = async ({
file,
settings: { deflection = 0.3 } = {},
} = {}) => {
const tempFile = await makeFile(file, '.py', nanoid)
const fullPath = `/tmp/${tempFile}/output.stl`
const command = [
`cq-cli/cq-cli`,
`--codec stl`,
`--infile /tmp/${tempFile}/main.py`,
`--outfile ${fullPath}`,
`--outputopts "deflection:${deflection};angularDeflection:${deflection};"`,
`&& gzip ${fullPath}`,
].join(' ')
console.log('command', command)
try {
const consoleMessage = await runCommand(command, 30000)
return { consoleMessage, fullPath }
} catch (error) {
return { error, fullPath }
}
}

View File

@@ -0,0 +1,61 @@
import { writeFiles, runCommand } from '../common/utils'
import { nanoid } from 'nanoid'
import { readFile } from 'fs/promises'
export const runCQ = async ({
file,
settings: { deflection = 0.3, parameters } = {},
} = {}) => {
const tempFile = await writeFiles(
[
{ file, fileName: 'main.py' },
{
file: JSON.stringify(parameters),
fileName: 'params.json',
},
],
'a' + nanoid() // 'a' ensure nothing funny happens if it start with a bad character like "-", maybe I should pick a safer id generator :shrug:
)
const fullPath = `/tmp/${tempFile}/output.gz`
const stlPath = `/tmp/${tempFile}/output.stl`
const customizerPath = `/tmp/${tempFile}/customizer.json`
const command = [
`/var/task/cq-cli/cq-cli.py`,
`--codec stl`,
`--infile /tmp/${tempFile}/main.py`,
`--outfile ${stlPath}`,
`--outputopts "deflection:${deflection};angularDeflection:${deflection};"`,
`--params /tmp/${tempFile}/params.json`,
`--getparams ${customizerPath}`,
].join(' ')
console.log('command', command)
let consoleMessage = ''
return { error: 'python execution currently disabled, see: https://github.com/Irev-Dev/cadhub/issues/611', fullPath }
// try {
// consoleMessage = await runCommand(command, 30000)
// const params = JSON.parse(
// await readFile(customizerPath, { encoding: 'ascii' })
// )
// await writeFiles(
// [
// {
// file: JSON.stringify({
// customizerParams: params,
// consoleMessage,
// type: 'stl',
// }),
// fileName: 'metadata.json',
// },
// ],
// tempFile
// )
// await runCommand(
// `cat ${stlPath} /var/task/cadhub-concat-split /tmp/${tempFile}/metadata.json | gzip > ${fullPath}`,
// 15000,
// true
// )
// return { consoleMessage, fullPath, tempFile }
// } catch (error) {
// return { error: consoleMessage || error, fullPath }
// }
}

View File

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

View File

@@ -0,0 +1,67 @@
FROM public.ecr.aws/lts/ubuntu:20.04_stable
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update --fix-missing -qq
RUN apt-get update --fix-missing && apt-get -y -qq install software-properties-common dirmngr apt-transport-https lsb-release ca-certificates xvfb
RUN apt-get update -qq
RUN apt-get -y -qq install git \
software-properties-common \
xvfb unzip maim clang cmake \
git-core libboost-all-dev \
libopenexr-dev libtbb-dev \
libglm-dev libpng-dev \
libeigen3-dev dbus-x11 \
libxcursor-dev libxinerama-dev \
libxrandr-dev libglu1-mesa-dev \
libgles2-mesa-dev libgl1-mesa-dev \
libxi-dev
# Use commit to make sure build is reproduceable
RUN git clone --recursive https://github.com/curv3d/curv && \
cd curv && \
git checkout b849eb57fba121f9f218dc065dc1f5ebc619836d && \
make && make install
# install node14, see comment at the top of node14source_setup.sh
ADD src/docker/common/node14source_setup.sh /nodesource_setup.sh
RUN ["chmod", "+x", "/nodesource_setup.sh"]
RUN bash nodesource_setup.sh
RUN apt-get install -y nodejs
# Install aws-lambda-cpp build dependencies, this is for the post install script in aws-lambda-ric (in package.json)
RUN apt-get update && \
apt-get install -y \
g++ \
make \
cmake \
unzip \
automake autoconf libtool \
libcurl4-openssl-dev
# Add the lambda emulator for local dev, (see entrypoint.sh for where it's used),
# I have the file locally (gitignored) to speed up build times (as it downloads everytime),
# but you can use the http version of the below ADD command or download it yourself from that url.
ADD src/docker/common/aws-lambda-rie /usr/local/bin/aws-lambda-rie
# ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v1.0/aws-lambda-rie /usr/local/bin/aws-lambda-rie
RUN ["chmod", "+x", "/usr/local/bin/aws-lambda-rie"]
WORKDIR /var/task/
COPY package*.json /var/task/
RUN npm install
RUN npm install aws-lambda-ric@1.0.0
RUN echo "cadhub-concat-split" > /var/task/cadhub-concat-split
# using built javascript from dist
# run `yarn rw build` before bulding this image
COPY dist/docker/curv/* /var/task/js/
COPY dist/docker/common/* /var/task/common/
COPY src/docker/common/entrypoint.sh /entrypoint.sh
RUN ["chmod", "+x", "/entrypoint.sh"]
ENTRYPOINT ["sh", "/entrypoint.sh"]
CMD [ "js/curv.preview" ]

View File

@@ -0,0 +1,48 @@
import { runCurv, stlExport } from './runCurv'
import middy from 'middy'
import { cors } from 'middy/middlewares'
import { loggerWrap, storeAssetAndReturnUrl } from '../common/utils'
const preview = async (req, _context, callback) => {
_context.callbackWaitsForEmptyEventLoop = false
const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
console.log('eventBody', eventBody)
const { file, settings } = JSON.parse(eventBody)
const { error, consoleMessage, fullPath, tempFile } = await runCurv({
file,
settings,
})
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
tempFile,
})
}
const stl = async (req, _context, callback) => {
_context.callbackWaitsForEmptyEventLoop = false
const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
console.log(eventBody, 'eventBody')
const { file, settings } = JSON.parse(eventBody)
const { error, consoleMessage, fullPath, tempFile } = await stlExport({
file,
settings,
})
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
tempFile,
})
}
module.exports = {
stl: middy(loggerWrap(stl)).use(cors()),
preview: middy(loggerWrap(preview)).use(cors()),
}

View File

@@ -0,0 +1,114 @@
import { writeFiles, runCommand } from '../common/utils'
import { nanoid } from 'nanoid'
export const runCurv = async ({
file,
settings: { size: { x = 500, y = 500 } = {}, parameters } = {}, // TODO add view settings
} = {}): Promise<{
error?: string
consoleMessage?: string
fullPath?: string
customizerPath?: string
tempFile?: string
}> => {
const tempFile = await writeFiles(
[
{ file, fileName: 'main.curv' },
{
file: JSON.stringify({
parameterSets: { default: parameters },
fileFormatVersion: '1',
}),
fileName: 'params.json',
},
],
'a' + nanoid() // 'a' ensure nothing funny happens if it start with a bad character like "-", maybe I should pick a safer id generator :shrug:
)
const fullPath = `/tmp/${tempFile}/output.gz`
const imPath = `/tmp/${tempFile}/output.png`
const customizerPath = `/tmp/${tempFile}/customizer.param`
const command = [
'xvfb-run --auto-servernum --server-args "-screen 0 3840x2160x24" curv',
`-o ${imPath}`,
`-O xsize=${x}`,
`-O ysize=${y}`,
`-O bg=webRGB[26,26,29]`, // #1A1A1D
`/tmp/${tempFile}/main.curv`,
].join(' ')
console.log('command', command)
try {
const consoleMessage = await runCommand(command, 15000)
await writeFiles(
[
{
file: JSON.stringify({
consoleMessage,
type: 'png',
}),
fileName: 'metadata.json',
},
],
tempFile
)
await runCommand(
`cat ${imPath} /var/task/cadhub-concat-split /tmp/${tempFile}/metadata.json | gzip > ${fullPath}`,
15000
)
return { consoleMessage, fullPath, customizerPath, tempFile }
} catch (dirtyError) {
return { error: dirtyError }
}
}
export const stlExport = async ({ file, settings: { parameters } } = {}) => {
const tempFile = await writeFiles(
[
{ file, fileName: 'main.curv' },
{
file: JSON.stringify({
parameterSets: { default: parameters },
fileFormatVersion: '1',
}),
fileName: 'params.json',
},
],
'a' + nanoid() // 'a' ensure nothing funny happens if it start with a bad character like "-", maybe I should pick a safer id generator :shrug:
)
const fullPath = `/tmp/${tempFile}/output.gz`
const stlPath = `/tmp/${tempFile}/output.stl`
const command = [
'(cd /tmp && curv',
'-o',
stlPath,
'-O jit',
'-O vcount=350000',
`/tmp/${tempFile}/main.curv`,
')',
].join(' ')
try {
// lambda will time out before this, we might need to look at background jobs if we do git integration stl generation
const consoleMessage = await runCommand(command, 60000)
await writeFiles(
[
{
file: JSON.stringify({
consoleMessage,
type: 'stl',
}),
fileName: 'metadata.json',
},
],
tempFile
)
await runCommand(
`cat ${stlPath} /var/task/cadhub-concat-split /tmp/${tempFile}/metadata.json | gzip > ${fullPath} && rm ${stlPath}`,
15000
)
return { consoleMessage, fullPath, tempFile }
} catch (error) {
return { error, fullPath }
}
}

View File

@@ -2,10 +2,14 @@ services:
openscad-preview:
build:
context: ./
dockerfile: ./openscad/Dockerfile
context: ../../
dockerfile: ./src/docker/openscad/Dockerfile
image: openscad
command: openscad.preview
command: js/openscad.preview
# Adding volumes so that the containers can be restarted for js only changes in local dev
volumes:
- ../../dist/docker/openscad:/var/task/js/
- ../../dist/docker/common:/var/task/common/
ports:
- "5052:8080"
environment:
@@ -15,7 +19,10 @@ services:
openscad-stl:
image: openscad
command: openscad.stl
volumes:
- ../../dist/docker/openscad:/var/task/js/
- ../../dist/docker/common:/var/task/common/
command: js/openscad.stl
ports:
- "5053:8080"
environment:
@@ -25,9 +32,12 @@ services:
cadquery-stl:
build:
context: ./
dockerfile: ./cadquery/Dockerfile
command: cadquery.stl
context: ../../
dockerfile: ./src/docker/cadquery/Dockerfile
volumes:
- ../../dist/docker/cadquery:/var/task/js/
- ../../dist/docker/common:/var/task/common/
command: js/cadquery.stl
ports:
- 5060:8080
environment:
@@ -35,3 +45,28 @@ services:
AWS_ACCESS_KEY_ID: "${DEV_AWS_ACCESS_KEY_ID}"
BUCKET: "${DEV_BUCKET}"
curv-preview:
build:
context: ../../
dockerfile: ./src/docker/curv/Dockerfile
image: curv
command: js/curv.preview
# Adding volumes so that the containers can be restarted for js only changes in local dev
volumes:
- ../../dist/docker/curv:/var/task/js/
- ../../dist/docker/common:/var/task/common/
ports:
- "5070:8080"
curv-stl:
build:
context: ../../
dockerfile: ./src/docker/curv/Dockerfile
image: curv
command: js/curv.stl
# Adding volumes so that the containers can be restarted for js only changes in local dev
volumes:
- ../../dist/docker/curv:/var/task/js/
- ../../dist/docker/common:/var/task/common/
ports:
- "5071:8080"

View File

@@ -3,16 +3,18 @@ FROM public.ecr.aws/lts/ubuntu:20.04_stable
ARG DEBIAN_FRONTEND=noninteractive
## install things needed to run openscad (xvfb is an important one)
RUN apt-get update -qq
RUN apt-get update --fix-missing -qq
# double check this below, I'm not sure we need inkscape etc
RUN apt-get -y -qq install software-properties-common dirmngr apt-transport-https lsb-release ca-certificates xvfb imagemagick unzip inkscape
RUN add-apt-repository ppa:openscad/releases
RUN apt-get update -qq
RUN apt-get install -y -qq openscad
RUN apt-get install -y curl wget
RUN touch /etc/apt/sources.list.d/openscad.list
RUN echo "deb https://download.opensuse.org/repositories/home:/t-paul/xUbuntu_20.04/ ./" >> /etc/apt/sources.list.d/openscad.list
RUN wget -qO - https://files.openscad.org/OBS-Repository-Key.pub | apt-key add -
RUN apt-get update -qq
RUN apt-get install -y openscad-nightly
# install node14, see comment at the to of node14source_setup.sh
ADD common/node14source_setup.sh /nodesource_setup.sh
ADD src/docker/common/node14source_setup.sh /nodesource_setup.sh
RUN ["chmod", "+x", "/nodesource_setup.sh"]
RUN bash nodesource_setup.sh
RUN apt-get install -y nodejs
@@ -30,13 +32,14 @@ RUN apt-get update && \
# Add the lambda emulator for local dev, (see entrypoint.sh for where it's used),
# I have the file locally (gitignored) to speed up build times (as it downloads everytime),
# but you can use the http version of the below ADD command or download it yourself from that url.
ADD common/aws-lambda-rie /usr/local/bin/aws-lambda-rie
ADD src/docker/common/aws-lambda-rie /usr/local/bin/aws-lambda-rie
# ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v1.0/aws-lambda-rie /usr/local/bin/aws-lambda-rie
RUN ["chmod", "+x", "/usr/local/bin/aws-lambda-rie"]
WORKDIR /var/task/
COPY openscad/package*.json /var/task/
COPY package*.json /var/task/
RUN npm install
RUN npm install aws-lambda-ric@1.0.0
# Install OpenSCAD libraries
# It's experimental, so only adding latest Round-Anything for now
@@ -44,12 +47,16 @@ RUN echo "OPENSCADPATH=/var/task/openscad" >>/etc/profile && \
wget -P /var/task/openscad/ https://github.com/Irev-Dev/Round-Anything/archive/refs/tags/1.0.4.zip && \
unzip /var/task/openscad/1.0.4
# Add our own theming (based on DeepOcean with a different "background" and "opencsg-face-back")
COPY openscad/cadhubtheme.json /usr/share/openscad/color-schemes/render/
COPY src/docker/openscad/cadhubtheme.json /usr/share/openscad-nightly/color-schemes/render/
COPY openscad/*.js /var/task/
COPY common/*.js /var/common/
COPY common/entrypoint.sh /entrypoint.sh
RUN echo "cadhub-concat-split" > /var/task/cadhub-concat-split
# using built javascript from dist
# run `yarn rw build` before bulding this image
COPY dist/docker/openscad/* /var/task/js/
COPY dist/docker/common/* /var/task/common/
COPY src/docker/common/entrypoint.sh /entrypoint.sh
RUN ["chmod", "+x", "/entrypoint.sh"]
ENTRYPOINT ["sh", "/entrypoint.sh"]
CMD [ "openscad.render" ]
CMD [ "js/openscad.render" ]

View File

@@ -1,126 +0,0 @@
const { runScad, stlExport } = require('./runScad')
const middy = require('middy')
const { cors } = require('middy/middlewares')
const AWS = require('aws-sdk')
const tk = require('timekeeper')
const {
makeHash,
checkIfAlreadyExists,
getObjectUrl,
loggerWrap,
storeAssetAndReturnUrl,
} = require('../common/utils')
const s3 = new AWS.S3()
const openScadStlKey = (eventBody) => {
const { file } = JSON.parse(eventBody)
return `${makeHash(JSON.stringify(file))}.stl`
}
const preview = async (req, _context, callback) => {
_context.callbackWaitsForEmptyEventLoop = false
const eventBody = req.body
console.log('eventBody', eventBody)
const key = `${makeHash(eventBody)}.png`
const stlKey = openScadStlKey(eventBody)
console.log('key', key)
const stlParams = {
Bucket: process.env.BUCKET,
Key: stlKey,
}
const params = {
Bucket: process.env.BUCKET,
Key: key,
}
const [previousAssetStl, previousAssetPng] = await Promise.all([
checkIfAlreadyExists(stlParams, s3),
checkIfAlreadyExists(params, s3),
])
const type = previousAssetStl.isAlreadyInBucket ? 'stl' : 'png'
const previousAsset = previousAssetStl.isAlreadyInBucket
? previousAssetStl
: previousAssetPng
if (previousAsset.isAlreadyInBucket) {
console.log('already in bucket')
const response = {
statusCode: 200,
body: JSON.stringify({
url: getObjectUrl(
{
Bucket: process.env.BUCKET,
Key: previousAssetStl.isAlreadyInBucket ? stlKey : key,
},
s3,
tk
),
consoleMessage:
previousAsset.consoleMessage || previousAssetPng.consoleMessage,
type,
}),
}
callback(null, response)
return
}
const { file, settings } = JSON.parse(eventBody)
const { error, consoleMessage, fullPath } = await runScad({ file, settings })
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
key,
s3,
params,
tk,
})
}
const stl = async (req, _context, callback) => {
_context.callbackWaitsForEmptyEventLoop = false
const eventBody = req.body
console.log(eventBody, 'eventBody')
const stlKey = openScadStlKey(eventBody)
console.log('key', stlKey)
const params = {
Bucket: process.env.BUCKET,
Key: stlKey,
}
console.log('original params', params)
const previousAsset = await checkIfAlreadyExists(params, s3)
if (previousAsset.isAlreadyInBucket) {
console.log('already in bucket')
const response = {
statusCode: 200,
body: JSON.stringify({
url: getObjectUrl({ ...params }, s3, tk),
consoleMessage: previousAsset.consoleMessage,
}),
}
callback(null, response)
return
}
const { file } = JSON.parse(eventBody)
const { error, consoleMessage, fullPath } = await stlExport({ file })
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
key: stlKey,
s3,
params,
tk,
})
}
module.exports = {
stl: middy(stl).use(cors()),
preview: middy(loggerWrap(preview)).use(cors()),
}

View File

@@ -0,0 +1,48 @@
import { runScad, stlExport } from './runScad'
import middy from 'middy'
import { cors } from 'middy/middlewares'
import { loggerWrap, storeAssetAndReturnUrl } from '../common/utils'
const preview = async (req, _context, callback) => {
_context.callbackWaitsForEmptyEventLoop = false
const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
console.log('eventBody', eventBody)
const { file, settings } = JSON.parse(eventBody)
const { error, consoleMessage, fullPath, tempFile } = await runScad({
file,
settings,
})
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
tempFile,
})
}
const stl = async (req, _context, callback) => {
_context.callbackWaitsForEmptyEventLoop = false
const eventBody = Buffer.from(req.body, 'base64').toString('ascii')
console.log(eventBody, 'eventBody')
const { file, settings } = JSON.parse(eventBody)
const { error, consoleMessage, fullPath, tempFile } = await stlExport({
file,
settings,
})
await storeAssetAndReturnUrl({
error,
callback,
fullPath,
consoleMessage,
tempFile,
})
}
module.exports = {
stl: middy(loggerWrap(stl)).use(cors()),
preview: middy(loggerWrap(preview)).use(cors()),
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
{
"name": "openscad-endpoint",
"version": "0.0.1",
"description": "endpoint for openscad",
"main": "index.js",
"author": "Kurt Hutten <kurt@kurthutten.com>",
"license": "",
"dependencies": {
"aws-sdk": "^2.907.0",
"cors": "^2.8.5",
"middy": "^0.36.0",
"nanoid": "^3.1.20",
"timekeeper": "2.2.0"
},
"devDependencies": {
"aws-lambda-ric": "^1.0.0"
}
}

View File

@@ -1,64 +0,0 @@
const { makeFile, runCommand } = require('../common/utils')
const { nanoid } = require('nanoid')
const OPENSCAD_COMMON = `xvfb-run --auto-servernum --server-args "-screen 0 1024x768x24" openscad`
/** Removes our generated/hash filename with just "main.scad", so that it's a nice message in the IDE */
const cleanOpenScadError = (error) =>
error.replace(/["|']\/tmp\/.+\/main.scad["|']/g, "'main.scad'")
module.exports.runScad = async ({
file,
settings: {
size: { x = 500, y = 500 } = {},
camera: {
position = { x: 40, y: 40, z: 40 },
rotation = { x: 55, y: 0, z: 25 },
dist = 200,
} = {},
} = {}, // TODO add view settings
} = {}) => {
const tempFile = await makeFile(file, '.scad', nanoid)
const { x: rx, y: ry, z: rz } = rotation
const { x: px, y: py, z: pz } = position
const cameraArg = `--camera=${px},${py},${pz},${rx},${ry},${rz},${dist}`
const fullPath = `/tmp/${tempFile}/output.png`
const command = [
OPENSCAD_COMMON,
`-o ${fullPath}`,
cameraArg,
`--imgsize=${x},${y}`,
`--colorscheme CadHub`,
`/tmp/${tempFile}/main.scad`,
`&& gzip ${fullPath}`,
].join(' ')
console.log('command', command)
try {
const consoleMessage = await runCommand(command, 15000)
return { consoleMessage, fullPath }
} catch (dirtyError) {
const error = cleanOpenScadError(dirtyError)
return { error }
}
}
module.exports.stlExport = async ({ file } = {}) => {
const tempFile = await makeFile(file, '.scad', nanoid)
const fullPath = `/tmp/${tempFile}/output.stl`
const command = [
OPENSCAD_COMMON,
`--export-format=binstl`,
`-o ${fullPath}`,
`/tmp/${tempFile}/main.scad`,
`&& gzip ${fullPath}`,
].join(' ')
try {
// lambda will time out before this, we might need to look at background jobs if we do git integration stl generation
const consoleMessage = await runCommand(command, 60000)
return { consoleMessage, fullPath }
} catch (error) {
return { error, fullPath }
}
}

View File

@@ -0,0 +1,151 @@
import { writeFiles, runCommand } from '../common/utils'
import { nanoid } from 'nanoid'
const { readFile } = require('fs/promises')
const OPENSCAD_COMMON = `xvfb-run --auto-servernum --server-args "-screen 0 1024x768x24" openscad-nightly`
/** Removes our generated/hash filename with just "main.scad", so that it's a nice message in the IDE */
const cleanOpenScadError = (error) =>
error.replace(/["|']\/tmp\/.+\/main.scad["|']/g, "'main.scad'")
export const runScad = async ({
file,
settings: {
viewAll = false,
size: { x = 500, y = 500 } = {},
parameters,
camera: {
position = { x: 40, y: 40, z: 40 },
rotation = { x: 55, y: 0, z: 25 },
dist = 200,
} = {},
} = {}, // TODO add view settings
} = {}): Promise<{
error?: string
consoleMessage?: string
fullPath?: string
customizerPath?: string
tempFile?: string
}> => {
const tempFile = await writeFiles(
[
{ file, fileName: 'main.scad' },
{
file: JSON.stringify({
parameterSets: { default: parameters },
fileFormatVersion: '1',
}),
fileName: 'params.json',
},
],
'a' + nanoid() // 'a' ensure nothing funny happens if it start with a bad character like "-", maybe I should pick a safer id generator :shrug:
)
const { x: rx, y: ry, z: rz } = rotation
const { x: px, y: py, z: pz } = position
const cameraArg = `--camera=${px},${py},${pz},${rx},${ry},${rz},${dist}`
const fullPath = `/tmp/${tempFile}/output.gz`
const imPath = `/tmp/${tempFile}/output.png`
const customizerPath = `/tmp/${tempFile}/customizer.param`
const summaryPath = `/tmp/${tempFile}/summary.json` // contains camera info
const command = [
OPENSCAD_COMMON,
`-o ${customizerPath}`,
`-o ${imPath}`,
`--summary camera --summary-file ${summaryPath}`,
viewAll ? '--viewall' : '',
`-p /tmp/${tempFile}/params.json -P default`,
cameraArg,
`--imgsize=${x},${y}`,
`--colorscheme CadHub`,
`/tmp/${tempFile}/main.scad`,
].join(' ')
console.log('command', command)
try {
const consoleMessage = await runCommand(command, 15000)
const files: string[] = await Promise.all(
[customizerPath, summaryPath].map((path) =>
readFile(path, { encoding: 'ascii' })
)
)
const [params, cameraInfo] = files.map((fileStr: string) =>
JSON.parse(fileStr)
)
await writeFiles(
[
{
file: JSON.stringify({
cameraInfo: viewAll ? cameraInfo.camera : undefined,
customizerParams: params.parameters,
consoleMessage,
type: 'png',
}),
fileName: 'metadata.json',
},
],
tempFile
)
await runCommand(
`cat ${imPath} /var/task/cadhub-concat-split /tmp/${tempFile}/metadata.json | gzip > ${fullPath}`,
15000
)
return { consoleMessage, fullPath, customizerPath, tempFile }
} catch (dirtyError) {
return { error: cleanOpenScadError(dirtyError) }
}
}
export const stlExport = async ({ file, settings: { parameters } } = {}) => {
const tempFile = await writeFiles(
[
{ file, fileName: 'main.scad' },
{
file: JSON.stringify({
parameterSets: { default: parameters },
fileFormatVersion: '1',
}),
fileName: 'params.json',
},
],
'a' + nanoid() // 'a' ensure nothing funny happens if it start with a bad character like "-", maybe I should pick a safer id generator :shrug:
)
const fullPath = `/tmp/${tempFile}/output.gz`
const stlPath = `/tmp/${tempFile}/output.stl`
const customizerPath = `/tmp/${tempFile}/customizer.param`
const command = [
OPENSCAD_COMMON,
// `--export-format=binstl`,
`-o ${customizerPath}`,
`-o ${stlPath}`,
`-p /tmp/${tempFile}/params.json -P default`,
`/tmp/${tempFile}/main.scad`,
].join(' ')
try {
// lambda will time out before this, we might need to look at background jobs if we do git integration stl generation
const consoleMessage = await runCommand(command, 60000)
const params = JSON.parse(
await readFile(customizerPath, { encoding: 'ascii' })
).parameters
await writeFiles(
[
{
file: JSON.stringify({
customizerParams: params,
consoleMessage,
type: 'stl',
}),
fileName: 'metadata.json',
},
],
tempFile
)
await runCommand(
`cat ${stlPath} /var/task/cadhub-concat-split /tmp/${tempFile}/metadata.json | gzip > ${fullPath}`,
15000
)
return { consoleMessage, fullPath, customizerPath, tempFile }
} catch (error) {
return { error, fullPath }
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +0,0 @@
{
"name": "aws-emulator",
"version": "1.0.0",
"description": "thin layer so that we can use docker lambdas locally",
"scripts": {
"lambdas": "docker-compose up --build",
"emulate": "nodemon ./aws-emulator.js",
"watch": "concurrently \"yarn lambdas\" \"yarn emulate\""
},
"main": "aws-emulator.js",
"dependencies": {
"axios": "^0.21.1",
"cors": "^2.8.5",
"express": "^4.17.1"
},
"devDependencies": {
"concurrently": "^6.0.0",
"nodemon": "^2.0.7"
}
}

View File

@@ -3,7 +3,8 @@ service: cad-lambdas
#app: your-app-name
#org: your-org-name
# plugins:
plugins:
- serverless-binary-cors
# - serverless-offline
# You can pin your service to only deploy with a specific Serverless version
@@ -17,13 +18,21 @@ provider:
images:
# this image is built locally and push to ECR
openscadimage:
path: ./
file: ./openscad/Dockerfile
path: ../../
file: ./src/docker/openscad/Dockerfile
cadqueryimage:
path: ./
file: ./cadquery/Dockerfile
path: ../../
file: ./src/docker/cadquery/Dockerfile
curvimage:
path: ../../
file: ./src/docker/curv/Dockerfile
apiGateway:
metrics: true
binaryMediaTypes:
# we need to allow binary types to be able to send back images and stls, but it would be better to be more specific
# ie image/png etc. as */* treats everything as binary including the json body as the input the lambdas
# which mean we need to decode the input bode from base64, but the images break with anything other than */* :(
- '*/*'
# you can overwrite defaults here
# stage: dev
@@ -52,7 +61,7 @@ functions:
image:
name: openscadimage
command:
- openscad.preview
- js/openscad.preview
entryPoint:
- '/entrypoint.sh'
events:
@@ -61,13 +70,14 @@ functions:
method: post
cors: true
timeout: 25
memorySize: 2048
environment:
BUCKET: cad-preview-bucket-prod-001
openscadstl:
image:
name: openscadimage
command:
- openscad.stl
- js/openscad.stl
entryPoint:
- '/entrypoint.sh'
events:
@@ -78,11 +88,12 @@ functions:
timeout: 30
environment:
BUCKET: cad-preview-bucket-prod-001
cadquerystl:
image:
name: cadqueryimage
command:
- cadquery.stl
- js/cadquery.stl
entryPoint:
- '/entrypoint.sh'
events:
@@ -93,6 +104,34 @@ functions:
timeout: 30
environment:
BUCKET: cad-preview-bucket-prod-001
curvpreview:
image:
name: curvimage
command:
- js/curv.preview
entryPoint:
- '/entrypoint.sh'
events:
- http:
path: curv/preview
method: post
cors: true
timeout: 25
memorySize: 3008
curvstl:
image:
name: curvimage
command:
- js/curv.stl
entryPoint:
- '/entrypoint.sh'
events:
- http:
path: curv/stl
method: post
cors: true
timeout: 30
# The following are a few example events you can configure
# NOTE: Please make sure to change your handler code to work with those events
# Check the event documentation for details

View File

@@ -0,0 +1,40 @@
import type { APIGatewayEvent /*, Context*/ } from 'aws-lambda'
import { logger } from 'src/lib/logger'
import { db } from 'src/lib/db'
/**
* The handler function is your code that processes http request events.
* You can use return and throw to send a response or error, respectively.
*
* Important: When deployed, a custom serverless function is an open API endpoint and
* is your responsibility to secure appropriately.
*
* @see {@link https://redwoodjs.com/docs/serverless-functions#security-considerations|Serverless Function Considerations}
* in the RedwoodJS documentation for more information.
*
* @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent
* @typedef { import('aws-lambda').Context } Context
* @param { APIGatewayEvent } event - an object which contains information from the invoker.
* @param { Context } context - contains information about the invocation,
* function, and execution environment.
*/
export const handler = async (event: APIGatewayEvent /*context: Context*/) => {
logger.info('Invoked checkUserName function')
const userName = event.queryStringParameters.username
let isUserNameAvailable = false
try {
const user = await db.user.findUnique({ where: { userName } })
isUserNameAvailable = !user
} catch (error) {
isUserNameAvailable = false
}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
isUserNameAvailable,
}),
}
}

View File

@@ -1,23 +1,22 @@
import {
createGraphQLHandler,
makeMergedSchema,
makeServices,
} from '@redwoodjs/api'
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
import { createSentryApolloPlugin } from 'src/lib/sentry'
import { logger } from 'src/lib/logger'
import schemas from 'src/graphql/**/*.{js,ts}'
import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
import { getCurrentUser } from 'src/lib/auth'
import { db } from 'src/lib/db'
export const handler = createGraphQLHandler({
loggerConfig: { logger, options: {} },
getCurrentUser,
schema: makeMergedSchema({
schemas,
services: makeServices({ services }),
}),
directives,
sdls,
services,
plugins: [createSentryApolloPlugin()],
onException: () => {
// Disconnect from your database with an unhandled exception.
db.$disconnect()

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 simply add any OpenSCAD or CadQuery models you have to the website, building out the library of parts atm is very important.
One very easy way to help out is to give the repo a star (https://github.com/Irev-Dev/cadhub), or simply add any OpenSCAD or CadQuery models you have to the website, building out the library of parts atm is very important.
Hit me up anytime for questions or concerns.
Cheers,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,61 +1,5 @@
// Define what you want `currentUser` to return throughout your app. For example,
// to return a real user from your database, you could do something like:
//
// export const getCurrentUser = async ({ email }) => {
// return await db.user.findUnique({ where: { email } })
// }
//
// If you want to enforce role-based access ...
//
// You'll need to set the currentUser's roles attributes to the
// collection of roles as defined by your app.
//
// This allows requireAuth() on the api side and hasRole() in the useAuth() hook on the web side
// to check if the user is assigned a given role or not.
//
// How you set the currentUser's roles depends on your auth provider and its implementation.
//
// For example, your decoded JWT may store `roles` in it namespaced `app_metadata`:
//
// {
// 'https://example.com/app_metadata': { authorization: { roles: ['admin'] } },
// 'https://example.com/user_metadata': {},
// iss: 'https://app.us.auth0.com/',
// sub: 'email|1234',
// aud: [
// 'https://example.com',
// 'https://app.us.auth0.com/userinfo'
// ],
// iat: 1596481520,
// exp: 1596567920,
// azp: '1l0w6JXXXXL880T',
// scope: 'openid profile email'
// }
//
// The parseJWT utility will extract the roles from decoded token.
//
// The app_medata claim may or may not be namespaced based on the auth provider.
// Note: Auth0 requires namespacing custom JWT claims
//
// Some providers, such as with Auth0, will set roles an authorization
// attribute in app_metadata (namespaced or not):
//
// 'app_metadata': { authorization: { roles: ['publisher'] } }
// 'https://example.com/app_metadata': { authorization: { roles: ['publisher'] } }
//
// Other providers may include roles simply within app_metadata:
//
// 'app_metadata': { roles: ['author'] }
// 'https://example.com/app_metadata': { roles: ['author'] }
//
// And yet other may define roles as a custom claim at the root of the decoded token:
//
// roles: ['admin']
//
// The function `getCurrentUser` should return the user information
// together with a collection of roles to check for role assignment:
import { AuthenticationError, ForbiddenError, parseJWT } from '@redwoodjs/api'
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'
import { parseJWT } from '@redwoodjs/api'
/**
* Use requireAuth in your services to check that a user is logged in,
@@ -97,8 +41,24 @@ import { AuthenticationError, ForbiddenError, parseJWT } from '@redwoodjs/api'
* }
* }
*/
export const getCurrentUser = async (decoded, { _token, _type }) => {
return { ...decoded, roles: parseJWT({ decoded }).roles }
export const getCurrentUser = async (
decoded,
{ _token, _type },
{ _event, _context }
) => {
if (!decoded) {
// if no decoded, then never set currentUser
return null
}
const { roles } = parseJWT({ decoded }) // extract and check roles separately
if (roles) {
return { ...decoded, roles }
}
return { ...decoded } // only return when certain you have
// the currentUser properties
}
/**

View File

@@ -0,0 +1,33 @@
import axios from 'axios'
let inst = null;
if (!process.env.DISCORD_TOKEN || !process.env.DISCORD_CHANNEL_ID) {
console.warn("Discord bot not configured - please set process.env.DISCORD_TOKEN and process.env.DISCORD_CHANNEL_ID to send discord chats");
} else {
inst = axios.create({
baseURL: 'https://discord.com/api'
});
inst.defaults.headers.common['Authorization'] = `Bot ${process.env.DISCORD_TOKEN}`
console.log(`Discord: using API token ${process.env.DISCORD_TOKEN}`);
}
export async function sendDiscordMessage(text: string, url?: string) {
if (!inst) {
console.error(`Discord: not configured to send message ("${text}")`);
} else {
const API_URL = `/channels/${process.env.DISCORD_CHANNEL_ID}/messages`;
if (url) {
return inst.post(API_URL, { embeds: [{
title: text,
image: {
url,
},
}] });
} else {
return inst.post(API_URL, {
content: text,
});
}
}
}

View File

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

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<any>,
isUniqueCallback: (seed: string) => Promise<boolean>,
count = 0
) => {
const seed = humanId({

View File

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

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,7 +12,8 @@ import {
} from 'src/services/helpers'
import { requireAuth } from 'src/lib/auth'
import { requireOwnership, requireProjectOwnership } from 'src/lib/owner'
import { socialCard } from '../socialCards/socialCards'
import { sendDiscordMessage } from 'src/lib/discord'
export const projects = ({ userName }) => {
if (!userName) {
@@ -48,15 +49,17 @@ export const projectByUserAndTitle = async ({ userName, projectTitle }) => {
},
})
}
const isUniqueProjectTitle = (userId: string) => async (seed: string) =>
db.project.findUnique({
const isUniqueProjectTitle =
(userId: string) =>
async (seed: string): Promise<boolean> =>
!!(await db.project.findUnique({
where: {
title_userId: {
title: seed,
userId,
},
},
})
}))
interface CreateProjectArgs {
input: Prisma.ProjectCreateArgs['data']
@@ -79,13 +82,27 @@ export const createProject = async ({ input }: CreateProjectArgs) => {
}
export const forkProject = async ({ input }) => {
// Only difference between create and fork project is that fork project will generate a unique title
// (for the user) if there is a conflict
requireAuth()
const projectData = await db.project.findUnique({
where: {
id: input.forkedFromId,
},
})
const isUniqueCallback = isUniqueProjectTitle(input.userId)
const title = await generateUniqueString(input.title, isUniqueCallback)
// TODO change the description to `forked from userName/projectName ${rest of description}`
let title = projectData.title
title = await generateUniqueString(title, isUniqueCallback)
const { code, description, cadPackage } = projectData
return db.project.create({
data: foreignKeyReplacement({ ...input, title }),
data: foreignKeyReplacement({
...input,
title,
code: input.code || code,
description,
cadPackage,
}),
})
}
@@ -103,7 +120,9 @@ 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}})
const socialCard = await db.socialCard.findUnique({
where: { projectId },
})
if (socialCard) {
return db.socialCard.update({
data: { outOfDate: true },
@@ -114,7 +133,6 @@ export const updateProject = async ({ id, input }: UpdateProjectArgs) => {
}
requireAuth()
const originalProject = await requireProjectOwnership({ projectId: id })
console.log('yooooo', originalProject)
if (input.title) {
input.title = enforceAlphaNumeric(input.title)
}
@@ -227,7 +245,19 @@ export const updateProjectImages = async ({
const [updatedProject] = await Promise.all([
projectPromise,
imageDestroyPromise,
])
]).then(async (result) => {
const { userName } = await db.user.findUnique({
where: { id: project.userId },
})
sendDiscordMessage([
`${userName} just added an image to their ${project.cadPackage} project:`,
` => ${project.title}`,
``,
`Check it out, leave a comment, make them feel welcome!`,
`https://cadhub.xyz/u/${userName}/${project.title}`
].join('\n'), `https://res.cloudinary.com/irevdev/image/upload/c_scale,w_700/v1/${mainImage}`)
return result
})
return updatedProject
}
@@ -242,17 +272,32 @@ export const updateProjectImages = async ({
export const deleteProject = async ({ id }: Prisma.ProjectWhereUniqueInput) => {
requireAuth()
await requireOwnership({ projectId: id })
return db.project.update({
data: {
deleted: true,
},
const project = await db.project.findUnique({
where: { id },
})
const childrenDeletePromises = [
db.comment.deleteMany({ where: { projectId: project.id } }),
db.projectReaction.deleteMany({ where: { projectId: project.id } }),
db.socialCard.deleteMany({ where: { projectId: project.id } }),
]
await Promise.all(childrenDeletePromises)
await db.project.delete({
where: { id },
})
return project
}
export const Project = {
forkedFrom: (_obj, { root }) =>
root.forkedFromId &&
db.project.findUnique({ where: { id: root.forkedFromId } }),
childForks: (_obj, { root }) => {
console.log(' ')
return []
},
// db.project.findMany({ where: { forkedFromId: root.id } }),
user: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
db.project.findUnique({ where: { id: root.id } }).user(),
db.user.findUnique({ where: { id: root.userId } }),
socialCard: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
db.project.findUnique({ where: { id: root.id } }).socialCard(),
Comment: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>

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,8 +1,30 @@
import { UserInputError, ForbiddenError } from '@redwoodjs/graphql-server'
import { db } from 'src/lib/db'
import { requireAuth } from 'src/lib/auth'
import { requireOwnership } from 'src/lib/owner'
import { UserInputError } from '@redwoodjs/api'
import { enforceAlphaNumeric, destroyImage } from 'src/services/helpers'
import type { Prisma } from '@prisma/client'
function userNameVerification(userName: string): string {
if (userName.length < 5) {
throw new ForbiddenError('userName too short')
}
if (userName && ['new', 'edit', 'update'].includes(userName)) {
//TODO complete this and use a regexp so that it's not case sensitive, don't want someone with the userName eDiT
throw new UserInputError(
`You've tried to used a protected word as you userName, try something other than `
)
}
if (userName) {
return enforceAlphaNumeric(userName)
}
}
function nameVerification(name: string) {
if (typeof name === 'string' && name.length < 3) {
throw new ForbiddenError('name too short')
}
}
export const users = () => {
requireAuth({ role: 'admin' })
@@ -25,32 +47,51 @@ export const createUser = ({ input }) => {
requireAuth({ role: 'admin' })
createUserInsecure({ input })
}
export const createUserInsecure = ({ input }) => {
export const createUserInsecure = ({
input,
}: {
input: Prisma.UserUncheckedCreateInput
}) => {
if (typeof input.userName === 'string') {
input.userName = userNameVerification(input.userName)
}
nameVerification(input.name)
return db.user.create({
data: input,
})
}
export const updateUser = ({ id, input }) => {
export const updateUser = ({
id,
input,
}: {
id: string
input: Prisma.UserUncheckedCreateInput
}) => {
requireAuth()
if (typeof input.userName === 'string') {
input.userName = userNameVerification(input.userName)
}
nameVerification(input.name)
return db.user.update({
data: input,
where: { id },
})
}
export const updateUserByUserName = async ({ userName, input }) => {
export const updateUserByUserName = async ({
userName,
input,
}: {
userName: string
input: Prisma.UserUncheckedCreateInput
}) => {
requireAuth()
await requireOwnership({ userName })
if (input.userName) {
input.userName = enforceAlphaNumeric(input.userName)
}
if (input.userName && ['new', 'edit', 'update'].includes(input.userName)) {
//TODO complete this and use a regexp so that it's not case sensitive, don't want someone with the userName eDiT
throw new UserInputError(
`You've tried to used a protected word as you userName, try something other than `
)
if (typeof input.userName === 'string') {
input.userName = userNameVerification(input.userName)
}
nameVerification(input.name)
const originalProject = await db.user.findUnique({ where: { userName } })
const imageToDestroy =
originalProject.image !== input.image && originalProject.image

View File

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

View File

@@ -7,7 +7,7 @@
[web]
port = 8910
apiProxyPath = "/.netlify/functions"
apiUrl = "/.netlify/functions"
includeEnvironmentVariables = [
'GOOGLE_ANALYTICS_ID',
'CLOUDINARY_API_KEY',
@@ -17,7 +17,7 @@
'SENTRY_AUTH_TOKEN',
'SENTRY_ORG',
'SENTRY_PROJECT',
'EMAIL_PASSWORD'
'EMAIL_PASSWORD',
]
# experimentalFastRefresh = true # this seems to break cascadeStudio
[api]

235
app/scripts/seed.ts Normal file
View File

@@ -0,0 +1,235 @@
import type { Prisma } from '@prisma/client'
import { db } from '$api/src/lib/db'
export default async () => {
try {
const users = [
{
id: "a2b21ce1-ae57-43a2-b6a3-b6e542fd9e60",
userName: "local-user-1",
name: "local 1",
email: "localUser1@kurthutten.com"
},
{
id: "682ba807-d10e-4caf-bf28-74054e46c9ec",
userName: "local-user-2",
name: "local 2",
email: "localUser2@kurthutten.com"
},
{
id: "5cea3906-1e8e-4673-8f0d-89e6a963c096",
userName: "local-admin-2",
name: "local admin",
email: "localAdmin@kurthutten.com"
},
]
let existing
existing = await db.user.findMany({ where: { id: users[0].id }})
if(!existing.length) {
await db.user.create({
data: users[0],
})
}
existing = await db.user.findMany({ where: { id: users[1].id }})
if(!existing.length) {
await db.user.create({
data: users[1],
})
}
const projects = [
{
title: 'demo-project1',
description: '# can be markdown',
mainImage: 'CadHub/kjdlgjnu0xmwksia7xox',
code: getOpenScadHingeCode(),
cadPackage: 'openscad',
user: {
connect: {
id: users[0].id,
},
},
},
{
title: 'demo-project2',
description: '## [hey](www.google.com)',
user: {
connect: {
id: users[1].id,
},
},
},
]
existing = await db.project.findMany({where: { title: projects[0].title}})
if(!existing.length) {
await db.project.create({
data: projects[0],
})
}
existing = await db.project.findMany({where: { title: projects[1].title}})
if(!existing.length) {
const result = await db.project.create({
data: projects[1],
})
await db.project.create({
data: {
...projects[1],
title: `${projects[1].title}-fork`,
forkedFrom: {
connect: {
id: result.id,
},
},
},
})
}
const aProject = await db.project.findUnique({where: {
title_userId: {
title: projects[0].title,
userId: users[0].id,
}
}})
await db.comment.create({
data: {
text: "nice project, I like it",
userId: users[0].id,
projectId: aProject.id,
// user: {connect: { id: users[0].id}},
// project: {connect: { id: aProject.id}},
}
})
await db.projectReaction.create({
data: {
emote: "❤️",
userId: users[0].id,
projectId: aProject.id,
// user: {connect: { id: users[0].id}},
// project: {connect: { id: aProject.id}},
}
})
} catch (error) {
console.warn('Please define your seed data.')
console.error(error)
}
}
function getOpenScadHingeCode () {
return `
baseWidth=15; // [0.1:0.1:50]
hingeLength=30; // [0.1:0.1:50]
// Hole mant mounting holes per half.
mountingHoleCount=3; // [1:20]
baseThickness=3; // [0.1:0.1:20]
pivotRadius=5; // [0.1:0.1:20]
// Pin that the hinge pivots on.
pinRadius=2; // [0.1:0.1:20]
mountingHoleRadius=1.5; // [0.1:0.1:10]
// How far away the hole is from the egde.
mountingHoleEdgeOffset=4; // [0:50]
// Depending on the accuracy of your printer this may need to be increased in order for print in place to work.
clearance=0.2; // [0.05:0.01:1]
// Radius difference in the ivot taper to stop the hinge from falling apart. Should be increased with large clearance values.
pinTaper=0.25; // [0.1:0.1:2]
// calculated values
hingeHalfExtrudeLength=hingeLength/2-clearance/2;
mountingHoleMoveIncrement=(hingeLength-2*mountingHoleEdgeOffset)/
(mountingHoleCount-1);
module costomizerEnd() {}
$fn=30;
tiny=0.005;
// modules
module hingeBaseProfile() {
translate([pivotRadius,0,0]){
square([baseWidth,baseThickness]);
}
}
module hingeBodyHalf() {
difference() {
union() {
linear_extrude(hingeHalfExtrudeLength){
offset(1)offset(-2)offset(1){
translate([0,pivotRadius,0]){
circle(pivotRadius);
}
square([pivotRadius,pivotRadius]);
hingeBaseProfile();
}
}
linear_extrude(hingeLength){
offset(1)offset(-1)hingeBaseProfile();
}
}
plateHoles();
}
}
module pin(rotateY, radiusOffset) {
translate([0,pivotRadius,hingeHalfExtrudeLength+tiny]){
rotate([0,rotateY,0]) {
cylinder(
h=hingeLength/2+clearance/2,
r1=pinRadius+radiusOffset,
r2=pinRadius+pinTaper+radiusOffset
);
}
}
}
module hingeHalfFemale() {
difference() {
hingeBodyHalf();
pin(rotateY=180, radiusOffset=clearance);
}
}
module hingeHalfMale() {
translate([0,0,hingeLength]) {
rotate([0,180,0]) {
hingeBodyHalf();
pin(rotateY=0, radiusOffset=0);
}
}
}
module plateHoles() {
for(i=[0:mountingHoleCount-1]){
translate([
baseWidth/2+pivotRadius,
-baseThickness,
i*mountingHoleMoveIncrement+mountingHoleEdgeOffset
]){
rotate([-90,0,0]){
cylinder(r=mountingHoleRadius,h=baseThickness*4);
}
}
}
}
// using high-level modules
translate([0,0,-15]) {
hingeHalfFemale();
hingeHalfMale();
}
`
}

View File

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

View File

@@ -1,5 +1,5 @@
module.exports = {
purge: ['./src/**/*.html', './src/**/*.js', './src/**/*.ts', './src/**/*.tsx'],
purge: ['src/**/*.{js,jsx,ts,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
@@ -13,27 +13,41 @@ module.exports = {
borderRadius: {
half: '50%',
},
boxShadow: {
ch: '0 4px 4px 0 rgba(0, 0, 0, 0.25), 0 4px 4px 0 rgba(13, 13, 19, 0.15)',
},
colors: {
'ch-gray': {
900: '#0D0D13',
800: '#1A1A1D',
750: '#222222',
760: '#232532',
710: '#2B303C', // TODO: Use HSL so I stop adding grays to fix the warm/cool problem
700: '#2A3038',
600: '#3B3E4B',
550: '#63636A',
500: '#9F9FB4',
400: '#A4A4B0',
300: '#CFCFD8',
},
'ch-purple': {
400: '#3B0480',
450: '#671BC6',
500: '#8732F2',
600: '#A663FA',
200: '#C99DFF',
},
'ch-purple-gray': {
200: '#DBDBEC',
},
'ch-blue': {
600: '#79B2F8',
500: '5098F1',
300: '#08466F'
700: '#08466F',
650: '#0958BA',
640: '#0A57B5',
630: '#3285EB',
500: '#5098F1',
400: '#79B2F8',
300: '#9BC8FF',
},
'ch-pink': {
800: '#93064F',
@@ -45,14 +59,17 @@ module.exports = {
grab: 'grab'
},
fontFamily: {
'ropa-sans': ['Ropa Sans', 'Arial', 'sans-serif'],
'ropa-sans': ['"Ropa Sans"', 'Arial', 'sans-serif'],
roboto: ['Roboto', 'Arial', 'sans-serif'],
'fira-code': ['Fira Code', 'monospace'],
'fira-sans': ['Fira Sans', 'sans-seri'],
'fira-code': ['"Fira Code"', 'monospace'],
'fira-sans': ['"Fira Sans"', 'sans-serif'],
},
gridAutoColumns: {
'preview-layout': 'minmax(30rem, 1fr) minmax(auto, 2fr)',
},
gridTemplateColumns: {
'profile-layout': 'minmax(32rem, 1fr) 2fr',
},
keyframes: {
'bounce-sm': {
'0%, 100%': {
@@ -81,6 +98,9 @@ module.exports = {
minHeight: {
md: '28rem',
},
outline: {
gray: ['2px solid #3B3E4B', '8px'],
},
skew: {
'-20': '-20deg',
},

View File

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

10
app/web/config/worker-loader.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
declare module "worker-loader!*" {
// You need to change `Worker`, if you specified a different value for the `workerType` option
class WebpackWorker extends Worker {
constructor();
}
// Uncomment this if you set the `esModule` option to `false`
// export = WebpackWorker;
export default WebpackWorker;
}

View File

@@ -13,21 +13,25 @@
]
},
"dependencies": {
"@headlessui/react": "^1.0.0",
"@headlessui/react": "^1.4.1",
"@heroicons/react": "^1.0.4",
"@material-ui/core": "^4.11.0",
"@monaco-editor/react": "^4.0.11",
"@react-three/drei": "^7.3.1",
"@react-three/fiber": "^7.0.5",
"@redwoodjs/auth": "^0.34.1",
"@redwoodjs/forms": "^0.34.1",
"@redwoodjs/router": "^0.34.1",
"@redwoodjs/web": "^0.34.1",
"@react-three/postprocessing": "^2.0.5",
"@redwoodjs/auth": "^0.38.1",
"@redwoodjs/forms": "^0.38.1",
"@redwoodjs/router": "^0.38.1",
"@redwoodjs/web": "^0.38.1",
"@sentry/browser": "^6.5.1",
"@tailwindcss/aspect-ratio": "0.2.1",
"axios": "^0.21.1",
"browser-fs-access": "^0.17.2",
"cloudinary-react": "^1.6.7",
"get-active-classes": "^0.0.11",
"gotrue-js": "^0.9.27",
"hotkeys-js": "^3.8.7",
"html-to-image": "^1.7.0",
"lodash": "^4.17.21",
"netlify-identity-widget": "^1.9.1",
@@ -38,19 +42,24 @@
"react-dropzone": "^11.2.1",
"react-ga": "^3.3.0",
"react-helmet": "^6.1.0",
"react-hotkeys-hook": "^3.4.0",
"react-image-crop": "^8.6.6",
"react-mosaic-component": "^4.1.1",
"react-intersection-observer": "^8.32.1",
"react-mosaic-component": "^5.0.0",
"react-tabs": "^3.2.2",
"rich-markdown-editor": "^11.0.2",
"styled-components": "^5.2.0",
"three": "^0.130.1"
"three": "^0.130.1",
"worker-loader": "^3.0.8"
},
"devDependencies": {
"@types/lodash": "^4.14.170",
"autoprefixer": "^10.2.5",
"autoprefixer": "^10.3.1",
"html-webpack-plugin": "^4.5.0",
"postcss": "^8.2.13",
"postcss-loader": "4.0.2",
"tailwindcss": "^2.1.2"
"postcss": "^8.3.6",
"postcss-import": "^14.0.2",
"postcss-loader": "^6.1.1",
"raw-loader": "^4.0.2",
"tailwindcss": "^2.2.7"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -1,623 +0,0 @@
(function(f) {
if (typeof exports === "object" && typeof module !== "undefined") {
module.exports = f()
} else if (typeof define === "function" && define.amd) {
define([], f)
} else {
var g;
if (typeof window !== "undefined") {
g = window
} else if (typeof global !== "undefined") {
g = global
} else if (typeof self !== "undefined") {
g = self
} else {
g = this
}
g.jscadWorker = f()
}
})(function() {
// multi purpose module
const setPoints = (points, p, i)=>{
points[i++] = p[0]
points[i++] = p[1]
points[i++] = p[2] || 0
}
function CSG2Vertices(csg){
let idx = 0
let vLen = 0, iLen = 0
for (let poly of csg.polygons){
let len = poly.vertices.length
vLen += len *3
iLen += 3 * (len-2)
}
const vertices = new Float32Array(vLen)
const indices = vLen > 65535 ? new Uint32Array(iLen) : new Uint16Array(iLen)
let vertOffset = 0
let indOffset = 0
let posOffset = 0
let first = 0
for (let poly of csg.polygons){
let arr = poly.vertices
let len = arr.length
first = posOffset
vertices.set(arr[0], vertOffset)
vertOffset +=3
vertices.set(arr[1], vertOffset)
vertOffset +=3
posOffset +=2
for(let i=2; i<len; i++){
vertices.set(arr[i], vertOffset)
indices[indOffset++] = first
indices[indOffset++] = first + i -1
indices[indOffset++] = first + i
vertOffset += 3
posOffset += 1
}
}
return {vertices, indices, type:'mesh'}
}
function CSG2LineVertices(csg){
let vLen = csg.points.length * 3
if(csg.isClosed) vLen += 3
var vertices = new Float32Array(vLen)
csg.points.forEach((p,idx)=>setPoints(vertices, p, idx * 3 ))
if(csg.isClosed){
setPoints(vertices, csg.points[0], vertices.length - 3 )
}
return {vertices, type:'line'}
}
function CSG2LineSegmentsVertices(csg){
let vLen = csg.sides.length * 6
var vertices = new Float32Array(vLen)
csg.sides.forEach((side,idx)=>{
let i = idx * 6
setPoints(vertices, side[0], i)
setPoints(vertices, side[1], i+3)
})
return {vertices, type:'lines'}
}
function CSGCached(func, data, cacheKey, transferable){
cacheKey = cacheKey || data
let geo = CSGToBuffers.cache.get(cacheKey)
if(geo) return geo
geo = func(data)
// fill transferable array for postMessage optimization
if(transferable){
const {vertices, indices} = geo
transferable.push(vertices)
if(indices) transferable.push(indices)
}
CSGToBuffers.cache.set(cacheKey, geo)
return geo
}
function CSGToBuffers(csg, transferable){
let obj
if(csg.polygons) obj = CSGCached(CSG2Vertices,csg,csg.polygons, transferable)
if(csg.sides && !csg.points) obj = CSGCached(CSG2LineSegmentsVertices,csg,csg.sides, transferable)
if(csg.points) obj = CSGCached(CSG2LineVertices,csg,csg.points, transferable)
return obj
}
CSGToBuffers.clearCache = ()=>{CSGToBuffers.cache = new WeakMap()}
CSGToBuffers.clearCache()
let workerBaseURI
function require(url){
url = require.alias[url] || url
if(url[0] != '/' && url.substr(0,2) != './' && url.substr(0,4) != 'http') url = 'https://unpkg.com/'+url
let exports=require.cache[url]; //get from cache
if (!exports) { //not cached
let module = requireModule(url)
require.cache[url] = exports = module.exports; //cache obj exported by module
}
return exports; //require returns object exported by module
}
function requireFile(url){
try{
let X=new XMLHttpRequest();
X.open("GET", new URL(url,workerBaseURI), 0); // sync
X.send();
if (X.status && X.status !== 200) throw new Error(X.statusText);
return X.responseText;
}catch(e){
console.log('problem loading url ',url,'base',workerBaseURI,' error:',e.message)
throw e
}
}
function requireModule(url, source){
try {
const exports={};
if(!source) source = requireFile(url)
const module = { id: url, uri: url, exports:exports, source }; //according to node.js modules
// fix, add comment to show source on Chrome Dev Tools
source="//@ sourceURL="+url+"\n" + source;
//------
const anonFn = new Function("require", "exports", "module", source); //create a Fn with module code, and 3 params: require, exports & module
anonFn(require, exports, module); // call the Fn, Execute the module
return module
} catch (err) {
console.error("Error loading module "+url, err.message);
throw err;
}
}
require.cache = {}
require.alias = {}
const initCanvas = (canvas, callback)=>{
// convert HTML events (mouse movement) to viewer changes
let lastX = 0
let lastY = 0
let pointerDown = false
const moveHandler = (ev) => {
if(!pointerDown) return
const cmd = {
worker: 'render',
dx: lastX - ev.pageX,
dy: ev.pageY - lastY
}
const shiftKey = (ev.shiftKey === true) || (ev.touches && ev.touches.length > 2)
cmd.action = shiftKey ? 'pan':'rotate'
callback(cmd)
lastX = ev.pageX
lastY = ev.pageY
ev.preventDefault()
}
const downHandler = (ev) => {
pointerDown = true
lastX = ev.pageX
lastY = ev.pageY
canvas.setPointerCapture(ev.pointerId)
ev.preventDefault()
}
const upHandler = (ev) => {
pointerDown = false
canvas.releasePointerCapture(ev.pointerId)
ev.preventDefault()
}
const wheelHandler = (ev) => {
callback({action:'zoom', dy:ev.deltaY, worker: 'render'})
ev.preventDefault()
}
canvas.onpointermove = moveHandler
canvas.onpointerdown = downHandler
canvas.onpointerup = upHandler
canvas.onwheel = wheelHandler
}
const cmdHandler = (handlers)=>(cmd)=>{
const fn = handlers[cmd.action]
if (!fn) throw new Error('no handler for type: ' + cmd.action)
fn(cmd);
}
const makeScriptWorker = ({callback, convertToSolids})=>{
let workerBaseURI, onInit
function runMain(params={}){
let time = Date.now()
let solids
let transfer = []
try{
solids = main(params)
}catch(e){
callback({action:'entities', worker:'render', error:e.message, stack:e.stack.toString()}, transfer)
return
}
let solidsTime = Date.now() - time
scriptStats = `generate solids ${solidsTime}ms`
if(convertToSolids === 'buffers'){
CSGToBuffers.clearCache()
entities = solids.map((csg)=>{
let obj = CSGToBuffers(csg, transfer)
obj.color = csg.color
obj.transforms = csg.transforms
return obj
})
}else if(convertToSolids === 'regl'){
const { entitiesFromSolids } = require('@jscad/regl-renderer')
time = Date.now()
entities = entitiesFromSolids({}, solids)
scriptStats += ` convert to entities ${Date.now()-time}ms`
}else{
entities = solids
}
callback({action:'entities', worker:'render', entities, scriptStats}, transfer)
}
let initialized = false
const handlers = {
runScript: ({script,url, params={}})=>{
if(!initialized){
onInit = ()=>handlers.runScript({script,url, params})
}
let script_module
try{
script_module = requireModule(url,script)
}catch(e){
callback({action:'entities', worker:'render', error:e.message, stack:e.stack.toString()})
return
}
main = script_module.exports.main
let gp = script_module.exports.getParameterDefinitions
if(gp){
callback({action:'parameterDefinitions', worker:'main', data:gp()})
}
runMain(params)
},
updateParams: ({params={}})=>{
runMain(params)
},
init: (params)=>{
let {baseURI, alias=[]} = params
if(!baseURI && typeof document != 'undefined' && document.baseURI){
baseURI = document.baseURI
}
if(baseURI) workerBaseURI = baseURI.toString()
alias.forEach(arr=>{
let [orig, ...aliases] = arr
aliases.forEach(a=>{
require.alias[a] = orig
if(a.toLowerCase().substr(-3)!=='.js') require.alias[a+'.js'] = orig
})
})
initialized = true
if(onInit) onInit()
},
}
return {
// called from outside to pass mesasges into worker
postMessage: cmdHandler(handlers),
}
}
/** Make render worker */
const makeRenderWorker = ()=>{
let perspectiveCamera
const state = {}
const rotateSpeed = 0.002
const panSpeed = 1
const zoomSpeed = 0.08
let rotateDelta = [0, 0]
let panDelta = [0, 0]
let zoomDelta = 0
let updateRender = true
let orbitControls, renderOptions, gridOptions, axisOptions, renderer
let entities = []
function createContext (canvas, contextAttributes) {
function get (type) {
try {
return {gl:canvas.getContext(type, contextAttributes), type}
} catch (e) {
return null
}
}
return (
get('webgl2') ||
get('webgl') ||
get('experimental-webgl') ||
get('webgl-experimental')
)
}
const startRenderer = ({canvas, cameraPosition, cameraTarget, axis={}, grid={}})=>{
const { prepareRender, drawCommands, cameras, controls } = require('@jscad/regl-renderer')
perspectiveCamera = cameras.perspective
orbitControls = controls.orbit
state.canvas = canvas
state.camera = Object.assign({}, perspectiveCamera.defaults)
if(cameraPosition) state.camera.position = cameraPosition
if(cameraTarget) state.camera.target = cameraTarget
resize({ width:canvas.width, height:canvas.height })
state.controls = orbitControls.defaults
const {gl, type} = createContext(canvas)
// prepare the renderer
const setupOptions = {
glOptions: {gl}
}
if(type == 'webgl'){
setupOptions.glOptions.optionalExtensions = ['oes_element_index_uint']
}
renderer = prepareRender(setupOptions)
gridOptions = {
visuals: {
drawCmd: 'drawGrid',
show: grid.show || grid.show === undefined ,
color: grid.color || [0, 0, 0, 1],
subColor: grid.subColor || [0, 0, 1, 0.5],
fadeOut: false,
transparent: true
},
size: grid.size || [200, 200],
ticks: grid.ticks || [10, 1]
}
axisOptions = {
visuals: {
drawCmd: 'drawAxis',
show: axis.show || axis.show === undefined
},
size: axis.size || 100,
}
// assemble the options for rendering
renderOptions = {
camera: state.camera,
drawCommands: {
drawAxis: drawCommands.drawAxis,
drawGrid: drawCommands.drawGrid,
drawLines: drawCommands.drawLines,
drawMesh: drawCommands.drawMesh
},
// define the visual content
entities: [
gridOptions,
axisOptions,
...entities
]
}
// the heart of rendering, as themes, controls, etc change
updateView()
}
let renderTimer
const tmFunc = typeof requestAnimationFrame === 'undefined' ? setTimeout : requestAnimationFrame
function updateView(delay=8){
if(renderTimer || !renderer) return
renderTimer = tmFunc(updateAndRender,delay)
}
const doRotatePanZoom = () => {
if (rotateDelta[0] || rotateDelta[1]) {
const updated = orbitControls.rotate({ controls: state.controls, camera: state.camera, speed: rotateSpeed }, rotateDelta)
state.controls = { ...state.controls, ...updated.controls }
rotateDelta = [0, 0]
}
if (panDelta[0] || panDelta[1]) {
const updated = orbitControls.pan({ controls:state.controls, camera:state.camera, speed: panSpeed }, panDelta)
state.controls = { ...state.controls, ...updated.controls }
panDelta = [0, 0]
state.camera.position = updated.camera.position
state.camera.target = updated.camera.target
}
if (zoomDelta) {
const updated = orbitControls.zoom({ controls:state.controls, camera:state.camera, speed: zoomSpeed }, zoomDelta)
state.controls = { ...state.controls, ...updated.controls }
zoomDelta = 0
}
}
const updateAndRender = (timestamp) => {
renderTimer = null
doRotatePanZoom()
const updates = orbitControls.update({ controls: state.controls, camera: state.camera })
state.controls = { ...state.controls, ...updates.controls }
if(state.controls.changed) updateView(16) // for elasticity in rotate / zoom
state.camera.position = updates.camera.position
perspectiveCamera.update(state.camera)
renderOptions.entities = [
gridOptions,
axisOptions,
...entities
]
let time = Date.now()
renderer(renderOptions)
if(updateRender){
updateRender = '';
}
}
function resize({width,height}){
state.canvas.width = width
state.canvas.height = height
perspectiveCamera.setProjection(state.camera, state.camera, { width, height })
perspectiveCamera.update(state.camera, state.camera)
updateView()
}
const handlers = {
pan: ({dx,dy})=>{
panDelta[0] += dx
panDelta[1] += dy
updateView()
},
rotate: ({dx,dy})=>{
rotateDelta[0] -= dx
rotateDelta[1] -= dy
updateView()
},
zoom: ({dy})=>{
zoomDelta += dy
updateView()
},
resize,
entities: (params)=>{
entities = params.entities
updateRender = params.scriptStats
updateView()
},
init: (params)=>{
if(params.canvas) startRenderer(params)
initialized = true
},
}
return {
// called from outside to pass mesasges into worker
postMessage: cmdHandler(handlers),
}
}
return (params)=>{
let { canvas, baseURI=(typeof document === 'undefined') ? '':document.location.toString(), scope='main', renderInWorker, render, callback=()=>{}, scriptUrl='demo-worker.js', alias, convertToSolids=false } = params
// by default 'render' messages go outside of this instance (result of modeling)
let sendToRender = callback
let scriptWorker, renderWorker
workerBaseURI = baseURI
const sendCmd = (params, transfer)=>{
if(params.worker === 'render')
sendToRender(params, transfer)
else if(params.worker === 'script')
scriptWorker.postMessage(params, transfer)
else{
// parameter definitions will arrive from scriptWorker
callback(params, transfer)
}
}
const updateSize = function({width,height}){
sendCmd({ action:'resize', worker:'render', width: canvas.offsetWidth, height: canvas.offsetHeight})
}
renderInWorker = !!(canvas && renderInWorker && canvas.transferControlToOffscreen)
const makeRenderWorkerHere = (scope === 'main' && canvas && !renderInWorker) || (scope === 'worker' && render)
// worker is in current thread
if(makeRenderWorkerHere){
renderWorker = makeRenderWorker({callback:sendCmd})
sendToRender = (params, transfer)=>renderWorker.postMessage(params, transfer)
}
if(scope === 'main'){
// let extraScript = renderInWorker ? `,'https://unpkg.com/@jscad/regl-renderer'`:''
let script =`let baseURI = '${baseURI}'
importScripts(new URL('${scriptUrl}',baseURI))
let worker = jscadWorker({
baseURI: baseURI,
convertToSolids: ${convertToSolids},
scope:'worker',
callback:(params)=>self.postMessage(params),
render:${renderInWorker}
})
self.addEventListener('message', (e)=>worker.postMessage(e.data))
`
let blob = new Blob([script],{type: 'text/javascript'})
scriptWorker = new Worker(window.URL.createObjectURL(blob))
scriptWorker.addEventListener('message',(e)=>sendCmd(e.data))
scriptWorker.postMessage({action:'init', baseURI, alias})
if(renderInWorker) renderWorker = scriptWorker
if(canvas){
initCanvas(canvas, sendCmd)
window.addEventListener('resize',updateSize)
}
}else{
scriptWorker = makeScriptWorker({callback:sendCmd, convertToSolids})
callback({action:'workerInit',worker:'main'})
}
if(canvas){
// redirect 'render' messages to renderWorker
sendToRender = (params, transfer)=>renderWorker.postMessage(params, transfer)
let width = canvas.width = canvas.clientWidth
let height = canvas.height = canvas.clientHeight
if(scope == 'main'){
const offscreen = renderInWorker ? canvas.transferControlToOffscreen() : canvas
renderWorker.postMessage({action:'init', worker:'render', canvas:offscreen, width, height}, [offscreen])
}
}
return {
updateSize,
updateParams:({params={}})=>sendCmd({ action:'updateParams', worker:'script', params}),
runScript: ({script,url=''})=>sendCmd({ action:'runScript', worker:'script', script, url}),
postMessage: sendCmd,
}
}
// multi purpose module
});

BIN
app/web/public/hinge.stl Normal file

Binary file not shown.

BIN
app/web/public/pumpjack.stl Normal file

Binary file not shown.

View File

@@ -1,15 +1,19 @@
import { AuthProvider } from '@redwoodjs/auth'
import GoTrue from 'gotrue-js'
import { FatalErrorBoundary } from '@redwoodjs/web'
import { RedwoodProvider } from '@redwoodjs/web'
import FatalErrorBoundary from 'src/components/FatalErrorBoundary/FatalErrorBoundary'
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
import FatalErrorPage from 'src/pages/FatalErrorPage'
import { createTheme } from '@material-ui/core/styles'
import { ThemeProvider } from '@material-ui/styles'
import ReactGA from 'react-ga'
ReactGA.initialize(process.env.GOOGLE_ANALYTICS_ID)
import Routes from 'src/Routes'
import './font-imports.css'
import './scaffold.css'
import './index.css'
@@ -18,13 +22,28 @@ const goTrueClient = new GoTrue({
setCookie: true,
})
const theme = createTheme({
palette: {
type: 'dark',
primary: {
light: '#C99DFF',
main: '#A663FA',
dark: '#3B0480',
},
},
})
const App = () => (
<FatalErrorBoundary page={FatalErrorPage}>
<RedwoodProvider>
<AuthProvider client={goTrueClient} type="goTrue">
<RedwoodApolloProvider>
<ThemeProvider theme={theme}>
<Routes />
</ThemeProvider>
</RedwoodApolloProvider>
</AuthProvider>
</RedwoodProvider>
</FatalErrorBoundary>
)

View File

@@ -35,6 +35,7 @@ const Routes = () => {
)
return (
<Router>
<Route path="/projects" page={ProjectsPage} name="projects" />
<Route path="/dev-ide/{cadPackage}" page={DevIdePage} name="devIde" />
<Route path="/policies/privacy-policy" page={PrivacyPolicyPage} name="privacyPolicy" />
<Route path="/policies/code-of-conduct" page={CodeOfConductPage} name="codeOfConduct" />
@@ -55,11 +56,12 @@ const Routes = () => {
<Route path="/u/{userName}" page={UserPage} name="user" />
<Route path="/u/{userName}/{projectTitle}" page={ProjectPage} name="project" />
<Route path="/u/{userName}/{projectTitle}/ide" page={IdeProjectPage} name="ide" />
<Route path="/u/{userName}/{projectTitle}/embed" page={EmbedProjectPage} name="embed" />
<Route path="/u/{userName}/{projectTitle}/social-card" page={SocialCardPage} name="socialCard" />
<Private unauthenticated="home" role="admin">
<Route path="/admin/users" page={UsersPage} name="users" />
<Route path="/admin/projects" page={AdminProjectsPage} name="projects" />
<Route path="/admin/projects" page={AdminProjectsPage} name="adminProjects" />
<Route path="/admin/subject-access-requests/{id}/edit" page={EditSubjectAccessRequestPage} name="editSubjectAccessRequest" />
<Route path="/admin/subject-access-requests/{id}" page={SubjectAccessRequestPage} name="subjectAccessRequest" />
<Route path="/admin/subject-access-requests" page={SubjectAccessRequestsPage} name="subjectAccessRequests" />

View File

@@ -1,29 +1,80 @@
import { ideTypeNameMap } from 'src/helpers/hooks/useIdeContext'
export type CadPackageType = 'openscad' | 'cadquery' | 'jscad' | 'curv' | 'INIT'
interface CadPackageProps {
cadPackage: string
className?: string
interface CadPackageConfig {
label: string
buttonClasses: string
dotClasses: string
}
const CadPackage = ({ cadPackage, className = '' }: CadPackageProps) => {
const cadName = ideTypeNameMap[cadPackage] || ''
const isOpenScad = cadPackage === 'openscad'
const isCadQuery = cadPackage === 'cadquery'
export const cadPackageConfigs: { [key in CadPackageType]: CadPackageConfig } =
{
openscad: {
label: 'OpenSCAD',
buttonClasses: 'bg-yellow-800',
dotClasses: 'bg-yellow-200',
},
cadquery: {
label: 'CadQuery',
buttonClasses: 'bg-ch-blue-700',
dotClasses: 'bg-blue-800',
},
jscad: {
label: 'JSCAD',
buttonClasses: 'bg-ch-purple-500',
dotClasses: 'bg-yellow-300',
},
curv: {
label: 'Curv',
buttonClasses: 'bg-blue-600',
dotClasses: 'bg-green-500',
},
INIT: {
label: '',
buttonClasses: '',
dotClasses: '',
},
}
interface CadPackageProps {
cadPackage: CadPackageType
className?: string
dotClass?: string
onClick?: any
}
const CadPackage = ({
cadPackage,
className = '',
dotClass = 'w-5 h-5',
onClick,
}: CadPackageProps) => {
const cadPackageConfig = cadPackageConfigs[cadPackage]
return (
<div
<ButtonOrDiv
onClick={onClick}
className={
`grid grid-flow-col-dense items-center gap-2 cursor-default text-gray-100 ${
isOpenScad && 'bg-yellow-800'
} ${isCadQuery && 'bg-ch-blue-300'} bg-opacity-30 ` + className
`grid grid-flow-col-dense items-center gap-2 text-gray-100 bg-opacity-30
${cadPackageConfig?.buttonClasses} ` + className
}
>
<div
className={`${isOpenScad && 'bg-yellow-200'} ${
isCadQuery && 'bg-blue-800'
} w-5 h-5 rounded-full`}
className={`${cadPackageConfig?.dotClasses} ${dotClass} rounded-full`}
/>
<div>{cadName}</div>
</div>
{cadPackageConfig?.label}
</ButtonOrDiv>
)
}
// Returns a proper button if an onClick handler is passed in, or a div
// if the element is meant to be a simple badge
function ButtonOrDiv({ onClick, className, children }) {
return onClick ? (
<button className={className + ' hover:bg-opacity-80'} onClick={onClick}>
{children}
</button>
) : (
<div className={className}>{children}</div>
)
}

View File

@@ -1,193 +1,267 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { toast } from '@redwoodjs/web/toast'
import Popover from '@material-ui/core/Popover'
import Svg from 'src/components/Svg/Svg'
import Button from 'src/components/Button/Button'
import { toJpeg } from 'html-to-image'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { canvasToBlob, blobTo64 } from 'src/helpers/canvasToBlob'
import { useUpdateProjectImages } from 'src/helpers/hooks/useUpdateProjectImages'
import { requestRenderStateless } from 'src/helpers/hooks/useIdeState'
import { PureIdeViewer } from 'src/components/IdeViewer/PureIdeViewer'
import { State } from 'src/helpers/hooks/useIdeState'
import SocialCardCell from 'src/components/SocialCardCell/SocialCardCell'
import { toJpeg } from 'html-to-image'
const anchorOrigin = {
vertical: 'bottom',
horizontal: 'center',
}
const transformOrigin = {
vertical: 'top',
horizontal: 'center',
}
export const captureSize = { width: 500, height: 522 }
const CaptureButton = ({
canEdit,
TheButton,
shouldUpdateImage,
projectTitle,
userName,
const CaptureButtonViewer = ({
onInit,
onScadImage,
canvasRatio = 1,
}: {
onInit: (a: any) => void
onScadImage: (a: any) => void
canvasRatio: number
}) => {
const [captureState, setCaptureState] = useState<any>({})
const [anchorEl, setAnchorEl] = useState(null)
const [whichPopup, setWhichPopup] = useState(null)
const { state, project } = useIdeContext()
const ref = React.useRef<HTMLDivElement>(null)
const { updateProjectImages } = useUpdateProjectImages({})
const onCapture = async () => {
const threeInstance = state.threeInstance
const isOpenScadImage = state?.objectData?.type === 'png'
let imgBlob
let image64
if (!isOpenScadImage) {
imgBlob = canvasToBlob(threeInstance, { width: 500, height: 375 })
image64 = blobTo64(
await canvasToBlob(threeInstance, { width: 500, height: 522 })
)
} else {
imgBlob = state.objectData.data
image64 = blobTo64(state.objectData.data)
const { state } = useIdeContext()
const threeInstance = React.useRef(null)
const [dataType, dataTypeSetter] = useState(state?.objectData?.type)
const [artifact, artifactSetter] = useState(state?.objectData?.data)
const [ideType] = useState(state?.ideType)
const [isLoading, isLoadingSetter] = useState(false)
const [camera, cameraSetter] = useState<State['camera'] | null>(null)
const getThreeInstance = (_threeInstance) => {
threeInstance.current = _threeInstance
onInit(_threeInstance)
}
const config = {
image: await imgBlob,
currImage: project?.mainImage,
imageObjectURL: window.URL.createObjectURL(await imgBlob),
callback: uploadAndUpdateImage,
cloudinaryImgURL: '',
updated: false,
image64: await image64,
}
setCaptureState(config)
async function uploadAndUpdateImage() {
const upload = async () => {
const socialCard64 = toJpeg(ref.current, {
cacheBust: true,
quality: 0.7,
})
// uploading in two separate mutations because of the 100kb limit of the lambda functions
const imageUploadPromise1 = updateProjectImages({
variables: {
id: project?.id,
mainImage64: await config.image64,
const onCameraChange = (camera, isFirstCameraChange) => {
const renderPromise =
(state.ideType === 'openscad' || state.ideType === 'curv') &&
requestRenderStateless({
state,
camera,
viewerSize: {
width: threeInstance.current.size.width * canvasRatio,
height: threeInstance.current.size.height * canvasRatio,
},
viewAll: isFirstCameraChange,
})
const imageUploadPromise2 = updateProjectImages({
variables: {
id: project?.id,
socialCard64: await socialCard64,
},
})
return Promise.all([imageUploadPromise2, imageUploadPromise1])
if (!renderPromise) {
return
}
isLoadingSetter(true)
renderPromise.then(async ({ objectData, camera }) => {
if (camera?.isScadUpdate) {
cameraSetter(camera)
}
isLoadingSetter(false)
dataTypeSetter(objectData?.type)
artifactSetter(objectData?.data)
if (objectData?.type === 'png') {
onScadImage(await blobTo64(objectData?.data))
}
const 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()
}}
<PureIdeViewer
scadRatio={canvasRatio}
dataType={dataType}
artifact={artifact}
onInit={getThreeInstance}
onCameraChange={onCameraChange}
isLoading={isLoading}
camera={camera}
isMinimal
ideType={ideType}
/>
)
}
function TabContent() {
return (
<div className="bg-ch-gray-800 h-full overflow-y-auto px-8 pb-16">
<IsolatedCanvas
size={{ width: 500, height: 375 }}
uploadKey="mainImage64"
RenderComponent={ThumbnailViewer}
/>
<IsolatedCanvas
canvasRatio={2}
size={captureSize}
uploadKey="socialCard64"
RenderComponent={SocialCardLiveViewer}
/>
<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">
)
}
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={ref}>
<div style={{ width: '1200px', height: '630px' }} ref={forwardRef}>
<SocialCardCell
userName={userName}
projectTitle={projectTitle}
image64={captureState.image64}
/>
userName={project.user.userName}
projectTitle={project.title}
image64={partSnapShot64}
>
{children}
</SocialCardCell>
</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,
</div>
<button className="bg-gray-200 p-2 rounded-sm" onClick={onUpload}>
save image
</button>
</>
)
}
function ThumbnailViewer({ forwardRef, onUpload, children, partSnapShot64 }) {
return (
<>
<h3 className="text-2xl text-ch-gray-300 pt-4">Set thumbnail</h3>
<div
style={{ width: '500px', height: '375px' }}
className="rounded-md shadow-ch border border-gray-400 overflow-hidden my-4"
>
<div className="h-full w-full relative" ref={forwardRef}>
{children}
{partSnapShot64 && (
<img src={partSnapShot64} className="absolute inset-0" />
)}
</div>
</div>
<button className="bg-gray-200 p-2 rounded-sm" onClick={onUpload}>
save thumbnail
</button>
</>
)
}
function IsolatedCanvas({
RenderComponent,
canvasRatio = 1,
size,
uploadKey,
}: {
canvasRatio?: number
uploadKey: 'socialCard64' | 'mainImage64'
size: {
width: number
height: number
}
RenderComponent: React.FC<{
forwardRef: React.Ref<any>
children: React.ReactNode
partSnapShot64: string
onUpload: (a: any) => void
}>
}) {
const { project } = useIdeContext()
const { updateProjectImages } = useUpdateProjectImages({})
const [partSnapShot64, partSnapShot64Setter] = React.useState('')
const [scadSnapShot64, scadSnapShot64Setter] = React.useState('')
const captureRef = React.useRef<HTMLDivElement>(null)
const threeInstance = React.useRef(null)
const onInit = (_threeInstance) => (threeInstance.current = _threeInstance)
const upload = async () => {
const uploadPromise = new Promise((resolve, reject) => {
const asyncHelper = async () => {
if (!scadSnapShot64) {
partSnapShot64Setter(
await blobTo64(await canvasToBlob(threeInstance.current, size))
)
} else {
partSnapShot64Setter(scadSnapShot64)
}
setTimeout(async () => {
const capturedImage = await toJpeg(captureRef.current, {
cacheBust: true,
quality: 0.7,
})
await updateProjectImages({
variables: {
id: project?.id,
[uploadKey]: capturedImage,
},
})
partSnapShot64Setter('')
resolve(capturedImage)
})
}
asyncHelper()
})
toast.promise(uploadPromise, {
loading: 'Saving Image',
success: (finalImg: string) => (
<div className="flex flex-col items-center">
<b className="py-2">Image saved!</b>
<img src={finalImg} />
</div>
),
error: <b>Problem saving.</b>,
})
}
return (
<div>
<RenderComponent
forwardRef={captureRef}
onUpload={upload}
partSnapShot64={partSnapShot64}
>
<div
style={{
width: `${size.width * canvasRatio}px`,
height: `${size.height * canvasRatio}px`,
}}
>
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
<CaptureButtonViewer
onInit={onInit}
onScadImage={scadSnapShot64Setter}
canvasRatio={canvasRatio}
/>
</div>
)}
</div>
</div>
</Popover>
</div>
)}
</RenderComponent>
</div>
)
}
export default CaptureButton
export default function CaptureButton({ TheButton }) {
const { state, thunkDispatch } = useIdeContext()
return (
<TheButton
onClick={() => {
thunkDispatch({
type: 'addEditorModel',
payload: {
type: 'component',
label: 'Social Media Card',
Component: TabContent,
},
})
thunkDispatch({
type: 'switchEditorModel',
payload: state.editorTabs.length,
})
}}
/>
)
}

View File

@@ -1,3 +1,6 @@
import { Listbox, Transition } from '@headlessui/react'
import { CheckIcon, SelectorIcon } from '@heroicons/react/solid'
import { useRender } from 'src/components/IdeWrapper/useRender'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { Switch } from '@headlessui/react'
@@ -6,15 +9,28 @@ import {
CadhubStringParam,
CadhubBooleanParam,
CadhubNumberParam,
CadhubStringChoiceParam,
CadhubNumberChoiceParam,
} from './customizerConverter'
const Customizer = () => {
const [open, setOpen] = React.useState(false)
const [shouldLiveUpdate, setShouldLiveUpdate] = React.useState(false)
const { state, thunkDispatch } = useIdeContext()
const isOpen = state.isCustomizerOpen
const customizerParams = state?.customizerParams
const currentParameters = state?.currentParameters || {}
const handleRender = useRender()
const toggleOpen = () => {
thunkDispatch({ type: 'setCustomizerOpenState', payload: !isOpen })
if (state.viewerContext === 'ide') {
// don't re-render on open/close in the project profile
setTimeout(() => handleRender())
}
}
const handleReset = () => {
thunkDispatch({ type: 'resetCustomizer' })
setTimeout(() => handleRender(true))
}
const updateCustomizerParam = (paramName: string, paramValue: any) => {
const payload = {
@@ -28,20 +44,20 @@ const Customizer = () => {
return (
<div
className={`absolute inset-x-0 bottom-0 bg-ch-gray-600 bg-opacity-60 text-ch-gray-300 text-lg font-fira-sans ${
open ? 'h-2/3' : ''
isOpen ? 'h-full max-h-96' : ''
}`}
>
<div className="flex justify-between px-6 py-2 items-center">
<div className="grid grid-flow-col-dense gap-6 items-center">
<button className="px-2" onClick={() => setOpen(!open)}>
<button className="px-2" onClick={toggleOpen}>
<Svg
name="chevron-down"
className={`h-8 w-8 ${!open && 'transform rotate-180'}`}
className={`h-8 w-8 ${!isOpen && 'transform rotate-180'}`}
/>
</button>
<div>Parameters</div>
</div>
{open && (
{isOpen && (
<>
<div className="flex items-center">
<div className="font-fira-sans text-sm mr-4">Auto Update</div>
@@ -61,11 +77,17 @@ const Customizer = () => {
} inline-block w-4 h-4 transform bg-white rounded-full`}
/>
</Switch>
<button
className="px-4 py-1 rounded bg-ch-gray-300 text-ch-gray-600 mr-2"
onClick={handleReset}
>
Reset
</button>
<button
className={`px-4 py-1 rounded bg-ch-gray-300 text-ch-gray-800 ${
shouldLiveUpdate && 'bg-opacity-30 cursor-default'
}`}
onClick={handleRender}
onClick={() => handleRender()}
disabled={shouldLiveUpdate}
>
Update
@@ -74,18 +96,29 @@ const Customizer = () => {
</>
)}
</div>
<div className={`${open ? 'h-full pb-32' : 'h-0'} overflow-y-auto px-12`}>
<div
className={`${isOpen ? 'h-full pb-32' : 'h-0'} overflow-y-auto px-12`}
>
<div>
{customizerParams.map((param, index) => {
const otherProps = {
value: currentParameters[param.name],
onChange: (value) => updateCustomizerParam(param.name, value),
onChange: (value) =>
updateCustomizerParam(
param.name,
param.type == 'number' ? Number(value) : value
),
}
if (param.type === 'string') {
if (
param.input === 'choice-string' ||
param.input === 'choice-number'
) {
return <ChoiceParam key={index} param={param} {...otherProps} />
} else if (param.input === 'default-string') {
return <StringParam key={index} param={param} {...otherProps} />
} else if (param.type === 'number') {
} else if (param.input === 'default-number') {
return <NumberParam key={index} param={param} {...otherProps} />
} else if (param.type === 'boolean') {
} else if (param.input === 'default-boolean') {
return <BooleanParam key={index} param={param} {...otherProps} />
}
return <div key={index}>{JSON.stringify(param)}</div>
@@ -110,7 +143,7 @@ function CustomizerParamBase({
return (
<li
className="grid items-center my-2"
style={{ gridTemplateColumns: 'auto 8rem' }}
style={{ gridTemplateColumns: 'auto 16rem' }}
>
<div className=" text-sm font-fira-sans">
<div className="font-bold text-base">{name}</div>
@@ -128,7 +161,7 @@ function BooleanParam({
}: {
param: CadhubBooleanParam
value: any
onChange: Function
onChange: (value: any) => void
}) {
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
@@ -158,7 +191,7 @@ function StringParam({
}: {
param: CadhubStringParam
value: any
onChange: Function
onChange: (value: any) => void
}) {
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
@@ -173,6 +206,79 @@ function StringParam({
)
}
function ChoiceParam({
param,
value,
onChange,
}: {
param: CadhubStringChoiceParam | CadhubNumberChoiceParam
value: any
onChange: (value: any) => void
}) {
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
<Listbox value={value} onChange={onChange}>
<div className="relative mt-1">
<Listbox.Button className="relative w-full h-8 text-left cursor-default focus:outline-none focus-visible:ring-2 focus-visible:ring-opacity-75 focus-visible:ring-white focus-visible:ring-offset-orange-300 focus-visible:ring-offset-2 focus-visible:border-indigo-500 sm:text-sm border border-ch-gray-300 px-2 text-sm">
<span className="block truncate">{value}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-1 pointer-events-none">
<SelectorIcon
className="w-5 h-5 text-gray-300"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute w-full py-1 mt-1 bg-ch-gray-600 bg-opacity-80 overflow-auto text-base rounded-sm shadow-lg max-h-60 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{param.options.map((option, optionIdx) => (
<Listbox.Option
key={optionIdx}
className={({ active }) =>
`${
active
? 'text-ch-blue-400 bg-ch-gray-700'
: 'text-ch-gray-300'
}
cursor-default select-none relative py-2 pl-10 pr-4`
}
value={option.value}
>
{({ selected, active }) => (
<>
<span
className={`${
selected ? 'font-medium' : 'font-normal'
} block truncate`}
>
{option.name}
</span>
{selected ? (
<span
className={`${
active ? 'text-ch-blue-400' : 'text-ch-gray-300'
}
absolute inset-y-0 left-0 flex items-center pl-3`}
>
<CheckIcon className="w-5 h-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</CustomizerParamBase>
)
}
function NumberParam({
param,
value,
@@ -180,10 +286,10 @@ function NumberParam({
}: {
param: CadhubNumberParam
value: any
onChange: Function
onChange: (value: any) => void
}) {
const [isFocused, isFocusedSetter] = React.useState(false)
const [localValue, localValueSetter] = React.useState(0)
const [localValue, localValueSetter] = React.useState(value)
const [isLocked, isLockedSetter] = React.useState(false)
const [pixelsDragged, pixelsDraggedSetter] = React.useState(0)
const step = param.step || 1
@@ -198,7 +304,7 @@ function NumberParam({
if (typeof param.max === 'number') {
num = Math.min(param.max, num)
}
num = Number(num.toFixed(2))
num = Number((num || 0).toFixed(2))
localValueSetter(num)
onChange(num)
}

View File

@@ -1,24 +1,45 @@
// CadHub
type CadhubTypeNames = 'number' | 'string' | 'boolean'
type CadhubInputNames =
| 'default-number'
| 'default-string'
| 'default-boolean'
| 'choice-string'
| 'choice-number'
export interface CadhubStringOption {
name: string
value: string
}
export interface CadhubNumberOption {
name: string
value: number
}
interface CadhubParamBase {
type: CadhubTypeNames
caption: string
name: string
input: CadhubInputNames
}
export interface CadhubStringParam extends CadhubParamBase {
type: 'string'
input: 'default-string'
initial: string
placeholder?: string
maxLength?: number
}
export interface CadhubBooleanParam extends CadhubParamBase {
type: 'boolean'
input: 'default-boolean'
initial?: boolean
}
export interface CadhubNumberParam extends CadhubParamBase {
type: 'number'
input: 'default-number'
initial: number
min?: number
max?: number
@@ -26,95 +47,22 @@ export interface CadhubNumberParam extends CadhubParamBase {
decimal?: number
}
export interface CadhubStringChoiceParam extends CadhubParamBase {
type: 'string'
input: 'choice-string'
initial: string
options: Array<CadhubStringOption>
}
export interface CadhubNumberChoiceParam extends CadhubParamBase {
type: 'number'
input: 'choice-number'
initial: number
options: Array<CadhubNumberOption>
}
export type CadhubParams =
| CadhubStringParam
| CadhubBooleanParam
| CadhubNumberParam
// OpenSCAD
const openscadValues = `
// slider widget for number with max. value
sliderWithMax =34; // [50]
// slider widget for number in range
sliderWithRange =34; // [10:100]
//step slider for number
stepSlider=2; //[0:5:100]
// slider widget for number in range
sliderCentered =0; // [-10:0.1:10]
// spinbox with step size 1
Spinbox= 5;
// Text box for string
String="hello";
// Text box for string with length 8
String2="length"; //8
//description
Variable = true;
`
const openscadConverted: CadhubParams[] = [
{
type: 'number',
name: 'sliderWithMax',
caption: 'slider widget for number with max. value',
initial: 34,
step: 1,
max: 50,
},
{
type: 'number',
name: 'sliderWithRange',
caption: 'slider widget for number in range',
initial: 34,
step: 1,
min: 10,
max: 100,
},
{
type: 'number',
name: 'stepSlider',
caption: 'step slider for number',
initial: 2,
step: 5,
min: 0,
max: 100,
},
{
type: 'number',
name: 'sliderCentered',
caption: 'slider widget for number in range',
initial: 0,
step: 0.1,
min: -10,
max: 10,
},
{
type: 'number',
name: 'Spinbox',
caption: 'spinbox with step size 1',
initial: 5,
step: 1,
},
{
type: 'string',
name: 'String',
caption: 'Text box for string',
initial: 'hello',
},
{
type: 'string',
name: 'String2',
caption: 'Text box for string with length 8',
initial: 'length',
maxLength: 8,
},
{ type: 'boolean', name: 'Variable', caption: 'description', initial: true },
]
| CadhubStringChoiceParam
| CadhubNumberChoiceParam

View File

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

View File

@@ -24,7 +24,7 @@ const EditableProjectTitle = ({
const [newTitle, setNewTitle] = useState(projectTitle)
const inputRef = React.useRef(null)
const { updateProject, loading, error } = useUpdateProject({
const { updateProject } = useUpdateProject({
onCompleted: ({ updateProject }) => {
const routeVars = {
userName: updateProject.user.userName,
@@ -42,18 +42,25 @@ const EditableProjectTitle = ({
}
setNewTitle(target.value.replace(/([^a-zA-Z\d_:])/g, '-').slice(0, 25))
}
const onKeyDown = (event) => {
if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
updateProject({ variables: { id, input: { title: newTitle } } });
}
}
return (
<>
{!inEditMode && (
<>
<Link
/<Link
className="underline-hovered"
to={routes.project({
userName,
projectTitle,
})}
className="pl-4"
>
/{projectTitle}
{projectTitle}
</Link>
{canEdit && (
<button
@@ -76,6 +83,7 @@ const EditableProjectTitle = ({
value={newTitle}
onChange={onTitleChange}
ref={inputRef}
onKeyDown={onKeyDown}
onBlur={() =>
setTimeout(() => {
setInEditMode(false)

View File

@@ -0,0 +1,53 @@
import { useMarkdownMetaData } from 'src/helpers/hooks/useMarkdownMetaData'
import Editor from 'rich-markdown-editor'
import { useRef } from 'react'
import KeyValue from 'src/components/KeyValue/KeyValue'
export default function EditorGuide({ content }) {
const [rawMetadata, metadata] = useMarkdownMetaData(content)
const processedContent = rawMetadata
? content.replace(rawMetadata[0], '')
: content
const ref = useRef(null)
return (
<div className="markdown-overrides py-6 px-8">
{metadata && (
<>
<h1 className="my-4">{metadata.title}</h1>
<section className="grid grid-cols-3 my-6 gap-y-4">
{Object.entries(metadata)
.filter(([key]) => key !== 'title')
.map(([key, value], i) => (
<KeyValue keyName={key.replace(/"/g, '')} key={key + '-' + i}>
<LinkOrParagraph>{value}</LinkOrParagraph>
</KeyValue>
))}
</section>
</>
)}
<Editor
ref={ref}
readOnly={true}
defaultValue={processedContent}
value={processedContent}
onChange={() => {}}
/>
</div>
)
}
function LinkOrParagraph({ children }) {
const markdownUrlExpression =
/\[(.*)\]\((https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})\)/i
const matches = children.match(markdownUrlExpression)
return matches === null ? (
<p>{children}</p>
) : (
<a href={matches[2]} rel="noopener noreferrer" target="_blank">
{matches[1]}
</a>
)
}

View File

@@ -0,0 +1,68 @@
import { createContext, useContext } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import Dialog from '@material-ui/core/Dialog'
import { editorMenuConfig } from './menuConfig'
const useStyles = makeStyles({
root: {
transform: `translate3d(0,0,50px)`,
},
})
interface ShortcutsModalContextType {
open: boolean
toggleOpen: () => any
}
export const ShortcutsModalContext = createContext<ShortcutsModalContextType>({
open: false,
toggleOpen: () => {},
})
export function useShortcutsModalContext() {
return useContext(ShortcutsModalContext)
}
const AllShortcutsModal = () => {
const classes = useStyles()
const { open, toggleOpen } = useShortcutsModalContext()
return (
<>
<Dialog
open={open}
onClose={() => toggleOpen()}
className={classes.root + ' bg-transparent'}
PaperProps={{
style: {
backgroundColor: 'transparent',
},
}}
>
<div className="bg-ch-gray-700 font-fira-sans shadow-lg text-ch-gray-300 p-4">
<h2 className="text-2xl mb-4">All Shortcuts</h2>
{editorMenuConfig
.filter((menu) => menu.items.length)
.map((menu) => (
<section key={'allshortcuts-' + menu.name} className="my-6">
<h3 className="text-xl border-b-2 pb-2 mb-2">{menu.label}</h3>
{menu.items.map((item) => (
<div
className="flex gap-16 justify-between"
key={'allshortcuts-' + menu.name + '-' + item.label}
>
<p>{item.label}</p>
<span className="text-right font-fira-code text-ch-gray-400">
{item.shortcutLabel}
</span>
</div>
))}
</section>
))}
</div>
</Dialog>
</>
)
}
export default AllShortcutsModal

View File

@@ -0,0 +1,71 @@
import { Menu } from '@headlessui/react'
import { useHotkeys } from 'react-hotkeys-hook'
export function DropdownItem({ config, state, thunkDispatch }) {
useHotkeys(config.shortcut, handleClick)
function handleClick(e) {
e.preventDefault()
config.callback(e, { state, thunkDispatch })
}
return (
<Menu.Item>
{({ active }) => (
<button
className={`${
active && 'bg-gray-600'
} px-2 py-1 flex justify-between`}
onClick={handleClick}
>
{config.label}
{config.shortcutLabel && (
<span className="text-gray-400 pl-6 text-right">
{config.shortcutLabel}
</span>
)}
</button>
)}
</Menu.Item>
)
}
export function Dropdown({
label,
disabled,
children,
}: {
label: string
disabled: boolean
children: React.ReactNode
}) {
return (
<div className="relative">
<Menu>
{({ open }) => (
<>
<Menu.Button
className={
'text-gray-100' +
(disabled ? ' text-gray-400 cursor-not-allowed' : '')
}
disabled={disabled}
>
{label}
</Menu.Button>
{children && (
<Menu.Items
static
className={
(open ? '' : 'hidden ') +
'absolute flex flex-col mt-4 bg-ch-gray-760 rounded text-gray-100 overflow-hidden whitespace-nowrap border border-ch-gray-700'
}
>
{children}
</Menu.Items>
)}
</>
)}
</Menu>
</div>
)
}

View File

@@ -1,133 +1,75 @@
import { Menu } from '@headlessui/react'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import Svg from 'src/components/Svg/Svg'
import { useRender } from 'src/components/IdeWrapper/useRender'
import { makeStlDownloadHandler, PullTitleFromFirstLine } from './helpers'
import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode'
import CadPackage from 'src/components/CadPackage/CadPackage'
import { editorMenuConfig } from './menuConfig'
import AllShortcutsModal from './AllShortcutsModal'
import { Dropdown } from './Dropdowns'
const EditorMenu = () => {
const handleRender = useRender()
const saveCode = useSaveCode()
const { state, thunkDispatch } = useIdeContext()
const onRender = () => {
handleRender()
saveCode({ code: state.code })
}
const handleStlDownload = makeStlDownloadHandler({
type: state.objectData?.type,
ideType: state.ideType,
geometry: state.objectData?.data,
quality: state.objectData?.quality,
fileName: PullTitleFromFirstLine(state.code || ''),
thunkDispatch,
})
return (
<>
<div className="flex justify-between bg-ch-gray-760 text-gray-100">
<div className="flex items-center h-9 w-full cursor-grab">
<div className=" text-ch-gray-760 bg-ch-gray-300 cursor-grab px-2 h-full flex items-center">
<Svg name="drag-grid" className="w-4 p-px" />
</div>
<div className="grid grid-flow-col-dense gap-6 px-5">
<FileDropdown
handleRender={onRender}
handleStlDownload={handleStlDownload}
/>
<button className="cursor-not-allowed" disabled>
Edit
</button>
<ViewDropdown
handleLayoutReset={() => thunkDispatch({ type: 'resetLayout' })}
{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 cursor-not-allowed"
className="text-ch-gray-300 h-full"
aria-label="editor settings"
disabled
onClick={() =>
thunkDispatch((dispatch) =>
dispatch({
type: 'settingsButtonClicked',
payload: ['Settings', 'editor'],
})
)
}
>
<Svg name="gear" className="w-6 p-px" />
</button>
</div>
<CadPackage cadPackage={state.ideType} className="px-3" />
<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>
<AllShortcutsModal />
</>
)
}
export default EditorMenu
function FileDropdown({ handleRender, handleStlDownload }) {
return (
<Dropdown name="File">
<Menu.Item>
{({ active }) => (
<button
className={`${active && 'bg-gray-600'} px-2 py-1`}
onClick={handleRender}
>
Save &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

@@ -0,0 +1,121 @@
import React from 'react'
import { useRender } from 'src/components/IdeWrapper/useRender'
import { makeStlDownloadHandler, PullTitleFromFirstLine } from 'src/helpers/download_stl'
import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode'
import { DropdownItem } from './Dropdowns'
import { useShortcutsModalContext } from './AllShortcutsModal'
import type { State } from 'src/helpers/hooks/useIdeState'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
export function cmdOrCtrl() {
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? '⌘' : 'Ctrl'
}
const fileMenuConfig: EditorMenuConfig = {
name: 'file',
label: 'File',
disabled: false,
items: [
{
label: 'Save & Render',
shortcut: 'ctrl+s, command+s',
shortcutLabel: cmdOrCtrl() + ' S',
Component: (props) => {
const { state, config } = props
const handleRender = useRender()
const saveCode = useSaveCode()
function onRender(e) {
e.preventDefault()
handleRender()
saveCode({ code: state.code })
}
config.callback = onRender
return <DropdownItem {...props} />
},
},
{
label: 'Download STL',
shortcut: 'ctrl+shift+d, command+shift+d',
shortcutLabel: cmdOrCtrl() + ' Shift D',
Component: (props) => {
const { state, thunkDispatch, config } = props
const { project } = useIdeContext()
const handleStlDownload = makeStlDownloadHandler({
type: state.objectData?.type,
ideType: state.ideType,
geometry: state.objectData?.data,
quality: state.objectData?.quality,
fileName: project
? `${project.title}.stl`
: PullTitleFromFirstLine(state.code || ''),
thunkDispatch,
})
config.callback = handleStlDownload
return <DropdownItem {...props} />
},
},
],
}
const editMenuConfig: EditorMenuConfig = {
name: 'edit',
label: 'Edit',
disabled: true,
items: [],
}
const viewMenuConfig: EditorMenuConfig = {
name: 'view',
label: 'View',
disabled: false,
items: [
{
label: 'Reset layout',
shortcut: 'ctrl+shift+r',
shortcutLabel: 'Ctrl Shift R',
Component: (props) => {
const { config, thunkDispatch } = props
config.callback = () => thunkDispatch({ type: 'resetLayout' })
return <DropdownItem {...props} />
},
},
{
label: 'All shortcuts',
shortcut: 'ctrl+shift+/',
shortcutLabel: 'Ctrl Shift /',
Component: (props) => {
const { config } = props
const { toggleOpen } = useShortcutsModalContext()
config.callback = toggleOpen
return <DropdownItem {...props} />
},
},
],
}
export const editorMenuConfig = [fileMenuConfig, editMenuConfig, viewMenuConfig]
interface EditorMenuItemConfigBase {
label: string
shortcut: string
shortcutLabel: React.ReactElement | string
callback?: (...a: any[]) => void
}
export interface EditorMenuItemConfig extends EditorMenuItemConfigBase {
Component: React.FC<{
config: EditorMenuItemConfigBase
state: State
thunkDispatch: any
}>
}
export interface EditorMenuConfig {
name: string
label: string
disabled: boolean
items: Array<EditorMenuItemConfig>
}

View File

@@ -0,0 +1,31 @@
import Seo from 'src/components/Seo/Seo'
import IdeViewer from 'src/components/IdeViewer/IdeViewer'
import { useIdeState } from 'src/helpers/hooks/useIdeState'
import type { Project } from 'src/components/EmbedProjectCell/EmbedProjectCell'
import { IdeContext } from 'src/helpers/hooks/useIdeContext'
import { use3dViewerResize } from 'src/helpers/hooks/use3dViewerResize'
import { useEffect } from 'react'
interface Props {
project?: Project
}
const EmbedProject = ({ project }: Props) => {
const [state, thunkDispatch] = useIdeState()
const { viewerDomRef, handleViewerSizeUpdate } = use3dViewerResize()
useEffect(() => {
handleViewerSizeUpdate()
}, [])
return (
<div className="flex flex-col h-screen" ref={viewerDomRef} >
<IdeContext.Provider value={{ state, thunkDispatch, project }}>
<IdeViewer />
</IdeContext.Provider>
</div>
)
}
export default EmbedProject

View File

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

View File

@@ -0,0 +1,16 @@
import { Loading, Empty, Success } from './EmbedProjectCell'
import { standard } from './EmbedProjectCell.mock'
export const loading = () => {
return Loading ? <Loading /> : null
}
export const empty = () => {
return Empty ? <Empty /> : null
}
export const success = () => {
return Success ? <Success {...standard()} /> : null
}
export default { title: 'Cells/IdeProjectCell' }

View File

@@ -0,0 +1,21 @@
import { render, screen } from '@redwoodjs/testing'
import { Loading, Empty, Success } from './EmbedProjectCell'
import { standard } from './EmbedProjectCell.mock'
describe('IdeProjectCell', () => {
test('Loading renders successfully', () => {
render(<Loading />)
// Use screen.debug() to see output
expect(screen.getByText('Loading...')).toBeInTheDocument()
})
test('Empty renders successfully', async () => {
render(<Empty />)
expect(screen.getByText('Empty')).toBeInTheDocument()
})
test('Success renders successfully', async () => {
render(<Success ideProject={standard().ideProject} />)
expect(screen.getByText(/42/i)).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,47 @@
import { useIdeState } from 'src/helpers/hooks/useIdeState'
import { IdeContext } from 'src/helpers/hooks/useIdeContext'
import EmbedViewer from '../EmbedViewer/EmbedViewer'
import { QUERY as IdeQuery } from 'src/components/IdeProjectCell'
export const QUERY = IdeQuery
export interface Project {
id: string
title: string
code: string
description: string
mainImage: string
createdAt: string
cadPackage: 'openscad' | 'cadquery'
user: {
id: string
userName: string
image: string
}
}
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>Project not found</div>
interface SaveCodeArgs {
input: any
id: string
isFork: boolean
}
export const Success = ({
project,
refetch,
}: {
project: Project
refetch: any
}) => {
const [state, thunkDispatch] = useIdeState()
return (
<IdeContext.Provider value={{ state, thunkDispatch, project }}>
<EmbedViewer project={project} />
</IdeContext.Provider>
)
}

View File

@@ -0,0 +1,34 @@
import { useIdeInit } from 'src/components/EncodedUrl/helpers'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import IdeViewer from 'src/components/IdeViewer/IdeViewer'
import { use3dViewerResize } from 'src/helpers/hooks/use3dViewerResize'
import CadPackage from '../CadPackage/CadPackage'
import LogoType from '../LogoType/LogoType'
import { Link, routes } from '@redwoodjs/router'
function EmbedViewer() {
const { state, project } = useIdeContext()
useIdeInit(project?.cadPackage, project?.code || state?.code, "viewer")
const { viewerDomRef } = use3dViewerResize()
return (
<div className="relative flex flex-col h-screen group" ref={viewerDomRef}>
<IdeViewer isMinimal={true} />
<div className="absolute top-5 left-5 text-ch-gray-300">
<h1 className="mb-4 text-4xl font-normal capitalize ">
{project?.title.replace(/-/g, ' ')}
</h1>
<h2 className="mb-2 transition-opacity duration-100 group-hover:opacity-0">by @{ project?.user?.userName }</h2>
<h2 className="transition-opacity duration-100 group-hover:opacity-0">built with <div className="inline-block"><CadPackage cadPackage={project?.cadPackage} className="px-3 py-2"/></div></h2>
</div>
<div className="absolute grid items-center grid-flow-col-dense gap-2 bottom-5 right-5 text-ch-gray-300">
View on <Link className="inline-block" to={routes.project({
userName: project?.user?.userName,
projectTitle: project?.title.toString(),
})}><LogoType className="inline-block" wrappedInLink={true}/></Link>
</div>
</div>
)
}
export default EmbedViewer

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { useRender } from 'src/components/IdeWrapper/useRender'
import { encode, decode } from 'src/helpers/compress'
import { isBrowser } from '@redwoodjs/prerender/browserUtils'
import type { State } from 'src/helpers/hooks/useIdeState'
const scriptKey = 'encoded_script'
const scriptKeyV2 = 'encoded_script_v2'
@@ -32,13 +33,17 @@ export function makeExternalUrl(resourceUrl: string): string {
}#${fetchText}=${prepareDecodedUrl(resourceUrl)}`
}
export function useIdeInit(cadPackage: string, code = '') {
export function useIdeInit(
cadPackage: State['ideType'],
code = '',
viewerContext: State['viewerContext'] = 'ide'
) {
const { thunkDispatch } = useIdeContext()
const handleRender = useRender()
useEffect(() => {
thunkDispatch({
type: 'initIde',
payload: { cadPackage, code },
payload: { cadPackage, code, viewerContext },
})
if (code) {
return

View File

@@ -2,7 +2,34 @@ import { FatalErrorBoundary as FatalErrorBoundaryBase } from '@redwoodjs/web'
import * as Sentry from '@sentry/browser'
class FatalErrorBoundary extends FatalErrorBoundaryBase {
componentDidCatch(error, errorInfo) {
async componentDidCatch(error, errorInfo) {
// debug netlify prerender code below
// const div = document.createElement('div')
// div.innerHTML = JSON.stringify(error)
// document.body.append(div)
/* More debug explanation.
If there's an error in netlify's prerendering service,
we don't have access to the log so we have to spin it up locally to check.
This can be with the following commands
```
$ git clone https://github.com/netlify/prerender.git
$ cd prerender
```
comment out the lines `server.use(require("./lib/plugins/basicAuth"));` and `server.use(require("./lib/plugins/s3HtmlCache"));` in `server.js`
then
```
$ npm install
$ npm start
```
This will spin up the service on port 3000, prerendering can than be tested with
http://localhost:3000/https://cadhub.xyz
or
http://localhost:3000/http://localhost:8910/
where the second url is the route you want to test.
However we don't have access to the console since it's run by a separate chrome instance,
so instead errors are put into the DOM
*/
Sentry.withScope((scope) => {
scope.setExtras(errorInfo)
Sentry.captureException(error)

View File

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

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 mt-20 text-sm">
<div className="bg-indigo-900 text-indigo-200 font-roboto text-sm">
<div className="flex h-16 md:justify-end items-center mx-2 md:mx-16 flex-wrap">
<OutBound className="mr-8" to="https://github.com/Irev-Dev/cadhub">
Github

View File

@@ -0,0 +1,111 @@
import React, { useRef, useMemo } from 'react'
import * as THREE from 'three'
import { useLoader, useThree, useFrame } from '@react-three/fiber'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { useEdgeSplit } from 'src/helpers/hooks/useEdgeSplit'
import texture from 'src/components/IdeViewer/dullFrontLitMetal.png'
import { MeshDistortMaterial, Sphere, useTexture } from '@react-three/drei'
const thresholdAngle = 10
export default function AssetWithGooey({
assetUrl,
scale,
}: {
assetUrl: string
scale: number
}) {
const geo = useLoader(STLLoader, assetUrl)
const edgeRef = useRef(null)
const coffeeRef = useRef(null)
const mesh = useEdgeSplit((thresholdAngle * Math.PI) / 180, true, geo)
const colorMap = useTexture(texture)
const edges = React.useMemo(() => new THREE.EdgesGeometry(geo, 12), [geo])
const position = [0, 0, 5]
const scaleArr = Array.from({ length: 3 }).map(() => scale)
const { mouse } = useThree()
const [rEuler, rQuaternion] = useMemo(
() => [new THREE.Euler(), new THREE.Quaternion()],
[]
)
useFrame((state, delta) => {
if (edgeRef.current) {
edgeRef.current.rotation.y += 0.01
}
if (coffeeRef.current) {
rEuler.set((-mouse.y * Math.PI) / 4, (mouse.x * Math.PI) / 2, 0)
coffeeRef.current.quaternion.slerp(rQuaternion.setFromEuler(rEuler), 0.1)
}
})
return (
<group dispose={null} ref={edgeRef} position={position}>
<group ref={coffeeRef}>
<mesh ref={mesh} scale={scaleArr} geometry={geo}>
<meshPhysicalMaterial
color="#FF6EBD"
map={colorMap}
clearcoat={0.5}
clearcoatRoughness={0.01}
roughness={0}
metalness={0.7}
smoothShading
/>
</mesh>
<lineSegments scale={scale} geometry={edges} renderOrder={100}>
<lineBasicMaterial color="#aaaaff" />
</lineSegments>
</group>
<ambientLight intensity={2} />
<Gooey />
<ambientLight intensity={1.8} />
</group>
)
}
function randomSign(num: number): number {
return Math.random() > 0.5 ? num : -num
}
function Gooey() {
const blobsData = useMemo(() => {
const firstSet = Array.from({ length: 5 }).map((_, index) => {
const dist = Math.random() * 3 + 2.5
const x = randomSign(Math.random() * dist)
const y = randomSign(Math.sqrt(dist * dist - x * x))
const z = randomSign(Math.random() * 2)
const position: [number, number, number] = [x, z, y]
const size = Math.random() * 0.8 + 0.1
const distort = size > 0.1 ? Math.random() * 0.6 * size + 0.2 : 0
const speed = size > 0.1 ? (Math.random() * 0.8) / size / size + 0.1 : 0
return { position, size, distort, speed }
})
const secondSet = Array.from({ length: 5 }).map((_, index) => {
const dist = Math.random() * 3 + 1.5
const x = randomSign(Math.random() * dist)
const y = randomSign(Math.sqrt(dist * dist - x * x))
const z = randomSign(Math.random() * 2)
const position: [number, number, number] = [x, z, y]
const size = Math.random() * 0.2 + 0.05
const distort = size > 0.1 ? Math.random() * 0.8 * size + 0.2 : 0
const speed = size > 0.1 ? (Math.random() * 0.5) / size / size + 0.1 : 0
return { position, size, distort, speed }
})
return [...firstSet, ...secondSet]
}, [])
return (
<>
{blobsData.map(({ position, size, distort, speed }, index) => (
<Sphere key={index} visible position={position} args={[size, 16, 200]}>
<MeshDistortMaterial
color="#173E6F"
attach="material"
distort={distort} // Strength, 0 disables the effect (default=1)
speed={speed} // Speed (default=1)
roughness={0.2}
opacity={0.6}
transparent
/>
</Sphere>
))}
</>
)
}

View File

@@ -0,0 +1,613 @@
import { Canvas, useLoader, useFrame } from '@react-three/fiber'
import { Suspense } from 'react'
import { Html, Stats } from '@react-three/drei'
import CadPackage, {
CadPackageType,
} from 'src/components/CadPackage/CadPackage'
import { navigate, routes, Link } from '@redwoodjs/router'
import { useInView } from 'react-intersection-observer'
import Svg, { SvgNames } from 'src/components/Svg/Svg'
import Gravatar from 'src/components/Gravatar/Gravatar'
import ProjectsCell from 'src/components/ProjectsCell'
import OutBound from 'src/components/OutBound/OutBound'
import { DynamicProjectButton } from 'src/components/NavPlusButton/NavPlusButton'
import FatalErrorBoundary from 'src/components/FatalErrorBoundary/FatalErrorBoundary'
// dynamic import to enable pre-render iof the homepage
const AssetWithGooey = React.lazy(
() => import('src/components/Hero/AssetWithGooey')
)
const cqCode = `module beam(r1, r2, shr, msr){
/* The walking beam acts as a class I lever transferring the
* movement from the pitmans arms to the horse head. */
H = 12; // Height
W = 10; // Width
e = 10; // Total added extension
difference(){
union(){
translate([(r2-r1)/2,0,H/2]) // Walking beam body
cube([r1+r2+e, W, H], center = true);
rotate([90, 0, 0]) // Fulcrum or pivoting point
cylinder(r = 2*msr, h = W, center = true);
}
rotate([90,0,0]) // Pivoting point hole
cylinder(r = msr, h = W+1, center = true);
translate([r2,0,H/2]) // Equalizer mounting screw hole
cylinder(r = shr, h = H+1, center = true);
`.split('\n')
const scadCode = `hingeHalfExtrudeLength=hingeLength/2-clearance/2;
mountingHoleMoveIncrement=(hingeLength-2*mountingHoleEdgeOffset)/
(mountingHoleCount-1);
module costomizerEnd() {}
$fn=30;
tiny=0.005;
// modules
module hingeBaseProfile() {
translate([pivotRadius,0,0]){
square([baseWidth,baseThickness]);
}
}
module hingeBodyHalf() {
difference() {
union() {
linear_extrude(hingeHalfExtrudeLength){
offset(1)offset(-2)offset(1){
translate([0,pivotRadius,0]){
circle(pivotRadius);
}
square([pivotRadius,pivotRadius]);
hingeBaseProfile();
}
}
linear_extrude(hingeLength){
offset(1)offset(-1)hingeBaseProfile();
}
}
plateHoles();
}
}`.split('\n')
export const Hero = () => {
return (
<div className="bg-ch-gray-800">
<div className="relative h-0 w-0">
<svg
viewBox="0 0 100 100"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<clipPath
id="code-blob-clip-path"
clipPathUnits="objectBoundingBox"
transform="scale(0.0038 0.0056)"
>
<path
d="M68.5 169.159C13.3 167.159 5.69181e-05 144.659 0 71.1594C3.99994 13.1594 50.9244 -14.591 121.5 7.65941C223 39.6594 266 25.1594 263.5 113.659C261.634 179.701 191.5 173.616 68.5 169.159Z"
fill="#C4C4C4"
/>
</clipPath>
</defs>
</svg>
</div>
<div className="grid lg:grid-cols-5 max-w-8xl mx-auto">
<div className="relative row-start-2 col-start-1 h-full lg:row-start-1 lg:col-span-3 lg:col-start-1 z-10">
<div
className="absolute inset-0 my-20 mx-10 lg:mr-40 bg-gradient-to-tr from-pink-400 to-blue-600 opacity-40 overflow-hidden"
style={{ clipPath: 'url(#code-blob-clip-path)' }}
>
<pre className="lg:ml-20 mt-12 text-blue-100 font-fira-code">
{cqCode.map((line, index) => (
<div key={index}>
<span className="w-12 pr-6 text-blue-200 text-opacity-50 inline-block text-right">
{index + 1}
</span>
{line}
</div>
))}
</pre>
</div>
<ModelSection assetUrl="/pumpjack.stl" scale={0.04} />
</div>
<div className="flex items-end justify-center row-start-2 col-start-1 pt-96 pr-12 pl-6 pb-24 lg:col-span-3 lg:col-start-1 lg:row-start-1 lg:pt-0 pointer-events-none">
<Link
to={routes.project({
userName: 'matiasmiche',
projectTitle: 'oil-pumpjack',
})}
>
<div
className="grid grid-flow-col gap-2 sm:gap-4 items-center bg-ch-gray-760 bg-opacity-95 text-ch-gray-300 rounded-md p-2 font-fira-sans relative z-10 shadow-ch pointer-events-auto"
style={{
transform: 'translate3d(3vw, -100px, 0.3px) scale(0.7)',
transformOrigin: 'top center',
}}
>
<div className="pl-1 sm:pl-4">
<Gravatar
image="CadHub/jjze0hyqncxvkvsg4agz"
className="w-12 h-12 mr-4"
size={60}
/>
</div>
<div>
<div className="text-xl sm:text-3xl">Oil Pumpjack</div>
<div>matiasmiche</div>
</div>
<div className="flex self-start">
<CadPackage
cadPackage="openscad"
className="px-3 py-1 sm:text-xl rounded transform translate-x-4 sm:translate-x-10"
/>
</div>
</div>
</Link>
</div>
<div className="col-start-1 px-4 py-32 lg:col-start-3 lg:row-start-1 lg:col-span-3 lg:pl-52">
<div>
<span
className="text-7xl text-ch-blue-400 bg-ch-blue-640 bg-opacity-30 font-fira-code px-6 rounded-2xl shadow-ch"
style={{
boxShadow: 'inset 0 4px 4px 0 rgba(255,255,255, 0.06)',
}}
>
Code
</span>
</div>
<div className="text-6xl font-fira-sans mt-8 text-ch-gray-300">
is the future of CAD
</div>
<div className="text-2xl text-gray-600 mt-8 max-w-4xl">
Designs backed by reliable, easy-to-write code open a world of new
workflows and collaboration. We're building a place where you can
build that future.
</div>
<OutlineButton
color="pink"
isLeft
svgName="terminal"
onClick={() =>
navigate(routes.draftProject({ cadPackage: 'openscad' }))
}
>
Start Hacking
</OutlineButton>
</div>
</div>
<ChooseYourCharacter />
<Community />
<div className="max-w-8xl mx-auto grid lg:grid-cols-5 py-16">
<div className="row-start-2 col-start-1 lg:col-span-3 lg:col-start-3 lg:row-start-1 lg:-mx-10 h-full relative z-10">
<div
className="absolute inset-0 mb-24 mt-16 ml:10 mr:10 lg:ml-40 lg:mr-52 bg-gradient-to-tr from-pink-400 to-blue-600 opacity-30 overflow-hidden"
style={{ clipPath: 'url(#code-blob-clip-path)' }}
>
<pre className="ml-10 mt-12 text-blue-100 text-xs font-fira-code">
{scadCode.map((line, index) => (
<div key={index}>
<span className="w-12 pr-6 text-blue-200 text-opacity-50 inline-block text-right">
{index + 1}
</span>
{line}
</div>
))}
</pre>
</div>
<ModelSection assetUrl="/hinge.stl" scale={0.12} />
</div>
<div className="py-12 pb-32 ml-4 row-start-1 col-start-1 pr-12 pl-6 lg:py-32 lg:col-start-1 lg:col-span-3">
<div className="text-4xl mb-6 text-ch-gray-300">Learn Code-CAD</div>
<p className="text-gray-600 max-w-lg">
We want you to learn Code-CAD today so it can change the way you
work tomorrow. Our community is writing tutorials to make this
powerful paradigm more accessible to people new to code and CAD.
</p>
<OutBound
to="https://learn.cadhub.xyz/docs/definitive-beginners/your-openscad-journey"
className=""
>
<OutlineButton color="pink" isLeft svgName="terminal">
Get Started with OpenSCAD
</OutlineButton>
</OutBound>
</div>
<div className="flex items-end justify-center row-start-2 col-start-1 pb-24 lg:row-start-1 lg:col-start-3 lg:col-span-3 pt-96 lg:pt-0 lg:pr-10 pointer-events-none">
<Link
to={routes.project({
userName: 'irevdev',
projectTitle: 'tutorial-hinge',
})}
>
<div
className="grid grid-flow-col sm:gap-2 items-center bg-ch-gray-760 bg-opacity-95 text-ch-gray-300 rounded-md py-2 pl-2 font-fira-sans relative z-10 shadow-ch pointer-events-auto"
style={{
transform: 'translate3d(-5vw, -100px, 0.3px) scale(0.7)',
transformOrigin: 'top center',
}}
>
<div className="pl-1 sm:pl-4">
<Gravatar
image="CadHub/xvrnxvarkv8tdzo4n65u"
className="w-12 h-12 mr-4"
size={60}
/>
</div>
<div>
<div className="text-lg sm:text-2xl w-28 sm:w-auto">
Print in Place Hinge
</div>
<div>IrevDev</div>
</div>
<div className="flex self-start">
<CadPackage
cadPackage="openscad"
className="px-3 py-1 sm:text-xl rounded transform translate-x-4 sm:translate-x-10"
/>
</div>
</div>
</Link>
</div>
</div>
<Roadmap />
<div className="h-3 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500" />
<Footer />
</div>
)
}
const DisableRender = () => useFrame(() => null, 1000)
function ModelSection({
assetUrl,
scale,
}: {
assetUrl: string
scale: number
}) {
const { ref, inView } = useInView()
return (
<div className="relative h-full">
<FatalErrorBoundary
page={() => (
<div className="bg-gray-800 p-8 rounded-md text-ch-gray-300">
something seams to have gone wrong here
</div>
)}
>
<div className="absolute inset-0" ref={ref}>
<Canvas
linear
dpr={[1, 2]}
orthographic
camera={{ zoom: 75, position: [0, 0, 500] }}
>
{!inView && <DisableRender />}
<pointLight position={[2, 3, 5]} color="#FFFFFF" intensity={2} />
<pointLight position={[2, 3, -5]} color="#FFFFFF" intensity={2} />
<pointLight position={[-6, 3, -5]} color="#FFFFFF" intensity={2} />
<pointLight position={[-6, 3, 5]} color="#FFFFFF" intensity={2} />
<pointLight position={[2, 1.5, 0]} color="#0000FF" intensity={2} />
<pointLight position={[2, 1.5, 0]} color="#FF0000" intensity={2} />
<Suspense
fallback={
<Html center className="loading" children="Loading..." />
}
>
<AssetWithGooey assetUrl={assetUrl} scale={scale} />
</Suspense>
{/* uncomment for framerate and render time */}
{/* <Stats showPanel={0} className="three-debug-panel-1" /> */}
{/* <Stats showPanel={1} className="three-debug-panel-2" /> */}
</Canvas>
</div>
</FatalErrorBoundary>
</div>
)
}
function ChooseYourCharacter() {
return (
<div className="text-ch-gray-300 grid lg:grid-cols-2 gap-12 font-fira-sans py-32 max-w-7xl mx-auto px-4">
<div className="">
<div className="text-4xl mb-6">Choose your character</div>
<p className="text-gray-600 text-2xl">
CadHub is the place you can try out Code-CAD packages to find the one
that's right for you. Our dedicated community is making CAD easy to
learn on the web. Try one of our three integrations today and keep an
eye out for more.
</p>
</div>
<ul className="flex-col flex justify-around items-center lg:items-start text-gray-600">
{[
{
cadPackage: 'openscad',
desc: 'A mature Code-CAD library focused on Constructed Solid Geometry (CSG) modeling with syntax like C++.',
},
{
cadPackage: 'cadquery',
desc: 'A Python-based library with support for CSG and sketch-based modeling and a clean-feeling API.',
},
{
cadPackage: 'jscad',
desc: 'A JavaScript Code-CAD library that will feel familiar to web developers, based on the same tech as OpenSCAD.',
},
{
cadPackage: 'curv',
desc: "Curv is a programming language for creating art using mathematics. It's a 2D and 3D geometric modelling tool.",
},
].map(
({
cadPackage,
desc,
}: {
cadPackage: CadPackageType
desc: string
}) => (
<li key={cadPackage} className="flex items-center">
<div className="mr-4 sm:mr-12">
<DynamicProjectButton ideType={cadPackage} className="">
<CadPackage
cadPackage={cadPackage}
className="px-3 py-1 w-40 text-xl rounded"
/>
</DynamicProjectButton>
</div>
<p className="text-sm my-2 max-w-sm">{desc}</p>
</li>
)
)}
</ul>
</div>
)
}
function Community() {
return (
<div className="max-w-7xl mx-auto py-40">
<div className="text-ch-gray-300 grid lg:grid-cols-2 gap-8 font-fira-sans px-4 mb-6">
<div className="text-4xl">Explore with our community</div>
<p className="text-gray-600 text-sm">
CadHub is a social platform. You can ask users how they designed a
part, fork their work to put your own spin on it, and find inspiration
in abundance.
</p>
</div>
<ProjectsCell shouldFilterProjectsWithoutImage projectLimit={8} />
<div className="flex justify-end pr-4">
<OutlineButton
color="blue"
svgName="arrow-right"
onClick={() => navigate(routes.projects())}
>
See All Projects
</OutlineButton>
</div>
</div>
)
}
function OutlineButton({
color,
svgName,
isLeft = false,
children,
onClick,
}: {
color: 'blue' | 'pink' | 'purple'
svgName: SvgNames
isLeft?: boolean
children: React.ReactNode
onClick?: () => void
}) {
return (
<button
onClick={onClick}
className={`grid grid-flow-col-dense gap-4 items-center border px-4 py-1 rounded mt-6 relative z-10 ${
color === 'pink' && 'border-ch-pink-500'
} ${color === 'blue' && 'border-ch-blue-630'} ${
color === 'purple' && 'border-ch-purple-500'
}`}
>
{isLeft && (
<Svg
name={svgName}
className={`${color === 'pink' && 'text-ch-pink-500'} ${
color === 'blue' && 'text-ch-blue-300'
} ${color === 'purple' && 'text-ch-purple-200'} w-6 h-6`}
/>
)}
<span
className={`text-2xl ${color === 'pink' && 'text-ch-pink-300'} ${
color === 'blue' && 'text-ch-blue-300'
} ${color === 'purple' && 'text-ch-purple-200'}`}
>
{children}
</span>
{!isLeft && (
<Svg
name={svgName}
className={`${color === 'pink' && 'text-ch-pink-500'} ${
color === 'blue' && 'text-ch-blue-300'
} ${color === 'purple' && 'text-ch-purple-200'} w-6 h-6`}
/>
)}
</button>
)
}
function Roadmap() {
const sections = [
{
title: 'Read our roadmap',
desc: 'Version control with GitHub, multi-file projects, and team collaboration tools. 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>
)
}

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