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:
@@ -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",
|
||||
|
||||
126
app/web/src/components/EditorMenu/EditorMenu.tsx
Normal file
126
app/web/src/components/EditorMenu/EditorMenu.tsx
Normal 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 & 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>
|
||||
)
|
||||
}
|
||||
61
app/web/src/components/EditorMenu/helpers.ts
Normal file
61
app/web/src/components/EditorMenu/helpers.ts
Normal 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))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
126
app/web/src/components/EncodedUrl/ExternalScript.tsx
Normal file
126
app/web/src/components/EncodedUrl/ExternalScript.tsx
Normal 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 & 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
|
||||
28
app/web/src/components/EncodedUrl/FullScriptEncoding.tsx
Normal file
28
app/web/src/components/EncodedUrl/FullScriptEncoding.tsx
Normal 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
|
||||
75
app/web/src/components/EncodedUrl/helpers.ts
Normal file
75
app/web/src/components/EncodedUrl/helpers.ts
Normal 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])
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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' ? (
|
||||
|
||||
@@ -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',
|
||||
|
||||
78
app/web/src/components/IdeHeader/IdeHeader.tsx
Normal file
78
app/web/src/components/IdeHeader/IdeHeader.tsx
Normal 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
|
||||
23
app/web/src/components/IdeSideBar/IdeSideBar.tsx
Normal file
23
app/web/src/components/IdeSideBar/IdeSideBar.tsx
Normal 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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
|
||||
51
app/web/src/components/IdeWrapper/IdeWrapper.tsx
Normal file
51
app/web/src/components/IdeWrapper/IdeWrapper.tsx
Normal 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
|
||||
21
app/web/src/components/IdeWrapper/useRender.ts
Normal file
21
app/web/src/components/IdeWrapper/useRender.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
25
app/web/src/components/PanelToolbar/PanelToolbar.tsx
Normal file
25
app/web/src/components/PanelToolbar/PanelToolbar.tsx
Normal 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
@@ -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)
|
||||
11
app/web/src/helpers/hooks/useIdeContext.ts
Normal file
11
app/web/src/helpers/hooks/useIdeContext.ts
Normal 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',
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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%': {
|
||||
|
||||
@@ -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"
|
||||
|
||||
37
docs/docs/general-cadhub/external-resource-url.mdx
Normal file
37
docs/docs/general-cadhub/external-resource-url.mdx
Normal 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.
|
||||
@@ -27,5 +27,6 @@ module.exports = {
|
||||
'round-anything/radii-conflict',
|
||||
],
|
||||
},
|
||||
'general-cadhub/external-resource-url'
|
||||
],
|
||||
}
|
||||
|
||||
BIN
docs/static/img/general-cadhub/external-script-1.png
vendored
Normal file
BIN
docs/static/img/general-cadhub/external-script-1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
BIN
docs/static/img/general-cadhub/external-script-2.png
vendored
Normal file
BIN
docs/static/img/general-cadhub/external-script-2.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
BIN
docs/static/img/general-cadhub/space-donut.png
vendored
Normal file
BIN
docs/static/img/general-cadhub/space-donut.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 224 KiB |
Reference in New Issue
Block a user