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 ( +
+
+
+ +
+
+ + + thunkDispatch({type: 'resetLayout'})} /> +
+ +
+
+
+
{cadName}
+
+
+ ) +} + +export default EditorMenu + +function FileDropdown({ handleRender, handleStlDownload }) { + return ( + + + {({ active }) => ( + + )} + + + {({ active }) => ( + + )} + + + ) +} + +function ViewDropdown({ handleLayoutReset }) { + return ( + + + {({ active }) => ( + + )} + + + ) +} + +function Dropdown({ name, children }: {name: string, children: React.ReactNode}) { + return ( +
+ + {name} + + {children} + + +
+ ) +} diff --git a/app/web/src/components/EditorMenu/helpers.ts b/app/web/src/components/EditorMenu/helpers.ts new file mode 100644 index 0000000..41bc93c --- /dev/null +++ b/app/web/src/components/EditorMenu/helpers.ts @@ -0,0 +1,61 @@ +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' +import { requestRender } from 'src/helpers/hooks/useIdeState' + +export 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' + ) +} + +export const makeStlDownloadHandler = + ({ geometry, fileName, type, thunkDispatch }) => + () => { + 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)) + }) + } + }) + } + } + } diff --git a/app/web/src/components/EncodedUrl/ExternalScript.tsx b/app/web/src/components/EncodedUrl/ExternalScript.tsx new file mode 100644 index 0000000..0cc04d2 --- /dev/null +++ b/app/web/src/components/EncodedUrl/ExternalScript.tsx @@ -0,0 +1,126 @@ +import { useState } from 'react' +import { useIdeContext, ideTypeNameMap } from 'src/helpers/hooks/useIdeContext' +import OutBound from 'src/components/OutBound/OutBound' +import { prepareEncodedUrl, makeExternalUrl } from './helpers' +import { copyTextToClipboard } from 'src/helpers/clipboard' +import { useRender } from 'src/components/IdeWrapper/useRender' +import { toast } from '@redwoodjs/web/toast' + +const ExternalScript = () => { + const { state, thunkDispatch } = useIdeContext() + const handleRender = useRender() + const [rawUrl, setRawUrl] = useState('') + const [script, setScript] = useState('') + const [asyncState, setAsyncState] = + useState<'INIT' | 'SUCCESS' | 'ERROR' | 'LOADING'>('INIT') + + const cadName = ideTypeNameMap[state.ideType] + + const onPaste: React.ClipboardEventHandler = async ({ + clipboardData, + }) => { + const url = clipboardData.getData('Text') + processUserUrl(url) + } + const onChange: React.ChangeEventHandler = async ({ + target, + }) => setRawUrl(target.value) + const onKeyDown = async ({ key, target }) => + key === 'Enter' && processUserUrl(target.value) + + async function processUserUrl(url: string) { + setRawUrl(url) + try { + setAsyncState('LOADING') + const response = await fetch(prepareEncodedUrl(url)) + if (response.status === 404) throw new Error("couldn't find script") + const script2 = await response.text() + if (script2.startsWith('')) + throw new Error('got html document, not a script') + setScript(script2) + setAsyncState('SUCCESS') + } catch (e) { + setAsyncState('ERROR') + toast.error( + "We had trouble with you're URL, are you sure it was correct?" + ) + } + } + const onCopyRender: React.MouseEventHandler = () => { + copyTextToClipboard(makeExternalUrl(rawUrl)) + thunkDispatch({ type: 'updateCode', payload: script }) + setTimeout(handleRender) + } + return ( +
+

+ Paste an external url containing a {cadName} script to generate a new + CadHub url for this resource.{' '} + + Learn more + {' '} + about this feature. +

+ {['INIT', 'ERROR'].includes(asyncState) && ( + <> +

Paste url

+ + + )} + {asyncState === 'ERROR' && ( +

That didn't work, try again.

+ )} + {asyncState === 'LOADING' && ( +
+
+
+
+
+ )} + {asyncState === 'SUCCESS' && ( + <> + + +
+ + +
+ + )} +
+ ) +} + +export default ExternalScript diff --git a/app/web/src/components/EncodedUrl/FullScriptEncoding.tsx b/app/web/src/components/EncodedUrl/FullScriptEncoding.tsx new file mode 100644 index 0000000..32687b5 --- /dev/null +++ b/app/web/src/components/EncodedUrl/FullScriptEncoding.tsx @@ -0,0 +1,28 @@ +import { makeEncodedLink } from './helpers' +import { copyTextToClipboard } from 'src/helpers/clipboard' +import { useIdeContext } from 'src/helpers/hooks/useIdeContext' + +const FullScriptEncoding = () => { + const { state } = useIdeContext() + const encodedLink = makeEncodedLink(state.code) + return ( +
+

+ Encodes your CodeCad script into a URL so that you can share your work +

+ + +
+ ) +} + +export default FullScriptEncoding diff --git a/app/web/src/components/EncodedUrl/helpers.ts b/app/web/src/components/EncodedUrl/helpers.ts new file mode 100644 index 0000000..ff23f13 --- /dev/null +++ b/app/web/src/components/EncodedUrl/helpers.ts @@ -0,0 +1,75 @@ +import { useEffect } from 'react' +import { flow } from 'lodash/fp' + +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' + +const scriptKey = 'encoded_script' +const scriptKeyV2 = 'encoded_script_v2' +const fetchText = 'fetch_text_v1' + +export const githubSafe = (url: string): string => + url.includes('github.com') + ? url + .replace('github.com', 'raw.githubusercontent.com') + .replace('/blob/', '/') + : url + +export const prepareEncodedUrl = flow(decodeURIComponent, githubSafe) + +const prepareDecodedUrl = flow(githubSafe, encodeURIComponent) + +export function makeEncodedLink(code: string): string { + const encodedScript = encode(code) + return `${location.origin}${location.pathname}#${scriptKeyV2}=${encodedScript}` +} + +export function makeExternalUrl(resourceUrl: string): string { + return `${location.origin}${ + location.pathname + }#${fetchText}=${prepareDecodedUrl(resourceUrl)}` +} + +export function useIdeInit(cadPackage: string) { + const { thunkDispatch } = useIdeContext() + const handleRender = useRender() + 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]) +} diff --git a/app/web/src/components/IdeConsole/IdeConsole.js b/app/web/src/components/IdeConsole/IdeConsole.js index 70a94f5..cea6168 100644 --- a/app/web/src/components/IdeConsole/IdeConsole.js +++ b/app/web/src/components/IdeConsole/IdeConsole.js @@ -1,9 +1,9 @@ -import { useContext, useEffect } from 'react' -import { IdeContext } from 'src/components/IdeToolbarNew' +import { useEffect } from 'react' +import { useIdeContext } from 'src/helpers/hooks/useIdeContext' import { matchEditorVsDarkTheme } from 'src/components/IdeEditor' const IdeConsole = () => { - const { state } = useContext(IdeContext) + const { state } = useIdeContext() useEffect(() => { const element = document.querySelector('.console-tile .mosaic-window-body') if (element) { @@ -12,13 +12,16 @@ const IdeConsole = () => { }, [state.consoleMessages]) return ( -
+
{state.consoleMessages?.map(({ type, message, time }, index) => (
             
, @@ -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': ( - + camera: ( + - + strokeLinecap="round" + /> + + strokeLinecap="round" + /> + + ), + checkmark: ( + + ), - 'checkmark': ( - - - ), 'chevron-down': ( { /> ), + 'drag-grid': ( + + + + ), 'exclamation-circle': ( { /> ), + gear: ( + + + + ), + 'big-gear': ( + + + + ), lightbulb: ( { /> ), + 'mac-cmd-key': ( + + + + ), pencil: ( { ), refresh: ( - + + strokeLinecap="round" + /> ), save: ( diff --git a/app/web/src/helpers/clipboard.js b/app/web/src/helpers/clipboard.tsx similarity index 53% rename from app/web/src/helpers/clipboard.js rename to app/web/src/helpers/clipboard.tsx index c783230..7a8d174 100644 --- a/app/web/src/helpers/clipboard.js +++ b/app/web/src/helpers/clipboard.tsx @@ -1,5 +1,7 @@ -function fallbackCopyTextToClipboard(text) { - var textArea = document.createElement('textarea') +import { toast } from '@redwoodjs/web/toast' + +function fallbackCopyTextToClipboard(text: string) { + const textArea = document.createElement('textarea') textArea.value = text // Avoid scrolling to bottom @@ -12,8 +14,8 @@ function fallbackCopyTextToClipboard(text) { textArea.select() try { - var successful = document.execCommand('copy') - var msg = successful ? 'successful' : 'unsuccessful' + const successful = document.execCommand('copy') + const msg = successful ? 'successful' : 'unsuccessful' console.log('Fallback: Copying text command was ' + msg) } catch (err) { console.error('Fallback: Oops, unable to copy', err) @@ -21,17 +23,29 @@ function fallbackCopyTextToClipboard(text) { document.body.removeChild(textArea) } -export function copyTextToClipboard(text) { + +const clipboardSuccessToast = (text: string) => + toast.success(() => ( +
+

link added to clipboard.

+
+ )) + +const makeClipboardCopier = (success: Function) => (text: string) => { if (!navigator.clipboard) { fallbackCopyTextToClipboard(text) + success(text) return } navigator.clipboard.writeText(text).then( function () { console.log('Async: Copying to clipboard was successful!') + success(text) }, function (err) { console.error('Async: Could not copy text: ', err) } ) } + +export const copyTextToClipboard = makeClipboardCopier(clipboardSuccessToast) diff --git a/app/web/src/helpers/hooks/useIdeContext.ts b/app/web/src/helpers/hooks/useIdeContext.ts new file mode 100644 index 0000000..dbd682a --- /dev/null +++ b/app/web/src/helpers/hooks/useIdeContext.ts @@ -0,0 +1,11 @@ +import { IdeContext } from 'src/pages/DevIdePage/DevIdePage' +import { useContext } from 'react' + +export function useIdeContext() { + return useContext(IdeContext) +} + +export const ideTypeNameMap = { + openScad: 'OpenSCAD', + cadQuery: 'CadQuery', +} diff --git a/app/web/src/helpers/hooks/useIdeState.js b/app/web/src/helpers/hooks/useIdeState.js index 2079340..55f3055 100644 --- a/app/web/src/helpers/hooks/useIdeState.js +++ b/app/web/src/helpers/hooks/useIdeState.js @@ -47,6 +47,16 @@ let mutableState = null export const useIdeState = () => { const code = '' + const initialLayout = { + direction: 'row', + first: 'Editor', + second: { + direction: 'column', + first: 'Viewer', + second: 'Console', + splitPercentage: 70, + }, + } const initialState = { ideType: 'INIT', consoleMessages: [ @@ -57,16 +67,7 @@ export const useIdeState = () => { type: 'INIT', data: null, }, - layout: { - direction: 'row', - first: 'Editor', - second: { - direction: 'column', - first: 'Viewer', - second: 'Console', - splitPercentage: 70, - }, - }, + layout: initialLayout, camera: {}, viewerSize: { width: 0, height: 0 }, isLoading: false, @@ -129,6 +130,11 @@ export const useIdeState = () => { ...state, isLoading: false, } + case 'resetLayout': + return { + ...state, + layout: initialLayout, + } default: return state } diff --git a/app/web/src/pages/DevIdePage/DevIdePage.js b/app/web/src/pages/DevIdePage/DevIdePage.js index f648de1..027a7d5 100644 --- a/app/web/src/pages/DevIdePage/DevIdePage.js +++ b/app/web/src/pages/DevIdePage/DevIdePage.js @@ -1,33 +1,23 @@ -import MainLayout from 'src/layouts/MainLayout' +import { createContext } from 'react' import Seo from 'src/components/Seo/Seo' -import IdeToolbar from 'src/components/IdeToolbarNew' -import OutBound from 'src/components/OutBound' +import IdeWrapper from 'src/components/IdeWrapper' +import { Toaster } from '@redwoodjs/web/toast' +import { useIdeState } from 'src/helpers/hooks/useIdeState' +export const IdeContext = createContext() const DevIdePage = ({ cadPackage }) => { + const [state, thunkDispatch] = useIdeState() return (
- - -
-
- We're still working on this. Since you're here, have a look what{' '} - - we've got planned - - . -
-
-
-
- -
+ + + + +
) } diff --git a/app/web/tailwind.config.js b/app/web/tailwind.config.js index ccc766c..7fc88b7 100644 --- a/app/web/tailwind.config.js +++ b/app/web/tailwind.config.js @@ -10,6 +10,9 @@ module.exports = { 'bounce-sm-slow': 'bounce-sm 5s linear infinite', 'twist-sm-slow': 'twist-sm 10s infinite', }, + cursor: { + grab: 'grab' + }, keyframes: { 'bounce-sm': { '0%, 100%': { diff --git a/app/yarn.lock b/app/yarn.lock index 46bdf77..bfeda86 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -6827,7 +6827,7 @@ cloudinary@^1.23.0: lodash "^4.17.11" q "^1.5.1" -clsx@^1.0.4, clsx@^1.1.1: +clsx@^1.0.4, clsx@^1.1.0, clsx@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== @@ -14898,7 +14898,7 @@ prompts@2.4.1, prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -15610,6 +15610,14 @@ react-syntax-highlighter@^13.5.0, react-syntax-highlighter@^13.5.3: prismjs "^1.21.0" refractor "^3.1.0" +react-tabs@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-3.2.2.tgz#07bdc3cdb17bdffedd02627f32a93cd4b3d6e4d0" + integrity sha512-/o52eGKxFHRa+ssuTEgSM8qORnV4+k7ibW+aNQzKe+5gifeVz8nLxCrsI9xdRhfb0wCLdgIambIpb1qCxaMN+A== + dependencies: + clsx "^1.1.0" + prop-types "^15.5.0" + react-textarea-autosize@^8.1.1: version "8.3.3" resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.3.tgz#f70913945369da453fd554c168f6baacd1fa04d8" diff --git a/docs/docs/general-cadhub/external-resource-url.mdx b/docs/docs/general-cadhub/external-resource-url.mdx new file mode 100644 index 0000000..06fdb81 --- /dev/null +++ b/docs/docs/general-cadhub/external-resource-url.mdx @@ -0,0 +1,37 @@ +--- +title: External resoure URLs +--- + +import Image from '@theme/IdealImage'; + +import spaceDonut from '../../static/img/general-cadhub/space-donut.png'; +import openscadSelect from '../../static/img/getting-started/openscad-select.jpg'; +import externalScript1 from '../../static/img/general-cadhub/external-script-1.png'; +import externalScript2 from '../../static/img/general-cadhub/external-script-2.png'; + +CadHub allows you to generate a URL that links to a script hosted on an external URL. +The typical usecase for this as a CodeCad script hosted on github, this way the repo can continue to update and the link will stay up-to-date. +Any URL that returns the script as plain text will work. + +Here's how to use it using github as an example. +Find the file you want on Github, in this case ToastedIce's [space donut](https://github.com/toastedice/random_openscad_creations_I_made/blob/main/donut/spacedonut.scad). + + + +Copy the URL. + +Open the IDE for the Cad package to match your script, in this case OpenSCAD. + + + +Click the share button in the top right, then select the "external srcipt" tab. + + + +Paste in the Github URL. + + + +From there you can copy the generated URL, or "copy and Render" to check the script is working as intended. + +Here's the [URL](http://cadhub.xyz/dev-ide/cadQuery#fetch_text_v1=https%3A%2F%2Fraw.githubusercontent.com%2Ftoastedice%2Frandom_openscad_creations_I_made%2Fmain%2Fdonut%2Fspacedonut.scad) from this example. diff --git a/docs/sidebars.js b/docs/sidebars.js index bba91e2..a3ae8f9 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -27,5 +27,6 @@ module.exports = { 'round-anything/radii-conflict', ], }, + 'general-cadhub/external-resource-url' ], } diff --git a/docs/static/img/general-cadhub/external-script-1.png b/docs/static/img/general-cadhub/external-script-1.png new file mode 100644 index 0000000..6452e71 Binary files /dev/null and b/docs/static/img/general-cadhub/external-script-1.png differ diff --git a/docs/static/img/general-cadhub/external-script-2.png b/docs/static/img/general-cadhub/external-script-2.png new file mode 100644 index 0000000..aab3abb Binary files /dev/null and b/docs/static/img/general-cadhub/external-script-2.png differ diff --git a/docs/static/img/general-cadhub/space-donut.png b/docs/static/img/general-cadhub/space-donut.png new file mode 100644 index 0000000..6d4c59d Binary files /dev/null and b/docs/static/img/general-cadhub/space-donut.png differ