From 77ee959c431f9397726e5e1eaf83043b74954a2b Mon Sep 17 00:00:00 2001 From: Davor Hrg Date: Mon, 2 Aug 2021 23:24:26 +0200 Subject: [PATCH] working somewhat ok --- app/web/public/demo-worker.js | 4 +- .../src/components/Customizer/Customizer.tsx | 25 +-- .../src/components/IdeWrapper/useRender.ts | 3 +- app/web/src/helpers/cadPackages/common.ts | 7 +- .../helpers/cadPackages/jsCadController.ts | 61 +++++-- .../src/helpers/cadPackages/jscadParams.js | 169 ++++++++++++++++++ app/web/src/helpers/hooks/useIdeState.ts | 20 ++- 7 files changed, 253 insertions(+), 36 deletions(-) create mode 100644 app/web/src/helpers/cadPackages/jscadParams.js diff --git a/app/web/public/demo-worker.js b/app/web/public/demo-worker.js index ebbb97c..d8b18eb 100644 --- a/app/web/public/demo-worker.js +++ b/app/web/public/demo-worker.js @@ -246,6 +246,8 @@ const makeScriptWorker = ({callback, convertToSolids})=>{ function runMain(params={}){ let time = Date.now() let solids + let transfer = [] + console.log('run main') try{ solids = main(params) }catch(e){ @@ -255,7 +257,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)=>{ @@ -549,6 +550,7 @@ return (params)=>{ workerBaseURI = baseURI const sendCmd = (params, transfer)=>{ + console.log('sendCmd', params.worker, params.action, params) if(params.worker === 'render') sendToRender(params, transfer) else if(params.worker === 'script') diff --git a/app/web/src/components/Customizer/Customizer.tsx b/app/web/src/components/Customizer/Customizer.tsx index 84da796..513220d 100644 --- a/app/web/src/components/Customizer/Customizer.tsx +++ b/app/web/src/components/Customizer/Customizer.tsx @@ -1,5 +1,6 @@ -import { useRender } from 'src/components/IdeWrapper/useRender' +import { useRender, } from 'src/components/IdeWrapper/useRender' import { useIdeContext } from 'src/helpers/hooks/useIdeContext' +import { genParams, getParams } from 'src/helpers/cadPackages/jscadParams' const Customizer = () => { const [open, setOpen] = React.useState(true) @@ -7,16 +8,18 @@ const Customizer = () => { const jsCadCustomizerElement = ref.current const { state } = useIdeContext() const customizerParams = state?.objectData?.customizerParams - console.log(state) - React.useEffect(() => { - console.log({ jsCadCustomizerElement, customizerParams }) - if (jsCadCustomizerElement && customizerParams) { - jsCadCustomizerElement.innerHTML = `
${JSON.stringify( - customizerParams - )}
` - } - }, [jsCadCustomizerElement, customizerParams]) + const lastParameters = state?.objectData?.lastParameters const handleRender = useRender() + const handleRender2 = ()=>handleRender(getParams(ref.current)) + + React.useEffect(() => { + console.log({ jsCadCustomizerElement, customizerParams, lastParameters }) + if (jsCadCustomizerElement && customizerParams) { + genParams(customizerParams, jsCadCustomizerElement, lastParameters || {}, (values)=>{ + handleRender(values) + },[]) + } + }, [jsCadCustomizerElement, customizerParams, lastParameters]) return (
{
diff --git a/app/web/src/components/IdeWrapper/useRender.ts b/app/web/src/components/IdeWrapper/useRender.ts index bc82f81..52d233d 100644 --- a/app/web/src/components/IdeWrapper/useRender.ts +++ b/app/web/src/components/IdeWrapper/useRender.ts @@ -3,7 +3,7 @@ import { useIdeContext } from 'src/helpers/hooks/useIdeContext' export const useRender = () => { const { state, thunkDispatch } = useIdeContext() - return () => { + return (parameters) => { thunkDispatch((dispatch, getState) => { const state = getState() dispatch({ type: 'setLoading' }) @@ -13,6 +13,7 @@ export const useRender = () => { code: state.code, viewerSize: state.viewerSize, camera: state.camera, + parameters, }) }) 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 3f5480c..5dab46d 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: any, settings: { camera: State['camera'] viewerSize: State['viewerSize'] @@ -31,6 +32,7 @@ export interface HealthyResponse { type: 'stl' | 'png' | 'geometry' } customizerParams?: any + lastParameters?: any } export function createHealthyResponse({ @@ -39,12 +41,14 @@ export function createHealthyResponse({ consoleMessage, type, customizerParams, -}: { + lastParameters, + }: { date: Date data: any consoleMessage: string type: HealthyResponse['objectData']['type'] customizerParams?: any + lastParameters?: any }): HealthyResponse { return { status: 'healthy', @@ -58,6 +62,7 @@ export function createHealthyResponse({ time: date, }, customizerParams, + lastParameters, } } diff --git a/app/web/src/helpers/cadPackages/jsCadController.ts b/app/web/src/helpers/cadPackages/jsCadController.ts index 02d7124..6178899 100644 --- a/app/web/src/helpers/cadPackages/jsCadController.ts +++ b/app/web/src/helpers/cadPackages/jsCadController.ts @@ -70,6 +70,7 @@ function CSG2Object3D(obj) { } let scriptWorker +let lastParameters = {} const scriptUrl = '/demo-worker.js' let resolveReference = null let response = null @@ -81,50 +82,73 @@ 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') { + console.log('message',data) + parameterDefinitions = data.data + } else if (data.action == 'entities') { if (data.error) { response = createUnhealthyResponse(new Date(), data.error) } else { + console.log('lastParameters',lastParameters) response = createHealthyResponse({ type: 'geometry', data: [...data.entities.map(CSG2Object3D).filter((o) => o)], consoleMessage: data.scriptStats, date: new Date(), - customizerParams: ['param1', 'abc'], + customizerParams: parameterDefinitions, + lastParameters, }) } callResolve() } }) - + callResolve() response = null scriptWorker.postMessage({ action: 'init', baseURI, alias: [] }) } - scriptWorker.postMessage({ - action: 'runScript', - worker: 'script', - script: code, - url: 'jscad_script', - }) + + if(parameters){ + // 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 + lastParameters = parameters || {} const waitResult = new Promise((resolve) => { resolveReference = resolve @@ -132,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/jscadParams.js b/app/web/src/helpers/cadPackages/jscadParams.js new file mode 100644 index 0000000..c72e80d --- /dev/null +++ b/app/web/src/helpers/cadPackages/jscadParams.js @@ -0,0 +1,169 @@ +const GROUP_SELECTOR = 'DIV[type="group"] LABEL' +const INPUT_SELECTOR = 'INPUT, SELECT' + +function forEachInput(target, callback){ + target.querySelectorAll(INPUT_SELECTOR).forEach(callback) +} + +function forEachGroup(target, callback){ + target.querySelectorAll(GROUP_SELECTOR).forEach(callback) +} + +const numeric = {number:1, float:1, int:1, range:1, slider:1} + +function applyRange(inp){ + let label = inp.previousElementSibling + if(label.tagName == 'LABEL'){ + let info = label.querySelector('I') + if(info) info.innerHTML = '('+inp.value+')' + } +} + +export function genParams(defs, target, storedParams={}, callback=undefined, buttons=['reset','save','load','edit','link']){ + + let funcs = { + group:function({name,type, caption, captions, value, min,max}){ + return '' + }, + choice:function({name,type, caption, captions, value, values, min, max}){ + if(!captions) captions = values + + let ret = `' + }, + 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,type, caption, captions, value, checked, min,max}){ + let checkedStr = (value === 'checked' || value === true) ? 'checked':'' + return `` + }, + number: inputNumber, + } + + function inputNumber(def){ + let {name,type, caption, captions, 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' + var str = `'; + } + + let html = ''; + let closed = false + let missing = {} + + defs.forEach(def=>{ + let {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}` + + if(funcs[type] && type != 'checkbox') html += funcs[type](def) + + if(!funcs[type]) missing[type] = 1 + + html +='
\n' + }) + + let missingKeys = Object.keys(missing) + if(missingKeys.length) console.log('missing param impl',missingKeys); + + function _callback(saveOnly){ + if(callback) callback(getParams(target)) + } + + html +='
' + buttons.forEach(b=>{ + if(typeof b === 'string') b = {id:b, name:b} + let {id,name} = b + html += `` + }) + html += '
' + + target.innerHTML = html + + forEachInput(target, inp=>{ + let type = inp.type + inp.addEventListener('input', function(evt){ + applyRange(inp) + if(inp.getAttribute('live') == '1') _callback(); + }) + if(inp.getAttribute('live') != '1') inp.addEventListener('change', _callback) + + }) + + function groupClick(evt){ + var groupDiv = evt.target.parentNode + var closed = (groupDiv.getAttribute('closed') == '1') ? '0':'1' + var name = evt.target.getAttribute('name') + do{ + groupDiv.setAttribute('closed', closed) + groupDiv = groupDiv.nextElementSibling + }while(groupDiv && groupDiv.getAttribute('type') != 'group') + callback(getParams(target),true) + } + + forEachGroup(target, label=>label.onclick=groupClick) +} + +export function getParams(target){ + let params = {} + if(!target) return params + + forEachGroup(target,elem=>{ + let name = elem.getAttribute('name') + params[name] = (groupDiv.getAttribute('closed') == '1') ? 'closed':'' + }) + + forEachInput(target,elem=>{ + let name = elem.name + let value = 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(value || 0) + params[name] = value + }) + return params; +} + diff --git a/app/web/src/helpers/hooks/useIdeState.ts b/app/web/src/helpers/hooks/useIdeState.ts index 9f5e68c..afd9783 100644 --- a/app/web/src/helpers/hooks/useIdeState.ts +++ b/app/web/src/helpers/hooks/useIdeState.ts @@ -43,13 +43,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=1}) => { const logo = [ colorize([1.0, 0.4, 1.0], subtract( cube({ size: 300 }), @@ -61,13 +62,18 @@ 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:210, caption:'Length', min:210, max:1500} + ] +} +module.exports = {main, getParameterDefinitions} `, } @@ -90,6 +96,7 @@ export interface State { data: any quality: 'low' | 'high' customizerParams?: any + lastParameters?: any } layout: any camera: { @@ -155,6 +162,7 @@ export const useIdeState = (): [State, (actionOrThunk: any) => any] => { type: payload.objectData?.type, data: payload.objectData?.data, customizerParams: payload.customizerParams, + lastParameters: payload.lastParameters, }, consoleMessages: payload.message ? [...state.consoleMessages, payload.message] @@ -219,6 +227,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'] @@ -234,6 +243,7 @@ export const requestRender = ({ viewerSize, quality = 'low', specialCadProcess = null, + parameters, }: RequestRenderArgs) => { if ( state.ideType !== 'INIT' && @@ -244,13 +254,14 @@ export const requestRender = ({ : cadPackages[state.ideType].render return renderFn({ code, + parameters, settings: { camera, viewerSize, quality, }, }) - .then(({ objectData, message, status, customizerParams }) => { + .then(({ objectData, message, status, customizerParams, lastParameters }) => { if (status === 'error') { dispatch({ type: 'errorRender', @@ -264,6 +275,7 @@ export const requestRender = ({ message, lastRunCode: code, customizerParams, + lastParameters, }, }) return objectData