IDE redesign, initial implementation #362

Merged
Irev-Dev merged 17 commits from kurt/update-ide-panel-toolbar-360 into main 2021-06-15 10:05:03 +02:00
30 changed files with 860 additions and 272 deletions

View File

@@ -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",

View File

@@ -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 = () => {
Irev-Dev commented 2021-06-14 02:21:24 +02:00 (Migrated from github.com)
Review

image

![image](https://user-images.githubusercontent.com/29681384/121826268-436c6680-ccfa-11eb-9b06-ba4d61270ab5.png)
franknoirot commented 2021-06-14 15:18:09 +02:00 (Migrated from github.com)
Review

Did you do all these snippet images manually? They're very helpful!

Did you do all these snippet images manually? They're very helpful!
Irev-Dev commented 2021-06-15 10:02:26 +02:00 (Migrated from github.com)
Review

:)
Yeah I did.
CContext is super important when trying to look at other's code. You could have figured this out yourself but 20x quicker for me to add a few screen shots.

:) Yeah I did. CContext is super important when trying to look at other's code. You could have figured this out yourself but 20x quicker for me to add a few screen shots.
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 (
<div className="flex justify-between bg-gray-500 text-gray-100">
<div className="flex items-center h-9 w-full cursor-grab">
<div className=" text-gray-500 bg-gray-300 cursor-grab px-2 h-full flex items-center">
<Svg name="drag-grid" className="w-4 p-px" />
</div>
<div className="flex gap-6 px-5">
<FileDropdown
handleRender={handleRender}
handleStlDownload={handleStlDownload}
/>
<button className="cursor-not-allowed" disabled>
Edit
</button>
<ViewDropdown handleLayoutReset={() => thunkDispatch({type: 'resetLayout'})} />
</div>
<button
className="text-gray-300 h-full cursor-not-allowed"
aria-label="editor settings"
disabled
>
<Svg name="gear" className="w-6 p-px" />
</button>
</div>
<div className="flex items-center cursor-default">
<div
className={`${isOpenScad && 'bg-yellow-200'} ${
franknoirot commented 2021-06-14 15:23:05 +02:00 (Migrated from github.com)
Review

With the additional background I added in the latest Figma designs we may want to start aliasing the CAD packages to class names in the next round so several elements can inherit styles.

With the additional background I added in the latest Figma designs we may want to start aliasing the CAD packages to class names in the next round so several elements can inherit styles.
Irev-Dev commented 2021-06-15 10:02:44 +02:00 (Migrated from github.com)
Review

Oh right, yeah that's a good idea.

Oh right, yeah that's a good idea.
isCadQuery && 'bg-blue-800'
} w-5 h-5 rounded-full`}
/>
<div className="px-2">{cadName}</div>
</div>
</div>
)
}
export default EditorMenu
function FileDropdown({ handleRender, handleStlDownload }) {
Irev-Dev commented 2021-06-14 02:21:45 +02:00 (Migrated from github.com)
Review

image

![image](https://user-images.githubusercontent.com/29681384/121826279-4ff0bf00-ccfa-11eb-8315-bd8a4cfc0ef7.png)
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 }) {
Irev-Dev commented 2021-06-14 02:21:59 +02:00 (Migrated from github.com)
Review

image

![image](https://user-images.githubusercontent.com/29681384/121826302-66971600-ccfa-11eb-8aa7-f48b627bc6a9.png)
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-gray-500 rounded shadow-md text-gray-100 overflow-hidden whitespace-nowrap">
{children}
</Menu.Items>
</Menu>
</div>
)
}

View File

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

View File

@@ -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 = () => {
Irev-Dev commented 2021-06-14 02:26:20 +02:00 (Migrated from github.com)
Review

image
and

image

![image](https://user-images.githubusercontent.com/29681384/121826427-e7eea880-ccfa-11eb-9e5b-c82fd9605d87.png) and ![image](https://user-images.githubusercontent.com/29681384/121826437-f2a93d80-ccfa-11eb-9b3d-b5fa0e0968d3.png)
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<HTMLInputElement> = async ({
clipboardData,
}) => {
const url = clipboardData.getData('Text')
processUserUrl(url)
}
const onChange: React.ChangeEventHandler<HTMLInputElement> = async ({
target,
}) => setRawUrl(target.value)
const onKeyDown = async ({ key, target }) =>
key === 'Enter' && processUserUrl(target.value)
Irev-Dev commented 2021-06-14 02:28:06 +02:00 (Migrated from github.com)
Review

When a user pastes a url it will immediately start processing it without having a "submit" button. I think this is appropriate since the whole point is to link to an external URL and no one will be typing those, you can type and hit enter though.

When a user pastes a url it will immediately start processing it without having a "submit" button. I think this is appropriate since the whole point is to link to an external URL and no one will be typing those, you can type and hit enter though.
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('<!DOCTYPE html>'))
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<HTMLButtonElement> = () => {
copyTextToClipboard(makeExternalUrl(rawUrl))
thunkDispatch({ type: 'updateCode', payload: script })
setTimeout(handleRender)
}
return (
<div className="p-4">
<p className="text-sm pb-4">
Paste an external url containing a {cadName} script to generate a new
CadHub url for this resource.{' '}
<OutBound
className="underline text-gray-500"
to="https://learn.cadhub.xyz/docs/general-cadhub/external-resource-url"
>
Learn more
</OutBound>{' '}
about this feature.
</p>
{['INIT', 'ERROR'].includes(asyncState) && (
<>
<p>Paste url</p>
<input
className="p-1 text-xs rounded border border-gray-700 w-full"
value={rawUrl}
onChange={onChange}
onPaste={onPaste}
onKeyDown={onKeyDown}
/>
</>
)}
{asyncState === 'ERROR' && (
<p className="text-sm text-red-800">That didn't work, try again.</p>
)}
{asyncState === 'LOADING' && (
<div className="h-10 relative">
<div className="inset-0 absolute flex items-center justify-center">
<div className="h-6 w-6 bg-pink-600 rounded-full animate-ping"></div>
</div>
</div>
)}
{asyncState === 'SUCCESS' && (
<>
<input
value={makeExternalUrl(rawUrl).replace(/^.+:\/\//g, '')}
readOnly
className="p-1 mt-4 text-xs rounded-t border border-gray-700 w-full"
/>
<button
className="w-full bg-gray-700 py-1 rounded-b text-gray-300"
onClick={() => copyTextToClipboard(makeExternalUrl(rawUrl))}
>
Copy URL
</button>
<div className="flex flex-col gap-2 pt-2">
<button
className="bg-gray-500 p-1 px-2 rounded text-gray-300"
onClick={onCopyRender}
>
Copy &amp; Render
</button>
<button
className="bg-gray-500 p-1 px-2 rounded text-gray-300"
onClick={() => {
setAsyncState('INIT')
setRawUrl('')
setScript('')
}}
>
Create another URL
</button>
</div>
</>
)}
</div>
)
}
export default ExternalScript

View File

@@ -0,0 +1,28 @@
import { makeEncodedLink } from './helpers'
import { copyTextToClipboard } from 'src/helpers/clipboard'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
const FullScriptEncoding = () => {
Irev-Dev commented 2021-06-14 02:29:31 +02:00 (Migrated from github.com)
Review

image

![image](https://user-images.githubusercontent.com/29681384/121826523-65b2b400-ccfb-11eb-8c97-3d9b03505ea8.png)
const { state } = useIdeContext()
const encodedLink = makeEncodedLink(state.code)
return (
<div className="p-4">
<p className="text-sm pb-4 border-b border-gray-700">
Encodes your CodeCad script into a URL so that you can share your work
</p>
<input
value={encodedLink.replace(/^.+:\/\//g, '')}
readOnly
className="p-1 mt-4 text-xs rounded-t border border-gray-700 w-full"
/>
<button
className="w-full bg-gray-700 py-1 rounded-b text-gray-300"
onClick={() => copyTextToClipboard(encodedLink)}
>
Copy URL
</button>
</div>
)
}
export default FullScriptEncoding

View File

@@ -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) {
Irev-Dev commented 2021-06-14 02:32:54 +02:00 (Migrated from github.com)
Review

All of the logic in this hook I had in the IdeToolbarNew because that was the main wrapping component that had the context provider, so it made sense to have init useEffects there. However since most of the logic relates to checking if there is something encoded in the URL, I thought it made sense to bundle the logic here with the rest of the encoded url code, and bring it into IdeToolbarNew as a custom hook.

I've also now rename IdeToolbarNew to IdeWrapper

All of the logic in this hook I had in the `IdeToolbarNew` because that was the main wrapping component that had the context provider, so it made sense to have init useEffects there. However since most of the logic relates to checking if there is something encoded in the URL, I thought it made sense to bundle the logic here with the rest of the encoded url code, and bring it into `IdeToolbarNew` as a custom hook. I've also now rename `IdeToolbarNew` to `IdeWrapper`
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])
}

View File

@@ -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 (
<div className="p-2 px-4 min-h-full" style={matchEditorVsDarkTheme.Bg}>
<div
className="p-2 px-8 pt-14 min-h-full"
style={matchEditorVsDarkTheme.Bg}
>
<div>
{state.consoleMessages?.map(({ type, message, time }, index) => (
<pre
className="font-mono text-sm"
style={matchEditorVsDarkTheme.Text}
key={message + index}
key={`${message} ${index}`}
>
<div
className="text-xs font-bold pt-2"

View File

@@ -1,11 +1,13 @@
import { useContext, useRef, useEffect } from 'react'
import { useRef, useEffect } from 'react'
import { Mosaic, MosaicWindow } from 'react-mosaic-component'
import { IdeContext } from 'src/components/IdeToolbarNew'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { requestRender } from 'src/helpers/hooks/useIdeState'
import IdeEditor, { matchEditorVsDarkTheme } from 'src/components/IdeEditor'
import IdeEditor from 'src/components/IdeEditor'
import IdeViewer from 'src/components/IdeViewer'
import IdeConsole from 'src/components/IdeConsole'
import 'react-mosaic-component/react-mosaic-component.css'
import EditorMenu from 'src/components/EditorMenu/EditorMenu'
import PanelToolbar from 'src/components/PanelToolbar'
const ELEMENT_MAP = {
Editor: <IdeEditor />,
@@ -13,8 +15,26 @@ const ELEMENT_MAP = {
Console: <IdeConsole />,
}
const TOOLBAR_MAP = {
Editor: (
<div className="w-full">
<EditorMenu />
</div>
),
Viewer: (
<div>
<PanelToolbar panelName="Viewer" />
</div>
),
Console: (
<div>
<PanelToolbar panelName="Console" />
</div>
),
}
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 (
<MosaicWindow
path={path}
renderToolbar={() => (
<div
className="text-xs text-gray-400 pl-4 w-full py-px font-bold leading-loose border-b border-gray-700"
style={matchEditorVsDarkTheme.lighterBg}
>
{id}
{id === 'Editor' && ` (${state.ideType})`}
</div>
)}
renderToolbar={() => TOOLBAR_MAP[id]}
className={`${id.toLowerCase()} ${id.toLowerCase()}-tile`}
>
{id === 'Viewer' ? (

View File

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

View File

@@ -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
}) => (
<button
onClick={onClick}
className={`flex bg-gray-200 h-10 justify-center items-center px-4 rounded ${className}`}
>
<div className={`rounded-full h-6 w-6 mr-4 ${iconColor}`} />
{children}
</button>
)
const IdeHeader = ({ handleRender }: { handleRender: () => void }) => {
Irev-Dev commented 2021-06-14 02:34:39 +02:00 (Migrated from github.com)
Review

image

![image](https://user-images.githubusercontent.com/29681384/121826642-1d47c600-ccfc-11eb-89d1-a0ac63141405.png)
return (
<div className="h-16 w-full bg-gray-900 flex justify-between items-center">
<div className="bg-gray-700 pr-48 h-full"></div>
<div className="text-gray-200 flex gap-4 mr-4">
<TopButton
className="bg-gray-600 text-gray-200"
iconColor="bg-gray-300"
onClick={handleRender}
>
Render
</TopButton>
<Popover className="relative outline-none w-full h-full">
{({ open }) => {
return (
<>
<Popover.Button className="h-full w-full outline-none">
<TopButton iconColor="bg-gray-600" className="text-gray-700">
Share
</TopButton>
</Popover.Button>
{open && (
<Popover.Panel className="absolute z-10 mt-4 right-0">
<Tabs
className="bg-gray-300 rounded-md shadow-md overflow-hidden text-gray-700"
selectedTabClassName="bg-gray-200"
>
<TabPanel>
<FullScriptEncoding />
</TabPanel>
<TabPanel>
<ExternalScript />
</TabPanel>
<TabList className="flex whitespace-nowrap text-gray-700 border-t border-gray-700">
<Tab className="p-3 px-5">encoded script</Tab>
<Tab className="p-3 px-5">external script</Tab>
</TabList>
</Tabs>
</Popover.Panel>
)}
</>
)
}}
</Popover>
{/* <TopButton>Fork</TopButton> */}
</div>
</div>
)
}
export default IdeHeader

View File

@@ -0,0 +1,23 @@
import { Link, routes } from '@redwoodjs/router'
import Svg from 'src/components/Svg/Svg'
const IdeSideBar = () => {
Irev-Dev commented 2021-06-14 02:35:11 +02:00 (Migrated from github.com)
Review

image

![image](https://user-images.githubusercontent.com/29681384/121826656-2fc1ff80-ccfc-11eb-998b-55c254eeaae6.png)
return (
<div className="h-full flex flex-col justify-between">
<div className="w-16 h-16 flex items-center justify-center bg-gray-900">
<Link to={routes.home()}>
<Svg className="w-12" name="favicon" />
</Link>
</div>
<button
className="text-gray-300 p-2 pb-4 flex justify-center cursor-not-allowed"
aria-label="IDE settings"
disabled
>
<Svg name="big-gear" />
</button>
</div>
)
}
export default IdeSideBar

View File

@@ -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 (
<IdeContext.Provider value={{ state, thunkDispatch }}>
<div className="h-full flex flex-col">
<nav className="flex">
<button
onClick={handleRender}
className="border-2 px-2 text-gray-700 text-sm m-1"
>
Render
</button>
<button
onClick={handleMakeLink}
className="border-2 text-gray-700 px-2 text-sm m-1 ml-2"
>
Copy link
</button>
<button
onClick={handleStlDownload}
className="border-2 text-gray-700 px-2 text-sm m-1 ml-2"
>
Download STL
</button>
</nav>
<IdeContainer />
</div>
</IdeContext.Provider>
)
}
export default IdeToolbarNew

View File

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

View File

@@ -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 (
<div className="h-full flex">
<div className="w-16 bg-gray-700 flex-shrink-0">
<IdeSideBar />
</div>
<div className="h-full flex flex-grow flex-col">
<nav className="flex">
<IdeHeader handleRender={handleRender} />
</nav>
{shouldShowConstructionMessage && (
<div className="py-2 bg-pink-200 flex">
<div className="flex-grow text-center">
We're still working on this. Since you're here, have a look what{' '}
<OutBound
className="text-pink-700"
to="https://github.com/Irev-Dev/cadhub/discussions/212"
>
we've got planned
</OutBound>
.
</div>
<button
className="flex mr-3"
onClick={() => setShouldShowConstructionMessage(false)}
>
<Svg className="h-4 w-6 text-gray-500 items-center" name="x" />
</button>
</div>
)}
<IdeContainer />
</div>
</div>
)
}
export default IdeToolbarNew

View File

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

View File

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

View File

@@ -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 (
<div className="absolute top-0 right-0 flex items-center h-9">
<button
className="bg-gray-500 text-gray-300 px-3 rounded-bl-lg h-full cursor-not-allowed"
aria-label={`${panelName} settings`}
disabled
>
<Svg name="gear" className="w-7 p-px" />
</button>
{mosaicWindowActions.connectDragSource(
franknoirot commented 2021-06-15 00:30:51 +02:00 (Migrated from github.com)
Review

Also rad!

Also rad!
<div className=" text-gray-500 bg-gray-300 cursor-grab px-2 h-full flex items-center">
<Svg name="drag-grid" className="w-4 p-px" />
</div>
)}
</div>
)
}
export default PanelToolbar

File diff suppressed because one or more lines are too long

View File

@@ -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(() => (
<div className="overflow-hidden">
<p>link added to clipboard.</p>
</div>
))
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)

View File

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

View File

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

View File

@@ -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 (
<div className="h-screen flex flex-col">
<MainLayout shouldRemoveFooterInIde>
<Seo
title="new ide in development"
description="new ide in development"
lang="en-US"
/>
<div className="py-2 bg-pink-200">
<div className="mx-auto max-w-3xl">
We're still working on this. Since you're here, have a look what{' '}
<OutBound
className="text-pink-700"
to="https://github.com/Irev-Dev/cadhub/discussions/212"
>
we've got planned
</OutBound>
.
</div>
</div>
</MainLayout>
<div className="flex-auto">
<IdeToolbar cadPackage={cadPackage} />
</div>
<Seo
title="new ide in development"
description="new ide in development"
lang="en-US"
/>
<Toaster timeout={9000} />
<IdeContext.Provider value={{ state, thunkDispatch }}>
<IdeWrapper cadPackage={cadPackage} />
</IdeContext.Provider>
</div>
)
}

View File

@@ -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%': {

View File

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

View File

@@ -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).
<Image img={spaceDonut} style={{backgroundSize: 'contain', paddingBottom: '2rem'}} />
Copy the URL.
Open the IDE for the Cad package to match your script, in this case OpenSCAD.
<Image img={openscadSelect} style={{backgroundSize: 'contain', paddingBottom: '2rem', width: '400px', margin: '0 auto'}} />
Click the share button in the top right, then select the "external srcipt" tab.
<Image img={externalScript1} style={{backgroundSize: 'contain', paddingBottom: '2rem', width: '400px', margin: '0 auto'}} />
Paste in the Github URL.
<Image img={externalScript2} style={{backgroundSize: 'contain', paddingBottom: '2rem', width: '400px', margin: '0 auto'}} />
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.

View File

@@ -27,5 +27,6 @@ module.exports = {
'round-anything/radii-conflict',
],
},
'general-cadhub/external-resource-url'
],
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB