Add a few type improvements to jscadParams.ts
This commit is contained in:
167
app/web/src/helpers/cadPackages/jsCad/jsCadController.ts
Normal file
167
app/web/src/helpers/cadPackages/jsCad/jsCadController.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import {
|
||||
RenderArgs,
|
||||
DefaultKernelExport,
|
||||
createUnhealthyResponse,
|
||||
createHealthyResponse,
|
||||
} from '../common'
|
||||
import {
|
||||
MeshPhongMaterial,
|
||||
LineBasicMaterial,
|
||||
BufferGeometry,
|
||||
BufferAttribute,
|
||||
Line,
|
||||
LineSegments,
|
||||
Color,
|
||||
Mesh,
|
||||
} from 'three'
|
||||
|
||||
const materials = {
|
||||
mesh: {
|
||||
def: new MeshPhongMaterial({ color: 0x0084d1, flatShading: true }),
|
||||
material: (params) => new MeshPhongMaterial(params),
|
||||
},
|
||||
line: {
|
||||
def: new LineBasicMaterial({ color: 0x0000ff }),
|
||||
material: ({ color, opacity, transparent }) =>
|
||||
new LineBasicMaterial({ color, opacity, transparent }),
|
||||
},
|
||||
lines: null,
|
||||
}
|
||||
materials.lines = materials.line
|
||||
|
||||
function CSG2Object3D(obj) {
|
||||
const { vertices, indices, color, transforms } = obj
|
||||
|
||||
const materialDef = materials[obj.type]
|
||||
if (!materialDef) {
|
||||
console.error('Can not hangle object type: ' + obj.type, obj)
|
||||
return null
|
||||
}
|
||||
|
||||
let material = materialDef.def
|
||||
if (color) {
|
||||
const c = color
|
||||
material = materialDef.material({
|
||||
color: new Color(c[0], c[1], c[2]),
|
||||
flatShading: true,
|
||||
opacity: c[3] === void 0 ? 1 : c[3],
|
||||
transparent: c[3] != 1 && c[3] !== void 0,
|
||||
})
|
||||
}
|
||||
|
||||
const geo = new BufferGeometry()
|
||||
geo.setAttribute('position', new BufferAttribute(vertices, 3))
|
||||
|
||||
let mesh
|
||||
switch (obj.type) {
|
||||
case 'mesh':
|
||||
geo.setIndex(new BufferAttribute(indices, 1))
|
||||
mesh = new Mesh(geo, material)
|
||||
break
|
||||
case 'line':
|
||||
mesh = new Line(geo, material)
|
||||
break
|
||||
case 'lines':
|
||||
mesh = new LineSegments(geo, material)
|
||||
break
|
||||
}
|
||||
if (transforms) mesh.applyMatrix4({ elements: transforms })
|
||||
return mesh
|
||||
}
|
||||
|
||||
let scriptWorker
|
||||
let lastParameters = {}
|
||||
const scriptUrl = '/demo-worker.js'
|
||||
let resolveReference = null
|
||||
let response = null
|
||||
|
||||
const callResolve = () => {
|
||||
if (resolveReference) resolveReference()
|
||||
resolveReference
|
||||
}
|
||||
|
||||
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))
|
||||
`
|
||||
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 == 'parameterDefinitions') {
|
||||
parameterDefinitions = data.data
|
||||
} else if (data.action == 'entities') {
|
||||
if (data.error) {
|
||||
response = 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,
|
||||
lastParameters,
|
||||
})
|
||||
}
|
||||
callResolve()
|
||||
}
|
||||
})
|
||||
|
||||
callResolve()
|
||||
response = null
|
||||
scriptWorker.postMessage({ action: 'init', baseURI, alias: [] })
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
await waitResult
|
||||
resolveReference = null
|
||||
if (parameters) delete response.customizerParams
|
||||
return response
|
||||
}
|
||||
|
||||
const jsCadController: DefaultKernelExport = {
|
||||
render,
|
||||
}
|
||||
|
||||
export default jsCadController
|
||||
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 = 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}"`
|
||||
if (live !== undefined) 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) 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)
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
export 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
|
||||
}
|
||||
Reference in New Issue
Block a user