Add demo for cadhub customizer

relates to #437
This commit is contained in:
Kurt Hutten
2021-08-07 11:30:27 +10:00
parent 1336ffc437
commit 0cc335ea9b
9 changed files with 526 additions and 414 deletions

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 = `
// 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"