From 4e4b72d4a8b09989ef834a38518f81ce63f34178 Mon Sep 17 00:00:00 2001 From: Matt Nichols Date: Thu, 19 Oct 2023 11:16:27 -0700 Subject: [PATCH] Allow preset hex values to set color arrays --- src/lib/base/ProjectLoading/ParamInference.ts | 5 +- src/lib/base/ProjectLoading/PresetUtil.ts | 73 ++++++++++++++++--- tests/unit/ParamInference.test.ts | 6 +- tests/unit/PresetUtil.test.ts | 70 +++++++++++++++++- 4 files changed, 135 insertions(+), 19 deletions(-) diff --git a/src/lib/base/ProjectLoading/ParamInference.ts b/src/lib/base/ProjectLoading/ParamInference.ts index de402cc..6b03c85 100644 --- a/src/lib/base/ProjectLoading/ParamInference.ts +++ b/src/lib/base/ProjectLoading/ParamInference.ts @@ -112,7 +112,7 @@ export default class ParamInference { // Assign numeric array value tokens via numeric array or hex string const hexMetaString = intentions?.metaStrings?.find((meta) => - meta.match(/^#([0-9a-f]{6})$/) + meta.match(/^#([0-9a-f]{6})$/i) ); if (ParamGuards.isNumericArrayParamConfig(newConfig)) { if (intentions?.numericArrayValues?.length) { @@ -208,7 +208,8 @@ export default class ParamInference { token.match(/^\[(\s*-?\d*\.{0,1}\d+\s*,\s*)+(\s*-?\d*\.{0,1}\d+\s*)\]$/) ); const potentialMetaTokens = stringTokens.filter( - (token) => token.match(/(^[a-zA-Z]+$)|(^#([0-9a-f]{6})$)/) && !token.match(/true|false/) + (token) => + token.match(/(^[a-zA-Z]+$)|(^#([0-9a-fA-F]{6})$)/) && !token.match(/true|false/) ); // Return an object with adapted tokens, if any diff --git a/src/lib/base/ProjectLoading/PresetUtil.ts b/src/lib/base/ProjectLoading/PresetUtil.ts index e55403f..d91efde 100644 --- a/src/lib/base/ProjectLoading/PresetUtil.ts +++ b/src/lib/base/ProjectLoading/PresetUtil.ts @@ -1,8 +1,10 @@ +import { NumericArrayParamStyle } from '../ConfigModels/ParamConfigs/NumericArrayParamConfig'; import { ParamGuards, type AnyParamValueType, type ParamValueType } from '../ConfigModels/ParamTypes'; +import ColorConversions from '../Util/ColorConversions'; import ParamValueProvider from './ParamValueProvider'; import { defaultPresetKey, type Preset } from './PresetLoader'; import type { ProjectTuple } from './ProjectLoader'; @@ -49,29 +51,54 @@ export default class PresetUtil { const paramConfig = projectTuple.params.find((param) => param.key === paramKey); if (!paramConfig) throw new Error(`Param ${paramKey} not found`); - // Don't continue for parameters that can't have values + // Don't proceed with parameters that can't have values if (!ParamGuards.isConfigTypeWithDefault(paramConfig)) continue; + // Type value assertion with config type, apologies + let typedValue = paramValue as ParamValueType; + + // Allow hex values to be used for color numeric array params + if ( + ParamGuards.isNumericArrayParamConfig(paramConfig) && + typeof typedValue === 'string' && + typedValue.match(/^#[0-9a-f]{6}$/i) + ) { + if ( + [NumericArrayParamStyle.ByteColor, NumericArrayParamStyle.UnitColor].includes( + paramConfig.style + ) + ) { + typedValue = ColorConversions.hexToRgb( + typedValue, + paramConfig.style === NumericArrayParamStyle.UnitColor + ); + } else { + throw new Error( + `Preset value type mismatch for param ${paramKey}: hex strings can only be assigned for numeric arrays with color style.` + ); + } + } + // Do some basic type checking const currentValue = Object.getOwnPropertyDescriptor( projectTuple.project, paramConfig.key )?.value; - if (typeof paramValue !== typeof currentValue) { + if (typeof typedValue !== typeof currentValue) { throw new Error( - `Preset value type mismatch for param ${paramKey}: ${typeof paramValue} vs ${typeof currentValue}` + `Preset value type mismatch for param ${paramKey}: ${typeof typedValue} vs ${typeof currentValue}` ); - } else if (Array.isArray(paramValue) !== Array.isArray(currentValue)) { + } else if (Array.isArray(typedValue) !== Array.isArray(currentValue)) { throw new Error( `Preset value type mismatch for param ${paramKey}: array vs non-array` ); - } else if (Array.isArray(paramValue) && Array.isArray(currentValue)) { - if (paramValue.length !== currentValue.length) { + } else if (Array.isArray(typedValue) && Array.isArray(currentValue)) { + if (typedValue.length !== currentValue.length) { throw new Error( - `Preset value type mismatch for param ${paramKey}: array lengths ${paramValue.length} vs ${currentValue.length}` + `Preset value type mismatch for param ${paramKey}: array lengths ${typedValue.length} vs ${currentValue.length}` ); } - const arrayTypeMismatch = paramValue.some( + const arrayTypeMismatch = typedValue.some( (v: number, i: number) => typeof v !== typeof currentValue[i] ); if (arrayTypeMismatch) { @@ -80,9 +107,9 @@ export default class PresetUtil { ); } } - const typedValue = ( - Array.isArray(paramValue) ? [...paramValue] : paramValue - ) as ParamValueType; // sorry + + // Copy array to avoid aliasing if need be + typedValue = Array.isArray(typedValue) ? [...typedValue] : typedValue; // Set object and stored values if they've changed if (JSON.stringify(typedValue) !== JSON.stringify(currentValue)) { @@ -116,7 +143,29 @@ export default class PresetUtil { projectTuple.project, paramKey )?.value; - if (JSON.stringify(paramValue) !== JSON.stringify(currentValue)) return false; + const possibleValues: (typeof paramValue)[] = [paramValue]; + + // Allow hex values to be used for three-element numeric array params + // More permissive than applyPreset, because we don't need user-legible errors here + if ( + Array.isArray(currentValue) && + currentValue.length === 3 && + typeof paramValue === 'string' && + paramValue.match(/^#[0-9a-f]{6}$/i) + ) { + possibleValues.push(ColorConversions.hexToRgb(paramValue, false)); + possibleValues.push(ColorConversions.hexToRgb(paramValue, true)); + } + + // Check if any of the possible values match the current value + let anyMatch = false; + for (const possibleValue of possibleValues) { + if (JSON.stringify(possibleValue) === JSON.stringify(currentValue)) { + anyMatch = true; + break; + } + } + if (!anyMatch) return false; } return true; } diff --git a/tests/unit/ParamInference.test.ts b/tests/unit/ParamInference.test.ts index 7a19bc9..f72d62d 100644 --- a/tests/unit/ParamInference.test.ts +++ b/tests/unit/ParamInference.test.ts @@ -217,7 +217,7 @@ describe('ParamInference.paramWithInference', () => { const inference2 = ParamInference.paramWithInference( NumericArrayParamConfigDefaults, InferenceMode.ShaderFile, - '23, origin, true, -4, #9933cc' + '23, origin, true, -4, #9933CC' ) as NumericArrayParamConfig; expect(inference2.default).toEqual([0.6, 0.2, 0.8]); }); @@ -609,14 +609,14 @@ describe('ParamInference.intentionsFrom', () => { }); it('includes hex strings in metaStrings', () => { - const intentions1 = ParamInference.intentionsFrom('#dd00ff'); + const intentions1 = ParamInference.intentionsFrom('#dd00FF'); expect(intentions1.name).toBeUndefined(); expect(intentions1.range).toBeUndefined(); expect(intentions1.step).toBeUndefined(); expect(intentions1.numberValues.length).toBe(0); expect(intentions1.booleanValues.length).toBe(0); expect(intentions1.numericArrayValues.length).toBe(0); - expect(intentions1.metaStrings).toEqual(['#dd00ff']); + expect(intentions1.metaStrings).toEqual(['#dd00FF']); const intentions2 = ParamInference.intentionsFrom( '#ff00ff, step, step 01, true, testDeux, false, 100.3 to 3' diff --git a/tests/unit/PresetUtil.test.ts b/tests/unit/PresetUtil.test.ts index fc480e1..74254ac 100644 --- a/tests/unit/PresetUtil.test.ts +++ b/tests/unit/PresetUtil.test.ts @@ -7,13 +7,18 @@ import { NumberParamConfigDefaults } from '$lib/base/ConfigModels/ParamConfigs/N import { defaultPresetKey } from '$lib/base/ProjectLoading/PresetLoader'; import type { ProjectTuple } from '$lib/base/ProjectLoading/ProjectLoader'; import { ProjectConfigDefaults } from '$lib/base/ConfigModels/ProjectConfig'; -import { NumericArrayParamConfigDefaults } from '$lib/base/ConfigModels/ParamConfigs/NumericArrayParamConfig'; +import { + NumericArrayParamConfigDefaults, + NumericArrayParamStyle +} from '$lib/base/ConfigModels/ParamConfigs/NumericArrayParamConfig'; import { FunctionParamConfigDefaults } from '$lib/base/ConfigModels/ParamConfigs/FunctionParamConfig'; class TestProject extends Project { testNumberKey = 42; testArrayKey = [42, 21, 10.5]; functionParamKey = () => 42; + colorArrayUnit = [0.5, 0.5, 0.5]; + colorArrayByte = [100, 100, 100]; } const testProject = new TestProject(); @@ -33,6 +38,16 @@ const projectTuple: ProjectTuple = { { ...FunctionParamConfigDefaults, key: 'functionParamKey' + }, + { + ...NumericArrayParamConfigDefaults, + key: 'colorArrayUnit', + style: NumericArrayParamStyle.UnitColor + }, + { + ...NumericArrayParamConfigDefaults, + key: 'colorArrayByte', + style: NumericArrayParamStyle.ByteColor } ], presets: { @@ -80,6 +95,21 @@ const projectTuple: ProjectTuple = { 'values': { testArrayKey: [42, 84] } + }, + 'arrayColorsWithHex': { + 'title': 'Test Preset', + 'key': 'arrayColorsWithHex', + 'values': { + colorArrayUnit: '#ffffff', + colorArrayByte: '#ffffff' + } + }, + 'badColor': { + 'title': 'Test Preset', + 'key': 'badColors', + 'values': { + testArrayKey: '#ffffff' + } } } }; @@ -115,6 +145,8 @@ describe('Preset application via PresetUtil.applyPreset', () => { vi.clearAllMocks(); testProject.testNumberKey = 42; testProject.testArrayKey = [42, 21, 10.5]; + testProject.colorArrayUnit = [0.5, 0.5, 0.5]; + testProject.colorArrayByte = [100, 100, 100]; }); it("applies a preset to a project's values", () => { @@ -177,6 +209,18 @@ describe('Preset application via PresetUtil.applyPreset', () => { 'Preset value type mismatch for param testArrayKey: array lengths 2 vs 3' ); }); + + it('assigns color strings as numeric arrays with color style', () => { + PresetUtil.applyPreset(projectTuple, 'arrayColorsWithHex'); + expect(testProject.colorArrayUnit).toEqual([1, 1, 1]); + expect(testProject.colorArrayByte).toEqual([255, 255, 255]); + }); + + it('throws an error if a color string is assigned to a non-color array', () => { + expect(() => PresetUtil.applyPreset(projectTuple, 'badColor')).toThrowError( + 'Preset value type mismatch for param testArrayKey: hex strings can only be assigned for numeric arrays with color style.' + ); + }); }); describe('Checking if presets are applied via PresetUtil.presetIsApplied', () => { @@ -186,6 +230,8 @@ describe('Checking if presets are applied via PresetUtil.presetIsApplied', () => vi.clearAllMocks(); testProject.testNumberKey = 42; testProject.testArrayKey = [42, 21, 10.5]; + testProject.colorArrayUnit = [0.5, 0.5, 0.5]; + testProject.colorArrayByte = [100, 100, 100]; }); it('returns true if preset has been applied', () => { @@ -194,6 +240,12 @@ describe('Checking if presets are applied via PresetUtil.presetIsApplied', () => expect(isApplied).toBe(true); }); + it('returns true if hex color preset has been applied', () => { + PresetUtil.applyPreset(projectTuple, 'arrayColorsWithHex'); + const isApplied = PresetUtil.presetIsApplied(projectTuple, 'arrayColorsWithHex'); + expect(isApplied).toBe(true); + }); + it('returns true if default is applied', () => { const isApplied = PresetUtil.presetIsApplied(projectTuple, defaultPresetKey); expect(isApplied).toBe(true); @@ -213,6 +265,16 @@ describe('Checking if presets are applied via PresetUtil.presetIsApplied', () => expect(isApplied).toBe(false); }); + it('returns false if hex color preset has been applied then modified, true if changed back', () => { + PresetUtil.applyPreset(projectTuple, 'arrayColorsWithHex'); + testProject.colorArrayUnit[1] = 0.3; + const isApplied = PresetUtil.presetIsApplied(projectTuple, 'arrayColorsWithHex'); + expect(isApplied).toBe(false); + testProject.colorArrayUnit[1] = 1.0; + const isApplied2 = PresetUtil.presetIsApplied(projectTuple, 'arrayColorsWithHex'); + expect(isApplied2).toBe(true); + }); + it('throws an error if the preset is not found', () => { expect(() => PresetUtil.presetIsApplied(projectTuple, 'ghostPreset')).toThrowError( 'Preset ghostPreset not found' @@ -227,6 +289,8 @@ describe('Preset export via PresetUtil.exportPresetFile', () => { vi.clearAllMocks(); testProject.testNumberKey = 42; testProject.testArrayKey = [42, 21, 10.5]; + testProject.colorArrayUnit = [0.5, 0.5, 0.5]; + testProject.colorArrayByte = [100, 100, 100]; }); it('should create a preset file', () => { @@ -255,7 +319,9 @@ describe('Preset export via PresetUtil.exportPresetFile', () => { key: 'TestPreset', values: { testNumberKey: 85, - testArrayKey: [18, 19, 21] + testArrayKey: [18, 19, 21], + colorArrayUnit: [0.5, 0.5, 0.5], + colorArrayByte: [100, 100, 100] } }, null,