diff --git a/.vscode/settings.json b/.vscode/settings.json index d71538b..e9d5f61 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "cSpell.words": [ + "Customizer", "Hutten", "cadquery", "jscad", diff --git a/app/web/public/demo-worker.js b/app/web/public/demo-worker.js index ebbb97c..10e086c 100644 --- a/app/web/public/demo-worker.js +++ b/app/web/public/demo-worker.js @@ -246,6 +246,7 @@ const makeScriptWorker = ({callback, convertToSolids})=>{ function runMain(params={}){ let time = Date.now() let solids + let transfer = [] try{ solids = main(params) }catch(e){ @@ -255,7 +256,6 @@ const makeScriptWorker = ({callback, convertToSolids})=>{ let solidsTime = Date.now() - time scriptStats = `generate solids ${solidsTime}ms` - let transfer = [] if(convertToSolids === 'buffers'){ CSGToBuffers.clearCache() entities = solids.map((csg)=>{ @@ -486,7 +486,6 @@ let perspectiveCamera let time = Date.now() renderer(renderOptions) if(updateRender){ - console.log(updateRender, ' first render', Date.now()-time); updateRender = ''; } } @@ -568,7 +567,6 @@ return (params)=>{ const makeRenderWorkerHere = (scope === 'main' && canvas && !renderInWorker) || (scope === 'worker' && render) // worker is in current thread if(makeRenderWorkerHere){ - console.log('render in scope: '+scope); renderWorker = makeRenderWorker({callback:sendCmd}) sendToRender = (params, transfer)=>renderWorker.postMessage(params, transfer) } diff --git a/app/web/src/components/Customizer/Customizer.tsx b/app/web/src/components/Customizer/Customizer.tsx new file mode 100644 index 0000000..d418538 --- /dev/null +++ b/app/web/src/components/Customizer/Customizer.tsx @@ -0,0 +1,100 @@ +import { useRender } from 'src/components/IdeWrapper/useRender' +import { useIdeContext } from 'src/helpers/hooks/useIdeContext' +import { genParams } from 'src/helpers/cadPackages/jsCad/jscadParams' +import { Switch } from '@headlessui/react' +import Svg from 'src/components/Svg/Svg' + +const Customizer = () => { + const [open, setOpen] = React.useState(false) + const [shouldLiveUpdate, setShouldLiveUpdate] = React.useState(false) + const ref = React.useRef() + const jsCadCustomizerElement = ref.current + const { state, thunkDispatch } = useIdeContext() + const customizerParams = state?.customizerParams + const currentParameters = state?.currentParameters + const handleRender = useRender() + + React.useEffect(() => { + if (jsCadCustomizerElement && customizerParams) { + genParams( + customizerParams, + jsCadCustomizerElement, + currentParameters || {}, + (values, source) => { + thunkDispatch({ type: 'setCurrentCustomizerParams', payload: values }) + if (shouldLiveUpdate) { + handleRender() + } + }, + [] + ) + } + }, [ + jsCadCustomizerElement, + customizerParams, + currentParameters, + shouldLiveUpdate, + ]) + if (!state.customizerParams) return null + return ( +
+
+
+ +
Parameters
+
+ {open && ( + <> +
+
Auto Update
+ { + setShouldLiveUpdate + if (newValue) handleRender() + setShouldLiveUpdate(newValue) + }} + className={`${ + shouldLiveUpdate ? 'bg-ch-purple-600' : 'bg-ch-gray-300' + } relative inline-flex items-center h-6 rounded-full w-11 mr-6`} + > + + + +
+ + )} +
+
+
+
+
+ ) +} + +export default Customizer diff --git a/app/web/src/components/DelayedPingAnimation/DelayedPingAnimation.tsx b/app/web/src/components/DelayedPingAnimation/DelayedPingAnimation.tsx new file mode 100644 index 0000000..bfc4995 --- /dev/null +++ b/app/web/src/components/DelayedPingAnimation/DelayedPingAnimation.tsx @@ -0,0 +1,29 @@ + + +let timeoutId = 0 +const DelayedPingAnimation = ({isLoading: isLoading}: {isLoading: boolean}) => { + const [showLoading, setShowLoading] = React.useState(false) + React.useEffect(() => { + if (!isLoading && showLoading) { + setShowLoading(isLoading) + clearTimeout(timeoutId) + } else if (isLoading && !showLoading) { + timeoutId = setTimeout(() => { + setShowLoading(isLoading) + console.log('setloading') + }, 300) as unknown as number + } else if (!isLoading) { + setShowLoading(isLoading) + clearTimeout(timeoutId) + } + }, [isLoading]) + + if (showLoading && isLoading) return ( +
+
+
+ ) + return null +} + +export default DelayedPingAnimation diff --git a/app/web/src/components/EditorMenu/helpers.ts b/app/web/src/components/EditorMenu/helpers.ts index 0ced278..6caaa14 100644 --- a/app/web/src/components/EditorMenu/helpers.ts +++ b/app/web/src/components/EditorMenu/helpers.ts @@ -74,6 +74,7 @@ export const makeStlDownloadHandler = camera: state.camera, quality: 'high', specialCadProcess, + parameters: state.currentParameters, }).then((result) => result && saveFile(result.data)) }) } diff --git a/app/web/src/components/IdeEditor/IdeEditor.tsx b/app/web/src/components/IdeEditor/IdeEditor.tsx index 2a514f5..1a5f6c6 100644 --- a/app/web/src/components/IdeEditor/IdeEditor.tsx +++ b/app/web/src/components/IdeEditor/IdeEditor.tsx @@ -62,6 +62,7 @@ const IdeEditor = ({ Loading }) => { code: state.code, viewerSize: state.viewerSize, camera: state.camera, + parameters: state.currentParameters, }) }) localStorage.setItem(makeCodeStoreKey(state.ideType), state.code) diff --git a/app/web/src/components/IdeViewer/IdeViewer.tsx b/app/web/src/components/IdeViewer/IdeViewer.tsx index 8930b9f..13d1db7 100644 --- a/app/web/src/components/IdeViewer/IdeViewer.tsx +++ b/app/web/src/components/IdeViewer/IdeViewer.tsx @@ -6,6 +6,8 @@ import { Vector3 } from 'three' import { requestRender } from 'src/helpers/hooks/useIdeState' import texture from './dullFrontLitMetal.png' import { TextureLoader } from 'three/src/loaders/TextureLoader' +import Customizer from 'src/components/Customizer/Customizer' + import DelayedPingAnimation from 'src/components/DelayedPingAnimation/DelayedPingAnimation' const loader = new TextureLoader() const colorMap = loader.load(texture) @@ -210,6 +212,7 @@ const IdeViewer = ({ Loading }) => { code: state.code, viewerSize: state.viewerSize, camera, + parameters: state.currentParameters, }) } }) @@ -238,11 +241,8 @@ const IdeViewer = ({ Loading }) => { />
- {state.isLoading && ( -
-
-
- )} + + ) } diff --git a/app/web/src/components/IdeWrapper/useRender.ts b/app/web/src/components/IdeWrapper/useRender.ts index bc82f81..4ac6312 100644 --- a/app/web/src/components/IdeWrapper/useRender.ts +++ b/app/web/src/components/IdeWrapper/useRender.ts @@ -13,6 +13,7 @@ export const useRender = () => { code: state.code, viewerSize: state.viewerSize, camera: state.camera, + parameters: state.currentParameters, }) }) localStorage.setItem(makeCodeStoreKey(state.ideType), state.code) diff --git a/app/web/src/helpers/cadPackages/common.ts b/app/web/src/helpers/cadPackages/common.ts index cbe789a..1a5acc1 100644 --- a/app/web/src/helpers/cadPackages/common.ts +++ b/app/web/src/helpers/cadPackages/common.ts @@ -12,6 +12,7 @@ export const stlToGeometry = (url) => export interface RenderArgs { code: State['code'] + parameters?: RawCustomizerParams settings: { camera: State['camera'] viewerSize: State['viewerSize'] @@ -30,6 +31,12 @@ export interface HealthyResponse { data: any type: 'stl' | 'png' | 'geometry' } + customizerParams?: any[] + currentParameters?: RawCustomizerParams +} + +export interface RawCustomizerParams { + [paramName: string]: number | string | boolean } export function createHealthyResponse({ @@ -37,11 +44,15 @@ export function createHealthyResponse({ data, consoleMessage, type, + customizerParams, + currentParameters, }: { date: Date data: any consoleMessage: string type: HealthyResponse['objectData']['type'] + customizerParams?: any + currentParameters?: any }): HealthyResponse { return { status: 'healthy', @@ -54,6 +65,8 @@ export function createHealthyResponse({ message: consoleMessage, time: date, }, + customizerParams, + currentParameters, } } diff --git a/app/web/src/helpers/cadPackages/index.ts b/app/web/src/helpers/cadPackages/index.ts index 145717e..568e9b9 100644 --- a/app/web/src/helpers/cadPackages/index.ts +++ b/app/web/src/helpers/cadPackages/index.ts @@ -3,7 +3,7 @@ import type { CadPackage } from 'src/helpers/hooks/useIdeState' import openscad from './openScadController' import cadquery from './cadQueryController' -import jscad from './jsCadController' +import jscad from './jsCad/jsCadController' export const cadPackages: { [key in CadPackage]: DefaultKernelExport } = { openscad, diff --git a/app/web/src/helpers/cadPackages/jsCadController.ts b/app/web/src/helpers/cadPackages/jsCad/jsCadController.ts similarity index 66% rename from app/web/src/helpers/cadPackages/jsCadController.ts rename to app/web/src/helpers/cadPackages/jsCad/jsCadController.ts index e26f92e..f0088da 100644 --- a/app/web/src/helpers/cadPackages/jsCadController.ts +++ b/app/web/src/helpers/cadPackages/jsCad/jsCadController.ts @@ -3,7 +3,7 @@ import { DefaultKernelExport, createUnhealthyResponse, createHealthyResponse, -} from './common' +} from '../common' import { MeshPhongMaterial, LineBasicMaterial, @@ -70,6 +70,7 @@ function CSG2Object3D(obj) { } let scriptWorker +let currentParameters = {} const scriptUrl = '/demo-worker.js' let resolveReference = null let response = null @@ -81,25 +82,32 @@ const callResolve = () => { export const render: DefaultKernelExport['render'] = async ({ code, + parameters, settings, }: RenderArgs) => { if (!scriptWorker) { + console.trace( + '************************** creating new worker ************************' + ) const baseURI = document.baseURI.toString() const script = `let baseURI = '${baseURI}' -importScripts(new URL('${scriptUrl}',baseURI)) -let worker = jscadWorker({ - baseURI: baseURI, - scope:'worker', - convertToSolids: 'buffers', - callback:(params)=>self.postMessage(params), -}) -self.addEventListener('message', (e)=>worker.postMessage(e.data)) -` + importScripts(new URL('${scriptUrl}',baseURI)) + let worker = jscadWorker({ + baseURI: baseURI, + scope:'worker', + convertToSolids: 'buffers', + callback:(params)=>self.postMessage(params), + }) + self.addEventListener('message', (e)=>worker.postMessage(e.data)) + ` const blob = new Blob([script], { type: 'text/javascript' }) scriptWorker = new Worker(window.URL.createObjectURL(blob)) + let parameterDefinitions = [] scriptWorker.addEventListener('message', (e) => { const data = e.data - if (data.action == 'entities') { + if (data.action == 'parameterDefinitions') { + parameterDefinitions = data.data + } else if (data.action == 'entities') { if (data.error) { response = createUnhealthyResponse(new Date(), data.error) } else { @@ -108,6 +116,8 @@ self.addEventListener('message', (e)=>worker.postMessage(e.data)) data: [...data.entities.map(CSG2Object3D).filter((o) => o)], consoleMessage: data.scriptStats, date: new Date(), + customizerParams: parameterDefinitions, + currentParameters, }) } callResolve() @@ -118,12 +128,27 @@ self.addEventListener('message', (e)=>worker.postMessage(e.data)) response = null scriptWorker.postMessage({ action: 'init', baseURI, alias: [] }) } - scriptWorker.postMessage({ - action: 'runScript', - worker: 'script', - script: code, - url: 'jscad_script', - }) + + if (parameters && currentParameters && JSON.stringify(parameters) !== JSON.stringify(currentParameters)) { + // we are not evaluating code, but reacting to parameters change + scriptWorker.postMessage({ + action: 'updateParams', + worker: 'script', + params: parameters, + }) + } else { + scriptWorker.postMessage({ + action: 'runScript', + worker: 'script', + script: code, + params: parameters || {}, + url: 'jscad_script', + }) + } + // we need this to keep the form filled with same data when new parameter definitions arrive + // each render of the script could provide new paramaters. In case some of them are still rpesent + // it is expected for them to stay the same and not just reset + currentParameters = parameters || {} const waitResult = new Promise((resolve) => { resolveReference = resolve @@ -131,6 +156,7 @@ self.addEventListener('message', (e)=>worker.postMessage(e.data)) await waitResult resolveReference = null + if (parameters) delete response.customizerParams return response } diff --git a/app/web/src/helpers/cadPackages/jsCad/jscadParams.ts b/app/web/src/helpers/cadPackages/jsCad/jscadParams.ts new file mode 100644 index 0000000..503f9f1 --- /dev/null +++ b/app/web/src/helpers/cadPackages/jsCad/jscadParams.ts @@ -0,0 +1,210 @@ +import type { RawCustomizerParams } from '../common' + +const GROUP_SELECTOR = 'DIV[type="group"]' +const INPUT_SELECTOR = 'INPUT, SELECT' + +function forEachInput( + target: HTMLElement, + callback: (e: HTMLInputElement) => void +) { + target.querySelectorAll(INPUT_SELECTOR).forEach(callback) +} + +function forEachGroup(target: HTMLElement, callback: (e: HTMLElement) => void) { + target.querySelectorAll(GROUP_SELECTOR).forEach(callback) +} + +const numeric = { number: 1, float: 1, int: 1, range: 1, slider: 1 } + +function applyRange(inp) { + const label = inp.previousElementSibling + if (label && label.tagName == 'LABEL') { + const info = label.querySelector('I') + if (info) info.innerHTML = inp.value + } +} + +export function genParams( + defs, + target, + storedParams = {}, + callback: (values: RawCustomizerParams, source: any) => void = undefined, + buttons = ['reset', 'save', 'load', 'edit', 'link'] +) { + const funcs = { + group: () => '', + choice: inputChoice, + radio: inputRadio, + float: inputNumber, + range: inputNumber, + slider: inputNumber, + int: inputNumber, + text: inputNumber, + url: inputNumber, + email: inputNumber, + date: inputNumber, + password: inputNumber, + color: inputNumber, + // TODO radio similar options as choice + checkbox: function ({ name, value }) { + const checkedStr = value === 'checked' || value === true ? 'checked' : '' + return `` + }, + number: inputNumber, + } + + function inputRadio({ name, type, captions, value, values }) { + if (!captions) captions = values + + let ret = '
' + + for (let i = 0; i < values.length; i++) { + const checked = + value == values[i] || value == captions[i] ? 'checked' : '' + ret += `` + } + return ret + '
' + } + + function inputChoice({ name, type, captions, value, values }) { + if (!captions) captions = values + + let ret = `' + } + + function inputNumber(def) { + let { name, type, value, min, max, step, placeholder, live } = def + if (value === null || value === undefined) value = numeric[type] ? 0 : '' + let inputType = type + if (type == 'int' || type == 'float') inputType = 'number' + if (type == 'range' || type == 'slider') inputType = 'range' + let str = `' + } + + let html = '' + let closed = false + const missing = {} + + defs.forEach((def) => { + const { type, caption, name } = def + + if (storedParams[name] !== undefined) { + def.value = storedParams[name] + } else { + def.value = def.initial || def['default'] || def.checked + } + + if (type == 'group') { + closed = def.value == 'closed' + } + def.closed = closed + + html += `
` + + html += `` + if (type == 'checkbox') html += funcs[type](def) + html += `${caption}${def.value}` + + if (funcs[type] && type != 'checkbox') html += funcs[type](def) + + if (!funcs[type]) missing[type] = 1 + + html += '
\n' + }) + + const missingKeys = Object.keys(missing) + if (missingKeys.length) console.log('missing param impl', missingKeys) + + function _callback(source = 'change') { + if (callback && source !== 'group') callback(getParams(target), source) + } + + html += '
' + buttons.forEach((button) => { + const { id, name } = + typeof button === 'string' ? { id: button, name: button } : button + html += `` + }) + html += '
' + + target.innerHTML = html + + forEachInput(target, (inp) => { + const type = inp.type + inp.addEventListener('input', function (evt) { + applyRange(inp) + if (inp.getAttribute('live') === '1') _callback('live') + }) + if (inp.getAttribute('live') !== '1') + inp.addEventListener('change', ()=>_callback('change')) + }) + + function groupClick(evt) { + let groupDiv = evt.target + if (groupDiv.tagName === 'LABEL') groupDiv = groupDiv.parentNode + const closed = groupDiv.getAttribute('closed') == '1' ? '0' : '1' + do { + groupDiv.setAttribute('closed', closed) + groupDiv = groupDiv.nextElementSibling + } while (groupDiv && groupDiv.getAttribute('type') != 'group') + _callback('group') + } + + forEachGroup(target, (div) => { + div.onclick = groupClick + }) +} + +function getParams(target: HTMLElement): RawCustomizerParams { + const params = {} + if (!target) return params + + forEachGroup(target, (elem) => { + const name = elem.getAttribute('name') + params[name] = elem.getAttribute('closed') == '1' ? 'closed' : '' + }) + + forEachInput(target, (elem) => { + const name = elem.name + let value: RawCustomizerParams[string] = elem.value + if (elem.tagName == 'INPUT') { + if (elem.type == 'checkbox') value = elem?.checked + if (elem.type == 'range' || elem.type == 'color') applyRange(elem) + } + + if ( + numeric[elem.getAttribute('type')] || + elem.getAttribute('numeric') == '1' + ) + value = parseFloat(String(value || 0)) + + if (elem.type == 'radio' && !elem.checked) return // skip if not checked radio button + + params[name] = value + }) + return params +} diff --git a/app/web/src/helpers/hooks/use3dViewerResize.ts b/app/web/src/helpers/hooks/use3dViewerResize.ts index 9ed6321..dd5fe24 100644 --- a/app/web/src/helpers/hooks/use3dViewerResize.ts +++ b/app/web/src/helpers/hooks/use3dViewerResize.ts @@ -26,6 +26,7 @@ export const use3dViewerResize = () => { code: state.code, viewerSize: { width, height }, camera: state.camera, + parameters: state.currentParameters, }) } }) diff --git a/app/web/src/helpers/hooks/useIdeState.ts b/app/web/src/helpers/hooks/useIdeState.ts index 9b671be..bd157b1 100644 --- a/app/web/src/helpers/hooks/useIdeState.ts +++ b/app/web/src/helpers/hooks/useIdeState.ts @@ -1,6 +1,7 @@ import { useReducer } from 'react' import { cadPackages } from 'src/helpers/cadPackages' import type { RootState } from '@react-three/fiber' +import type { RawCustomizerParams } from 'src/helpers/cadPackages/common' function withThunk(dispatch, getState) { return (actionOrThunk) => @@ -43,13 +44,14 @@ result = (cq.Workplane().circle(diam).extrude(20.0) show_object(result) `, jscad: ` + const { booleans, colors, primitives } = require('@jscad/modeling') // modeling comes from the included MODELING library const { intersect, subtract } = booleans const { colorize } = colors const { cube, cuboid, line, sphere, star } = primitives -const main = ({scale=1}) => { +const main = ({length=340}) => { const logo = [ colorize([1.0, 0.4, 1.0], subtract( cube({ size: 300 }), @@ -61,13 +63,37 @@ const main = ({scale=1}) => { )) ] - const transpCube = colorize([1, 0, 0, 0.75], cuboid({ size: [100 * scale, 100, 210 + (200 * scale)] })) + const transpCube = colorize([1, 0, 0, 0.75], cuboid({ size: [100, 100, length] })) const star2D = star({ vertices: 8, innerRadius: 150, outerRadius: 200 }) const line2D = colorize([1.0, 0, 0], line([[220, 220], [-220, 220], [-220, -220], [220, -220], [220, 220]])) return [transpCube, star2D, line2D, ...logo] } -module.exports = {main} +const getParameterDefinitions = ()=>{ + return [ + {type:'slider', name:'length', initial:340, caption:'Length', min:210, max:1500}, + { name: 'group1', type: 'group', caption: 'Group 1: Text Entry' }, + { name: 'text', type: 'text', initial: '', size: 20, maxLength: 20, caption: 'Plain Text:', placeholder: '20 characters' }, + { name: 'int', type: 'int', initial: 20, min: 1, max: 100, step: 1, caption: 'Integer:' }, + { name: 'number', type: 'number', initial: 2.0, min: 1.0, max: 10.0, step: 0.1, caption: 'Number:' }, + { name: 'date', type: 'date', initial: '2020-01-01', min: '2020-01-01', max: '2030-12-31', caption: 'Date:', placeholder: 'YYYY-MM-DD' }, + { name: 'email', type: 'email', initial: 'me@example.com', caption: 'Email:' }, + { name: 'url', type: 'url', initial: 'www.example.com', size: 40, maxLength: 40, caption: 'Url:', placeholder: '40 characters' }, + { name: 'password', type: 'password', initial: '', caption: 'Password:' }, + + { name: 'group2', type: 'group', caption: 'Group 2: Interactive Controls' }, + { name: 'checkbox', type: 'checkbox', checked: true, initial: '20', caption: 'Checkbox:' }, + { name: 'color', type: 'color', initial: '#FFB431', caption: 'Color:' }, + { name: 'slider', type: 'slider', initial: 3, min: 1, max: 10, step: 1, caption: 'Slider:' }, + { name: 'choice1', type: 'choice', caption: 'Dropdown Menu:', values: [0, 1, 2, 3], captions: ['No', 'Yes', 'Maybe', 'So so'], initial: 2 }, + { name: 'choice3', type: 'choice', caption: 'Dropdown Menu:', values: ['No', 'Yes', 'Maybe', 'So so'], initial: 'No' }, + { name: 'choice2', type: 'radio', caption: 'Radio Buttons:', values:[0, 1, 2, 3], captions: ['No', 'Yes', 'Maybe', 'So so'], initial: 2 }, + + { name: 'group3', type: 'group', initial: 'closed', caption: 'Group 3: Initially Closed Group' }, + { name: 'checkbox2', type: 'checkbox', checked: true, initial: '20', caption: 'Optional Checkbox:' }, + ] +} +module.exports = {main, getParameterDefinitions} `, } @@ -90,6 +116,8 @@ export interface State { data: any quality: 'low' | 'high' } + customizerParams?: any[] + currentParameters?: RawCustomizerParams layout: any camera: { dist?: number @@ -147,6 +175,7 @@ export const useIdeState = (): [State, (actionOrThunk: any) => any] => { case 'updateCode': return { ...state, code: payload } case 'healthyRender': + const currentParameters = (payload.currentParameters && Object.keys(payload.currentParameters).length) ? payload.currentParameters : state.currentParameters return { ...state, objectData: { @@ -154,6 +183,8 @@ export const useIdeState = (): [State, (actionOrThunk: any) => any] => { type: payload.objectData?.type, data: payload.objectData?.data, }, + customizerParams: payload.customizerParams || state.customizerParams, + currentParameters, consoleMessages: payload.message ? [...state.consoleMessages, payload.message] : payload.message, @@ -167,6 +198,12 @@ export const useIdeState = (): [State, (actionOrThunk: any) => any] => { : payload.message, isLoading: false, } + case 'setCurrentCustomizerParams': + if (!Object.keys(payload).length) return state + return { + ...state, + currentParameters: payload, + } case 'setLayout': return { ...state, @@ -217,6 +254,7 @@ export const useIdeState = (): [State, (actionOrThunk: any) => any] => { interface RequestRenderArgs { state: State dispatch: any + parameters: any code: State['code'] camera: State['camera'] viewerSize: State['viewerSize'] @@ -232,6 +270,7 @@ export const requestRender = ({ viewerSize, quality = 'low', specialCadProcess = null, + parameters, }: RequestRenderArgs) => { if ( state.ideType !== 'INIT' && @@ -242,26 +281,41 @@ export const requestRender = ({ : cadPackages[state.ideType].render return renderFn({ code, + parameters, settings: { camera, viewerSize, quality, }, }) - .then(({ objectData, message, status }) => { - if (status === 'error') { - dispatch({ - type: 'errorRender', - payload: { message }, - }) - } else { - dispatch({ - type: 'healthyRender', - payload: { objectData, message, lastRunCode: code }, - }) - return objectData + .then( + ({ + objectData, + message, + status, + customizerParams, + currentParameters, + }) => { + if (status === 'error') { + dispatch({ + type: 'errorRender', + payload: { message }, + }) + } else { + dispatch({ + type: 'healthyRender', + payload: { + objectData, + message, + lastRunCode: code, + customizerParams, + currentParameters, + }, + }) + return objectData + } } - }) + ) .catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here } } diff --git a/app/web/src/index.css b/app/web/src/index.css index 66183b1..7d3b8c9 100644 --- a/app/web/src/index.css +++ b/app/web/src/index.css @@ -115,3 +115,102 @@ input.error, textarea.error { border: 1px solid red; } +#jscad-customizer-block { + padding-bottom: 60px; /* hack because it gets cut off at the bottom for some reason*/ +} +#jscad-customizer-block > .form-line{ + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 5px 15px; + position: relative; +} +#jscad-customizer-block > .form-line:hover{ + background: rgba(0,0,0,0.3); +} + +#jscad-customizer-block > .form-line[type="group"]{ + background: rgba(255, 255, 255, 0.15); + padding-left: 50px; + padding-bottom: 12px; + cursor: pointer; +} +#jscad-customizer-block > .form-line[type="group"] > label{ + cursor: pointer; +} +#jscad-customizer-block > .form-line[closed="1"]:not([type="group"]){ + display: none; +} + +#jscad-customizer-block > .form-line[type="group"]:before { + position: absolute; + content: ">"; + left: 18px; + top: 13px; + font-size: 30px; + transform: rotate(90deg); + font-family: monospace; + +} +#jscad-customizer-block > .form-line[type="group"][closed="1"]:before { + transform: rotate(0deg); +} + +#jscad-customizer-block > .form-line select, +#jscad-customizer-block > .form-line input[type="text"], +#jscad-customizer-block > .form-line input[type="range"], +#jscad-customizer-block > .form-line input[type="slider"], +#jscad-customizer-block > .form-line input[type="number"], +#jscad-customizer-block > .form-line input[type="int"], +#jscad-customizer-block > .form-line input[type="date"], +#jscad-customizer-block > .form-line input[type="email"], +#jscad-customizer-block > .form-line input[type="url"], +#jscad-customizer-block > .form-line input[type="password"] +{ + background: rgba(73, 73, 73, 0.65); + border: 1px solid #FFFFFF; + width: 50%; + padding: 2px 8px; +} +#jscad-customizer-block > .form-line > div{ + width: 50%; +} +#jscad-customizer-block > .form-line > div[type="radio"] > label{ + display: inline-block; + margin-left: 10px; +} +#jscad-customizer-block > .form-line[type="checkbox"] > label > input{ + position: absolute; + right: 14px; +} + +#jscad-customizer-block > .form-line > label i{ + display: none; + font-style: normal; +} + +#jscad-customizer-block > .form-line[type="range"] > label i, +#jscad-customizer-block > .form-line[type="slider"] > label i, +#jscad-customizer-block > .form-line[type="color"] > label i{ + display: inline-block; +} + +#jscad-customizer-block > .form-line input[type="range"]{ + position: relative; + top: 6px; +} +#jscad-customizer-block > .form-line > label > i{ + position: absolute; + top: 2px; + left: 70%; +} + +#jscad-customizer-block .form-line label{ + font-family: Fira Sans; + font-style: normal; + font-weight: bold; + font-size: 12px; + line-height: 14px; + + color: #CFCFD8; +} \ No newline at end of file