diff --git a/app/web/package.json b/app/web/package.json index 92b9ba2..234eea2 100644 --- a/app/web/package.json +++ b/app/web/package.json @@ -42,6 +42,7 @@ "react-helmet": "^6.1.0", "react-image-crop": "^8.6.6", "react-mosaic-component": "^4.1.1", + "react-tabs": "^3.2.2", "react-three-fiber": "^5.3.19", "rich-markdown-editor": "^11.0.2", "styled-components": "^5.2.0", diff --git a/app/web/src/components/EditorMenu/EditorMenu.tsx b/app/web/src/components/EditorMenu/EditorMenu.tsx new file mode 100644 index 0000000..57d8f6e --- /dev/null +++ b/app/web/src/components/EditorMenu/EditorMenu.tsx @@ -0,0 +1,126 @@ +import { Menu } from '@headlessui/react' + +import { useIdeContext, ideTypeNameMap } from 'src/helpers/hooks/useIdeContext' +import Svg from 'src/components/Svg/Svg' +import { useRender } from 'src/components/IdeWrapper/useRender' +import { makeStlDownloadHandler, PullTitleFromFirstLine } from './helpers' + +const EditorMenu = () => { + const handleRender = useRender() + const { state, thunkDispatch } = useIdeContext() + const handleStlDownload = makeStlDownloadHandler({ + type: state.objectData?.type, + geometry: state.objectData?.data, + fileName: PullTitleFromFirstLine(state.code || ''), + thunkDispatch, + }) + const cadName = ideTypeNameMap[state.ideType] || '' + const isOpenScad = state.ideType === 'openScad' + const isCadQuery = state.ideType === 'cadQuery' + return ( +
+ Paste an external url containing a {cadName} script to generate a new
+ CadHub url for this resource.{' '}
+
Paste url
+ + > + )} + {asyncState === 'ERROR' && ( +That didn't work, try again.
+ )} + {asyncState === 'LOADING' && ( ++ Encodes your CodeCad script into a URL so that you can share your work +
+ + +
,
@@ -13,8 +15,26 @@ const ELEMENT_MAP = {
Console: ,
}
+const TOOLBAR_MAP = {
+ Editor: (
+
+
+
+ ),
+ Viewer: (
+
+
+
+ ),
+ Console: (
+
+
+
+ ),
+}
+
const IdeContainer = () => {
- const { state, thunkDispatch } = useContext(IdeContext)
+ const { state, thunkDispatch } = useIdeContext()
const viewerDOM = useRef(null)
const debounceTimeoutId = useRef
@@ -64,15 +84,7 @@ const IdeContainer = () => {
return (
(
-
- {id}
- {id === 'Editor' && ` (${state.ideType})`}
-
- )}
+ renderToolbar={() => TOOLBAR_MAP[id]}
className={`${id.toLowerCase()} ${id.toLowerCase()}-tile`}
>
{id === 'Viewer' ? (
diff --git a/app/web/src/components/IdeEditor/IdeEditor.js b/app/web/src/components/IdeEditor/IdeEditor.js
index 93932d6..507a13f 100644
--- a/app/web/src/components/IdeEditor/IdeEditor.js
+++ b/app/web/src/components/IdeEditor/IdeEditor.js
@@ -1,5 +1,5 @@
-import { useContext, Suspense, lazy } from 'react'
-import { IdeContext } from 'src/components/IdeToolbarNew'
+import { Suspense, lazy } from 'react'
+import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { makeCodeStoreKey } from 'src/helpers/hooks/useIdeState'
import { requestRender } from 'src/helpers/hooks/useIdeState'
const Editor = lazy(() => import('@monaco-editor/react'))
@@ -7,13 +7,12 @@ const Editor = lazy(() => import('@monaco-editor/react'))
export const matchEditorVsDarkTheme = {
// Some colors to roughly match the vs-dark editor theme
Bg: { backgroundColor: 'rgb(30,30,30)' },
- lighterBg: { backgroundColor: 'rgb(55,55,55)' },
Text: { color: 'rgb(212,212,212)' },
TextBrown: { color: 'rgb(206,144,120)' },
}
const IdeEditor = () => {
- const { state, thunkDispatch } = useContext(IdeContext)
+ const { state, thunkDispatch } = useIdeContext()
const ideTypeToLanguageMap = {
cadQuery: 'python',
openScad: 'cpp',
diff --git a/app/web/src/components/IdeHeader/IdeHeader.tsx b/app/web/src/components/IdeHeader/IdeHeader.tsx
new file mode 100644
index 0000000..5368d31
--- /dev/null
+++ b/app/web/src/components/IdeHeader/IdeHeader.tsx
@@ -0,0 +1,78 @@
+import { Popover } from '@headlessui/react'
+import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'
+import FullScriptEncoding from 'src/components/EncodedUrl/FullScriptEncoding'
+import ExternalScript from 'src/components/EncodedUrl/ExternalScript'
+
+const TopButton = ({
+ onClick,
+ children,
+ className,
+ iconColor,
+}: {
+ onClick?: () => void
+ children: React.ReactNode
+ className?: string
+ iconColor: string
+}) => (
+
+)
+
+const IdeHeader = ({ handleRender }: { handleRender: () => void }) => {
+ return (
+
+
+
+
+ Render
+
+
+
+ {({ open }) => {
+ return (
+ <>
+
+
+ Share
+
+
+ {open && (
+
+
+
+
+
+
+
+
+
+
+ encoded script
+ external script
+
+
+
+ )}
+ >
+ )
+ }}
+
+ {/* Fork */}
+
+
+ )
+}
+
+export default IdeHeader
diff --git a/app/web/src/components/IdeSideBar/IdeSideBar.tsx b/app/web/src/components/IdeSideBar/IdeSideBar.tsx
new file mode 100644
index 0000000..f887a68
--- /dev/null
+++ b/app/web/src/components/IdeSideBar/IdeSideBar.tsx
@@ -0,0 +1,23 @@
+import { Link, routes } from '@redwoodjs/router'
+import Svg from 'src/components/Svg/Svg'
+
+const IdeSideBar = () => {
+ return (
+
+
+
+
+
+
+
+
+ )
+}
+
+export default IdeSideBar
diff --git a/app/web/src/components/IdeToolbarNew/IdeToolbarNew.js b/app/web/src/components/IdeToolbarNew/IdeToolbarNew.js
deleted file mode 100644
index 78faaf6..0000000
--- a/app/web/src/components/IdeToolbarNew/IdeToolbarNew.js
+++ /dev/null
@@ -1,173 +0,0 @@
-import { createContext, useEffect } from 'react'
-import IdeContainer from 'src/components/IdeContainer'
-import { isBrowser } from '@redwoodjs/prerender/browserUtils'
-import { useIdeState, makeCodeStoreKey } from 'src/helpers/hooks/useIdeState'
-import { copyTextToClipboard } from 'src/helpers/clipboard'
-import { requestRender } from 'src/helpers/hooks/useIdeState'
-import { encode, decode } from 'src/helpers/compress'
-import { flow, identity } from 'lodash/fp'
-import { fileSave } from 'browser-fs-access'
-import { MeshBasicMaterial, Mesh, Scene } from 'three'
-import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'
-
-export const githubSafe = (url) =>
- url.includes('github.com')
- ? url
- .replace('github.com', 'raw.githubusercontent.com')
- .replace('/blob/', '/')
- : url
-
-const prepareEncodedUrl = flow(decodeURIComponent, githubSafe)
-
-export const IdeContext = createContext()
-const IdeToolbarNew = ({ cadPackage }) => {
- const [state, thunkDispatch] = useIdeState()
- const scriptKey = 'encoded_script'
- const scriptKeyV2 = 'encoded_script_v2'
- const fetchText = 'fetch_text_v1'
- useEffect(() => {
- thunkDispatch({
- type: 'initIde',
- payload: { cadPackage },
- })
- // load code from hash if it's there
- const triggerRender = () =>
- setTimeout(() => {
- // definitely a little hacky, timeout with no delay is just to push it into the next event loop.
- handleRender()
- })
- let hash
- if (isBrowser) {
- hash = window.location.hash
- }
- const [key, encodedScript] = hash.slice(1).split('=')
- if (key === scriptKey) {
- const script = atob(encodedScript)
- thunkDispatch({ type: 'updateCode', payload: script })
- triggerRender()
- } else if (key === scriptKeyV2) {
- const script = decode(encodedScript)
- thunkDispatch({ type: 'updateCode', payload: script })
- triggerRender()
- } else if (key === fetchText) {
- const url = prepareEncodedUrl(encodedScript)
- fetch(url).then((response) =>
- response.text().then((script) => {
- thunkDispatch({ type: 'updateCode', payload: script })
- triggerRender()
- })
- )
- } else {
- triggerRender()
- }
- window.location.hash = ''
- }, [cadPackage])
- function handleRender() {
- thunkDispatch((dispatch, getState) => {
- const state = getState()
- dispatch({ type: 'setLoading' })
- requestRender({
- state,
- dispatch,
- code: state.code,
- viewerSize: state.viewerSize,
- camera: state.camera,
- })
- })
- localStorage.setItem(makeCodeStoreKey(state.ideType), state.code)
- }
- function handleMakeLink() {
- if (isBrowser) {
- const encodedScript = encode(state.code)
- window.location.hash = `${scriptKeyV2}=${encodedScript}`
- copyTextToClipboard(window.location.href)
- }
- }
- const PullTitleFromFirstLine = (code = '') => {
- const firstLine = code.split('\n').filter(identity)[0] || ''
- if (!(firstLine.startsWith('//') || firstLine.startsWith('#'))) {
- return 'object.stl'
- }
- return (
- (firstLine.replace(/^(\/\/|#)\s*(.+)/, (_, __, titleWithSpaces) =>
- titleWithSpaces.replaceAll(/\s/g, '-')
- ) || 'object') + '.stl'
- )
- }
-
- const handleStlDownload = (({ geometry, fileName, type }) => () => {
- const makeStlBlobFromGeo = flow(
- (geo) => new Mesh(geo, new MeshBasicMaterial()),
- (mesh) => new Scene().add(mesh),
- (scene) => new STLExporter().parse(scene),
- (stl) =>
- new Blob([stl], {
- type: 'text/plain',
- })
- )
- const saveFile = (geometry) => {
- const blob = makeStlBlobFromGeo(geometry)
- fileSave(blob, {
- fileName,
- extensions: ['.stl'],
- })
- }
- if (geometry) {
- if (type === 'geometry') {
- saveFile(geometry)
- } else {
- thunkDispatch((dispatch, getState) => {
- const state = getState()
- if (state.ideType === 'openScad') {
- thunkDispatch((dispatch, getState) => {
- const state = getState()
- dispatch({ type: 'setLoading' })
- requestRender({
- state,
- dispatch,
- code: state.code,
- viewerSize: state.viewerSize,
- camera: state.camera,
- specialCadProcess: 'stl',
- }).then((result) => result && saveFile(result.data))
- })
- }
- })
- }
- }
- })({
- type: state.objectData?.type,
- geometry: state.objectData?.data,
- fileName: PullTitleFromFirstLine(state.code),
- })
-
- return (
-
-
-
-
-
-
- )
-}
-
-export default IdeToolbarNew
diff --git a/app/web/src/components/IdeViewer/IdeViewer.js b/app/web/src/components/IdeViewer/IdeViewer.js
index d3fa40d..8f854e8 100644
--- a/app/web/src/components/IdeViewer/IdeViewer.js
+++ b/app/web/src/components/IdeViewer/IdeViewer.js
@@ -1,5 +1,5 @@
-import { IdeContext } from 'src/components/IdeToolbarNew'
-import { useRef, useState, useEffect, useContext } from 'react'
+import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
+import { useRef, useState, useEffect } from 'react'
import {
Canvas,
extend,
@@ -137,7 +137,7 @@ function Sphere(props) {
)
}
const IdeViewer = () => {
- const { state, thunkDispatch } = useContext(IdeContext)
+ const { state, thunkDispatch } = useIdeContext()
const [isDragging, setIsDragging] = useState(false)
const [image, setImage] = useState()
diff --git a/app/web/src/components/IdeToolbarNew/IdeToolbarNew.test.js b/app/web/src/components/IdeWrapper/IdeWrapper.test.js
similarity index 100%
rename from app/web/src/components/IdeToolbarNew/IdeToolbarNew.test.js
rename to app/web/src/components/IdeWrapper/IdeWrapper.test.js
diff --git a/app/web/src/components/IdeWrapper/IdeWrapper.tsx b/app/web/src/components/IdeWrapper/IdeWrapper.tsx
new file mode 100644
index 0000000..3743c9b
--- /dev/null
+++ b/app/web/src/components/IdeWrapper/IdeWrapper.tsx
@@ -0,0 +1,51 @@
+import { useState } from 'react'
+import IdeContainer from 'src/components/IdeContainer/IdeContainer'
+import { useRender } from './useRender'
+import OutBound from 'src/components/OutBound/OutBound'
+import IdeSideBar from 'src/components/IdeSideBar/IdeSideBar'
+import IdeHeader from 'src/components/IdeHeader/IdeHeader'
+import Svg from 'src/components/Svg/Svg'
+import { useIdeInit } from 'src/components/EncodedUrl/helpers'
+
+const IdeToolbarNew = ({ cadPackage }) => {
+ const [shouldShowConstructionMessage, setShouldShowConstructionMessage] =
+ useState(true)
+ const handleRender = useRender()
+ useIdeInit(cadPackage)
+
+ return (
+
+
+
+
+
+
+ {shouldShowConstructionMessage && (
+
+
+ We're still working on this. Since you're here, have a look what{' '}
+
+ we've got planned
+
+ .
+
+
+
+ )}
+
+
+
+ )
+}
+
+export default IdeToolbarNew
diff --git a/app/web/src/components/IdeWrapper/useRender.ts b/app/web/src/components/IdeWrapper/useRender.ts
new file mode 100644
index 0000000..86ae5a4
--- /dev/null
+++ b/app/web/src/components/IdeWrapper/useRender.ts
@@ -0,0 +1,21 @@
+import { makeCodeStoreKey } from 'src/helpers/hooks/useIdeState'
+import { requestRender } from 'src/helpers/hooks/useIdeState'
+import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
+
+export const useRender = () => {
+ const { state, thunkDispatch } = useIdeContext()
+ return () => {
+ thunkDispatch((dispatch, getState) => {
+ const state = getState()
+ dispatch({ type: 'setLoading' })
+ requestRender({
+ state,
+ dispatch,
+ code: state.code,
+ viewerSize: state.viewerSize,
+ camera: state.camera,
+ })
+ })
+ localStorage.setItem(makeCodeStoreKey(state.ideType), state.code)
+ }
+}
diff --git a/app/web/src/components/ImageUploader/ImageUploader.js b/app/web/src/components/ImageUploader/ImageUploader.js
index b70a38f..d61f67a 100644
--- a/app/web/src/components/ImageUploader/ImageUploader.js
+++ b/app/web/src/components/ImageUploader/ImageUploader.js
@@ -6,7 +6,7 @@ import ReactCrop from 'react-image-crop'
import { Dialog } from '@material-ui/core'
import { Image as CloudinaryImage } from 'cloudinary-react'
import 'react-image-crop/dist/ReactCrop.css'
-import Svg from 'src/components/Svg/Svg.js'
+import Svg from 'src/components/Svg'
const CLOUDINARY_UPLOAD_PRESET = 'CadHub_project_images'
const CLOUDINARY_UPLOAD_URL = 'https://api.cloudinary.com/v1_1/irevdev/upload'
diff --git a/app/web/src/components/PanelToolbar/PanelToolbar.tsx b/app/web/src/components/PanelToolbar/PanelToolbar.tsx
new file mode 100644
index 0000000..1b88675
--- /dev/null
+++ b/app/web/src/components/PanelToolbar/PanelToolbar.tsx
@@ -0,0 +1,25 @@
+import { useContext } from 'react'
+import { MosaicWindowContext } from 'react-mosaic-component'
+import Svg from 'src/components/Svg/Svg'
+
+const PanelToolbar = ({ panelName }: { panelName: string }) => {
+ const { mosaicWindowActions } = useContext(MosaicWindowContext)
+ return (
+
+
+ {mosaicWindowActions.connectDragSource(
+
+
+
+ )}
+
+ )
+}
+
+export default PanelToolbar
diff --git a/app/web/src/components/Svg/Svg.js b/app/web/src/components/Svg/Svg.tsx
similarity index 70%
rename from app/web/src/components/Svg/Svg.js
rename to app/web/src/components/Svg/Svg.tsx
index 47bbb76..c45838a 100644
--- a/app/web/src/components/Svg/Svg.js
+++ b/app/web/src/components/Svg/Svg.tsx
@@ -1,5 +1,40 @@
-const Svg = ({ name, className: className2, strokeWidth = 2 }) => {
- const svgs = {
+type SvgNames =
+ | 'arrow-down'
+ | 'arrow'
+ | 'arrow-left'
+ | 'big-gear'
+ | 'camera'
+ | 'checkmark'
+ | 'chevron-down'
+ | 'dots-vertical'
+ | 'drag-grid'
+ | 'exclamation-circle'
+ | 'favicon'
+ | 'flag'
+ | 'fork'
+ | 'gear'
+ | 'lightbulb'
+ | 'logout'
+ | 'mac-cmd-key'
+ | 'pencil'
+ | 'plus'
+ | 'plus-circle'
+ | 'refresh'
+ | 'save'
+ | 'terminal'
+ | 'trash'
+ | 'x'
+
+const Svg = ({
+ name,
+ className: className2 = '',
+ strokeWidth = 2,
+}: {
+ name: SvgNames
+ className?: string
+ strokeWidth?: number
+}) => {
+ const svgs: { [name in SvgNames]: React.ReactElement } = {
'arrow-down': (
),
- 'camera': (
-