Merge pull request #440 from Irev-Dev/kurt/customizer-start-320
jscad customizer with non-react component
This commit was merged in pull request #440.
This commit is contained in:
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
|
"Customizer",
|
||||||
"Hutten",
|
"Hutten",
|
||||||
"cadquery",
|
"cadquery",
|
||||||
"jscad",
|
"jscad",
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ const makeScriptWorker = ({callback, convertToSolids})=>{
|
|||||||
function runMain(params={}){
|
function runMain(params={}){
|
||||||
let time = Date.now()
|
let time = Date.now()
|
||||||
let solids
|
let solids
|
||||||
|
let transfer = []
|
||||||
try{
|
try{
|
||||||
solids = main(params)
|
solids = main(params)
|
||||||
}catch(e){
|
}catch(e){
|
||||||
@@ -255,7 +256,6 @@ const makeScriptWorker = ({callback, convertToSolids})=>{
|
|||||||
let solidsTime = Date.now() - time
|
let solidsTime = Date.now() - time
|
||||||
scriptStats = `generate solids ${solidsTime}ms`
|
scriptStats = `generate solids ${solidsTime}ms`
|
||||||
|
|
||||||
let transfer = []
|
|
||||||
if(convertToSolids === 'buffers'){
|
if(convertToSolids === 'buffers'){
|
||||||
CSGToBuffers.clearCache()
|
CSGToBuffers.clearCache()
|
||||||
entities = solids.map((csg)=>{
|
entities = solids.map((csg)=>{
|
||||||
@@ -486,7 +486,6 @@ let perspectiveCamera
|
|||||||
let time = Date.now()
|
let time = Date.now()
|
||||||
renderer(renderOptions)
|
renderer(renderOptions)
|
||||||
if(updateRender){
|
if(updateRender){
|
||||||
console.log(updateRender, ' first render', Date.now()-time);
|
|
||||||
updateRender = '';
|
updateRender = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -568,7 +567,6 @@ return (params)=>{
|
|||||||
const makeRenderWorkerHere = (scope === 'main' && canvas && !renderInWorker) || (scope === 'worker' && render)
|
const makeRenderWorkerHere = (scope === 'main' && canvas && !renderInWorker) || (scope === 'worker' && render)
|
||||||
// worker is in current thread
|
// worker is in current thread
|
||||||
if(makeRenderWorkerHere){
|
if(makeRenderWorkerHere){
|
||||||
console.log('render in scope: '+scope);
|
|
||||||
renderWorker = makeRenderWorker({callback:sendCmd})
|
renderWorker = makeRenderWorker({callback:sendCmd})
|
||||||
sendToRender = (params, transfer)=>renderWorker.postMessage(params, transfer)
|
sendToRender = (params, transfer)=>renderWorker.postMessage(params, transfer)
|
||||||
}
|
}
|
||||||
|
|||||||
100
app/web/src/components/Customizer/Customizer.tsx
Normal file
100
app/web/src/components/Customizer/Customizer.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
className={`absolute inset-x-0 bottom-0 bg-ch-gray-600 bg-opacity-60 text-ch-gray-300 text-lg font-fira-sans ${
|
||||||
|
open ? 'h-2/3' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between px-6 py-2 items-center">
|
||||||
|
<div className="flex gap-6 items-center">
|
||||||
|
<button className="px-2" onClick={() => setOpen(!open)}>
|
||||||
|
<Svg
|
||||||
|
name="chevron-down"
|
||||||
|
className={`h-8 w-8 ${!open && 'transform rotate-180'}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div>Parameters</div>
|
||||||
|
</div>
|
||||||
|
{open && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="font-fira-sans text-sm mr-4">Auto Update</div>
|
||||||
|
<Switch
|
||||||
|
checked={shouldLiveUpdate}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
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`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
shouldLiveUpdate ? 'translate-x-6' : 'translate-x-1'
|
||||||
|
} inline-block w-4 h-4 transform bg-white rounded-full`}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
<button
|
||||||
|
className={`px-4 py-1 rounded bg-ch-gray-300 text-ch-gray-800 ${
|
||||||
|
shouldLiveUpdate && 'bg-opacity-30 cursor-default'
|
||||||
|
}`}
|
||||||
|
onClick={handleRender}
|
||||||
|
disabled={shouldLiveUpdate}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Customizer
|
||||||
@@ -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 (
|
||||||
|
<div className="inset-0 absolute flex items-center justify-center">
|
||||||
|
<div className="h-16 w-16 bg-pink-600 rounded-full animate-ping"></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DelayedPingAnimation
|
||||||
@@ -74,6 +74,7 @@ export const makeStlDownloadHandler =
|
|||||||
camera: state.camera,
|
camera: state.camera,
|
||||||
quality: 'high',
|
quality: 'high',
|
||||||
specialCadProcess,
|
specialCadProcess,
|
||||||
|
parameters: state.currentParameters,
|
||||||
}).then((result) => result && saveFile(result.data))
|
}).then((result) => result && saveFile(result.data))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ const IdeEditor = ({ Loading }) => {
|
|||||||
code: state.code,
|
code: state.code,
|
||||||
viewerSize: state.viewerSize,
|
viewerSize: state.viewerSize,
|
||||||
camera: state.camera,
|
camera: state.camera,
|
||||||
|
parameters: state.currentParameters,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
localStorage.setItem(makeCodeStoreKey(state.ideType), state.code)
|
localStorage.setItem(makeCodeStoreKey(state.ideType), state.code)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { Vector3 } from 'three'
|
|||||||
import { requestRender } from 'src/helpers/hooks/useIdeState'
|
import { requestRender } from 'src/helpers/hooks/useIdeState'
|
||||||
import texture from './dullFrontLitMetal.png'
|
import texture from './dullFrontLitMetal.png'
|
||||||
import { TextureLoader } from 'three/src/loaders/TextureLoader'
|
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 loader = new TextureLoader()
|
||||||
const colorMap = loader.load(texture)
|
const colorMap = loader.load(texture)
|
||||||
@@ -210,6 +212,7 @@ const IdeViewer = ({ Loading }) => {
|
|||||||
code: state.code,
|
code: state.code,
|
||||||
viewerSize: state.viewerSize,
|
viewerSize: state.viewerSize,
|
||||||
camera,
|
camera,
|
||||||
|
parameters: state.currentParameters,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -238,11 +241,8 @@ const IdeViewer = ({ Loading }) => {
|
|||||||
/>
|
/>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</div>
|
</div>
|
||||||
{state.isLoading && (
|
<DelayedPingAnimation isLoading={state.isLoading} />
|
||||||
<div className="inset-0 absolute flex items-center justify-center">
|
<Customizer />
|
||||||
<div className="h-16 w-16 bg-pink-600 rounded-full animate-ping"></div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const useRender = () => {
|
|||||||
code: state.code,
|
code: state.code,
|
||||||
viewerSize: state.viewerSize,
|
viewerSize: state.viewerSize,
|
||||||
camera: state.camera,
|
camera: state.camera,
|
||||||
|
parameters: state.currentParameters,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
localStorage.setItem(makeCodeStoreKey(state.ideType), state.code)
|
localStorage.setItem(makeCodeStoreKey(state.ideType), state.code)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const stlToGeometry = (url) =>
|
|||||||
|
|
||||||
export interface RenderArgs {
|
export interface RenderArgs {
|
||||||
code: State['code']
|
code: State['code']
|
||||||
|
parameters?: RawCustomizerParams
|
||||||
settings: {
|
settings: {
|
||||||
camera: State['camera']
|
camera: State['camera']
|
||||||
viewerSize: State['viewerSize']
|
viewerSize: State['viewerSize']
|
||||||
@@ -30,6 +31,12 @@ export interface HealthyResponse {
|
|||||||
data: any
|
data: any
|
||||||
type: 'stl' | 'png' | 'geometry'
|
type: 'stl' | 'png' | 'geometry'
|
||||||
}
|
}
|
||||||
|
customizerParams?: any[]
|
||||||
|
currentParameters?: RawCustomizerParams
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RawCustomizerParams {
|
||||||
|
[paramName: string]: number | string | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHealthyResponse({
|
export function createHealthyResponse({
|
||||||
@@ -37,11 +44,15 @@ export function createHealthyResponse({
|
|||||||
data,
|
data,
|
||||||
consoleMessage,
|
consoleMessage,
|
||||||
type,
|
type,
|
||||||
|
customizerParams,
|
||||||
|
currentParameters,
|
||||||
}: {
|
}: {
|
||||||
date: Date
|
date: Date
|
||||||
data: any
|
data: any
|
||||||
consoleMessage: string
|
consoleMessage: string
|
||||||
type: HealthyResponse['objectData']['type']
|
type: HealthyResponse['objectData']['type']
|
||||||
|
customizerParams?: any
|
||||||
|
currentParameters?: any
|
||||||
}): HealthyResponse {
|
}): HealthyResponse {
|
||||||
return {
|
return {
|
||||||
status: 'healthy',
|
status: 'healthy',
|
||||||
@@ -54,6 +65,8 @@ export function createHealthyResponse({
|
|||||||
message: consoleMessage,
|
message: consoleMessage,
|
||||||
time: date,
|
time: date,
|
||||||
},
|
},
|
||||||
|
customizerParams,
|
||||||
|
currentParameters,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { CadPackage } from 'src/helpers/hooks/useIdeState'
|
|||||||
|
|
||||||
import openscad from './openScadController'
|
import openscad from './openScadController'
|
||||||
import cadquery from './cadQueryController'
|
import cadquery from './cadQueryController'
|
||||||
import jscad from './jsCadController'
|
import jscad from './jsCad/jsCadController'
|
||||||
|
|
||||||
export const cadPackages: { [key in CadPackage]: DefaultKernelExport } = {
|
export const cadPackages: { [key in CadPackage]: DefaultKernelExport } = {
|
||||||
openscad,
|
openscad,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
DefaultKernelExport,
|
DefaultKernelExport,
|
||||||
createUnhealthyResponse,
|
createUnhealthyResponse,
|
||||||
createHealthyResponse,
|
createHealthyResponse,
|
||||||
} from './common'
|
} from '../common'
|
||||||
import {
|
import {
|
||||||
MeshPhongMaterial,
|
MeshPhongMaterial,
|
||||||
LineBasicMaterial,
|
LineBasicMaterial,
|
||||||
@@ -70,6 +70,7 @@ function CSG2Object3D(obj) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let scriptWorker
|
let scriptWorker
|
||||||
|
let currentParameters = {}
|
||||||
const scriptUrl = '/demo-worker.js'
|
const scriptUrl = '/demo-worker.js'
|
||||||
let resolveReference = null
|
let resolveReference = null
|
||||||
let response = null
|
let response = null
|
||||||
@@ -81,25 +82,32 @@ const callResolve = () => {
|
|||||||
|
|
||||||
export const render: DefaultKernelExport['render'] = async ({
|
export const render: DefaultKernelExport['render'] = async ({
|
||||||
code,
|
code,
|
||||||
|
parameters,
|
||||||
settings,
|
settings,
|
||||||
}: RenderArgs) => {
|
}: RenderArgs) => {
|
||||||
if (!scriptWorker) {
|
if (!scriptWorker) {
|
||||||
|
console.trace(
|
||||||
|
'************************** creating new worker ************************'
|
||||||
|
)
|
||||||
const baseURI = document.baseURI.toString()
|
const baseURI = document.baseURI.toString()
|
||||||
const script = `let baseURI = '${baseURI}'
|
const script = `let baseURI = '${baseURI}'
|
||||||
importScripts(new URL('${scriptUrl}',baseURI))
|
importScripts(new URL('${scriptUrl}',baseURI))
|
||||||
let worker = jscadWorker({
|
let worker = jscadWorker({
|
||||||
baseURI: baseURI,
|
baseURI: baseURI,
|
||||||
scope:'worker',
|
scope:'worker',
|
||||||
convertToSolids: 'buffers',
|
convertToSolids: 'buffers',
|
||||||
callback:(params)=>self.postMessage(params),
|
callback:(params)=>self.postMessage(params),
|
||||||
})
|
})
|
||||||
self.addEventListener('message', (e)=>worker.postMessage(e.data))
|
self.addEventListener('message', (e)=>worker.postMessage(e.data))
|
||||||
`
|
`
|
||||||
const blob = new Blob([script], { type: 'text/javascript' })
|
const blob = new Blob([script], { type: 'text/javascript' })
|
||||||
scriptWorker = new Worker(window.URL.createObjectURL(blob))
|
scriptWorker = new Worker(window.URL.createObjectURL(blob))
|
||||||
|
let parameterDefinitions = []
|
||||||
scriptWorker.addEventListener('message', (e) => {
|
scriptWorker.addEventListener('message', (e) => {
|
||||||
const data = e.data
|
const data = e.data
|
||||||
if (data.action == 'entities') {
|
if (data.action == 'parameterDefinitions') {
|
||||||
|
parameterDefinitions = data.data
|
||||||
|
} else if (data.action == 'entities') {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
response = createUnhealthyResponse(new Date(), data.error)
|
response = createUnhealthyResponse(new Date(), data.error)
|
||||||
} else {
|
} else {
|
||||||
@@ -108,6 +116,8 @@ self.addEventListener('message', (e)=>worker.postMessage(e.data))
|
|||||||
data: [...data.entities.map(CSG2Object3D).filter((o) => o)],
|
data: [...data.entities.map(CSG2Object3D).filter((o) => o)],
|
||||||
consoleMessage: data.scriptStats,
|
consoleMessage: data.scriptStats,
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
|
customizerParams: parameterDefinitions,
|
||||||
|
currentParameters,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
callResolve()
|
callResolve()
|
||||||
@@ -118,12 +128,27 @@ self.addEventListener('message', (e)=>worker.postMessage(e.data))
|
|||||||
response = null
|
response = null
|
||||||
scriptWorker.postMessage({ action: 'init', baseURI, alias: [] })
|
scriptWorker.postMessage({ action: 'init', baseURI, alias: [] })
|
||||||
}
|
}
|
||||||
scriptWorker.postMessage({
|
|
||||||
action: 'runScript',
|
if (parameters && currentParameters && JSON.stringify(parameters) !== JSON.stringify(currentParameters)) {
|
||||||
worker: 'script',
|
// we are not evaluating code, but reacting to parameters change
|
||||||
script: code,
|
scriptWorker.postMessage({
|
||||||
url: 'jscad_script',
|
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) => {
|
const waitResult = new Promise((resolve) => {
|
||||||
resolveReference = resolve
|
resolveReference = resolve
|
||||||
@@ -131,6 +156,7 @@ self.addEventListener('message', (e)=>worker.postMessage(e.data))
|
|||||||
|
|
||||||
await waitResult
|
await waitResult
|
||||||
resolveReference = null
|
resolveReference = null
|
||||||
|
if (parameters) delete response.customizerParams
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
210
app/web/src/helpers/cadPackages/jsCad/jscadParams.ts
Normal file
210
app/web/src/helpers/cadPackages/jsCad/jscadParams.ts
Normal file
@@ -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 `<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')
|
||||||
|
})
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ export const use3dViewerResize = () => {
|
|||||||
code: state.code,
|
code: state.code,
|
||||||
viewerSize: { width, height },
|
viewerSize: { width, height },
|
||||||
camera: state.camera,
|
camera: state.camera,
|
||||||
|
parameters: state.currentParameters,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useReducer } from 'react'
|
import { useReducer } from 'react'
|
||||||
import { cadPackages } from 'src/helpers/cadPackages'
|
import { cadPackages } from 'src/helpers/cadPackages'
|
||||||
import type { RootState } from '@react-three/fiber'
|
import type { RootState } from '@react-three/fiber'
|
||||||
|
import type { RawCustomizerParams } from 'src/helpers/cadPackages/common'
|
||||||
|
|
||||||
function withThunk(dispatch, getState) {
|
function withThunk(dispatch, getState) {
|
||||||
return (actionOrThunk) =>
|
return (actionOrThunk) =>
|
||||||
@@ -43,13 +44,14 @@ result = (cq.Workplane().circle(diam).extrude(20.0)
|
|||||||
show_object(result)
|
show_object(result)
|
||||||
`,
|
`,
|
||||||
jscad: `
|
jscad: `
|
||||||
|
|
||||||
const { booleans, colors, primitives } = require('@jscad/modeling') // modeling comes from the included MODELING library
|
const { booleans, colors, primitives } = require('@jscad/modeling') // modeling comes from the included MODELING library
|
||||||
|
|
||||||
const { intersect, subtract } = booleans
|
const { intersect, subtract } = booleans
|
||||||
const { colorize } = colors
|
const { colorize } = colors
|
||||||
const { cube, cuboid, line, sphere, star } = primitives
|
const { cube, cuboid, line, sphere, star } = primitives
|
||||||
|
|
||||||
const main = ({scale=1}) => {
|
const main = ({length=340}) => {
|
||||||
const logo = [
|
const logo = [
|
||||||
colorize([1.0, 0.4, 1.0], subtract(
|
colorize([1.0, 0.4, 1.0], subtract(
|
||||||
cube({ size: 300 }),
|
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 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]]))
|
const line2D = colorize([1.0, 0, 0], line([[220, 220], [-220, 220], [-220, -220], [220, -220], [220, 220]]))
|
||||||
|
|
||||||
return [transpCube, star2D, line2D, ...logo]
|
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
|
data: any
|
||||||
quality: 'low' | 'high'
|
quality: 'low' | 'high'
|
||||||
}
|
}
|
||||||
|
customizerParams?: any[]
|
||||||
|
currentParameters?: RawCustomizerParams
|
||||||
layout: any
|
layout: any
|
||||||
camera: {
|
camera: {
|
||||||
dist?: number
|
dist?: number
|
||||||
@@ -147,6 +175,7 @@ export const useIdeState = (): [State, (actionOrThunk: any) => any] => {
|
|||||||
case 'updateCode':
|
case 'updateCode':
|
||||||
return { ...state, code: payload }
|
return { ...state, code: payload }
|
||||||
case 'healthyRender':
|
case 'healthyRender':
|
||||||
|
const currentParameters = (payload.currentParameters && Object.keys(payload.currentParameters).length) ? payload.currentParameters : state.currentParameters
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
objectData: {
|
objectData: {
|
||||||
@@ -154,6 +183,8 @@ export const useIdeState = (): [State, (actionOrThunk: any) => any] => {
|
|||||||
type: payload.objectData?.type,
|
type: payload.objectData?.type,
|
||||||
data: payload.objectData?.data,
|
data: payload.objectData?.data,
|
||||||
},
|
},
|
||||||
|
customizerParams: payload.customizerParams || state.customizerParams,
|
||||||
|
currentParameters,
|
||||||
consoleMessages: payload.message
|
consoleMessages: payload.message
|
||||||
? [...state.consoleMessages, payload.message]
|
? [...state.consoleMessages, payload.message]
|
||||||
: payload.message,
|
: payload.message,
|
||||||
@@ -167,6 +198,12 @@ export const useIdeState = (): [State, (actionOrThunk: any) => any] => {
|
|||||||
: payload.message,
|
: payload.message,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
}
|
}
|
||||||
|
case 'setCurrentCustomizerParams':
|
||||||
|
if (!Object.keys(payload).length) return state
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentParameters: payload,
|
||||||
|
}
|
||||||
case 'setLayout':
|
case 'setLayout':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -217,6 +254,7 @@ export const useIdeState = (): [State, (actionOrThunk: any) => any] => {
|
|||||||
interface RequestRenderArgs {
|
interface RequestRenderArgs {
|
||||||
state: State
|
state: State
|
||||||
dispatch: any
|
dispatch: any
|
||||||
|
parameters: any
|
||||||
code: State['code']
|
code: State['code']
|
||||||
camera: State['camera']
|
camera: State['camera']
|
||||||
viewerSize: State['viewerSize']
|
viewerSize: State['viewerSize']
|
||||||
@@ -232,6 +270,7 @@ export const requestRender = ({
|
|||||||
viewerSize,
|
viewerSize,
|
||||||
quality = 'low',
|
quality = 'low',
|
||||||
specialCadProcess = null,
|
specialCadProcess = null,
|
||||||
|
parameters,
|
||||||
}: RequestRenderArgs) => {
|
}: RequestRenderArgs) => {
|
||||||
if (
|
if (
|
||||||
state.ideType !== 'INIT' &&
|
state.ideType !== 'INIT' &&
|
||||||
@@ -242,26 +281,41 @@ export const requestRender = ({
|
|||||||
: cadPackages[state.ideType].render
|
: cadPackages[state.ideType].render
|
||||||
return renderFn({
|
return renderFn({
|
||||||
code,
|
code,
|
||||||
|
parameters,
|
||||||
settings: {
|
settings: {
|
||||||
camera,
|
camera,
|
||||||
viewerSize,
|
viewerSize,
|
||||||
quality,
|
quality,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(({ objectData, message, status }) => {
|
.then(
|
||||||
if (status === 'error') {
|
({
|
||||||
dispatch({
|
objectData,
|
||||||
type: 'errorRender',
|
message,
|
||||||
payload: { message },
|
status,
|
||||||
})
|
customizerParams,
|
||||||
} else {
|
currentParameters,
|
||||||
dispatch({
|
}) => {
|
||||||
type: 'healthyRender',
|
if (status === 'error') {
|
||||||
payload: { objectData, message, lastRunCode: code },
|
dispatch({
|
||||||
})
|
type: 'errorRender',
|
||||||
return objectData
|
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
|
.catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,3 +115,102 @@ input.error, textarea.error {
|
|||||||
border: 1px solid red;
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user