CadHub Customizer #461

Merged
Irev-Dev merged 1 commits from kurt/cadhub-customizer-test-code-437 into main 2021-08-21 03:02:56 +02:00
9 changed files with 526 additions and 414 deletions

View File

@@ -1,5 +1,6 @@
{
"cSpell.words": [
"Cadhub",
"Customizer",
"Hutten",
"cadquery",

View File

@@ -1,41 +1,30 @@
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'
import {
CadhubStringParam,
CadhubBooleanParam,
CadhubNumberParam,
} from './customizerConverter'
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 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()
}
},
[]
)
const updateCustomizerParam = (paramName: string, paramValue: any) => {
const payload = {
...currentParameters,
[paramName]: paramValue,
}
}, [
jsCadCustomizerElement,
customizerParams,
currentParameters,
shouldLiveUpdate,
])
if (!state.customizerParams) return null
thunkDispatch({ type: 'setCurrentCustomizerParams', payload })
shouldLiveUpdate && setTimeout(() => handleRender())
}
if (!customizerParams?.length) return null
return (
<div
className={`absolute inset-x-0 bottom-0 bg-ch-gray-600 bg-opacity-60 text-ch-gray-300 text-lg font-fira-sans ${
@@ -59,7 +48,6 @@ const Customizer = () => {
<Switch
checked={shouldLiveUpdate}
onChange={(newValue) => {
setShouldLiveUpdate
if (newValue) handleRender()
setShouldLiveUpdate(newValue)
}}
@@ -86,15 +74,184 @@ const Customizer = () => {
</>
)}
</div>
<div className={`${open ? 'h-full' : 'h-0'} overflow-y-auto px-12`}>
<div
id="jscad-customizer-block"
ref={ref}
// JSCAD param UI injected here.
/>
<div className={`${open ? 'h-full pb-32' : 'h-0'} overflow-y-auto px-12`}>
<div>
{customizerParams.map((param, index) => {
const otherProps = {
value: currentParameters[param.name],
onChange: (value) => updateCustomizerParam(param.name, value),
}
if (param.type === 'string') {
return <StringParam key={index} param={param} {...otherProps} />
} else if (param.type === 'number') {
return <NumberParam key={index} param={param} {...otherProps} />
} else if (param.type === 'boolean') {
return <BooleanParam key={index} param={param} {...otherProps} />
}
return <div key={index}>{JSON.stringify(param)}</div>
})}
</div>
</div>
</div>
)
}
export default Customizer
function CustomizerParamBase({
name,
caption,
children,
}: {
name: string
caption: string
children: React.ReactNode
}) {
return (
<li
className="grid items-center my-2"
style={{ gridTemplateColumns: 'auto 8rem' }}
>
<div className=" text-sm font-fira-sans">
<div className="font-bold text-base">{name}</div>
<div>{caption}</div>
</div>
<div className="w-full">{children}</div>
</li>
)
}
function BooleanParam({
param,
value,
onChange,
}: {
param: CadhubBooleanParam
value: any
onChange: Function
}) {
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
<Switch
checked={value}
onChange={(newValue) => {
onChange(newValue)
}}
className={`${
value ? 'bg-ch-gray-300' : 'bg-ch-gray-600'
} relative inline-flex items-center h-6 rounded-full w-11 mr-6 border border-ch-gray-300`}
>
<span
className={`${
value ? 'translate-x-6' : 'translate-x-1'
} inline-block w-4 h-4 transform bg-white rounded-full`}
/>
</Switch>
</CustomizerParamBase>
)
}
function StringParam({
param,
value,
onChange,
}: {
param: CadhubStringParam
value: any
onChange: Function
}) {
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
<input
className="bg-transparent h-8 border border-ch-gray-300 px-2 text-sm w-full"
type="text"
value={value}
placeholder={param.placeholder}
onChange={({ target }) => onChange(target?.value)}
/>
</CustomizerParamBase>
)
}
function NumberParam({
param,
value,
onChange,
}: {
param: CadhubNumberParam
value: any
onChange: Function
}) {
const [isFocused, isFocusedSetter] = React.useState(false)
const [localValue, localValueSetter] = React.useState(0)
const [isLocked, isLockedSetter] = React.useState(false)
const [pixelsDragged, pixelsDraggedSetter] = React.useState(0)
const step = param.step || 1
const commitChange = () => {
let num = localValue
if (typeof param.step === 'number') {
num = Math.round(num / step) * step
}
if (typeof param.min === 'number') {
num = Math.max(param.min, num)
}
if (typeof param.max === 'number') {
num = Math.min(param.max, num)
}
num = Number(num.toFixed(2))
localValueSetter(num)
onChange(num)
}
React.useEffect(() => {
if (!isFocused) commitChange()
}, [isFocused])
React.useEffect(() => localValueSetter(value), [value])
return (
<CustomizerParamBase name={param.name} caption={param.caption}>
<div className="flex h-8 border border-ch-gray-300">
<input
className={`bg-transparent px-2 text-sm w-full ${
(param.max && param.max < localValue) ||
(param.min && param.min > localValue)
? 'text-red-500'
: ''
}`}
type="number"
value={localValue}
onFocus={() => isFocusedSetter(true)}
onBlur={() => isFocusedSetter(false)}
onKeyDown={({ key }) => key === 'Enter' && commitChange()}
onChange={({ target }) => {
const num = Number(target?.value)
localValueSetter(num)
}}
max={param.max}
min={param.min}
step={step}
/>
<div
className="w-6 border-l border-ch-gray-500 items-center hidden md:flex"
style={{ cursor: 'ew-resize' }}
onMouseDown={({ target }) => {
isLockedSetter(true)
target?.requestPointerLock?.()
pixelsDraggedSetter(localValue)
}}
onMouseUp={() => {
isLockedSetter(false)
document?.exitPointerLock?.()
commitChange()
}}
onMouseMove={({ movementX }) => {
if (isLocked && movementX) {
pixelsDraggedSetter(pixelsDragged + (movementX * step) / 8) // one step per 8 pixels
localValueSetter(Number(pixelsDragged.toFixed(2)))
}
}}
>
<Svg className="w-6" name="switch-horizontal" />
</div>
</div>
</CustomizerParamBase>
)
}

View File

@@ -0,0 +1,120 @@
// CadHub
type CadhubTypeNames = 'number' | 'string' | 'boolean'
interface CadhubParamBase {
type: CadhubTypeNames
caption: string
name: string
}
export interface CadhubStringParam extends CadhubParamBase {
type: 'string'
initial: string
placeholder?: string
maxLength?: number
}
export interface CadhubBooleanParam extends CadhubParamBase {
type: 'boolean'
initial?: boolean
}
export interface CadhubNumberParam extends CadhubParamBase {
type: 'number'
initial: number
min?: number
max?: number
step?: number
decimal?: number
}
export type CadhubParams =
| CadhubStringParam
| CadhubBooleanParam
| CadhubNumberParam
// OpenSCAD
const openscadValues = `
Irev-Dev commented 2021-08-20 14:12:51 +02:00 (Migrated from github.com)
Review

The following is just here to test implementation for OpenSCAD and will be removed later.

The following is just here to test implementation for OpenSCAD and will be removed later.
// slider widget for number with max. value
sliderWithMax =34; // [50]
// slider widget for number in range
sliderWithRange =34; // [10:100]
//step slider for number
stepSlider=2; //[0:5:100]
// slider widget for number in range
sliderCentered =0; // [-10:0.1:10]
// spinbox with step size 1
Spinbox= 5;
// Text box for string
String="hello";
// Text box for string with length 8
String2="length"; //8
//description
Variable = true;
`
const openscadConverted: CadhubParams[] = [
{
type: 'number',
name: 'sliderWithMax',
caption: 'slider widget for number with max. value',
initial: 34,
step: 1,
max: 50,
},
{
type: 'number',
name: 'sliderWithRange',
caption: 'slider widget for number in range',
initial: 34,
step: 1,
min: 10,
max: 100,
},
{
type: 'number',
name: 'stepSlider',
caption: 'step slider for number',
initial: 2,
step: 5,
min: 0,
max: 100,
},
{
type: 'number',
name: 'sliderCentered',
caption: 'slider widget for number in range',
initial: 0,
step: 0.1,
min: -10,
max: 10,
},
{
type: 'number',
name: 'Spinbox',
caption: 'spinbox with step size 1',
initial: 5,
step: 1,
},
{
type: 'string',
name: 'String',
caption: 'Text box for string',
initial: 'hello',
},
{
type: 'string',
name: 'String2',
caption: 'Text box for string with length 8',
initial: 'length',
maxLength: 8,
},
{ type: 'boolean', name: 'Variable', caption: 'description', initial: true },
]

View File

@@ -27,6 +27,7 @@ type SvgNames =
| 'refresh'
| 'save'
| 'share'
| 'switch-horizontal'
| 'terminal'
| 'trash'
| 'x'
@@ -503,6 +504,21 @@ const Svg = ({
/>
</svg>
),
'switch-horizontal': (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={strokeWidth}
d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"
/>
</svg>
),
terminal: (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,5 +1,6 @@
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { State } from 'src/helpers/hooks/useIdeState'
import { CadhubParams } from 'src/components/Customizer/customizerConverter'
export const lambdaBaseURL =
process.env.CAD_LAMBDA_BASE_URL ||
@@ -45,14 +46,12 @@ export function createHealthyResponse({
consoleMessage,
type,
customizerParams,
currentParameters,
}: {
date: Date
data: any
consoleMessage: string
type: HealthyResponse['objectData']['type']
customizerParams?: any
currentParameters?: any
customizerParams?: CadhubParams[]
}): HealthyResponse {
return {
status: 'healthy',
@@ -66,7 +65,6 @@ export function createHealthyResponse({
time: date,
},
customizerParams,
currentParameters,
}
}
@@ -99,6 +97,7 @@ export function createUnhealthyResponse(
export const timeoutErrorMessage = `timeout: We're currently limited to a 30s execution time. You can try again, sometimes it works the second time`
export type RenderResponse = HealthyResponse | ErrorResponse
export interface DefaultKernelExport {
render: (arg: RenderArgs) => Promise<HealthyResponse | ErrorResponse>
render: (arg: RenderArgs) => Promise<RenderResponse>
}

View File

@@ -1,6 +1,7 @@
import {
RenderArgs,
DefaultKernelExport,
RenderResponse,
createUnhealthyResponse,
createHealthyResponse,
} from '../common'
@@ -14,6 +15,7 @@ import {
Color,
Mesh,
} from 'three'
import { jsCadToCadhubParams } from './jscadParams'
const materials = {
mesh: {
@@ -70,15 +72,46 @@ function CSG2Object3D(obj) {
}
let scriptWorker
let currentParameters = {}
const scriptUrl = '/demo-worker.js'
let resolveReference = null
let response = null
const callResolve = () => {
if (resolveReference) resolveReference()
resolveReference
type ResolveFn = (RenderResponse) => void
class WorkerHelper {
callResolve: null | ResolveFn = null
previousCode = ''
resolver = (response: RenderResponse) => {
this.callResolve && this.callResolve(response)
this.callResolve = null
}
render = (
code: string,
parameters: { [key: string]: any }
): Promise<RenderResponse> => {
const response: Promise<RenderResponse> = new Promise(
(resolve: ResolveFn) => {
this.callResolve = resolve
}
)
if (!(code && this.previousCode !== code)) {
// 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',
})
Irev-Dev commented 2021-08-20 14:20:33 +02:00 (Migrated from github.com)
Review

fundamentally i haven't changed how your code works, I just wanted to group things a little better in this file as I found some of the module level variables hard to keep track of and so instead moved some of the stuff that required state into this class. Plus one other thing that made me uncomfortable was how the function awaited a promise just to return another module level variable response.

This render function now sets up promise -> posts a message to the worker -> then returns the promise where the promise now contains the final response. I at least personally find it easier to reason about.

fundamentally i haven't changed how your code works, I just wanted to group things a little better in this file as I found some of the module level variables hard to keep track of and so instead moved some of the stuff that required state into this class. Plus one other thing that made me uncomfortable was how the function awaited a promise just to return another module level variable `response`. This render function now sets up promise -> posts a message to the worker -> then returns the promise where the promise now contains the final response. I at least personally find it easier to reason about.
hrgdavor commented 2021-08-20 15:41:41 +02:00 (Migrated from github.com)
Review

yes, it looks cleaner. thnx :)

yes, it looks cleaner. thnx :)
}
this.previousCode = code
return response
}
}
const workerHelper = new WorkerHelper()
export const render: DefaultKernelExport['render'] = async ({
code,
@@ -86,10 +119,8 @@ export const render: DefaultKernelExport['render'] = async ({
settings,
}: RenderArgs) => {
if (!scriptWorker) {
console.trace(
'************************** creating new worker ************************'
)
const baseURI = document.baseURI.toString()
const scriptUrl = '/demo-worker.js'
const script = `let baseURI = '${baseURI}'
importScripts(new URL('${scriptUrl}',baseURI))
let worker = jscadWorker({
@@ -103,65 +134,31 @@ export const render: DefaultKernelExport['render'] = async ({
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
scriptWorker.addEventListener('message', ({ data }) => {
if (data.action == 'parameterDefinitions') {
parameterDefinitions = data.data
} else if (data.action == 'entities') {
if (data.error) {
response = createUnhealthyResponse(new Date(), data.error)
workerHelper.resolver(createUnhealthyResponse(new Date(), data.error))
} else {
response = createHealthyResponse({
type: 'geometry',
data: [...data.entities.map(CSG2Object3D).filter((o) => o)],
consoleMessage: data.scriptStats,
date: new Date(),
customizerParams: parameterDefinitions,
currentParameters,
})
workerHelper.resolver(
createHealthyResponse({
type: 'geometry',
data: data.entities.map(CSG2Object3D).filter((o) => o),
consoleMessage: data.scriptStats,
date: new Date(),
customizerParams: jsCadToCadhubParams(parameterDefinitions || []),
})
)
}
callResolve()
}
})
callResolve()
response = null
workerHelper.resolver()
scriptWorker.postMessage({ action: 'init', baseURI, alias: [] })
}
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
})
await waitResult
resolveReference = null
if (parameters) delete response.customizerParams
return response
return workerHelper.render(code, parameters)
}
const jsCadController: DefaultKernelExport = {

View File

@@ -1,216 +1,130 @@
import type { RawCustomizerParams } from '../common'
import { CadhubParams } from 'src/components/Customizer/customizerConverter'
const GROUP_SELECTOR = 'DIV[type="group"]'
const INPUT_SELECTOR = 'INPUT, SELECT'
type JscadTypeNames =
| 'group'
| 'text'
| 'int'
| 'number'
| 'slider'
| 'email'
| 'password'
| 'date'
| 'url'
| 'checkbox'
| 'color'
| 'choice'
| 'radio'
function forEachInput(
target: HTMLElement,
callback: (e: HTMLInputElement) => void
) {
target.querySelectorAll(INPUT_SELECTOR).forEach(callback)
interface JscadParamBase {
type: JscadTypeNames
caption: string
name: string
}
interface JscadGroupParam extends JscadParamBase {
type: 'group'
initial?: 'open' | 'closed'
}
interface JscadTextParam extends JscadParamBase {
type: 'text'
initial: string
placeholder: string
size: number
maxLength: number
}
interface JscadIntNumberSliderParam extends JscadParamBase {
type: 'int' | 'number' | 'slider'
initial: number
min?: number
max?: number
step?: number
}
interface JscadDateParam extends JscadParamBase {
type: 'date'
initial: string
min: string
max: string
placeholder: string
}
interface JscadEmailPasswordColorParam extends JscadParamBase {
type: 'email' | 'password' | 'color'
initial: string
}
interface JscadUrlParam extends JscadParamBase {
type: 'url'
initial: string
maxLength: number
size: number
placeholder: string
}
interface JscadCheckboxParam extends JscadParamBase {
type: 'checkbox'
checked: boolean
initial: boolean
}
interface JscadChoiceRadioParam extends JscadParamBase {
type: 'choice' | 'radio'
initial: number | string
values: (string | number)[]
captions?: string[]
}
function forEachGroup(target: HTMLElement, callback: (e: HTMLElement) => void) {
target.querySelectorAll(GROUP_SELECTOR).forEach(callback)
}
type JsCadParams =
| JscadGroupParam
| JscadTextParam
| JscadIntNumberSliderParam
| JscadDateParam
| JscadEmailPasswordColorParam
| JscadUrlParam
| JscadCheckboxParam
| JscadChoiceRadioParam
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 `<input type="checkbox" name="${name}" ${checkedStr}/>`
},
number: inputNumber,
}
function inputRadio({ name, type, captions, value, values }) {
if (!captions) captions = values
let ret = '<div type="radio">'
for (let i = 0; i < values.length; i++) {
const checked =
value == values[i] || value == captions[i] ? 'checked' : ''
ret += `<label><input type="radio" _type="${type}" name="${name}" numeric="${
typeof values[0] == 'number' ? '1' : '0'
}" value="${values[i]}" ${checked}/>${captions[i]}</label>`
}
return ret + '</div>'
}
function inputChoice({ name, type, captions, value, values }) {
if (!captions) captions = values
let ret = `<select _type="${type}" name="${name}" numeric="${
typeof values[0] == 'number' ? '1' : '0'
}">`
for (let i = 0; i < values.length; i++) {
const checked =
value == values[i] || value == captions[i] ? 'selected' : ''
ret += `<option value="${values[i]}" ${checked}>${captions[i]}</option>`
}
return ret + '</select>'
}
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 = `<input _type="${type}" type="${inputType}" name="${name}"`
if (step !== undefined) str += ` step="${step || ''}"`
if (min !== undefined) str += ` min="${min || ''}"`
if (max !== undefined) str += ` max="${max || ''}"`
if (value !== undefined) str += ` value="${value}"`
str += ` live="${live ? 1 : 0}"`
if (placeholder !== undefined) str += ` placeholder="${placeholder}"`
return 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 += `<div class="form-line" type="${def.type}" closed="${
closed ? 1 : 0
}" `
if (type == 'group') html += ` name="${name}"`
html += `">`
html += `<label`
if (type == 'group') html += ` name="${name}"`
html += `>`
if (type == 'checkbox') html += funcs[type](def)
html += `${caption}<i>${def.value}</i></label>`
if (funcs[type] && type != 'checkbox') html += funcs[type](def)
if (!funcs[type]) missing[type] = 1
html += '</div>\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 += '<div class="jscad-param-buttons"><div>'
buttons.forEach((button) => {
const { id, name } =
typeof button === 'string' ? { id: button, name: button } : button
html += `<button action="${id}"><b>${name}</b></button>`
})
html += '</div></div>'
target.innerHTML = html
forEachInput(target, (inp) => {
const type = inp.type
inp.addEventListener('input', function (evt) {
applyRange(inp)
if (inp.getAttribute('live') === '1') _callback('live')
export function jsCadToCadhubParams(input: JsCadParams[]): CadhubParams[] {
return input
.map((param): CadhubParams => {
switch (param.type) {
case 'slider':
case 'number':
case 'int':
return {
type: 'number',
caption: param.caption,
name: param.name,
initial: param.initial,
min: param.min,
max: param.max,
step: param.step,
decimal: param.step % 1 === 0 && param.initial % 1 === 0 ? 0 : 2,
}
case 'text':
case 'url':
case 'email':
case 'password':
case 'color':
case 'date':
return {
type: 'string',
caption: param.caption,
name: param.name,
initial: param.initial,
placeholder:
param.type === 'text' ||
param.type === 'date' ||
param.type === 'url'
? param.placeholder
: '',
maxLength:
param.type === 'text' || param.type === 'url'
? param.maxLength
: undefined,
}
case 'checkbox':
return {
type: 'boolean',
caption: param.caption,
name: param.name,
initial: !!param.initial,
}
}
})
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))
} else if (
value &&
typeof value === 'string' &&
/^(\d+|\d+\.\d+)$/.test(value.trim())
) {
value = parseFloat(String(value || 0))
}
if (elem.type == 'radio' && !elem.checked) return // skip if not checked radio button
params[name] = value
})
return params
.filter((a) => a)
}

View File

@@ -2,6 +2,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'
import { CadhubParams } from 'src/components/Customizer/customizerConverter'
function withThunk(dispatch, getState) {
return (actionOrThunk) =>
@@ -69,14 +70,14 @@ const main = ({length=200}) => {
return [transpCube, star2D, line2D, ...logo]
}
const getParameterDefinitions = ()=>{
const getParameterDefinitions = () => {
return [
{type:'slider', name:'length', initial:200, caption:'Length', min:100, max:500},
{ 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: 'text', type: 'text', initial: '', size: 20, maxLength: 20, caption: 'Hooks “thickness” = object\s width = prints height', 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: 'date', type: 'date', initial: '2020-01-01', min: '2020-01-01', max: '2030-12-31', caption: 'Choose between classic hook with screw holes (0) or “bracket” system (1)', 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:' },
@@ -116,7 +117,7 @@ export interface State {
data: any
quality: 'low' | 'high'
}
customizerParams?: any[]
customizerParams: CadhubParams[]
currentParameters?: RawCustomizerParams
layout: any
camera: {
@@ -152,6 +153,7 @@ export const initialState: State = {
data: null,
quality: 'low',
},
customizerParams: [],
layout: initialLayout,
camera: {},
viewerSize: { width: 0, height: 0 },
@@ -175,11 +177,17 @@ 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
const customizerParams: CadhubParams[] = payload?.customizerParams
?.length
? payload.customizerParams
: state.customizerParams
const currentParameters = {}
customizerParams.forEach((param) => {
currentParameters[param.name] =
typeof state?.currentParameters?.[param.name] !== 'undefined'
? state?.currentParameters?.[param.name]
: param.initial
})
return {
...state,
objectData: {
@@ -187,7 +195,7 @@ export const useIdeState = (): [State, (actionOrThunk: any) => any] => {
type: payload.objectData?.type,
data: payload.objectData?.data,
},
customizerParams: payload.customizerParams || state.customizerParams,
customizerParams,
currentParameters,
consoleMessages: payload.message
? [...state.consoleMessages, payload.message]
@@ -203,7 +211,7 @@ export const useIdeState = (): [State, (actionOrThunk: any) => any] => {
isLoading: false,
}
case 'setCurrentCustomizerParams':
if (!Object.keys(payload).length) return state
if (!Object.keys(payload || {}).length) return state
return {
...state,
currentParameters: payload,

View File

@@ -114,103 +114,3 @@ label {
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;
}