diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 7b6ff884..fb749ec8 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -19,7 +19,6 @@ module.exports = { { patterns: [ '../*', // Disallow relative imports that go up a directory - './*/**', // Disallow relative imports that go down a directory 'src/*', // Disallow absolute imports starting with `src/` and enforce alias usage ], }, diff --git a/packages/desktop/src/renderer/components/ParamVector3/ParamVector3.module.css b/packages/desktop/src/renderer/components/ParamVector3/ParamVector3.module.css new file mode 100644 index 00000000..edfa8611 --- /dev/null +++ b/packages/desktop/src/renderer/components/ParamVector3/ParamVector3.module.css @@ -0,0 +1,13 @@ +/* TODO: This CSS will eventually be removed when Vector3 is a dumb (compound) component in core */ +.container { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; + height: 100%; +} + +.item { + flex: 1; + height: 100%; +} diff --git a/packages/desktop/src/renderer/components/ParamVector3/ParamVector3.tsx b/packages/desktop/src/renderer/components/ParamVector3/ParamVector3.tsx new file mode 100644 index 00000000..7919e830 --- /dev/null +++ b/packages/desktop/src/renderer/components/ParamVector3/ParamVector3.tsx @@ -0,0 +1,42 @@ +import { useRef } from 'react' +import { useInterval } from 'usehooks-ts' +import { NodeParamVector3 } from '@hedron/engine' +import c from './ParamVector3.module.css' +import { FloatSlider, FloatSliderHandle } from '@components/core/FloatSlider/FloatSlider' +import { useOnNodeValueChange } from '@components/hooks/useOnNodeValueChange' +import { engineStore, useEngineStore } from '@renderer/engine' + +interface ParamVector3Props { + id: string +} + +const SingleSlider = ({ id }: ParamVector3Props) => { + const ref = useRef(null) + const onValueChange = useOnNodeValueChange(id) + + useInterval(() => { + const nodeValue = engineStore.getState().nodeValues[id] + if (typeof nodeValue !== 'number') { + throw new Error('SingleSlider value was not a number') + } + ref.current?.drawBar(nodeValue) + }, 100) + + return +} + +export const ParamVector3 = ({ id }: ParamVector3Props) => { + const node = useEngineStore((state) => state.nodes[id] as NodeParamVector3) + + const { childNodeIds } = node + + return ( +
+ {childNodeIds.map((childId) => ( +
+ +
+ ))} +
+ ) +} diff --git a/packages/desktop/src/renderer/components/SketchParams/SketchParams.tsx b/packages/desktop/src/renderer/components/SketchParams/SketchParams.tsx index e6310c6b..5a3321e5 100644 --- a/packages/desktop/src/renderer/components/SketchParams/SketchParams.tsx +++ b/packages/desktop/src/renderer/components/SketchParams/SketchParams.tsx @@ -4,6 +4,7 @@ import { ParamWithInfo, useActiveSketchParams } from '@components/hooks/useActiv import { ParamNumber } from '@components/ParamNumber/ParamNumber' import { ParamBoolean } from '@components/ParamBoolean/ParamBoolean' import { ParamEnum } from '@components/ParamEnum/ParamEnum' +import { ParamVector3 } from '@components/ParamVector3/ParamVector3' import { NodeControl, NodeControlInner, @@ -25,6 +26,8 @@ const getInputElement = (valueType: NodeTypes, id: string) => { return case NodeTypes.Enum: return + case NodeTypes.Vector3: + return default: return Unsupported type {valueType} } diff --git a/packages/engine/src/HedronEngine.ts b/packages/engine/src/HedronEngine/HedronEngine.ts similarity index 88% rename from packages/engine/src/HedronEngine.ts rename to packages/engine/src/HedronEngine/HedronEngine.ts index 410fac43..152e6740 100644 --- a/packages/engine/src/HedronEngine.ts +++ b/packages/engine/src/HedronEngine/HedronEngine.ts @@ -1,6 +1,6 @@ import { listenToStore } from './storeListener' -import { importSketchModule } from './importSketchModule' import { Result } from './types' +import { importSketchModule } from './importSketchModule' import { stripForSave } from '@utils/stripForSave' import { Renderer } from '@world/Renderer' import { SketchManager } from '@world/SketchManager' @@ -8,6 +8,7 @@ import { createDebugScene } from '@world/debugScene' import { EngineData, SketchModuleItem } from '@store/types' import { getSketchesOfModuleId } from '@store/selectors/getSketchesOfModuleId' import { createEngineStore, EngineStore } from '@store/engineStore' +import { getSketchParamValues } from '@store/selectors/getSketchParamValues' export class HedronEngine { private renderer: Renderer @@ -105,20 +106,14 @@ export class HedronEngine { const debugScene = createDebugScene(this.renderer) const loop = (): void => { - const { sketches, nodeValues, nodes } = this.store.getState() + const state = this.store.getState() const sketchInstances = this.sketchManager!.getSketchInstances() debugScene.clearPasses() - Object.values(sketches).forEach((sketch) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const paramValues: { [key: string]: any } = {} - sketch.paramIds.forEach((id) => { - const value = nodeValues[id] - const paramKey = nodes[id].key - paramValues[paramKey] = value - }) + Object.keys(state.sketches).forEach((sketchId) => { + const paramValues = getSketchParamValues(state, sketchId) - const instance = sketchInstances[sketch.id] + const instance = sketchInstances[sketchId] if (instance.getPasses) { instance.getPasses(debugScene).forEach((pass) => { diff --git a/packages/engine/src/importSketchModule.ts b/packages/engine/src/HedronEngine/importSketchModule.ts similarity index 100% rename from packages/engine/src/importSketchModule.ts rename to packages/engine/src/HedronEngine/importSketchModule.ts diff --git a/packages/engine/src/storeListener.ts b/packages/engine/src/HedronEngine/storeListener.ts similarity index 100% rename from packages/engine/src/storeListener.ts rename to packages/engine/src/HedronEngine/storeListener.ts diff --git a/packages/engine/src/types.ts b/packages/engine/src/HedronEngine/types.ts similarity index 100% rename from packages/engine/src/types.ts rename to packages/engine/src/HedronEngine/types.ts diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index 993e183c..192cadd4 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -1,4 +1,4 @@ -export * from './HedronEngine' +export * from './HedronEngine/HedronEngine' export type * from '@store/types' export * from '@store/types' import './globalVars' diff --git a/packages/engine/src/store/actionCreators/addSketch.ts b/packages/engine/src/store/actionCreators/addSketch.ts index 90fa52c1..53ab120d 100644 --- a/packages/engine/src/store/actionCreators/addSketch.ts +++ b/packages/engine/src/store/actionCreators/addSketch.ts @@ -1,4 +1,5 @@ -import { SetterCreator, NodeTypes } from '@store/types' +import { addNode } from '@store/shared/addNode' +import { SetterCreator } from '@store/types' import { createUniqueId } from '@utils/createUniqueId' export const createAddSketch: SetterCreator<'addSketch'> = (setState) => (moduleId: string) => { @@ -8,31 +9,9 @@ export const createAddSketch: SetterCreator<'addSketch'> = (setState) => (module const paramIds = [] for (const paramConfig of config.params) { - const valueType = paramConfig.valueType ?? NodeTypes.Number - const { key, defaultValue } = paramConfig const id = createUniqueId() - paramIds.push(id) - - state.nodes[id] = { - id, - key, - type: 'param' as const, - valueType, - sketchId: newSketchId, - } - - if ( - (typeof defaultValue === 'number' && valueType === NodeTypes.Number) || - (typeof defaultValue === 'boolean' && valueType === NodeTypes.Boolean) || - (typeof defaultValue === 'string' && valueType === NodeTypes.Enum) - ) { - state.nodeValues[id] = defaultValue - } else { - throw new Error( - `valueType of param ${paramConfig.key} does not match defaultValue for sketch ${moduleId}`, - ) - } + addNode(state, id, newSketchId, paramConfig) } state.sketches[newSketchId] = { diff --git a/packages/engine/src/store/actionCreators/updateSketchParams.ts b/packages/engine/src/store/actionCreators/updateSketchParams.ts index 7e82afae..5a49a8e5 100644 --- a/packages/engine/src/store/actionCreators/updateSketchParams.ts +++ b/packages/engine/src/store/actionCreators/updateSketchParams.ts @@ -1,3 +1,4 @@ +import { addNode } from '@store/shared/addNode' import { NodeTypes, SetterCreator } from '@store/types' import { createUniqueId } from '@utils/createUniqueId' @@ -17,35 +18,15 @@ export const createUpdateSketchParams: SetterCreator<'updateSketchParams'> = // 1. Add new params that are in the new config but not in the current sketch. for (const paramConfig of config.params) { - const valueType = paramConfig.valueType ?? NodeTypes.Number - const { key, defaultValue } = paramConfig - // Find existing node for this key, if any. - let paramId = Array.from(existingParamIds).find((id) => state.nodes[id]?.key === key) + let paramId = Array.from(existingParamIds).find( + (id) => state.nodes[id]?.key === paramConfig.key, + ) // If no existing node, create a new one. if (!paramId) { paramId = createUniqueId() - state.nodes[paramId] = { - id: paramId, - key, - type: 'param' as const, - valueType, - sketchId, - } - - // Set the default value if it matches the valueType. - if ( - (typeof defaultValue === 'number' && valueType === NodeTypes.Number) || - (typeof defaultValue === 'boolean' && valueType === NodeTypes.Boolean) || - (typeof defaultValue === 'string' && valueType === NodeTypes.Enum) - ) { - state.nodeValues[paramId] = defaultValue - } else { - throw new Error( - `valueType of param ${paramConfig.key} does not match defaultValue for sketch ${moduleId}`, - ) - } + addNode(state, paramId, sketchId, paramConfig) } // Add this paramId to the new list. @@ -55,8 +36,17 @@ export const createUpdateSketchParams: SetterCreator<'updateSketchParams'> = // 2. Remove params that are no longer in the new config. for (const oldParamId of existingParamIds) { + const oldNode = state.nodes[oldParamId] delete state.nodes[oldParamId] delete state.nodeValues[oldParamId] + + // Remove vector child nodes if they exist + if (oldNode.valueType === NodeTypes.Vector3) { + oldNode.childNodeIds.forEach((childNodeId) => { + delete state.nodes[childNodeId] + delete state.nodeValues[childNodeId] + }) + } } // 3. Update the sketch with the new list of param IDs. diff --git a/packages/engine/src/store/selectors/getSketchParamValues.ts b/packages/engine/src/store/selectors/getSketchParamValues.ts new file mode 100644 index 00000000..65ecbb12 --- /dev/null +++ b/packages/engine/src/store/selectors/getSketchParamValues.ts @@ -0,0 +1,27 @@ +import { EngineState } from '@store/types' + +// Get the values of the parameters of a sketch, dealing with child nodes +export const getSketchParamValues = (state: EngineState, sketchId: string) => { + const { sketches, nodeValues, nodes } = state + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const paramValues: { [key: string]: any } = {} + const sketch = sketches[sketchId] + + sketch.paramIds.forEach((id) => { + const { key, valueType } = nodes[id] + + let value + + if (valueType === 'vector3') { + // Return an array of values for vector3 + const childNodeIds = nodes[id].childNodeIds + value = childNodeIds.map((childNodeId) => nodeValues[childNodeId]) + } else { + value = nodeValues[id] + } + + paramValues[key] = value + }) + + return paramValues +} diff --git a/packages/engine/src/store/shared/addNode.ts b/packages/engine/src/store/shared/addNode.ts new file mode 100644 index 00000000..0ff5456b --- /dev/null +++ b/packages/engine/src/store/shared/addNode.ts @@ -0,0 +1,63 @@ +import { EngineState, NodeTypes, SketchConfigParam } from '@store/types' +import { createUniqueId } from '@utils/createUniqueId' + +const vector3Keys = ['x', 'y', 'z'] + +export const addNode = ( + state: EngineState, + paramId: string, + sketchId: string, + { key, valueType = NodeTypes.Number, defaultValue }: SketchConfigParam, +) => { + if (valueType === NodeTypes.Vector3) { + if (!Array.isArray(defaultValue)) { + throw new Error(`Expected defaultValue to be an array for Vector3 type`) + } + + const childNodeIds = Array.from({ length: 3 }, createUniqueId) as [string, string, string] + + state.nodes[paramId] = { + id: paramId, + key, + type: 'param', + valueType, + sketchId, + childNodeIds, + } + + childNodeIds.forEach((childNodeId, index) => { + state.nodes[childNodeId] = { + id: childNodeId, + key: vector3Keys[index], + type: 'param', + valueType: NodeTypes.Number, + sketchId, + } + }) + + defaultValue.forEach((value: number, index: number) => { + state.nodeValues[childNodeIds[index]] = value + }) + } else { + state.nodes[paramId] = { + id: paramId, + key, + type: 'param' as const, + valueType, + sketchId, + } + + // Set the default value if it matches the valueType. + if ( + (typeof defaultValue === 'number' && valueType === NodeTypes.Number) || + (typeof defaultValue === 'boolean' && valueType === NodeTypes.Boolean) || + (typeof defaultValue === 'string' && valueType === NodeTypes.Enum) + ) { + state.nodeValues[paramId] = defaultValue + } else { + throw new Error( + `valueType of param ${key} does not match defaultValue for sketch ${sketchId}`, + ) + } + } +} diff --git a/packages/engine/src/store/types.ts b/packages/engine/src/store/types.ts index 0312f8e4..54fb06e9 100644 --- a/packages/engine/src/store/types.ts +++ b/packages/engine/src/store/types.ts @@ -22,6 +22,7 @@ export enum NodeTypes { Number = 'number', Boolean = 'boolean', Enum = 'enum', + Vector3 = 'vector3', } export interface NodeParamBase extends NodeBase { @@ -43,7 +44,13 @@ export interface NodeParamEnum extends NodeParamBase { valueType: NodeTypes.Enum } -export type Param = NodeParamBoolean | NodeParamNumber | NodeParamEnum +export interface NodeParamVector3 extends NodeParamBase { + type: 'param' + valueType: NodeTypes.Vector3 + childNodeIds: [string, string, string] +} + +export type Param = NodeParamBoolean | NodeParamNumber | NodeParamEnum | NodeParamVector3 export type Node = Param export type Nodes = { [key: string]: Node } @@ -72,10 +79,16 @@ export interface SketchConfigParamEnum extends SketchConfigParamBase { options: EnumOption[] } +export interface SketchConfigParamVector3 extends SketchConfigParamBase { + valueType: NodeTypes.Vector3 + defaultValue: [number, number, number] +} + export type SketchConfigParam = | SketchConfigParamNumber | SketchConfigParamBoolean | SketchConfigParamEnum + | SketchConfigParamVector3 export interface SketchConfig { title: string diff --git a/packages/example-project/sketches/solid/config.js b/packages/example-project/sketches/solid/config.ts similarity index 92% rename from packages/example-project/sketches/solid/config.js rename to packages/example-project/sketches/solid/config.ts index 350f3179..7741489a 100644 --- a/packages/example-project/sketches/solid/config.js +++ b/packages/example-project/sketches/solid/config.ts @@ -2,6 +2,12 @@ export default { title: 'Solid', description: 'Platonic solids! Rotate, scale, wireframe mode.', params: [ + { + key: 'position', + title: 'Position', + valueType: 'vector3', + defaultValue: [0, 0, 0], + }, { key: 'rotSpeedX', title: 'Rotation Speed X', diff --git a/packages/example-project/sketches/solid/index.js b/packages/example-project/sketches/solid/index.ts similarity index 96% rename from packages/example-project/sketches/solid/index.js rename to packages/example-project/sketches/solid/index.ts index 73989996..c0e1dee2 100644 --- a/packages/example-project/sketches/solid/index.js +++ b/packages/example-project/sketches/solid/index.ts @@ -2,9 +2,10 @@ const { THREE } = window.HEDRON.dependencies const geomSize = 1 export default class Solid { + root = new THREE.Group() + constructor() { // All sketches need root property to add things to - this.root = new THREE.Group() this.meshes = {} @@ -39,6 +40,8 @@ export default class Solid { this.root.rotation.y += params.rotSpeedY * baseSpeed * deltaFrame this.root.rotation.z += params.rotSpeedZ * baseSpeed * deltaFrame + this.root.position.set(...params.position) + // Update scale using params this.root.scale.set(params.scale, params.scale, params.scale)