diff --git a/app/web/config/webpack.config.js b/app/web/config/webpack.config.js
index 5f8af79..103151b 100644
--- a/app/web/config/webpack.config.js
+++ b/app/web/config/webpack.config.js
@@ -5,5 +5,9 @@ module.exports = (config, { env }) => {
plugin.userOptions.favicon = './src/favicon.svg'
}
})
+ config.module.rules.push({
+ test: /\.(md|jscad\.js|py|scad)$/i,
+ use: 'raw-loader',
+ });
return config
}
diff --git a/app/web/package.json b/app/web/package.json
index d3f435e..1dd0060 100644
--- a/app/web/package.json
+++ b/app/web/package.json
@@ -58,6 +58,7 @@
"postcss": "^8.3.6",
"postcss-import": "^14.0.2",
"postcss-loader": "^6.1.1",
+ "raw-loader": "^4.0.2",
"tailwindcss": "^2.2.7"
}
}
diff --git a/app/web/src/components/CadPackage/CadPackage.tsx b/app/web/src/components/CadPackage/CadPackage.tsx
index fa3aa82..76ddb3c 100644
--- a/app/web/src/components/CadPackage/CadPackage.tsx
+++ b/app/web/src/components/CadPackage/CadPackage.tsx
@@ -1,43 +1,75 @@
-export type CadPackageType = 'openscad' | 'cadquery' | 'jscad'
+export type CadPackageType = 'openscad' | 'cadquery' | 'jscad' | 'INIT'
-export const ideTypeNameMap: { [key in CadPackageType]: string } = {
- openscad: 'OpenSCAD',
- cadquery: 'CadQuery',
- jscad: 'JSCAD',
+interface CadPackageConfig {
+ label: string
+ buttonClasses: string
+ dotClasses: string
}
+export const cadPackageConfigs: { [key in CadPackageType]: CadPackageConfig } =
+ {
+ openscad: {
+ label: 'OpenSCAD',
+ buttonClasses: 'bg-yellow-800',
+ dotClasses: 'bg-yellow-200',
+ },
+ cadquery: {
+ label: 'CadQuery',
+ buttonClasses: 'bg-ch-blue-700',
+ dotClasses: 'bg-blue-800',
+ },
+ jscad: {
+ label: 'JSCAD',
+ buttonClasses: 'bg-ch-purple-500',
+ dotClasses: 'bg-yellow-300',
+ },
+ INIT: {
+ label: '',
+ buttonClasses: '',
+ dotClasses: '',
+ },
+ }
+
interface CadPackageProps {
cadPackage: CadPackageType
className?: string
dotClass?: string
+ onClick?: any
}
const CadPackage = ({
cadPackage,
className = '',
dotClass = 'w-5 h-5',
+ onClick,
}: CadPackageProps) => {
- const cadName = ideTypeNameMap[cadPackage] || ''
- const isOpenScad = cadPackage === 'openscad'
- const isCadQuery = cadPackage === 'cadquery'
- const isJsCad = cadPackage === 'jscad'
+ const cadPackageConfig = cadPackageConfigs[cadPackage]
+
return (
-
+ {cadPackageConfig?.label}
+
+ )
+}
+
+// Returns a proper button if an onClick handler is passed in, or a div
+// if the element is meant to be a simple badge
+function ButtonOrDiv({ onClick, className, children }) {
+ return onClick ? (
+
+ {children}
+
+ ) : (
+ {children}
)
}
diff --git a/app/web/src/components/EditorGuide/EditorGuide.tsx b/app/web/src/components/EditorGuide/EditorGuide.tsx
new file mode 100644
index 0000000..2b43395
--- /dev/null
+++ b/app/web/src/components/EditorGuide/EditorGuide.tsx
@@ -0,0 +1,53 @@
+import { useMarkdownMetaData } from 'src/helpers/hooks/useMarkdownMetaData'
+import Editor from 'rich-markdown-editor'
+import { useRef } from 'react'
+import KeyValue from 'src/components/KeyValue/KeyValue'
+
+export default function EditorGuide({ content }) {
+ const [rawMetadata, metadata] = useMarkdownMetaData(content)
+
+ const processedContent = rawMetadata
+ ? content.replace(rawMetadata[0], '')
+ : content
+ const ref = useRef(null)
+
+ return (
+
+ {metadata && (
+ <>
+
{metadata.title}
+
+ {Object.entries(metadata)
+ .filter(([key]) => key !== 'title')
+ .map(([key, value], i) => (
+
+ {value}
+
+ ))}
+
+ >
+ )}
+ {}}
+ />
+
+ )
+}
+
+function LinkOrParagraph({ children }) {
+ const markdownUrlExpression =
+ /\[(.*)\]\((https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})\)/i
+ const matches = children.match(markdownUrlExpression)
+
+ return matches === null ? (
+ {children}
+ ) : (
+
+ {matches[1]}
+
+ )
+}
diff --git a/app/web/src/components/EditorMenu/EditorMenu.tsx b/app/web/src/components/EditorMenu/EditorMenu.tsx
index 9ef9456..33c0517 100644
--- a/app/web/src/components/EditorMenu/EditorMenu.tsx
+++ b/app/web/src/components/EditorMenu/EditorMenu.tsx
@@ -48,7 +48,24 @@ const EditorMenu = () => {
-
+ {
+ thunkDispatch({
+ type: 'addEditorModel',
+ payload: {
+ type: 'guide',
+ label: 'Guide',
+ content: state.ideGuide,
+ },
+ })
+ thunkDispatch({
+ type: 'switchEditorModel',
+ payload: state.models.length,
+ })
+ }}
+ />
>
diff --git a/app/web/src/components/EncodedUrl/ExternalScript.tsx b/app/web/src/components/EncodedUrl/ExternalScript.tsx
index 99687dd..db152a4 100644
--- a/app/web/src/components/EncodedUrl/ExternalScript.tsx
+++ b/app/web/src/components/EncodedUrl/ExternalScript.tsx
@@ -1,6 +1,6 @@
import { useState } from 'react'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
-import { ideTypeNameMap } from 'src/components/CadPackage/CadPackage'
+import { cadPackageConfigs } from 'src/components/CadPackage/CadPackage'
import OutBound from 'src/components/OutBound/OutBound'
import { prepareEncodedUrl, makeExternalUrl } from './helpers'
import { copyTextToClipboard } from 'src/helpers/clipboard'
@@ -16,7 +16,7 @@ const ExternalScript = () => {
'INIT' | 'SUCCESS' | 'ERROR' | 'LOADING'
>('INIT')
- const cadName = ideTypeNameMap[state.ideType]
+ const cadName = cadPackageConfigs[state.ideType].label
const onPaste: React.ClipboardEventHandler = async ({
clipboardData,
diff --git a/app/web/src/components/IdeEditor/IdeEditor.tsx b/app/web/src/components/IdeEditor/IdeEditor.tsx
index 97b9060..7840d4c 100644
--- a/app/web/src/components/IdeEditor/IdeEditor.tsx
+++ b/app/web/src/components/IdeEditor/IdeEditor.tsx
@@ -1,10 +1,11 @@
-import { useEffect, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { makeCodeStoreKey, requestRender } from 'src/helpers/hooks/useIdeState'
import Editor, { useMonaco } from '@monaco-editor/react'
import { theme } from 'src/../config/tailwind.config'
import { useSaveCode } from 'src/components/IdeWrapper/useSaveCode'
import type { CadPackageType } from 'src/components/CadPackage/CadPackage'
+import EditorGuide from 'src/components/EditorGuide/EditorGuide'
const colors = theme.extend.colors
@@ -17,6 +18,7 @@ const IdeEditor = ({ Loading }) => {
cadquery: 'python',
openscad: 'cpp',
jscad: 'javascript',
+ INIT: '',
}
const monaco = useMonaco()
useEffect(() => {
@@ -73,16 +75,64 @@ const IdeEditor = ({ Loading }) => {
className="h-full"
onKeyDown={handleSaveHotkey}
>
-
+ {state.models.length > 1 && (
+
+ {state.models.map((model, i) => (
+
+ {model.label}
+
+ thunkDispatch({ type: 'switchEditorModel', payload: i })
+ }
+ />
+ {model.type !== 'code' && (
+
+ thunkDispatch({ type: 'removeEditorModel', payload: i })
+ }
+ className="block p-1 m-.5 hover:bg-ch-gray-550"
+ >
+
+
+
+
+ )}
+
+ ))}
+
+ )}
+ {state.models[state.currentModel].type === 'code' ? (
+
+ ) : (
+
+
+
+ )}
)
}
diff --git a/app/web/src/components/ProjectsOfUserCell/ProjectsOfUserCell.tsx b/app/web/src/components/ProjectsOfUserCell/ProjectsOfUserCell.tsx
index 07959ec..e77725c 100644
--- a/app/web/src/components/ProjectsOfUserCell/ProjectsOfUserCell.tsx
+++ b/app/web/src/components/ProjectsOfUserCell/ProjectsOfUserCell.tsx
@@ -8,6 +8,7 @@ export const QUERY = gql`
id
title
mainImage
+ cadPackage
createdAt
updatedAt
user {
diff --git a/app/web/src/globals.d.ts b/app/web/src/globals.d.ts
new file mode 100644
index 0000000..e35b1e2
--- /dev/null
+++ b/app/web/src/globals.d.ts
@@ -0,0 +1,7 @@
+// While the raw-loader Webpack plugin actually makes these imports work, this
+// eliminates noisy TypeScript errors by registering these file endings as types.
+// Learned this method of registering modules from https://stackoverflow.com/a/57444766
+declare module '*.md'
+declare module '*.scad'
+declare module '*.py'
+declare module '*.jscad.js'
diff --git a/app/web/src/helpers/cadPackages/cadQuery/initialCode.py b/app/web/src/helpers/cadPackages/cadQuery/initialCode.py
new file mode 100644
index 0000000..bfa5c23
--- /dev/null
+++ b/app/web/src/helpers/cadPackages/cadQuery/initialCode.py
@@ -0,0 +1,17 @@
+# demo shaft coupler
+
+# ^ first comment is used for download title (i.e. "demo-shaft-coupler.stl")
+
+# CadQuery docs: https://cadquery.readthedocs.io/
+
+import cadquery as cq
+from cadquery import exporters
+
+diam = 5.0
+
+result = (cq.Workplane().circle(diam).extrude(20.0)
+ .faces(">Z").workplane(invert=True).circle(1.05).cutBlind(8.0)
+ .faces("
@@ -13,77 +14,6 @@ function withThunk(dispatch, getState) {
? actionOrThunk(dispatch, getState)
: dispatch(actionOrThunk)
}
-import { CadPackageType } from 'src/components/CadPackage/CadPackage'
-
-const initCodeMap: { [key in CadPackageType]: string } = {
- openscad: `// involute donut
-
-// ^ first comment is used for download title (i.e "involute-donut.stl")
-
-// Follow the OpenSCAD tutorial: https://learn.cadhub.xyz/docs/
-
-radius=3;
-color(c="DarkGoldenrod")rotate_extrude()translate([20,0])circle(d=30);
-color(c="hotpink")rotate_extrude()translate([20,0])offset(radius)offset(-radius)difference(){
- circle(d=34);
- translate([-200,-500])square([500,500]);
-}`,
- cadquery: `# demo shaft coupler
-
-# ^ first comment is used for download title (i.e. "demo-shaft-coupler.stl")
-
-# CadQuery docs: https://cadquery.readthedocs.io/
-
-import cadquery as cq
-from cadquery import exporters
-
-diam = 5.0
-
-result = (cq.Workplane().circle(diam).extrude(20.0)
- .faces(">Z").workplane(invert=True).circle(1.05).cutBlind(8.0)
- .faces(" `${codeStorageKey}-${ideType}`
@@ -95,10 +25,19 @@ interface XYZ {
z: number
}
+interface EditorModel {
+ type: 'code' | 'guide'
+ label: string
+ content?: string
+}
+
export interface State {
ideType: 'INIT' | CadPackageType
+ ideGuide?: string
consoleMessages: { type: 'message' | 'error'; message: string; time: Date }[]
code: string
+ models: EditorModel[]
+ currentModel: number
objectData: {
type: 'INIT' | ArtifactTypes
data: any
@@ -136,6 +75,8 @@ export const initialState: State = {
{ type: 'message', message: 'Initialising', time: new Date() },
],
code,
+ models: [{ type: 'code', label: 'Code' }],
+ currentModel: 0,
objectData: {
type: 'INIT',
data: null,
@@ -161,6 +102,7 @@ const reducer = (state: State, { type, payload }): State => {
initCodeMap[payload.cadPackage] ||
'',
ideType: payload.cadPackage,
+ ideGuide: initGuideMap[payload.cadPackage],
}
case 'updateCode':
return { ...state, code: payload }
@@ -269,6 +211,44 @@ const reducer = (state: State, { type, payload }): State => {
...state,
sideTray: payload,
}
+ case 'switchEditorModel':
+ return {
+ ...state,
+ currentModel: payload,
+ }
+ case 'addEditorModel':
+ return {
+ ...state,
+ models: [...state.models, payload],
+ }
+ case 'removeEditorModel':
+ return {
+ ...state,
+ models: [
+ ...state.models.slice(0, payload),
+ ...state.models.slice(payload + 1),
+ ],
+ currentModel: payload === 0 ? 0 : payload - 1,
+ }
+ // case 'updateEditorModel': {
+ // const newModels = [...state.models]
+ // newModels[state.currentModel].content = payload
+ // return {
+ // ...state,
+ // models: newModels,
+ // }
+ // }
+ // case 'reorderEditorModels': {
+ // const newModels = [
+ // ...state.models.slice(0, state.currentModel),
+ // ...state.models.slice(state.currentModel + 1),
+ // ].splice(payload, 0, state.models[state.currentModel])
+ // return {
+ // ...state,
+ // models: newModels,
+ // currentModel: payload,
+ // }
+ // }
default:
return state
}
diff --git a/app/web/src/helpers/hooks/useMarkdownMetaData.ts b/app/web/src/helpers/hooks/useMarkdownMetaData.ts
new file mode 100644
index 0000000..cb9ea3e
--- /dev/null
+++ b/app/web/src/helpers/hooks/useMarkdownMetaData.ts
@@ -0,0 +1,27 @@
+// Extracts YAML frontmatter from Markdown files
+// Gotten from this helpful comment on a react-markdown GitHub Issue: https://github.com/remarkjs/react-markdown/issues/164#issuecomment-890497653
+export function useMarkdownMetaData(text: string): Array {
+ const metaData = {} as any
+ return React.useMemo(() => {
+ const metaRegExp = RegExp(
+ /^---[\r\n](((?!---).|[\r\n])*)[\r\n]---$/m
+ ) as any
+ // get metadata
+ const rawMetaData = metaRegExp.exec(text)
+
+ let keyValues
+
+ if (rawMetaData !== null) {
+ // rawMeta[1] are the stuff between "---"
+ keyValues = rawMetaData[1].split('\n')
+
+ // which returns a list of key values: ["key1: value", "key2: value"]
+ keyValues.forEach((keyValue) => {
+ // split each keyValue to keys and values
+ const [, key, value] = keyValue.split(/(.+): (.+)/)
+ metaData[key] = value.trim()
+ })
+ }
+ return [rawMetaData, metaData]
+ }, [text])
+}
diff --git a/app/web/src/index.css b/app/web/src/index.css
index 31e4b74..a0cd4de 100644
--- a/app/web/src/index.css
+++ b/app/web/src/index.css
@@ -48,19 +48,24 @@
.markdown-overrides {
@apply bg-transparent;
}
+.markdown-overrides,
.markdown-overrides div {
@apply text-ch-gray-300 bg-transparent;
}
+.markdown-overrides a,
.markdown-overrides div a {
@apply text-ch-pink-500 underline bg-transparent;
}
+.markdown-overrides h3,
.markdown-overrides div h3 {
@apply text-xl;
}
+.markdown-overrides h2,
.markdown-overrides div h2 {
@apply text-2xl;
}
+.markdown-overrides h1,
.markdown-overrides div h1 {
@apply text-3xl;
}
diff --git a/app/yarn.lock b/app/yarn.lock
index 20e5d23..02984a2 100644
--- a/app/yarn.lock
+++ b/app/yarn.lock
@@ -15425,7 +15425,7 @@ raw-body@2.4.0:
raw-loader@^4.0.2:
version "4.0.2"
- resolved "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz"
+ resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6"
integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==
dependencies:
loader-utils "^2.0.0"