Merge pull request #362 from Irev-Dev/kurt/update-ide-panel-toolbar-360

IDE redesign, initial implementation
This commit was merged in pull request #362.
This commit is contained in:
Kurt Hutten
2021-06-15 18:05:02 +10:00
committed by GitHub
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 = () => {
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'} ${
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 }) {
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-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 = () => {
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)
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 = () => {
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) {
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 }) => {
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 = () => {
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(
<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