From 0e5784339b3065eae17b40992e95e007a25a1460 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Sat, 6 Jun 2026 20:02:26 +0100 Subject: [PATCH 01/30] wip: sketch schema --- .../lib/Graphics/ElementPropertiesSchemas.ts | 111 ++++++++++++++ shared-lib/lib/Model/Options.ts | 8 + shared-lib/lib/Model/StyleLayersModel.ts | 26 ++++ shared-lib/lib/ValidateInputValue.ts | 38 +++++ .../__tests__/validate-input-value.test.ts | 145 ++++++++++++++++++ tools/generate_graphics_types.mts | 3 + 6 files changed, 331 insertions(+) diff --git a/shared-lib/lib/Graphics/ElementPropertiesSchemas.ts b/shared-lib/lib/Graphics/ElementPropertiesSchemas.ts index 8e3787c6d2..3dc17441c4 100644 --- a/shared-lib/lib/Graphics/ElementPropertiesSchemas.ts +++ b/shared-lib/lib/Graphics/ElementPropertiesSchemas.ts @@ -1,6 +1,7 @@ import { CompanionFieldVariablesSupport, type SomeCompanionInputField } from '../Model/Options.js' import type { ButtonGraphicsBoxElement, + ButtonGraphicsGaugeElement, ButtonGraphicsImageElement, ButtonGraphicsTextElement, } from '../Model/StyleLayersModel.js' @@ -479,6 +480,111 @@ export const referenceElementSchema: ElementSchemaSection[] = [ }, ] +export const gaugeElementSchema: ElementSchemaSection[] = [ + { id: 'layer', label: 'Layer', fields: [...commonElementFields] }, + { id: 'position', label: 'Position & Size', fields: [...boundsFields, ...rotationFields] }, + { + id: 'content', + label: 'Content', + fields: [ + { + type: 'number', + id: 'value', + label: 'Value (0-100)', + tooltip: 'The current value of the gauge, in the range 0-100.', + default: 0, + min: 0, + max: 100, + step: 1, + }, + ], + }, + { + id: 'appearance', + label: 'Appearance', + fields: [ + { + type: 'dropdown', + id: 'orientation', + label: 'Orientation', + choices: [ + { id: 'horizontal', label: 'Horizontal' }, + { id: 'vertical', label: 'Vertical' }, + ], + default: 'horizontal', + }, + { + type: 'checkbox', + id: 'reverse', + label: 'Reverse direction', + tooltip: + 'When enabled, the gauge fills from the opposite end (right-to-left for horizontal, top-to-bottom for vertical).', + default: false, + }, + { + type: 'checkbox', + id: 'multiSegment', + label: 'Multi-segment colours', + tooltip: + 'When enabled, each colour threshold segment is visible in the filled portion. When disabled, only the active threshold colour is used for the entire filled area.', + default: true, + }, + { + type: 'internal:table', + id: 'thresholds', + label: 'Colour thresholds', + tooltip: 'Define colour stops for the gauge. Each row specifies the value (0-100) at which that colour starts.', + disableAutoExpression: true, + columns: [ + { id: 'value', type: 'number', label: 'Value', min: 0, max: 100, step: 1, default: 0 }, + { + id: 'color', + type: 'colorpicker', + label: 'Colour', + default: 0x00ff00, + enableAlpha: false, + returnType: 'number', + }, + ], + default: [ + { value: 0, color: 0x00ff00 }, + { value: 66, color: 0xffff00 }, + { value: 85, color: 0xff0000 }, + ], + }, + ], + }, + { + id: 'inactive', + label: 'Inactive portion', + fields: [ + { + type: 'dropdown', + id: 'inactiveStyle', + label: 'Style', + tooltip: 'How to render the unfilled portion of the gauge.', + choices: [ + { id: 'transparent', label: 'Transparent' }, + { id: 'dimmed', label: 'Dimmed (darker)' }, + ], + default: 'transparent', + }, + { + type: 'number', + id: 'inactiveAmount', + label: 'Amount (%)', + tooltip: + 'For transparent: opacity of the inactive segments (0 = invisible, 100 = fully opaque). For dimmed: how much to darken (0 = no dimming, 100 = black).', + default: 70, + min: 0, + max: 100, + step: 1, + range: true, + }, + ], + }, +] + /** * Section-structured schemas per element type. */ @@ -492,6 +598,7 @@ export const elementSchemas = { composite: compositeElementSchema, canvas: canvasElementSchema, reference: referenceElementSchema, + gauge: gaugeElementSchema, } as const satisfies Record export function getElementSchemaProperty( @@ -536,4 +643,8 @@ export const elementSimpleModeFields = { // 'color', ] satisfies ReadonlyArray, + gauge: [ + // + 'value', + ] satisfies ReadonlyArray, } as const diff --git a/shared-lib/lib/Model/Options.ts b/shared-lib/lib/Model/Options.ts index 94c95875fb..c9a8743cb7 100644 --- a/shared-lib/lib/Model/Options.ts +++ b/shared-lib/lib/Model/Options.ts @@ -74,6 +74,7 @@ export interface CompanionInputFieldBaseExtended { | 'internal:horizontal-alignment' | 'internal:vertical-alignment' | 'internal:image-file' + | 'internal:table' /** The label of the field */ label: string /** A hover tooltip for this field */ @@ -170,6 +171,12 @@ export interface InternalInputFieldPngImage extends CompanionInputFieldBaseExten max?: { width: number; height: number } } +export interface InternalInputFieldTable extends CompanionInputFieldBaseExtended { + type: 'internal:table' + columns: SomeCompanionInputField[] + default: Record[] +} + export type InternalInputField = | InternalInputFieldTime | InternalInputFieldDate @@ -185,6 +192,7 @@ export type InternalInputField = | InternalInputFieldHorizontalAlignment | InternalInputFieldVerticalAlignment | InternalInputFieldPngImage + | InternalInputFieldTable export interface CompanionInputFieldStaticTextExtended extends CompanionInputFieldBaseExtended { type: 'static-text' diff --git a/shared-lib/lib/Model/StyleLayersModel.ts b/shared-lib/lib/Model/StyleLayersModel.ts index cc1a85fbc9..39e665916c 100644 --- a/shared-lib/lib/Model/StyleLayersModel.ts +++ b/shared-lib/lib/Model/StyleLayersModel.ts @@ -212,6 +212,30 @@ export interface ButtonGraphicsReferenceElement location: ExpressionOrValue } +export interface ButtonGraphicsGaugeDrawElement + extends ButtonGraphicsDrawBase, ButtonGraphicsDrawBounds, ButtonGraphicsDrawRotation { + type: 'gauge' + value: number + orientation: 'horizontal' | 'vertical' + reverse: boolean + multiSegment: boolean + thresholds: Record[] + inactiveStyle: 'transparent' | 'dimmed' + inactiveAmount: number +} + +export interface ButtonGraphicsGaugeElement + extends ButtonGraphicsElementBase, ButtonGraphicsBounds, ButtonGraphicsRotation { + type: 'gauge' + value: ExpressionOrValue + orientation: ExpressionOrValue<'horizontal' | 'vertical'> + reverse: ExpressionOrValue + multiSegment: ExpressionOrValue + thresholds: ExpressionOrValue[]> + inactiveStyle: ExpressionOrValue<'transparent' | 'dimmed'> + inactiveAmount: ExpressionOrValue +} + export type SomeButtonGraphicsDrawElement = | ButtonGraphicsCanvasDrawElement | ButtonGraphicsTextDrawElement @@ -221,6 +245,7 @@ export type SomeButtonGraphicsDrawElement = | ButtonGraphicsGroupDrawElement | ButtonGraphicsCircleDrawElement | ButtonGraphicsReferenceDrawElement + | ButtonGraphicsGaugeDrawElement export type SomeButtonGraphicsElement = | ButtonGraphicsCanvasElement @@ -232,3 +257,4 @@ export type SomeButtonGraphicsElement = | ButtonGraphicsCircleElement | ButtonGraphicsCompositeElement | ButtonGraphicsReferenceElement + | ButtonGraphicsGaugeElement diff --git a/shared-lib/lib/ValidateInputValue.ts b/shared-lib/lib/ValidateInputValue.ts index 4285d1ed80..7573c6cda3 100644 --- a/shared-lib/lib/ValidateInputValue.ts +++ b/shared-lib/lib/ValidateInputValue.ts @@ -300,6 +300,44 @@ export function validateInputValue( return { sanitisedValue, validationError: undefined, validationWarnings } } + case 'internal:table': { + if (!Array.isArray(value)) { + return { sanitisedValue: value, validationError: 'Value must be an array', validationWarnings } + } + + const sanitisedRows: JsonValue[] = [] + for (let rowIndex = 0; rowIndex < value.length; rowIndex++) { + const row = value[rowIndex] + if (typeof row !== 'object' || row === null || Array.isArray(row)) { + return { + sanitisedValue: value, + validationError: `Row ${rowIndex} must be an object`, + validationWarnings, + } + } + + const sanitisedRow: Record = {} + for (const col of definition.columns) { + const cellValue = (row as Record)[col.id] + const result = validateInputValue(col, cellValue, options) + if (result.validationError) { + return { + sanitisedValue: value, + validationError: `Row ${rowIndex}, column "${col.label}": ${result.validationError}`, + validationWarnings, + } + } + validationWarnings.push( + ...result.validationWarnings.map((w) => `Row ${rowIndex}, column "${col.label}": ${w}`) + ) + sanitisedRow[col.id] = result.sanitisedValue as JsonValue + } + sanitisedRows.push(sanitisedRow) + } + + return { sanitisedValue: sanitisedRows, validationError: undefined, validationWarnings } + } + case 'internal:connection_id': case 'internal:connection_collection': case 'internal:custom_variable': diff --git a/shared-lib/lib/__tests__/validate-input-value.test.ts b/shared-lib/lib/__tests__/validate-input-value.test.ts index 0dc26d9b62..457cf9e1c2 100644 --- a/shared-lib/lib/__tests__/validate-input-value.test.ts +++ b/shared-lib/lib/__tests__/validate-input-value.test.ts @@ -11,6 +11,7 @@ import type { CompanionInputFieldSecretExtended, CompanionInputFieldStaticTextExtended, CompanionInputFieldTextInputExtended, + InternalInputFieldTable, InternalInputFieldTime, } from '../Model/Options.js' import { validateInputValue } from '../ValidateInputValue.js' @@ -1334,6 +1335,150 @@ describe('validateInputValue', () => { }) }) + describe('internal:table', () => { + const definition: InternalInputFieldTable = { + id: 'test', + type: 'internal:table', + label: 'Test', + columns: [ + { id: 'value', type: 'number', label: 'Value', min: 0, max: 100, step: 1, default: 0 }, + { + id: 'color', + type: 'colorpicker', + label: 'Colour', + default: 0x00ff00, + enableAlpha: false, + returnType: 'number', + }, + ], + default: [], + } + + it('should return error when value is not an array', () => { + expect(validateInputValue(definition, 'not-an-array')).toEqual({ + sanitisedValue: 'not-an-array', + validationError: 'Value must be an array', + validationWarnings: [], + }) + expect(validateInputValue(definition, 42)).toEqual({ + sanitisedValue: 42, + validationError: 'Value must be an array', + validationWarnings: [], + }) + expect(validateInputValue(definition, { value: 0, color: 0 })).toEqual({ + sanitisedValue: { value: 0, color: 0 }, + validationError: 'Value must be an array', + validationWarnings: [], + }) + }) + + it('should return empty array for empty input', () => { + expect(validateInputValue(definition, [])).toEqual({ + sanitisedValue: [], + validationError: undefined, + validationWarnings: [], + }) + }) + + it('should return error when a row is not an object', () => { + expect(validateInputValue(definition, ['not-a-row'])).toEqual({ + sanitisedValue: ['not-a-row'], + validationError: 'Row 0 must be an object', + validationWarnings: [], + }) + expect(validateInputValue(definition, [42])).toEqual({ + sanitisedValue: [42], + validationError: 'Row 0 must be an object', + validationWarnings: [], + }) + expect(validateInputValue(definition, [[]])).toEqual({ + sanitisedValue: [[]], + validationError: 'Row 0 must be an object', + validationWarnings: [], + }) + }) + + it('should validate and sanitise a valid row', () => { + expect(validateInputValue(definition, [{ value: 50, color: 0x00ff00 }])).toEqual({ + sanitisedValue: [{ value: 50, color: 0x00ff00 }], + validationError: undefined, + validationWarnings: [], + }) + }) + + it('should validate multiple valid rows', () => { + expect( + validateInputValue(definition, [ + { value: 0, color: 0x00ff00 }, + { value: 66, color: 0xffff00 }, + { value: 85, color: 0xff0000 }, + ]) + ).toEqual({ + sanitisedValue: [ + { value: 0, color: 0x00ff00 }, + { value: 66, color: 0xffff00 }, + { value: 85, color: 0xff0000 }, + ], + validationError: undefined, + validationWarnings: [], + }) + }) + + it('should return error with row/column context when a cell value is invalid', () => { + expect(validateInputValue(definition, [{ value: 150, color: 0x00ff00 }])).toEqual({ + sanitisedValue: [{ value: 150, color: 0x00ff00 }], + validationError: 'Row 0, column "Value": Value must be less than or equal to 100', + validationWarnings: [], + }) + }) + + it('should report the failing row index correctly', () => { + expect( + validateInputValue(definition, [ + { value: 0, color: 0x00ff00 }, + { value: -5, color: 0xffff00 }, + ]) + ).toEqual({ + sanitisedValue: [ + { value: 0, color: 0x00ff00 }, + { value: -5, color: 0xffff00 }, + ], + validationError: 'Row 1, column "Value": Value must be greater than or equal to 0', + validationWarnings: [], + }) + }) + + it('should propagate column warnings with row/column context', () => { + const clampDef: InternalInputFieldTable = { + ...definition, + columns: [ + { id: 'value', type: 'number', label: 'Value', min: 0, max: 100, step: 1, default: 0, clampValues: true }, + { + id: 'color', + type: 'colorpicker', + label: 'Colour', + default: 0x00ff00, + enableAlpha: false, + returnType: 'number', + }, + ], + } + expect(validateInputValue(clampDef, [{ value: 150, color: 0x00ff00 }])).toEqual({ + sanitisedValue: [{ value: 100, color: 0x00ff00 }], + validationError: undefined, + validationWarnings: ['Row 0, column "Value": Value was clamped to 100'], + }) + }) + + it('should coerce string cell values using the column definition', () => { + expect(validateInputValue(definition, [{ value: '75', color: 0x00ff00 }])).toEqual({ + sanitisedValue: [{ value: 75, color: 0x00ff00 }], + validationError: undefined, + validationWarnings: [], + }) + }) + }) + describe('sanitization edge cases and potential bugs', () => { it('should handle object values in dropdown', () => { const definition: CompanionInputFieldDropdownExtended = { diff --git a/tools/generate_graphics_types.mts b/tools/generate_graphics_types.mts index 39836fb2ef..d2993ba7e6 100644 --- a/tools/generate_graphics_types.mts +++ b/tools/generate_graphics_types.mts @@ -48,6 +48,9 @@ function convertFieldType(field: SomeCompanionInputField, isExpressionable: bool case 'internal:vertical-alignment': tsType = 'VerticalAlignment' break + case 'internal:table': + tsType = 'Record[]' + break default: // assertNever(field.type) throw new Error(`Unhandled field type: ${field.type}`) From 5199848b88044c3fb01dc50aaf28c935c79445bb Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Sat, 6 Jun 2026 20:23:31 +0100 Subject: [PATCH 02/30] wip: initial table input element --- .../Components/TableInputField.stories.tsx | 116 +++++++++++++ webui/src/Components/TableInputField.tsx | 156 ++++++++++++++++++ webui/src/Controls/OptionsInputField.tsx | 4 + 3 files changed, 276 insertions(+) create mode 100644 webui/src/Components/TableInputField.stories.tsx create mode 100644 webui/src/Components/TableInputField.tsx diff --git a/webui/src/Components/TableInputField.stories.tsx b/webui/src/Components/TableInputField.stories.tsx new file mode 100644 index 0000000000..91663ea52e --- /dev/null +++ b/webui/src/Components/TableInputField.stories.tsx @@ -0,0 +1,116 @@ +import type { Decorator, Meta, StoryObj } from '@storybook/react' +import { useState } from 'react' +import type { JsonValue } from 'type-fest' +import type { InternalInputFieldTable } from '@companion-app/shared/Model/Options.js' +import { MenuPortalContext } from './MenuPortalContext.js' +import { TableInputField } from './TableInputField.js' + +const withPortal: Decorator = (Story) => ( + + + +) + +const gaugeThresholdDefinition: InternalInputFieldTable = { + id: 'thresholds', + type: 'internal:table', + label: 'Colour thresholds', + columns: [ + { id: 'value', type: 'number', label: 'Value', min: 0, max: 100, step: 1, default: 0 }, + { id: 'color', type: 'colorpicker', label: 'Colour', default: 0x00ff00, enableAlpha: false, returnType: 'number' }, + ], + default: [ + { value: 0, color: 0x00ff00 }, + { value: 66, color: 0xffff00 }, + { value: 85, color: 0xff0000 }, + ], +} + +function StatefulTable({ + definition, + initialValue, + disabled, +}: { + definition: InternalInputFieldTable + initialValue: Record[] + disabled?: boolean +}): React.JSX.Element { + const [value, setValue] = useState(initialValue) + return ( +
+ +
{JSON.stringify(value, null, 2)}
+
+ ) +} + +const meta = { + title: 'Components/TableInputField', + decorators: [withPortal], + render: (args) => , +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + definition: gaugeThresholdDefinition, + initialValue: [ + { value: 0, color: 0x00ff00 }, + { value: 66, color: 0xffff00 }, + { value: 85, color: 0xff0000 }, + ], + }, +} + +export const Empty: Story = { + args: { + definition: gaugeThresholdDefinition, + initialValue: [], + }, +} + +export const Disabled: Story = { + args: { + definition: gaugeThresholdDefinition, + initialValue: [ + { value: 0, color: 0x00ff00 }, + { value: 66, color: 0xffff00 }, + ], + disabled: true, + }, +} + +export const TextColumn: Story = { + args: { + definition: { + id: 'labels', + type: 'internal:table', + label: 'Labels', + columns: [ + { id: 'value', type: 'number', label: 'Position', min: 0, max: 100, step: 1, default: 0 }, + { id: 'label', type: 'textinput', label: 'Text', default: '' }, + ], + default: [], + } satisfies InternalInputFieldTable, + initialValue: [ + { value: 0, label: 'Low' }, + { value: 50, label: 'Mid' }, + { value: 100, label: 'High' }, + ], + }, +} + +export const SingleColumn: Story = { + args: { + definition: { + id: 'values', + type: 'internal:table', + label: 'Values', + columns: [{ id: 'value', type: 'number', label: 'Value', min: 0, max: 100, step: 1, default: 50 }], + default: [], + } satisfies InternalInputFieldTable, + initialValue: [{ value: 25 }, { value: 75 }], + }, +} diff --git a/webui/src/Components/TableInputField.tsx b/webui/src/Components/TableInputField.tsx new file mode 100644 index 0000000000..d09202aa5b --- /dev/null +++ b/webui/src/Components/TableInputField.tsx @@ -0,0 +1,156 @@ +import { faPlus, faTrash } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { useCallback, useMemo } from 'react' +import type { JsonValue } from 'type-fest' +import type { InternalInputFieldTable, SomeCompanionInputField } from '@companion-app/shared/Model/Options.js' +import { Button } from './Button.js' +import { ColorInputField } from './ColorInputField.js' +import { NumberInputField } from './NumberInputField.js' +import { TextInputFieldSimple } from './TextInputField.js' + +function columnDefault(col: SomeCompanionInputField): JsonValue { + if ('default' in col && col.default !== undefined) return col.default + return null +} + +function newRow(columns: SomeCompanionInputField[]): Record { + const row: Record = {} + for (const col of columns) row[col.id] = columnDefault(col) + return row +} + +function firstNumberColId(columns: SomeCompanionInputField[]): string | undefined { + return columns.find((c) => c.type === 'number')?.id +} + +interface TableCellProps { + col: SomeCompanionInputField + value: JsonValue | undefined + setValue: (v: JsonValue) => void + disabled?: boolean +} + +function TableCell({ col, value, setValue, disabled }: TableCellProps): React.JSX.Element { + switch (col.type) { + case 'number': + return ( + + ) + case 'colorpicker': + return ( + + id={undefined} + value={(value as number | undefined) ?? 0} + setValue={setValue} + enableAlpha={col.enableAlpha ?? false} + returnType={col.returnType ?? 'number'} + disabled={disabled} + /> + ) + case 'textinput': + return ( + + ) + default: + return Unsupported column type + } +} + +interface TableInputFieldProps { + definition: InternalInputFieldTable + value: Record[] | undefined + setValue: (rows: Record[]) => void + disabled?: boolean +} + +export function TableInputField({ definition, value, setValue, disabled }: TableInputFieldProps): React.JSX.Element { + const { columns } = definition + const sortColId = useMemo(() => firstNumberColId(columns), [columns]) + + const sortedRows = useMemo(() => { + const rows = value ?? [] + if (!sortColId) return rows + return [...rows].sort((a, b) => Number(a[sortColId] ?? 0) - Number(b[sortColId] ?? 0)) + }, [value, sortColId]) + + const addRow = useCallback(() => { + setValue([...(value ?? []), newRow(columns)]) + }, [value, columns, setValue]) + + const removeRow = useCallback( + (rowIndex: number) => { + setValue(sortedRows.filter((_, i) => i !== rowIndex)) + }, + [sortedRows, setValue] + ) + + const updateCell = useCallback( + (rowIndex: number, colId: string, cellValue: JsonValue) => { + setValue(sortedRows.map((row, i) => (i === rowIndex ? { ...row, [colId]: cellValue } : row))) + }, + [sortedRows, setValue] + ) + + return ( +
+ {sortedRows.length > 0 && ( + + + + {columns.map((col) => ( + + ))} + + + + {sortedRows.map((row, rowIndex) => ( + + {columns.map((col) => ( + + ))} + + + ))} + +
+ {col.label} + +
+ updateCell(rowIndex, col.id, v)} + disabled={disabled} + /> + + +
+ )} + +
+ ) +} diff --git a/webui/src/Controls/OptionsInputField.tsx b/webui/src/Controls/OptionsInputField.tsx index 2b45836f4b..646969ff85 100644 --- a/webui/src/Controls/OptionsInputField.tsx +++ b/webui/src/Controls/OptionsInputField.tsx @@ -24,6 +24,7 @@ import { InlineHelpCustom, InlineHelpIcon } from '~/Components/InlineHelp.js' import { MultiDropdownInputField } from '~/Components/MultiDropdownInputField.js' import { NumberInputField } from '~/Components/NumberInputField.js' import { SwitchInputField } from '~/Components/SwitchInputField.js' +import { TableInputField } from '~/Components/TableInputField.js' import { TextInputField } from '~/Components/TextInputField.js' import { InternalCustomVariableDropdown, InternalModuleField } from './InternalModuleField.js' import type { LocalVariablesStore } from './LocalVariablesStore.js' @@ -310,6 +311,9 @@ export const OptionsInputControl = observer(function OptionsInputControl({ } break } + case 'internal:table': { + return + } case 'bonjour-device': case 'secret-text': // Not supported here From e0e62f285e6ca5c72797702af9932f92ba77963d Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Sat, 6 Jun 2026 20:41:30 +0100 Subject: [PATCH 03/30] wip: drawing --- .../ControlTypes/Button/LayerDefaults.ts | 28 +++++++ .../lib/Graphics/ConvertGraphicsElements.ts | 50 +++++++++++ .../ConvertGraphicsElements/Helper.ts | 12 +++ .../lib/Graphics/ElementPropertiesSchemas.ts | 2 +- shared-lib/lib/Graphics/LayeredRenderer.ts | 82 +++++++++++++++++++ .../LayeredButtonEditor/Buttons.tsx | 8 ++ 6 files changed, 181 insertions(+), 1 deletion(-) diff --git a/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts b/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts index 551a23e8a0..fb4bb273f4 100644 --- a/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts +++ b/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts @@ -119,6 +119,34 @@ export function CreateElementOfType(type: SomeButtonGraphicsElement['type']): So borderPosition: { value: 'center', isExpression: false }, borderOnlyArc: { value: false, isExpression: false }, } + case 'gauge': + return { + id: nanoid(), + name: 'Gauge', + usage: ButtonGraphicsElementUsage.Automatic, + type: 'gauge', + enabled: { value: true, isExpression: false }, + opacity: { value: 100, isExpression: false }, + x: { value: 0, isExpression: false }, + y: { value: 0, isExpression: false }, + width: { value: 100, isExpression: false }, + height: { value: 100, isExpression: false }, + rotation: { value: 0, isExpression: false }, + value: { value: 0, isExpression: false }, + orientation: { value: 'horizontal', isExpression: false }, + reverse: { value: false, isExpression: false }, + multiSegment: { value: true, isExpression: false }, + thresholds: { + value: [ + { value: 0, color: 0x00ff00 }, + { value: 66, color: 0xffff00 }, + { value: 85, color: 0xff0000 }, + ], + isExpression: false, + }, + inactiveStyle: { value: 'transparent', isExpression: false }, + inactiveAmount: { value: 70, isExpression: false }, + } case 'composite': // Composite elements should not be created directly through this function // They are created with custom logic in layeredStyleAddElement diff --git a/companion/lib/Graphics/ConvertGraphicsElements.ts b/companion/lib/Graphics/ConvertGraphicsElements.ts index 21aa372ba1..c7a1e1f333 100644 --- a/companion/lib/Graphics/ConvertGraphicsElements.ts +++ b/companion/lib/Graphics/ConvertGraphicsElements.ts @@ -17,6 +17,8 @@ import type { ButtonGraphicsDrawBorder, ButtonGraphicsDrawBounds, ButtonGraphicsElementBase, + ButtonGraphicsGaugeDrawElement, + ButtonGraphicsGaugeElement, ButtonGraphicsGroupDrawElement, ButtonGraphicsGroupElement, ButtonGraphicsImageDrawElement, @@ -164,6 +166,10 @@ async function convertElements( cacheEntry = convertCircleElementForDrawing(context, element, idPrefix) break } + case 'gauge': { + cacheEntry = convertGaugeElementForDrawing(context, element, idPrefix) + break + } case 'composite': { cacheEntry = await convertCompositeElementForDrawing(context, element, idPrefix) break @@ -494,6 +500,7 @@ function parseCompositeElementChildOptions( break case 'multidropdown': + case 'internal:table': case 'internal:connection_collection': case 'internal:connection_id': case 'internal:custom_variable': @@ -779,6 +786,49 @@ function convertCircleElementForDrawing( return { drawElement, usedVariables, compositeElement: null } } +function convertGaugeElementForDrawing( + context: ParseElementsContext, + element: ButtonGraphicsGaugeElement, + idPrefix: string +): ElementConversionCacheEntry { + const { helper, usedVariables } = context.createHelper(element) + + const enabled = helper.getBoolean('enabled', true) + if (!enabled && context.onlyEnabled) return { drawElement: null, usedVariables, compositeElement: null } + + const orientation = helper.getTolerantEnum('orientation', ['horizontal', 'vertical'] as const, 'horizontal') + const inactiveStyle = helper.getTolerantEnum('inactiveStyle', ['transparent', 'dimmed'] as const, 'transparent') + + const thresholdsRaw = (element.thresholds as ExpressionOrValue).value + const thresholds: ButtonGraphicsGaugeDrawElement['thresholds'] = Array.isArray(thresholdsRaw) + ? thresholdsRaw.map((row) => ({ + value: Math.max(0, Math.min(100, Number((row as any)?.value ?? 0))), + color: Number((row as any)?.color ?? 0), + })) + : [] + + const drawElement: ButtonGraphicsGaugeDrawElement = { + id: idPrefix + element.id, + type: 'gauge', + usage: element.usage, + enabled, + opacity: helper.getNumber('opacity', 1, 0.01), + ...convertDrawBounds(helper), + rotation: helper.getNumber('rotation', 0), + value: Math.round(Math.max(0, Math.min(100, helper.getNumber('value', 0))) * 10) / 10, + orientation, + reverse: helper.getBoolean('reverse', false), + multiSegment: helper.getBoolean('multiSegment', true), + thresholds, + inactiveStyle, + inactiveAmount: helper.getNumber('inactiveAmount', 70), + contentHash: '', + } + + drawElement.contentHash = computeElementContentHash(drawElement) + return { drawElement, usedVariables, compositeElement: null } +} + function convertBorderProperties( helper: ElementExpressionHelper ): ButtonGraphicsDrawBorder { diff --git a/companion/lib/Graphics/ConvertGraphicsElements/Helper.ts b/companion/lib/Graphics/ConvertGraphicsElements/Helper.ts index 348db84904..a8fff05159 100644 --- a/companion/lib/Graphics/ConvertGraphicsElements/Helper.ts +++ b/companion/lib/Graphics/ConvertGraphicsElements/Helper.ts @@ -157,6 +157,18 @@ export class ElementExpressionHelper { return actualValue } + /** + * Like getEnum, but tolerant of leading whitespace and case differences. + * Matches from the first non-whitespace character, case-insensitively. + */ + getTolerantEnum(propertyName: keyof T, values: readonly TVal[], defaultValue: TVal): TVal { + const raw = this.getString(propertyName, defaultValue) + const trimmed = String(raw ?? '') + .trimStart() + .toLowerCase() + return values.find((v) => v.toLowerCase().startsWith(trimmed)) ?? defaultValue + } + getBoolean(propertyName: keyof T, defaultValue: boolean): boolean { const value = this.#getValue(propertyName) diff --git a/shared-lib/lib/Graphics/ElementPropertiesSchemas.ts b/shared-lib/lib/Graphics/ElementPropertiesSchemas.ts index 3dc17441c4..b31a83f4d2 100644 --- a/shared-lib/lib/Graphics/ElementPropertiesSchemas.ts +++ b/shared-lib/lib/Graphics/ElementPropertiesSchemas.ts @@ -574,7 +574,7 @@ export const gaugeElementSchema: ElementSchemaSection[] = [ id: 'inactiveAmount', label: 'Amount (%)', tooltip: - 'For transparent: opacity of the inactive segments (0 = invisible, 100 = fully opaque). For dimmed: how much to darken (0 = no dimming, 100 = black).', + 'How much of the original colour remains in the inactive portion. 0 = invisible / black, 100 = same as the active colour.', default: 70, min: 0, max: 100, diff --git a/shared-lib/lib/Graphics/LayeredRenderer.ts b/shared-lib/lib/Graphics/LayeredRenderer.ts index f082c07bdb..23161111c7 100644 --- a/shared-lib/lib/Graphics/LayeredRenderer.ts +++ b/shared-lib/lib/Graphics/LayeredRenderer.ts @@ -3,6 +3,7 @@ import type { ButtonGraphicsBoxDrawElement, ButtonGraphicsCanvasDrawElement, ButtonGraphicsCircleDrawElement, + ButtonGraphicsGaugeDrawElement, ButtonGraphicsGroupDrawElement, ButtonGraphicsImageDrawElement, ButtonGraphicsLineDrawElement, @@ -155,6 +156,9 @@ export class GraphicsLayeredButtonRenderer { case 'circle': elementBounds = await this.#drawCircleElement(img, drawBounds, element, skipDraw) break + case 'gauge': + elementBounds = await this.#drawGaugeElement(img, drawBounds, element, skipDraw) + break default: assertNever(element) } @@ -450,6 +454,84 @@ export class GraphicsLayeredButtonRenderer { return drawBounds } + static async #drawGaugeElement( + img: ImageBase, + parentBounds: DrawBounds, + element: ButtonGraphicsGaugeDrawElement, + skipDraw: boolean + ): Promise { + const drawBounds = parentBounds.compose(element.x, element.y, element.width, element.height) + if (skipDraw) return drawBounds + + const sorted = [...element.thresholds].sort((a, b) => Number(a.value) - Number(b.value)) + if (sorted.length === 0) return drawBounds + + const { x, y, width, height, maxX, maxY } = drawBounds + const { value, orientation, reverse, multiSegment, inactiveStyle, inactiveAmount } = element + + // For single-color mode, find the highest threshold whose start <= current value + let singleActiveColor = Number(sorted[0].color) + if (!multiSegment) { + for (const t of sorted) { + if (Number(t.value) <= value) singleActiveColor = Number(t.color) + } + } + + // Convert a gauge position range [p1, p2] (0–100) to pixel box coordinates [x1, y1, x2, y2]. + // Coordinates are rounded to the nearest integer so that adjacent segments always share an + // exact pixel edge and anti-aliasing does not leave a visible seam between them. + const segmentBox = (p1: number, p2: number): [number, number, number, number] => { + if (orientation === 'horizontal') { + return reverse + ? [Math.round(maxX - (p2 / 100) * width), y, Math.round(maxX - (p1 / 100) * width), maxY] + : [Math.round(x + (p1 / 100) * width), y, Math.round(x + (p2 / 100) * width), maxY] + } else { + return reverse + ? [x, Math.round(y + (p1 / 100) * height), maxX, Math.round(y + (p2 / 100) * height)] + : [x, Math.round(maxY - (p2 / 100) * height), maxX, Math.round(maxY - (p1 / 100) * height)] + } + } + + const dimmedColor = (color: number): string => { + const { r, g, b, a } = rgbRev(color, true) + const factor = inactiveAmount / 100 + if (inactiveStyle === 'transparent') { + return `rgba(${r}, ${g}, ${b}, ${a * factor})` + } else { + return `rgba(${Math.round(r * factor)}, ${Math.round(g * factor)}, ${Math.round(b * factor)}, ${a})` + } + } + + await img.usingAlpha(element.opacity, async () => { + await img.usingRotation(drawBounds, element.rotation, async () => { + for (let i = 0; i < sorted.length; i++) { + const segStart = Number(sorted[i].value) + const segEnd = i + 1 < sorted.length ? Number(sorted[i + 1].value) : 100 + const color = Number(sorted[i].color) + + if (segStart >= segEnd) continue + + // Active portion: segStart → min(segEnd, value) + const activeEnd = Math.min(segEnd, value) + if (activeEnd > segStart) { + const activeColor = multiSegment ? color : singleActiveColor + const [ax1, ay1, ax2, ay2] = segmentBox(segStart, activeEnd) + img.box(ax1, ay1, ax2, ay2, parseColor(activeColor)) + } + + // Inactive portion: max(segStart, value) → segEnd + const inactiveStart = Math.max(segStart, value) + if (inactiveStart < segEnd) { + const [ix1, iy1, ix2, iy2] = segmentBox(inactiveStart, segEnd) + img.box(ix1, iy1, ix2, iy2, dimmedColor(color)) + } + } + }) + }) + + return drawBounds + } + /** * Draw some bounds lines over the whole image, to give a visual indicator of the selected element * Note: this intentionally overshoots everything to make it very visible diff --git a/webui/src/Buttons/EditButton/LayeredButtonEditor/Buttons.tsx b/webui/src/Buttons/EditButton/LayeredButtonEditor/Buttons.tsx index 32db868067..3b73bd6458 100644 --- a/webui/src/Buttons/EditButton/LayeredButtonEditor/Buttons.tsx +++ b/webui/src/Buttons/EditButton/LayeredButtonEditor/Buttons.tsx @@ -4,6 +4,7 @@ import { faCopy, faCube, faEye, + faGauge, faImage, faLayerGroup, faLink, @@ -274,6 +275,13 @@ const AddElementDropdownPopoverContent = observer(function AddElementDropdownPop label="Circle" icon={faCircle} /> + Date: Sat, 6 Jun 2026 20:43:58 +0100 Subject: [PATCH 04/30] wip: tests --- .../test/Graphics/LayeredRenderer.test.ts | 117 ++++++++++++++++++ ...ement_empty_thresholds_-_nothing_drawn.png | Bin 0 -> 124 bytes ...Amount_0_-_inactive_portions_invisible.png | Bin 0 -> 234 bytes ...t_100_-_inactive_same_as_active_colour.png | Bin 0 -> 243 bytes ...le_dimmed_-_inactive_portions_darkened.png | Bin 0 -> 247 bytes ...single_colour_for_entire_active_region.png | Bin 0 -> 247 bytes ...ical_reverse_false_-_fills_from_bottom.png | Bin 0 -> 267 bytes ...vertical_reverse_true_-_fills_from_top.png | Bin 0 -> 263 bytes ...lement_reverse_true_-_fills_from_right.png | Bin 0 -> 244 bytes ...single_threshold_-_full_bar_one_colour.png | Bin 0 -> 245 bytes ...d_thresholds_-_sorted_before_rendering.png | Bin 0 -> 249 bytes ...e_0_-_only_inactive_background_visible.png | Bin 0 -> 246 bytes ..._100_-_all_segments_active_no_inactive.png | Bin 0 -> 243 bytes ...ue_50_-_first_segment_partially_active.png | Bin 0 -> 249 bytes ...ments_active_green_yellow_red_inactive.png | Bin 0 -> 249 bytes 15 files changed, 117 insertions(+) create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_empty_thresholds_-_nothing_drawn.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_inactiveAmount_0_-_inactive_portions_invisible.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_inactiveAmount_100_-_inactive_same_as_active_colour.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_inactiveStyle_dimmed_-_inactive_portions_darkened.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_multiSegment_false_-_single_colour_for_entire_active_region.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_orientation_vertical_reverse_false_-_fills_from_bottom.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_orientation_vertical_reverse_true_-_fills_from_top.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_reverse_true_-_fills_from_right.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_single_threshold_-_full_bar_one_colour.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_unsorted_thresholds_-_sorted_before_rendering.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_value_0_-_only_inactive_background_visible.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_value_100_-_all_segments_active_no_inactive.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_value_50_-_first_segment_partially_active.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_value_75_-_two_segments_active_green_yellow_red_inactive.png diff --git a/companion/test/Graphics/LayeredRenderer.test.ts b/companion/test/Graphics/LayeredRenderer.test.ts index a15f8c6448..e67e8f2018 100644 --- a/companion/test/Graphics/LayeredRenderer.test.ts +++ b/companion/test/Graphics/LayeredRenderer.test.ts @@ -8,6 +8,7 @@ import type { ButtonGraphicsBoxDrawElement, ButtonGraphicsCanvasDrawElement, ButtonGraphicsCircleDrawElement, + ButtonGraphicsGaugeDrawElement, ButtonGraphicsGroupDrawElement, ButtonGraphicsImageDrawElement, ButtonGraphicsLineDrawElement, @@ -1061,4 +1062,120 @@ describe('GraphicsLayeredButtonRenderer', () => { await expect(img.canvasImage).toMatchImageSnapshot() }) }) + + describe('gauge element', () => { + const DEFAULT_THRESHOLDS: ButtonGraphicsGaugeDrawElement['thresholds'] = [ + { value: 0, color: 0x00ff00 }, + { value: 66, color: 0xffff00 }, + { value: 85, color: 0xff0000 }, + ] + + function makeGaugeElement(overrides: Partial = {}): ButtonGraphicsGaugeDrawElement { + return { + ...ELEMENT_BASE, + id: 'gauge-1', + type: 'gauge', + x: 0, + y: 0, + width: 1, + height: 1, + rotation: 0, + value: 50, + orientation: 'horizontal', + reverse: false, + multiSegment: true, + thresholds: DEFAULT_THRESHOLDS, + inactiveStyle: 'transparent', + inactiveAmount: 70, + ...overrides, + } + } + + const drawOpts = { show_topbar: false, show_status_icons: false } as const + + async function drawGauge(gauge: ButtonGraphicsGaugeDrawElement, size = { w: 72, h: 58 }): Promise { + const img = Image.create(size.w, size.h, 1, null) + await GraphicsLayeredButtonRenderer.draw( + img, + makeStyle({ ...drawOpts, elements: [gauge] }), + new Set(), + null, + DEFAULT_PADDING + ) + return img.canvasImage + } + + test('value=0 - only inactive background visible', async () => { + await expect(await drawGauge(makeGaugeElement({ value: 0 }))).toMatchImageSnapshot() + }) + + test('value=50 - first segment partially active', async () => { + await expect(await drawGauge(makeGaugeElement({ value: 50 }))).toMatchImageSnapshot() + }) + + test('value=75 - two segments active (green + yellow), red inactive', async () => { + await expect(await drawGauge(makeGaugeElement({ value: 75 }))).toMatchImageSnapshot() + }) + + test('value=100 - all segments active, no inactive', async () => { + await expect(await drawGauge(makeGaugeElement({ value: 100 }))).toMatchImageSnapshot() + }) + + test('reverse=true - fills from right', async () => { + await expect(await drawGauge(makeGaugeElement({ value: 50, reverse: true }))).toMatchImageSnapshot() + }) + + test('multiSegment=false - single colour for entire active region', async () => { + await expect(await drawGauge(makeGaugeElement({ value: 75, multiSegment: false }))).toMatchImageSnapshot() + }) + + test('inactiveStyle=dimmed - inactive portions darkened', async () => { + await expect( + await drawGauge(makeGaugeElement({ value: 50, inactiveStyle: 'dimmed', inactiveAmount: 70 })) + ).toMatchImageSnapshot() + }) + + test('inactiveAmount=0 - inactive portions invisible', async () => { + await expect(await drawGauge(makeGaugeElement({ value: 50, inactiveAmount: 0 }))).toMatchImageSnapshot() + }) + + test('inactiveAmount=100 - inactive same as active colour', async () => { + await expect(await drawGauge(makeGaugeElement({ value: 50, inactiveAmount: 100 }))).toMatchImageSnapshot() + }) + + test('orientation=vertical reverse=false - fills from bottom', async () => { + await expect(await drawGauge(makeGaugeElement({ value: 50, orientation: 'vertical' }))).toMatchImageSnapshot() + }) + + test('orientation=vertical reverse=true - fills from top', async () => { + await expect( + await drawGauge(makeGaugeElement({ value: 50, orientation: 'vertical', reverse: true })) + ).toMatchImageSnapshot() + }) + + test('empty thresholds - nothing drawn', async () => { + await expect(await drawGauge(makeGaugeElement({ thresholds: [] }))).toMatchImageSnapshot() + }) + + test('single threshold - full bar one colour', async () => { + await expect( + await drawGauge(makeGaugeElement({ value: 50, thresholds: [{ value: 0, color: 0x0088ff }] })) + ).toMatchImageSnapshot() + }) + + test('unsorted thresholds - sorted before rendering', async () => { + await expect( + await drawGauge( + makeGaugeElement({ + value: 75, + thresholds: [ + { value: 85, color: 0xff0000 }, + { value: 0, color: 0x00ff00 }, + { value: 66, color: 0xffff00 }, + ], + }) + ) + ).toMatchImageSnapshot() + }) + }) }) diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_empty_thresholds_-_nothing_drawn.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_empty_thresholds_-_nothing_drawn.png new file mode 100644 index 0000000000000000000000000000000000000000..f74b9535792ded8962b56d43715c58e656478116 GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^9zblx!3HFS+2wnI6icy_X9x!n)NrJ90Qro?LGDfr z>(0r%1aj0oT^vIy=Da<~$Oz;$Dg0lb=ivZiB7;3rj#Uhd*OYATgG4=D{an^LB{Ts5 DX%ZOJ literal 0 HcmV?d00001 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_inactiveAmount_0_-_inactive_portions_invisible.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_inactiveAmount_0_-_inactive_portions_invisible.png new file mode 100644 index 0000000000000000000000000000000000000000..9f71ce2a67b589d085b234ae068627538449dddc GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0vp^9zblx!3HFS+2wnI6icy_X9x!n)NrJ90Qro?LGDfr z>(0r%1ahW$x;TbZ%z1l3k*~pkf#u+||NnPtaOANnx}I4vdAcm~y&2!5l5=Z|X7>Br z7re3iP|n2K*yP~2Kp{XtgF}Rci|Sm3A6GaFB$#rRoXd3sx{txr)z4*}Q$iB}2lYJR literal 0 HcmV?d00001 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_inactiveAmount_100_-_inactive_same_as_active_colour.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_inactiveAmount_100_-_inactive_same_as_active_colour.png new file mode 100644 index 0000000000000000000000000000000000000000..03a2cac63ef99dd4eef5eaac63350207ab6dac51 GIT binary patch literal 243 zcmeAS@N?(olHy`uVBq!ia0vp^9zblx!3HFS+2wnI6icy_X9x!n)NrJ90Qro?LGDfr z>(0r%1ajtjx;TbZ%z1mkkn6Am4};^}|NqmM8|;WJR$TJIXQ`9IM&l2?rT)+7)c;L6 zeC_b-gXgwxOH1pUE1j(0r%1acO7x;TbZ%z1mkkgwT+hr#ji|BBpC_A5MgcW^8{;q&y+uFYj^`!?1rtNk`V zuBa+LzV>cq{q8;6&YW3O{_lr66Ki9WgX02)009jS5f(10a}_GCFvf?o>!pMoa(z literal 0 HcmV?d00001 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_multiSegment_false_-_single_colour_for_entire_active_region.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_multiSegment_false_-_single_colour_for_entire_active_region.png new file mode 100644 index 0000000000000000000000000000000000000000..7132bf8c85a1f6de6b4126be76aa2be88928a12c GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^9zblx!3HFS+2wnI6icy_X9x!n)NrJ90Qro?LGDfr z>(0r%1acO7x;TbZ%z1mkkn6Am4}+s6|CjW?2fU}MC2ZT^t8gZSX{CTQb9m1D#I=$8 zYoCT4f0<@|w`TrZW8?L=<9-D*u{Jh2I4)2K5YXTdVd0`WSE1sH^!xuTIk7z(Er5(0r%1ah`|x;TbZ%z1mmkn2!@fWyJw3OoMStjP=Aa%x&Vn9hal_t(0r%1adZdx;TbZ%z1ljBiA7V5r>OzfBw(^$oyI%EOw9cL>8O$Qx_f(0r%1ajtix;TbZ%z1mkkk27Nz~SKezcJTXHC_I8a2)m7dN$2GpQj?psy(;o>!oAg z9-J^e{g+#RM&I`5>;M0-ZftUJT%Zsjpur)+!o|c&b*{jkEcSr8%sIO6_;&zZ%HZkh K=d#Wzp$Pz_y+u6$ literal 0 HcmV?d00001 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_single_threshold_-_full_bar_one_colour.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_single_threshold_-_full_bar_one_colour.png new file mode 100644 index 0000000000000000000000000000000000000000..12e1dec6c448523a28225cc98c0991d07a6360a4 GIT binary patch literal 245 zcmeAS@N?(olHy`uVBq!ia0vp^9zblx!3HFS+2wnI6icy_X9x!n)NrJ90Qro?LGDfr z>(0r%1ajtkx;TbZ%z1lZp&%0@1M@)})*yb>3;e82%#}@vavN7G8E-#m-uHc%{ru&2 zKP%?cCfqKwUU$39^7rxm!WtYREL=>ijZF@Y3lstbsLnm;zn5X&XO^5l-HFeDPG#_P L^>bP0l+XkKz5_uM literal 0 HcmV?d00001 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_unsorted_thresholds_-_sorted_before_rendering.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_unsorted_thresholds_-_sorted_before_rendering.png new file mode 100644 index 0000000000000000000000000000000000000000..df391c8a7a07f488b322fedfc907ab673c61d43a GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^9zblx!3HFS+2wnI6icy_X9x!n)NrJ90Qro?LGDfr z>(0r%1acO8x;TbZ%z1mkkgq8~#Pwi1cgbcw0c$Q{8%Af1IVF) z=}FFbw*6)Lr{6vOe7iX9Sz*n!dO-~i5f&~c*2X3W#{~)j0#xT7Tzb!DGfU1Tz7^+y P&Smg)^>bP0l+XkK8OcNt literal 0 HcmV?d00001 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_value_0_-_only_inactive_background_visible.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_value_0_-_only_inactive_background_visible.png new file mode 100644 index 0000000000000000000000000000000000000000..fd7ee41bfedf7ba7c9dc516e5923b12dda4e0261 GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0vp^9zblx!3HFS+2wnI6icy_X9x!n)NrJ90Qro?LGDfr z>(0r%1acO5x;TbZ%z1mkkn6Am4}+s6|Ci*?j>`>p+$~Vt@xf=Q)A7!^jqy3xOV0eh z<}D_p-M>x0?A+Gcv}apC9iPl0!otPG+SugaxIiI5K!f_+2kY%-FJsPei^v7}kipZ{ K&t;ucLK6UO(?iz) literal 0 HcmV?d00001 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_value_100_-_all_segments_active_no_inactive.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_value_100_-_all_segments_active_no_inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..03a2cac63ef99dd4eef5eaac63350207ab6dac51 GIT binary patch literal 243 zcmeAS@N?(olHy`uVBq!ia0vp^9zblx!3HFS+2wnI6icy_X9x!n)NrJ90Qro?LGDfr z>(0r%1ajtjx;TbZ%z1mkkn6Am4};^}|NqmM8|;WJR$TJIXQ`9IM&l2?rT)+7)c;L6 zeC_b-gXgwxOH1pUE1j(0r%1acO8x;TbZ%z1mkkgGXB#Pwjh*o(By4#6DDY?3C3oD12%(0r%1acO8x;TbZ%z1mkkgq8~#Pwi1cgbcw0c$Q{8%Af1IVF) z=}FFbw*6)Lr{6vOe7iX9Sz*n!dO-~i5f&~c*2X3W#{~)j0#xT7Tzb!DGfU1Tz7^+y P&Smg)^>bP0l+XkK8OcNt literal 0 HcmV?d00001 From fb870caa75f69474914f683992b8c509bc2edcd5 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Sat, 6 Jun 2026 21:08:25 +0100 Subject: [PATCH 05/30] wip: ring mode --- .../ControlTypes/Button/LayerDefaults.ts | 1 + .../lib/Graphics/ConvertGraphicsElements.ts | 3 +- companion/test/Graphics/Image.test.ts | 92 ++++++++++++++++++ .../test/Graphics/LayeredRenderer.test.ts | 71 ++++++++++++++ ...in_non-square_element_-_stays_circular.png | Bin 0 -> 2124 bytes ..._false_value_75_-_single_colour_active.png | Bin 0 -> 2735 bytes ...erse_true_value_75_-_counter-clockwise.png | Bin 0 -> 2666 bytes ..._gauge_element_ring_thick_thickness_40.png | Bin 0 -> 2610 bytes ...er_gauge_element_ring_thin_thickness_8.png | Bin 0 -> 2661 bytes ...tive_arc_only_dark_bg_makes_it_visible.png | Bin 0 -> 2541 bytes ..._element_ring_value_100_-_fully_active.png | Bin 0 -> 2670 bytes ...e_33_-_one_colour_within_first_segment.png | Bin 0 -> 2627 bytes ...alue_50_-_midway_through_first_segment.png | Bin 0 -> 2663 bytes ..._-_exactly_at_first_threshold_boundary.png | Bin 0 -> 2635 bytes ...alue_75_-_crossing_into_yellow_segment.png | Bin 0 -> 2666 bytes ...inactive_-_both_halves_clearly_visible.png | Bin 0 -> 2646 bytes ...g_value_90_-_crossing_into_red_segment.png | Bin 0 -> 2736 bytes ...napshot_-_full_circle_and_quarter_arcs.png | Bin 0 -> 4021 bytes ...arcStroke_snapshot_-_thick_vs_thin_arc.png | Bin 0 -> 3290 bytes .../lib/Graphics/ElementPropertiesSchemas.ts | 13 ++- shared-lib/lib/Graphics/ImageBase.ts | 28 ++++++ shared-lib/lib/Graphics/LayeredRenderer.ts | 81 +++++++++++---- shared-lib/lib/Model/StyleLayersModel.ts | 6 +- 23 files changed, 272 insertions(+), 23 deletions(-) create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_in_non-square_element_-_stays_circular.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_multiSegment_false_value_75_-_single_colour_active.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_reverse_true_value_75_-_counter-clockwise.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_thick_thickness_40.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_thin_thickness_8.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_0_-_inactive_arc_only_dark_bg_makes_it_visible.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_100_-_fully_active.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_33_-_one_colour_within_first_segment.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_50_-_midway_through_first_segment.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_66_-_exactly_at_first_threshold_boundary.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_75_-_crossing_into_yellow_segment.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_75_dimmed_inactive_-_both_halves_clearly_visible.png create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_90_-_crossing_into_red_segment.png create mode 100644 companion/test/Graphics/__snapshots__/Image_drawing_arcStroke_snapshot_-_full_circle_and_quarter_arcs.png create mode 100644 companion/test/Graphics/__snapshots__/Image_drawing_arcStroke_snapshot_-_thick_vs_thin_arc.png diff --git a/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts b/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts index fb4bb273f4..832d51535b 100644 --- a/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts +++ b/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts @@ -135,6 +135,7 @@ export function CreateElementOfType(type: SomeButtonGraphicsElement['type']): So value: { value: 0, isExpression: false }, orientation: { value: 'horizontal', isExpression: false }, reverse: { value: false, isExpression: false }, + thickness: { value: 20, isExpression: false }, multiSegment: { value: true, isExpression: false }, thresholds: { value: [ diff --git a/companion/lib/Graphics/ConvertGraphicsElements.ts b/companion/lib/Graphics/ConvertGraphicsElements.ts index c7a1e1f333..a4f355e908 100644 --- a/companion/lib/Graphics/ConvertGraphicsElements.ts +++ b/companion/lib/Graphics/ConvertGraphicsElements.ts @@ -796,7 +796,7 @@ function convertGaugeElementForDrawing( const enabled = helper.getBoolean('enabled', true) if (!enabled && context.onlyEnabled) return { drawElement: null, usedVariables, compositeElement: null } - const orientation = helper.getTolerantEnum('orientation', ['horizontal', 'vertical'] as const, 'horizontal') + const orientation = helper.getTolerantEnum('orientation', ['horizontal', 'vertical', 'ring'] as const, 'horizontal') const inactiveStyle = helper.getTolerantEnum('inactiveStyle', ['transparent', 'dimmed'] as const, 'transparent') const thresholdsRaw = (element.thresholds as ExpressionOrValue).value @@ -818,6 +818,7 @@ function convertGaugeElementForDrawing( value: Math.round(Math.max(0, Math.min(100, helper.getNumber('value', 0))) * 10) / 10, orientation, reverse: helper.getBoolean('reverse', false), + thickness: Math.max(1, Math.min(50, helper.getNumber('thickness', 20))), multiSegment: helper.getBoolean('multiSegment', true), thresholds, inactiveStyle, diff --git a/companion/test/Graphics/Image.test.ts b/companion/test/Graphics/Image.test.ts index 72a122f8dc..3f104a8048 100644 --- a/companion/test/Graphics/Image.test.ts +++ b/companion/test/Graphics/Image.test.ts @@ -280,6 +280,98 @@ describe('Image drawing', () => { // circle // ------------------------------------------------------------------------- + describe('arcStroke', () => { + function pixelAt(buf: Buffer, width: number, x: number, y: number) { + const idx = (y * width + x) * 4 + return { r: buf[idx]!, g: buf[idx + 1]!, b: buf[idx + 2]!, a: buf[idx + 3]! } + } + + test('full circle - arc pixels are colored, center is transparent', () => { + const img = Image.create(60, 60, 1, null) + // radius=20, center=(30,30), lineWidth=4 → arc spans r=18..22 from center + img.arcStroke(30, 30, 20, 0, Math.PI * 2, false, { color: '#ff0000', width: 4 }) + const buf = img.buffer() + + // Center pixel (well inside ring) should be transparent + expect(pixelAt(buf, 60, 30, 30).a).toBe(0) + + // Top of circle: (30, 10) = cy - r → should be red + const top = pixelAt(buf, 60, 30, 10) + expect(top.r).toBeGreaterThan(100) + expect(top.a).toBeGreaterThan(0) + + // Right of circle: (50, 30) = cx + r → should be red + const right = pixelAt(buf, 60, 50, 30) + expect(right.r).toBeGreaterThan(100) + expect(right.a).toBeGreaterThan(0) + }) + + test('lineWidth=0 - draws nothing', () => { + const img = Image.create(60, 60, 1, null) + img.arcStroke(30, 30, 20, 0, Math.PI * 2, false, { color: '#ff0000', width: 0 }) + const buf = img.buffer() + for (let i = 3; i < buf.length; i += 4) { + expect(buf[i]).toBe(0) + } + }) + + test('radius=0 - draws nothing', () => { + const img = Image.create(60, 60, 1, null) + img.arcStroke(30, 30, 0, 0, Math.PI * 2, false, { color: '#ff0000', width: 4 }) + const buf = img.buffer() + for (let i = 3; i < buf.length; i += 4) { + expect(buf[i]).toBe(0) + } + }) + + test('clockwise semicircle - colors lower half, not upper', () => { + const img = Image.create(60, 60, 1, null) + // CW from 0 (3 o'clock) to π (9 o'clock) = lower half (right→bottom→left) + img.arcStroke(30, 30, 20, 0, Math.PI, false, { color: '#00ff00', width: 4 }) + const buf = img.buffer() + + // Bottom of circle (cy+r = 50) should have color + expect(pixelAt(buf, 60, 30, 50).a).toBeGreaterThan(0) + + // Top of circle (cy-r = 10) should be transparent + expect(pixelAt(buf, 60, 30, 10).a).toBe(0) + }) + + test('anticlockwise semicircle - colors upper half, not lower', () => { + const img = Image.create(60, 60, 1, null) + // CCW from 0 to π = upper half (right→top→left) + img.arcStroke(30, 30, 20, 0, Math.PI, true, { color: '#0000ff', width: 4 }) + const buf = img.buffer() + + // Top of circle (cy-r = 10) should have color + expect(pixelAt(buf, 60, 30, 10).a).toBeGreaterThan(0) + + // Bottom of circle (cy+r = 50) should be transparent + expect(pixelAt(buf, 60, 30, 50).a).toBe(0) + }) + + test('snapshot - full circle and quarter arcs', async () => { + const img = Image.create(144, 72, 1, null) + img.fillColor('#111111') + // Full circle on left + img.arcStroke(36, 36, 28, 0, Math.PI * 2, false, { color: '#ff0000', width: 5 }) + // CW bottom-half in middle (green) + img.arcStroke(72, 36, 28, 0, Math.PI, false, { color: '#00ff00', width: 5 }) + // CCW top-half on right (blue) + img.arcStroke(108, 36, 28, 0, Math.PI, true, { color: '#0088ff', width: 5 }) + await expect(img.canvasImage).toMatchImageSnapshot() + }) + + test('snapshot - thick vs thin arc', async () => { + const img = Image.create(144, 72, 1, null) + img.fillColor('#111111') + // 270° arc (0 → 3π/2) – avoids angles that normalise to the same point + img.arcStroke(36, 36, 28, 0, Math.PI * 1.5, false, { color: '#ff8800', width: 2 }) + img.arcStroke(108, 36, 28, 0, Math.PI * 1.5, false, { color: '#ff8800', width: 14 }) + await expect(img.canvasImage).toMatchImageSnapshot() + }) + }) + describe('circle', () => { test('filled full circle', async () => { const img = Image.create(72, 58, 1, null) diff --git a/companion/test/Graphics/LayeredRenderer.test.ts b/companion/test/Graphics/LayeredRenderer.test.ts index e67e8f2018..9c42a7a4c0 100644 --- a/companion/test/Graphics/LayeredRenderer.test.ts +++ b/companion/test/Graphics/LayeredRenderer.test.ts @@ -1083,6 +1083,7 @@ describe('GraphicsLayeredButtonRenderer', () => { value: 50, orientation: 'horizontal', reverse: false, + thickness: 20, multiSegment: true, thresholds: DEFAULT_THRESHOLDS, inactiveStyle: 'transparent', @@ -1163,6 +1164,76 @@ describe('GraphicsLayeredButtonRenderer', () => { ).toMatchImageSnapshot() }) + // Helper: draw a gauge on top of a dark box so inactive transparent arcs are visible + async function drawRing( + overrides: Partial, + size = { w: 72, h: 72 } + ): Promise { + const img = Image.create(size.w, size.h, 1, null) + const bg = makeBoxElement({ color: 0x222222 }) + const gauge = makeGaugeElement({ orientation: 'ring', ...overrides }) + await GraphicsLayeredButtonRenderer.draw( + img, + makeStyle({ ...drawOpts, elements: [bg, gauge] }), + new Set(), + null, + DEFAULT_PADDING + ) + return img.canvasImage + } + + test('ring value=33 - one colour, within first segment', async () => { + await expect(await drawRing({ value: 33 })).toMatchImageSnapshot() + }) + + test('ring value=50 - midway through first segment', async () => { + await expect(await drawRing({ value: 50 })).toMatchImageSnapshot() + }) + + test('ring value=66 - exactly at first threshold boundary', async () => { + await expect(await drawRing({ value: 66 })).toMatchImageSnapshot() + }) + + test('ring value=75 - crossing into yellow segment', async () => { + await expect(await drawRing({ value: 75 })).toMatchImageSnapshot() + }) + + test('ring value=90 - crossing into red segment', async () => { + await expect(await drawRing({ value: 90 })).toMatchImageSnapshot() + }) + + test('ring value=0 - inactive arc only (dark bg makes it visible)', async () => { + await expect(await drawRing({ value: 0 })).toMatchImageSnapshot() + }) + + test('ring value=100 - fully active', async () => { + await expect(await drawRing({ value: 100 })).toMatchImageSnapshot() + }) + + test('ring value=75 dimmed inactive - both halves clearly visible', async () => { + await expect(await drawRing({ value: 75, inactiveStyle: 'dimmed', inactiveAmount: 40 })).toMatchImageSnapshot() + }) + + test('ring reverse=true value=75 - counter-clockwise', async () => { + await expect(await drawRing({ value: 75, reverse: true })).toMatchImageSnapshot() + }) + + test('ring thin thickness=8', async () => { + await expect(await drawRing({ value: 75, thickness: 8 })).toMatchImageSnapshot() + }) + + test('ring thick thickness=40', async () => { + await expect(await drawRing({ value: 75, thickness: 40 })).toMatchImageSnapshot() + }) + + test('ring multiSegment=false value=75 - single colour active', async () => { + await expect(await drawRing({ value: 75, multiSegment: false })).toMatchImageSnapshot() + }) + + test('ring in non-square element - stays circular', async () => { + await expect(await drawRing({ value: 50 }, { w: 72, h: 58 })).toMatchImageSnapshot() + }) + test('unsorted thresholds - sorted before rendering', async () => { await expect( await drawGauge( diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_in_non-square_element_-_stays_circular.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_in_non-square_element_-_stays_circular.png new file mode 100644 index 0000000000000000000000000000000000000000..74edbea8e56e805b6d6f8e0b8bc32940836555b4 GIT binary patch literal 2124 zcmV-S2($NzP)Px+_DMuRRCt{2TV0G3*A@Qe*B+18-m%$@cPXS8p{5(6 z23D}qqNXJUDWXaq(srffp{bf81yxa06(Kbe%|mF_v{WL^LzN~`(xR#l6;)JKQBxKv zfI``bv=2xiAkc>01utHE{j+0_J#%OJu($#4^>RHP8zb>aBUyK@&b{~Zx#ymH&Kcky z?x7tBjUVZB+Uv%EB7n4QW6*J65Q3!Z!XN|&AxIE{C$m*ZS!wB+mmp_l$d_eAjzyqc zP`WWi0Klb7mjnQiyES0iwz1i=Flt$d6VkMwtGRGjyRcW=H?`+p%0c-|fqq0sUns&RKk{O zwG-k20M3wu>@Tt??kpm)KY_?UA{`k=3|inS$HB|FTt~^d5%sloWPY2$+?#XoBEg%{ zKs9K)Y2sUpix54&iB?4s1yMvv6j78U6h#sJZ>g|X*>Hv(IKvJ|l5hW*kC~V~G>g>p zDafA%x2(Wy(9@+7wpMO$Kd#9#PG}lVgu^IE(rx>@eJ-rc7OXJ~)v+q-BXxLU^Qnqu z5vg4%g#Q_Cc?~zBVS@r8=yB7;Q{{5g_nsi&L^zC(G!37ss^4J%0M7jm^1Jg`+_BiS zkKhVO>`x&6M%?!}fe|!Tt>U?2vFZCOvW&O;`!OvCdmhvtsv);G2W!-7+Q)d^fN{X^ zCBH#XTSj_)t%mQHO3ZhzAmIII6t5(cfs+FOq0=D@PYfgZY7&AgF#9aLumE$1+15C| z30h;@*i|S%U>M{%S;o(jNxT=0f(Y!%2Zi|W;z;dGK{VM*xwiir>LYdE@_i8$iMW_} zxWqgHXB7qeI;1ttpna%ewKNV4me=edV zNjP8_sC8ItSWrGyF!bFah-G?7&Y*+&SLS`mZJD68Yi*2=FFAqzqXhb!9)_zmm zMrA8sNCmABw0Se_v3eeG_^{4BShtbbpMXGUgI#&F0|bqX&?mr|Ga+QNy{A15$kQ^k zV;a?7`C7mP83th*=Ti(r*lUO!r2}csumd}7GyC5PE`lC>knO6UJ=@;Ryc^Ib*(RX+ zC|^L2i=fr3X|{7Y3Hf~QZm$NUs}hv|(#KZ)0lxbg_ZGVqGc$MIDddHEj@ESENF^sh zF4a~j^uG2qAT7{yC5b=@qJ=Fm2JKad1$vo*YLKSUF`r7M`!{yKkQmw?s0P_KO-3lx zl}vRoU`>_t|K%d6UZfJ*SSwU-y#OdI25=6Ht%3VYdBbq32-XFOLVj1KCpCQaU z0Tw}Wmd{P$B8cVlq&Z#nUO?>u`W_kR7Mq!&JyF9Th(>AusMkg z#*NLBcD;gTH}}YgeAOUa1WivXs8r~#Teftkf}zKYkp6j*`D}i0%+mzZ7J7Ie3X%~OYm^?x{)>? zGhvP0`D5Q2voQa2b_O;1Dmz!U99j+wPEBdZjMgyqFil4Bd&Q*?=jHAdeBTKBi#9TE zWZ+4(VGf-RK|9KCdm8XnX>jS!eGqE&4d0VI6kaI6jdkC{w%6IXdhjaTK6-4aB;mdn z@ADzFgzQ+l)PTC85Soq-r>R-BjGETjyLOZGp>6Dq`?QgNPh%@7s+)2?78}Kxpy0>lo+pqy7wex?jpTmGo`1P>%eKw<<~`FVTg z8~l|Q4G6Y?_#fgheq%sb3^pAVc>Y+M(x9GGQQTQXeVzaMWM1e;bR_pBAz$uHD!e07 zrGkXSuM((#xsJ-#3Tj`eH8rSyp literal 0 HcmV?d00001 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_multiSegment_false_value_75_-_single_colour_active.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_multiSegment_false_value_75_-_single_colour_active.png new file mode 100644 index 0000000000000000000000000000000000000000..7034afac542848803fd964312a17b12f5db48ab4 GIT binary patch literal 2735 zcmV;g3Q+ZlP)Px-8Qf+jow}cEK9>^y!YDO&7ihcflHT^V{w~k zXlS4SKoaQ>n&fsP-Qhr5e?QV34#aSrXnVPwgTcVTU~mZ@HyRmSxS&Esg&Nh>O7!Bs=}u=r=RRez`rX!MqfuS0#E~N!)YK?~xKlENJt(XK$#@?5 z77PAjwL%}7pNuV@` z1OMpiLRx=+$Two4X*5cuxPD!M_V&=eeVL5Mx^+gJJJ*Da3`a=U3d#mi01C`zEHj%C zs1Iq8NKmg(pp#+1G7NfX8eI$n7v+5=osL67f*Z-nE@Wj{k)LnDOzT(ML4laTH*`AP zKgCW6M4TF>61#Oe+>pz`GK|mm!E)!049AYep}k##haYwzCPv&SKlYdnHXDt)I^Qu9 z%d-DMAVzw5U!PBL+!TeQ8V&xV(Fod-DHsj?G+YG-fk}jT;R(cu;%@(KL_!`#UgWhJO*hPk{_B7uFk% zqHRnxja_;@8Y5(D{DtR%4?onSs3-~j{p3eet2rD#+zyS#cbp_D5W@iLicByW$j$5& zg;Kp9JrT4ufpYe&8mm?%qqCFjF{e#);=q9ppVtxKZ_Wmvm*GVFHpBQ9BD!{*K6ql0+1M#Ev*vOWL+MFUfcTrmEfhDCkv z6du68wOag7CJX*4BFyiH{O_p-d4!`vJgGOC7~Hrq{=JqjPM=oe)-B=CBsP}AqD7w0$wQ#jRC3zj(j_H=%)^g6jvmzz zkLBceE*f|UG-r;mKs*n>X3EjR_urEz)K5J{5J;ush~BHINg9=&i^QLQO3>OWJUyB* z!+~kjhIJTv>bY~t!>gg;{?5`Pf!K(6^wD8=KO~Tt{rTgMi6@ZQFiT7n=s~2la7@r1 zbo(}WlAV}19Lo#|q*Rh~_F{RJXoY3T`KzQPfgYq%2@7PmQxnF$kHlo6!EwTAkV%sS zdyudSHrvGUUL)Z+5Iu8Jl7K*?b3cPvtrJrq0L0n-NFzODT%LT=2D#k37l|3OXon}5 zU3x^H82YofSGcFd#Z5p1G{B@u!k)XcbGT;>sX`i${w$3|BU0(T$*B-sT>=7`Ou_;w zm7GVHn<$L5Gouj{h-9z;VAia_>I|cSk*<6+Bjdgyff^gh(ek``!U_CnCOw@zwG!+> z_4VXr*t~h6SCEZ2va-mH#~;N6npi!EVSp!|Ag7jZ-W=9qNTAkM3A($5?a=Jm zo;i|eXW>FSaprLMt^_x44#$2T{z+`b;@M}(Yh}@f*zh`k-cth)fxiAamU!ao)usvI z?&WeG&pcx$93w+??r3ZnvjqnY%c6}8kaA7Cel0v z;(6fYN%9)@jvXxI@-f*I6cJP^9$U8b5|3ZFpz>Px^BOeeAl8g)o_U^qa+UD?K4cgnunGDg;Z~uNR>g&m6`-AnylP8Zqqn)1az@bC!P$(?7#vx$2f zp8_>CN$}1)38HP1lU+D@vIR?)-2YjBVPP-I$~uT13j-1J%P$)|(i$ZE+U@h(Z!)M< zJQge<3m}*CSiam0sT8QK9s61$xtzyGA9doz7mcDJ2+vd4x-|*cu8HsX33#_paj_mp zj*ttFFkrY}d4*Pw!hqJV+|3$|}(F>|KxFT?Go@WBUqeEO;Qmpn*F`{sA- zU`1n#(KI$nar$%&Dl1j^>8H@@95XZRSi01L{CtaOx$;QNW*S9BNvN*&f2&r=KP-@& zYei}4U8q!n8JpHt32JJTxPD!Z`g(;&_HHzTn|h~AaUn0yg1kHnbh^MoipEAMHf=KC z&Yi$Yc*63_3#qA296HqI_cuv`Wf>TaG+35_RLUbE!7W;^E>jIjMQ zg-XSvu#m;Nbw*+d@mSNm3C~kFb4HC_yW-K&5uTlo5ff+-osPo`FPQM$ zbEc4O6p3rsl&Gjs;hS&N#IoLCiMT)`S-8-Sl`G9CC@}ln+b%|bKZPHDP@vzbPFy98}*l7UwS9UT%h pH%m}grwG}$_{*R`4|qUm{2$2O&3~ypR!IN=002ovPDHLkV1g|vI#>Vz literal 0 HcmV?d00001 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_reverse_true_value_75_-_counter-clockwise.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_reverse_true_value_75_-_counter-clockwise.png new file mode 100644 index 0000000000000000000000000000000000000000..4dc3ea7811fa79a2254f3f947c29ad3e2e8c74cb GIT binary patch literal 2666 zcmV-w3YGPVP)Px<6iGxuRCt{2Tz_yB)gAxr-tO()?QJf1m!Bqx0SPcr zB$NRDpp^=>qE#3=$c)x@s#uw!Q+2d-ySG34M+lco_U+x}?vhL7Gn1Li?R#(Ee(w9ezuxyPa0OS852&FJ zGiJ;%#^Z+aY#H(#8FHfxd9Do2uR(X~&;=cuU&A3cj|4?Qq-ls0g)mLy0?VRVN)oCR*u6jJRU@7IaEOD(6y%v{oDHUBF7f8fj!5aD?F&g|K9^4rYY~91G@xxezmkfeKB7PuJnqG*oID7D*CnR5ho28UXeR0{+PJ zIrSSd^J5|?@?ZpkCjwdDO!&P3cZ(a&_nbLR(hT*gie^#75=nwz)3WaOp3{k6c|6GW zA;;_og&qkZv^kWq9i>rl?{~xd6EB$ag)^2N41*3qz+OSXOhrMnC}OcBp-i`4;x~#S z0OUjxawTZszJaXB;r`8`^07+T|6|W(_?+t}8k@!=X|Xe(v$$^|CT(8GN7{MG<$#vUf$YB1l>$q5u2VhG+F=VzX*Z(e8|b-D1I&xc5c@Z9gyP+1g!1zSj*&t}MVW-!Nq{VWJ z)zG+qa|D^95=(+YkAxsCl8D#Yb1Z7VUJEK&_#iP~{EubvqSp(31Oqxvp>q`j|H~R0 znq;h4VI4CP2~z7+gufXkcBEtqH4oH)4(9Lm77J(XcI+NzvEn?9$QBxP-%>$cn)aGC zQFuIL7lHV6!6$<-Y-GLT+u;M-&gL=w@xgJ21HUaRL(jDg>_0W&ZBADg4u^sD>j#Ma zO$kaiB@z1)*%UfIaKiKI#96Cc@fVj1y!tll$0u+U;E7z`#ntnfuAZdvNsfnzZ%U>*qt+#Y6R<;NRZD?t66R$TkD5$R= z9SDyiDEbw1CUMCb^z5EIOKT+Few#;UCpo%WZjuE}Bgk+Wh}}WfJ-+9Cpybjs>DfZl zfVbWvOVJHCh+vp>J(WgK?2Z@=&Rmh%f;PDKkq4hFICY9cEM{)JInKcR`RU3!tz}{* z*=Z8?2*AW=RP%6ryR!11Zm~wIUh=3|!!UPxoz9U;+5n|_%q55pmu|i%Uqqwk1i4(Jt(ZU> zWU5LK0EnXbk3W2g4Jj950A@JGrSNP*(A+Fx)~wNaAW2FQH~5KyPAAOSEQN}VVxgR% zcf;Y41YIgDFaycEo(ia_FxR`0hzUWYB$!sA!LU!gW#)7mpa^??;c$wJ%LLim%w2E8 zW>`9rlNHxoLw+XHCB|B~-g0ApMM$NHMHc&g=g+6OND*Y=dP~=n2b-*DY9jB=pFEig ztx`+Oq8cNWW9J@N51|EGhNW9+C;vSXE z&xs(VUctaU#$tvR5 zF&94mc(fHajZiC|sUQxd`89+$kqfIz66|0p;o|8EcD(O2t@7Bu-EZ2}lpx379pI0U zms)sZ7}Ao-;}-?K7eI1;60+UE6H7%b?qbn8EcR?^5%AGR<~|@XpXz(o2TC(fNgAwy zo>zJx&o2EKEq-epk$WR)-<_T=|TbGJh_>vbrJ!I2tnUVMY$O}43ZlYGI=y|mVxL_wY> zqwBRU=nnGSoF6!0ykcGNWJwSmrcwRvYEU}4CPj-QMpyObNWqs2S_(MF(ew}@-Q%>8amEWm^>!>SZ zJGDwh-}8Ow+}jCxevw=O+u-@N3r8*>a&N@4=zR$818!75T%FTxazB#E4%Sgez8cA* zF+dpp9b_3#a5s8%v#BYgPD&diH@V?+h`Ag+l-z%>(s{oK# zl|bKfeL3qJ=U<(0oN$0U!@=G&Mf)E zkvBoObl|0LS0AJWV-Gv%Bn>)}`ZL@HG@k~wDt`ym7OMJH?@HO&yYpl8CO4BDOjP zoiC2#$ab>e5Afg*^5FhiSYn1y^fyo$yn)#27-DzDAYPx@k;??8i-G%~8~g#j;Lfr0Sc>VHgx<-OK!@+*m4z860cZvgdx_G1v#}GkRa0U6{ Y{~1=PlLl8P3;+NC07*qoM6N<$g8x(X6aWAK literal 0 HcmV?d00001 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_thick_thickness_40.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_thick_thickness_40.png new file mode 100644 index 0000000000000000000000000000000000000000..66800ddbb7d3cac956bf07e08b70b02f268c8ad1 GIT binary patch literal 2610 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 literal 0 HcmV?d00001 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_33_-_one_colour_within_first_segment.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_33_-_one_colour_within_first_segment.png new file mode 100644 index 0000000000000000000000000000000000000000..7c406aedca4badd7c940de93faddc1368b694c0d GIT binary patch literal 2627 zcmV-J3cU4+P)Px;?MXyIRCt{2Tv=>f)fxWIUB-9utd8x(OIQlUDWHTj zAo`G?qy&eh5JBOgqN+k7wNwqXDp09?=wp%E3Uy)WA}X;38lWnbphkfZQ=|Y9N<~?k zhS0=uu$$Q9>$}XIyPZCamyCVSo!uUrd@tkiIdjfE-<*Fv{~6#M&Y>2N=|3*K@Itx} z6L$DMdR!Mho`>^X7l|N%LMf!F*Xze};IbU}3BK^EO}&oskPb&nJF#-TW zqbF+jeXMaD^yTyDa~!k=L1h1+B|xw;fVa{MZRa%}%4-VLS5?S^GDx0;_Nx~fLXbcy zuC#4zwk(|Qx^*4fPx{br)uG?2gUfM{2W2FmNJ2U&)ped})RmwZrRXosIWu&%L?+mu7=Ut5uWC=KCc zeU?`9D9IqePEA8!K3|syss)r#=-YJ|HyB7fok05GG`K8R)0j1Vy3cX&(=%u4LXOFC zI4+8i4oeV5YWv=3j6&O?VQ63o&ZSOGV^wA534j|612-E6DzzcQf`ETY5>E3xvOJG* zj>8$Ahezg*6r=^)g{ozdbqe&0BVN09tOvg|p^R?^#by(Bq4A$Tg8#N)|i!^xSQ6o@|?NAGpLP~K2V zb{~IW9Jx*9tB0}(T5sFfW}1&puVi4>-H`Pr`amj?R7Ew$Y{y2A_>~E#l!_yxM+LS z$M9AG0avV7d_wT+bQ;4on(js*_CXA*zPSpV&0b>e>KwFhM@t9u)2Wn??b{6i0O5e{ z-5#``a4^-vM<{^brBXN=i&ejh21ERhh>m+ZKq%W_Wbe)*NU=qYNzjfRlMuyX*C>6Z zkJw`rpIt0qPa=WCEiKhP`+`B)ry%`Mn%N%217z>XGTTE5y67SYYu1E&sbZ31^G5LKY!ixR(kEV76=Is%QTJ0 z>#r{z(VC0Io&>}pwy&mrYraJ*B1o1gHg05#*T8^^1<0f}4la=SWro>oU1I@rq_Bvf zwQKEpmTMZeO)xmPIAKIRq5e%p?BC(8Ne~ZUUuhTpuZW;aE@7t)_U~8VI13bjHW@VH#nB`)IpkzeyXkudNdCsN4XW99yvvb=e2?_!NU0;3#ag!WK z5bn14?IAlTkq9R!TjSAf(;@&kMoj5aH6|=8SbEn;5_zG2th+b>~zLu zm(^Cr){L+=u~W+}AJ1klW(YcShDR60}1jY;^7)H0`Yklrc2(on>P?-RWD9{bM2ZMW&*`6cW$#(;^tCz#Fg=i`O4 zm9sE!yBv(T6w=n_N1od^JB)1xRIKO;v9zjQr#BlsL+b3GLF$ceG zaqeT(0(Z3w<0b=v57Bq zpzVw!ghioIPP{JE-q3dT(a?DfrS`n?{nP+k{N zQV{=39Q^;-CmhiU=a*X}>uCT$+o{1_-F#t)yV}M0{o@GY?BlH1Cow3mMb|q;6XXM_ zM^lj(XQ@b$y)WBTJIU#D3gj>nVgNw$Q+GB>*qV|@7W^3=iKnA$5(6o~#CIm3-KBxb zi)Y;^Phs3*AbW2Xv_uv(u{ROw56wkcB^&ZV8D5VUsq8RSnC*Z86<{Z<|3w&>@Y zhPqEh{Lk^S$F8X8C;n_4ff68hb?NIw=Jh75%@*W$WXK2PlFGhP`0YNdO%`%r$uUnS z=0e%8R74J{y0c0Oj+b%05otc>Li|*OazKIft_0#%x2vc?VQ;XJ+n9rMab;OfWxs;N zp7QIvtG=^3jr_ViCU#DgREA*?heQY?g(@%MqyS+=06$u+F#VKYxX|0`6d+xALE@s0%>= zKwBt`8x2@ntwIpk1R#_^9+VM(F9u-7iG!zc_3NIsE- z_-TENLfuMN`O`d752p%wmsUU4;OSn;#?-vUuop|CHvkzb!j{?dHSi8hiah4X+L-7J>Y>40-BE$zAQj>+<0BdhoiunJNpaPy|W<;t~*>fU`Len}C=E;wcfr lr$WI!SVjfy=kR&J{{b2hwwXoRf7t*4002ovPDHLkV1n3D?G*q3 literal 0 HcmV?d00001 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_50_-_midway_through_first_segment.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_50_-_midway_through_first_segment.png new file mode 100644 index 0000000000000000000000000000000000000000..b760211496cebaabb62e5479126a25312fd58137 GIT binary patch literal 2663 zcmV-t3YhhYP)Px<5lKWrRCt{2Tx*aNM;-t5ymn^i_IcccJ17NFAW0Pv zfl90b3goy*sw|2meo9Iq_(%*0rBN$dz`5v-9x7aU6R)J^Pxw+avtG?A~>6_w;Z3|LlJ+a1%FC3&`-BnKNg) z<1t~gZDU$Kk7<^L59RZSIu63F3-Rjfr~0#C6b9@7JM^kveeGu<9u*`9R0EV>Z%j(@3vPgUK?G4oir=5QBI`40N7h1WJ(Ly6Dt2tk8AH zC3Z>>a3lv>rv|N4L->zjG;VDKf0_?;j=%`|xMAQk=`<3y9n_E&L0_gLyEKc2-!>qz zEdlOAFiWd>l$hh-K~=?^TrQ9Yss%1_kzSKVW_1S9T~Q>SN`O(An#Qc@({oG{-|6cM zgdB}w(8co*kBi{CYx~|@!G*d`#rb{bVcu%iG*(q+o&Z>#$>7tO3@WuD7da096h-v1 zEEJYSieb>lvam?;NKV3mJ;jFAW) zMf^<^ah4C(vJ6A()S!G-f%%d0#3=bh67lWvvier+C{{ww5lO-e(Ws}`1POVc4EcbJ z^nGb4n-o|r{^FPVS_;G>i2W>9_8es^={4zeNpfsLurn6Jj#zBW$r+gxi2Ndg>8qwA z{ARdh_te)@$Sy5kJ(NYz?S_H7G|jh<%Cgv+OyaOKKC#khkPJcVhE~L%@fRVNanbXW z9^_|~=i!$msMWHtUR8bj3^EKJPbSedM!KeKxIpszNwjQi0h6OI&5<2+Ki3T>R@&S5 zC5UB#bsJR-w$i&9gkW1Ti4@D0oNT-x@0HPZUmLhyx{A$D&!gw*o|0qv60~B4h8YX; zXxwh&Vh0C@Dby=AA^1@ufr~Yo?y-PyLcr7qrh+l(OU&Mph5AK*>7af(9=EY(O$Go! z7@&Ql1@&7DG7qs@Xx+L&@O+_b zlvrd#c*e!$k8;=@jpBGHRPD2m8^Q;|NNh_`+ntC5Ws^c}_atc6EE99*dV8sS%tiP8 z9Nr9b8qPwHu&(nQ1^|Qqmt$-j&aUR6x+&xpacwetYuoo?%FTqOV2&B!!@+CL775_lZP0&EV3d`U@wj1$T}E zM_G?K!-O?`#3#B&ke_&+I8px|bEWXh9p&bYnEPu4O`~{{0C4J5{l;6>!cWYsW{({d ziO>)HE?k)4JkJzUE;Bq)`lD`P(|d$Qts zQ$8BkpQl}8<=i>Cx7^+yY$0Yn!1T#nc=^yd!#($;!LpOrX>`Lb1|Axqwuj#c1Ebud zqV00T=Tb;xqwRZcU++*KRAI;I4r1*2;^Uwqf=-_n(A7oviRLfK`c6`>a3T%{zChnS z@MO4_ zz%U73yu@L1!a>)loONbx2IejG?_ujJbtsR}E67NmAo)XjK?9Ybo*oXH_Q}}OX2JGI zy}87N@+E~@nN<%kZ!vM@$t%8nV$a9GcGFKd{1eW58+_|&T+u~pbE+cO`@H&k$NlzbI!!;q=82U(6haV8jRwq$K3k4)`GMKwQo6-N=GEf`(@ug zj^seuq9C&-19t+vZ8zlN%8#!gwIv0YQbGs-#3g9n*bL#l^6CxBu4#$W?9%Sipmk`l+R8HwOqxNR@4Z+93A45%AaxRjz#8d!ye@SXtveIEQ-9{feVD1siwwM)J27PM6wv{f4HL}e}o z-^HW#^R3|jTiF*=^`+kHVYOROHY%_wrF-RoxCBHeU~~o~O<>$<43R?l?T$(y{Z&HC z!!2M2E3YpIgdhxO=3h>K7ED~$k;yTL?T8`%bR0wvCfyL29cFu3#J9&`x7g4+G-U70R;-_@1#t=F-^*xx zyb)|qus0tAcU0IO7SWwiH2kIk*=1SiOZ1v_6b-~C5dR?}ygv+ipG+<5trcVD$uSuQ z^6N6>*JYTqOk_Wig|X0pD^|a&NRB}I%aB(uACSTL)Dr}Saj6a$&Il0B2#D>9A>WpV z)n>t(X2EK+;6xp`VHb`#^vYxyaNQiZ%N)3F4(w$XTn`8C0tfM!7?^IkiJKT7{2#g6 VyZq`d?FIk<002ovPDHLkV1jdo4i5kT literal 0 HcmV?d00001 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_66_-_exactly_at_first_threshold_boundary.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_66_-_exactly_at_first_threshold_boundary.png new file mode 100644 index 0000000000000000000000000000000000000000..685146c568ae1e8970c7200098b9b554270b8249 GIT binary patch literal 2635 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 Ef*@xCH~;_u literal 0 HcmV?d00001 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_90_-_crossing_into_red_segment.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_90_-_crossing_into_red_segment.png new file mode 100644 index 0000000000000000000000000000000000000000..ce8cad26f9e94ae80f4f7f60636692d95f9633d4 GIT binary patch literal 2736 zcmV;h3QzTkP)PxVA^V;2+*@t&+?6tu_poKuB zrpiM`NgzTASOtrcw*E1*Lh=mPAyP3Y1nQB@syoA(SRG zR$G)b32C4OL2T@`V~=+|vya)CJN;n%SbOg5dUw2jh@WKnF?U}7J@?*o&OO(_HLg(# zO#csa=FG9mal;@Xn3c_<-Y^h0O#}()`!Y?02*IkZ6j+4?*TeOFk6-=X#liPaAH=_j z<#zHX62$`m1d*US^Q34}s*dFKN9Hh4;$j4-er$jIr z#VqduE;>yo1)d1n%- zB`Ns-;zxLQ7<`L6uU7I^H~?Q$RjkxBSG!U%Sd4|ieaF!EPNmI4bZefx)3ang?LjO>b)vzn`+_t#8dROik{|g z%4G0jdpj=kJUTe8pmR)y!6iY!XM%w99uNNO@gU96XCV?MFsn=$bq114lF%A87`3Al zj!m+td8B64?m_=FUz9*AG!45uI>2A;Qb+xMcnP7lPp~ZBktDn$Nl3D#nY=z1(iwG+AR20;7$j>9F*<1rQ zLw_|Q6SVJZ2MN(9mVO)s;y7T(o-gqEG`jm9<9V!}J{|9g<%zNjK|U&@?*2OPopd)Z zTc1Vjg;?HeISRCHT^zTpOku$l9nEV*WQWZ}agM{c>gsS#a5w&O1J5TO)IL}XHcfxU z)S?vP8{&@Fp`IO$n%J_XZ#rjK3k`ot;nG0?ZN9!w#bg+)k4DkLk9o$VXoT|usvoZg zW6@mx|BL5>o1J6Q3*)`DKK9IC*EXF9SG)9D$ZC(?-l({irI7g9j(C_4FgZD#P=c zeFBXH2-3HvbN-ei(4s~3Anw3{AkyivU1BL0p#$`gD7iFu{LK;Qw%h0ez4FTBnbz|m zzDo~_l6O%AnmN-zU7fuz(ACA^{P~F&P817&nFoKxJ_69M(_z#P>M$UXQ};S`Du143 zvf-3F%G@|O-46&fe?C2JdHVFkiPYV~shb(qHWpM>MbH0z`f2gYCKDCUIr=-9)q`3L z2sCXPU7%QO;zsPn!YTA4z5G+YZvHl)br$;$#lK@<3aMU~PV+3FPyQZ^gz~Vo-(^ zZb&+3K%g`wxh;xQR3M=*IJJZO!bTvP1qFj9Bx&l2G<2L9j;%m#ZS>jd`s>HOoUYuM z4u0Rz^|S$j&Yq=vmQG>FWJO;u0smoo_ka` zpK)Aq>R!@Yxp|@-fsP!ZuRg9?H8ljXycX1N(%(fsF6aE7Bhbl{65?_DZdO-kVC729 zt>+v!5)UR|R8f|h(=5dIa(hvZK-nzt`s?!W^9I^~U2x^6n&*P4o&fCd(kQi(b$`73ktc0bluT z1%A?yg2|0ccUYW-*q#{l2^{I`H|Xeiro*vKXkQ3iJAH-2IpO?V1$-aUw>tLM8=JKg zZ_w5k=xBeb9fVS?0s!8#UW9)|FRDTzh2+(bSA$K`n;F#xT3>E0xN^N5B$p*|<&RgW zDOFs}`9!}L1w%;`_LEm2{&OBxd1?}kIs>hTTajLrc5GJ;dN%bSwkJlNv2fL6j;@R5 zq&28*3%RNPupbqBo%cYH072XSHYi&ZU>v+{%WI+I*$#9+*$s=Kr-&JgLG|Wpc>Yt^ za(cmwUXkq)g!Vh{#jrRF%A*Qej|BKd)HNzv-fV%oB7Z;~*&cy(qVQ^!Q7xNR z{v=thZClu&&@_!hN$sn>bg3ybj z+i+9VmQ3YI#nHDIj&MnU|5ZPv;}X0MVN4;U8`990_3f26CVS3Z?f{WSxAMwg;UT^+!ux?2z7Ksl*}LHkZ0enEKwGXsTdu(jkItnKE(wS} z9EI?|(G83x3j_e!d0Di*)K>5(NrO{3uyGcg!od=(K2b{87T%BecM&y@)PPNlzP`W} zfp9g)qHIzSZ;He6OpFCa{tF)A-C+b?4gf`#7L0n&Dh(#tH~7^z)B)Ao1-)$zv1^@gE|jV-n;yWNKM&vA8w{6+-%!G}JXJ)D literal 0 HcmV?d00001 diff --git a/companion/test/Graphics/__snapshots__/Image_drawing_arcStroke_snapshot_-_full_circle_and_quarter_arcs.png b/companion/test/Graphics/__snapshots__/Image_drawing_arcStroke_snapshot_-_full_circle_and_quarter_arcs.png new file mode 100644 index 0000000000000000000000000000000000000000..dc6351883d6ed055a0c0d4093c40c0ef730f37df GIT binary patch literal 4021 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 { await img.usingRotation(drawBounds, element.rotation, async () => { - for (let i = 0; i < sorted.length; i++) { - const segStart = Number(sorted[i].value) - const segEnd = i + 1 < sorted.length ? Number(sorted[i + 1].value) : 100 - const color = Number(sorted[i].color) - - if (segStart >= segEnd) continue - - // Active portion: segStart → min(segEnd, value) - const activeEnd = Math.min(segEnd, value) - if (activeEnd > segStart) { - const activeColor = multiSegment ? color : singleActiveColor - const [ax1, ay1, ax2, ay2] = segmentBox(segStart, activeEnd) - img.box(ax1, ay1, ax2, ay2, parseColor(activeColor)) + if (orientation === 'ring') { + const cx = x + width / 2 + const cy = y + height / 2 + const outerRadius = Math.min(width, height) / 2 + const thicknessPx = outerRadius * (element.thickness / 100) + const arcRadius = outerRadius - thicknessPx / 2 + + // p=0 → top (−π/2); CW increases, CCW decreases + const posToAngle = (p: number) => -Math.PI / 2 + (reverse ? -1 : 1) * (p / 100) * (Math.PI * 2) + + for (let i = 0; i < sorted.length; i++) { + const segStart = Number(sorted[i].value) + const segEnd = i + 1 < sorted.length ? Number(sorted[i + 1].value) : 100 + const color = Number(sorted[i].color) + if (segStart >= segEnd) continue + + // Active portion: segStart → min(segEnd, value) + const activeEnd = Math.min(segEnd, value) + if (activeEnd > segStart) { + const activeColor = multiSegment ? color : singleActiveColor + const [a1, a2] = reverse + ? [posToAngle(activeEnd), posToAngle(segStart)] + : [posToAngle(segStart), posToAngle(activeEnd)] + img.arcStroke(cx, cy, arcRadius, a1, a2, false, { + color: parseColor(activeColor), + width: thicknessPx, + }) + } + + // Inactive portion: max(segStart, value) → segEnd + const inactiveStart = Math.max(segStart, value) + if (inactiveStart < segEnd) { + const [a1, a2] = reverse + ? [posToAngle(segEnd), posToAngle(inactiveStart)] + : [posToAngle(inactiveStart), posToAngle(segEnd)] + img.arcStroke(cx, cy, arcRadius, a1, a2, false, { + color: dimmedColor(color), + width: thicknessPx, + }) + } } - - // Inactive portion: max(segStart, value) → segEnd - const inactiveStart = Math.max(segStart, value) - if (inactiveStart < segEnd) { - const [ix1, iy1, ix2, iy2] = segmentBox(inactiveStart, segEnd) - img.box(ix1, iy1, ix2, iy2, dimmedColor(color)) + } else { + for (let i = 0; i < sorted.length; i++) { + const segStart = Number(sorted[i].value) + const segEnd = i + 1 < sorted.length ? Number(sorted[i + 1].value) : 100 + const color = Number(sorted[i].color) + + if (segStart >= segEnd) continue + + // Active portion: segStart → min(segEnd, value) + const activeEnd = Math.min(segEnd, value) + if (activeEnd > segStart) { + const activeColor = multiSegment ? color : singleActiveColor + const [ax1, ay1, ax2, ay2] = segmentBox(segStart, activeEnd) + img.box(ax1, ay1, ax2, ay2, parseColor(activeColor)) + } + + // Inactive portion: max(segStart, value) → segEnd + const inactiveStart = Math.max(segStart, value) + if (inactiveStart < segEnd) { + const [ix1, iy1, ix2, iy2] = segmentBox(inactiveStart, segEnd) + img.box(ix1, iy1, ix2, iy2, dimmedColor(color)) + } } } }) diff --git a/shared-lib/lib/Model/StyleLayersModel.ts b/shared-lib/lib/Model/StyleLayersModel.ts index 39e665916c..554f8b22c3 100644 --- a/shared-lib/lib/Model/StyleLayersModel.ts +++ b/shared-lib/lib/Model/StyleLayersModel.ts @@ -216,8 +216,9 @@ export interface ButtonGraphicsGaugeDrawElement extends ButtonGraphicsDrawBase, ButtonGraphicsDrawBounds, ButtonGraphicsDrawRotation { type: 'gauge' value: number - orientation: 'horizontal' | 'vertical' + orientation: 'horizontal' | 'vertical' | 'ring' reverse: boolean + thickness: number multiSegment: boolean thresholds: Record[] inactiveStyle: 'transparent' | 'dimmed' @@ -228,8 +229,9 @@ export interface ButtonGraphicsGaugeElement extends ButtonGraphicsElementBase, ButtonGraphicsBounds, ButtonGraphicsRotation { type: 'gauge' value: ExpressionOrValue - orientation: ExpressionOrValue<'horizontal' | 'vertical'> + orientation: ExpressionOrValue<'horizontal' | 'vertical' | 'ring'> reverse: ExpressionOrValue + thickness: ExpressionOrValue multiSegment: ExpressionOrValue thresholds: ExpressionOrValue[]> inactiveStyle: ExpressionOrValue<'transparent' | 'dimmed'> From cdff5e2a6b9a0aa4ccdb830efda6135d908dbf89 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Sat, 6 Jun 2026 21:27:27 +0100 Subject: [PATCH 06/30] wip: rounded ends for ring --- .../ControlTypes/Button/LayerDefaults.ts | 1 + .../lib/Graphics/ConvertGraphicsElements.ts | 1 + .../test/Graphics/LayeredRenderer.test.ts | 5 +++ ...in_non-square_element_-_stays_circular.png | Bin 2124 -> 2172 bytes ..._false_value_75_-_single_colour_active.png | Bin 2735 -> 2793 bytes ...erse_true_value_75_-_counter-clockwise.png | Bin 2666 -> 2727 bytes ...roundedEnds_false_value_75_-_flat_ends.png | Bin 0 -> 2666 bytes ..._gauge_element_ring_thick_thickness_40.png | Bin 2610 -> 2807 bytes ...er_gauge_element_ring_thin_thickness_8.png | Bin 2661 -> 2682 bytes ...e_33_-_one_colour_within_first_segment.png | Bin 2627 -> 2674 bytes ...alue_50_-_midway_through_first_segment.png | Bin 2663 -> 2730 bytes ..._-_exactly_at_first_threshold_boundary.png | Bin 2635 -> 2696 bytes ...alue_75_-_crossing_into_yellow_segment.png | Bin 2666 -> 2735 bytes ...inactive_-_both_halves_clearly_visible.png | Bin 2646 -> 2727 bytes ...g_value_90_-_crossing_into_red_segment.png | Bin 2736 -> 2774 bytes .../lib/Graphics/ElementPropertiesSchemas.ts | 7 ++++ shared-lib/lib/Graphics/LayeredRenderer.ts | 38 ++++++++++++++++++ shared-lib/lib/Model/StyleLayersModel.ts | 2 + 18 files changed, 54 insertions(+) create mode 100644 companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_roundedEnds_false_value_75_-_flat_ends.png diff --git a/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts b/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts index 832d51535b..040c3a9818 100644 --- a/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts +++ b/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts @@ -135,6 +135,7 @@ export function CreateElementOfType(type: SomeButtonGraphicsElement['type']): So value: { value: 0, isExpression: false }, orientation: { value: 'horizontal', isExpression: false }, reverse: { value: false, isExpression: false }, + roundedEnds: { value: true, isExpression: false }, thickness: { value: 20, isExpression: false }, multiSegment: { value: true, isExpression: false }, thresholds: { diff --git a/companion/lib/Graphics/ConvertGraphicsElements.ts b/companion/lib/Graphics/ConvertGraphicsElements.ts index a4f355e908..0649216e71 100644 --- a/companion/lib/Graphics/ConvertGraphicsElements.ts +++ b/companion/lib/Graphics/ConvertGraphicsElements.ts @@ -818,6 +818,7 @@ function convertGaugeElementForDrawing( value: Math.round(Math.max(0, Math.min(100, helper.getNumber('value', 0))) * 10) / 10, orientation, reverse: helper.getBoolean('reverse', false), + roundedEnds: helper.getBoolean('roundedEnds', true), thickness: Math.max(1, Math.min(50, helper.getNumber('thickness', 20))), multiSegment: helper.getBoolean('multiSegment', true), thresholds, diff --git a/companion/test/Graphics/LayeredRenderer.test.ts b/companion/test/Graphics/LayeredRenderer.test.ts index 9c42a7a4c0..b5bf856afe 100644 --- a/companion/test/Graphics/LayeredRenderer.test.ts +++ b/companion/test/Graphics/LayeredRenderer.test.ts @@ -1083,6 +1083,7 @@ describe('GraphicsLayeredButtonRenderer', () => { value: 50, orientation: 'horizontal', reverse: false, + roundedEnds: true, thickness: 20, multiSegment: true, thresholds: DEFAULT_THRESHOLDS, @@ -1230,6 +1231,10 @@ describe('GraphicsLayeredButtonRenderer', () => { await expect(await drawRing({ value: 75, multiSegment: false })).toMatchImageSnapshot() }) + test('ring roundedEnds=false value=75 - flat ends', async () => { + await expect(await drawRing({ value: 75, roundedEnds: false })).toMatchImageSnapshot() + }) + test('ring in non-square element - stays circular', async () => { await expect(await drawRing({ value: 50 }, { w: 72, h: 58 })).toMatchImageSnapshot() }) diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_in_non-square_element_-_stays_circular.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_in_non-square_element_-_stays_circular.png index 74edbea8e56e805b6d6f8e0b8bc32940836555b4..cc4a45005eee4f508d830b5cf436d04dc04ea265 100644 GIT binary patch delta 2131 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 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_multiSegment_false_value_75_-_single_colour_active.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_multiSegment_false_value_75_-_single_colour_active.png index 7034afac542848803fd964312a17b12f5db48ab4..f131774e88b56ad02782ac818383c65d18b0505c 100644 GIT binary patch delta 2756 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 KMNUMnLSTYdd^uPE delta 2698 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 delta 2628 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 Y|A3*gMo&*^Q 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 Z{{UMz(^}Ya=TiUx002ovPDHLkV1kISe8B(! delta 2573 zcmV+o3i9>$6|xkNKz|9`Nklklr7>V68ftCRmZUY3G#Vg;z#oOi6;qUGVr(K1NlKB$h%IhKP+A*70~Yqd z?C$K&`*rs|`UmV|cJA!nd+wdxA>os3=6TLN^F8OD^ZT9O?|&?Cm%CVj8hkQi#tdyD zE>@!`XdD>8R9QyjzyN%z3PIDr-F{rvmjxZA!NizbkMY}&eGItIIPf3yfpEGd=LaPArlnDMT4=PXTe$Ot- zA`q)-Sd`0QS$`&j=>r3{)~)yyB-baAT%QD;rNN)&;XC33*T&g;ok46B$f;>qnNH)! znGD2nMv5J%yo&V7G}0^65IzzR+#Q7DYlp4ZuuY)v<#O1NOv0}yc2#ML%+d_94`ks! z?uLKAA8dy`Kh1J2K2^o$cpP){d0QG}1&z{>T$e;@b$<%px4rNm^n;Glmg=-rdX^+% zMLL<|0x)=z6yc*(F)xI!p^h z19&0Jg;ASezyAk;y<~H1zE2LF~Dh>9U3e zx>Lm&Nq_%u`w@R8ZmNu7fz~FIceFTz^l$G+_5tI+!;nDDxg1ud(^lG5F=(`g?gQNz zxW|}$R2`ErKDpaouX{H3ZPTwyhMHO#N z+zsm*K*z=&BV&5=3P!wG#AiX+` z=r5y??zLtB4ielGgs;U{^!~U4`Ftu~ez_MMH~e<=JOk%G4Px53ahRoP{KeBZOEM@>$s6#AClUqX0Dymy0%wbc@V&-dxXUgVc2rl} zE)D>Q?~7<$+X&Zxtktc0R79VTmi$~-pn3E1Xl~Bw7I1hq)b5jU^KljuQ+l)NBFACB z-;X$L%~>1^x}Qe<jm3I)>)ZlnS$2^0?nRHCV$Y$lOiN(66T#!NPz#KpQvbNc?p38K{dR3jYO6y zw6#r4w9^cp_dKAp=I0{m9tm>Ount2t%q861zHos7+ zAQmfjH7W(Vp9T{)-#$5{GlB%cyg)rYl`GC5Ow@cEl^Wd(0$smOW)WAiK)q}nV*MfTEo_@<`a|3O=4pL8h^p|G26q!`K5E{ zb-~j0;fG|~1CKtML@+ox>A!ktZVl02m5!ESv@FK|CYEb$_wg)X{aoUU?;0^q!_s zc;izZb6hIkY98CZh;ND`vMWNg%$h}WYX~0+Be{P3jFeJPDuuvK{b;PNKv%9fapJ`A zt@h0g2Y&Rm6Pp)j5%m~zP7@mv=sebmf$8OQMdWERI*xZBv&5P;?>_2=<6C`KtE3CR zcW(gMERCIQJb&ib$v8XP{DBo|wuH|2JCR;xZwj%-X^6fMMaPK_%N;-sPzdZR9m_Ad zrnk4VSnz5UuKv2;QUg>$MfZVja2Ggu-}b_7a1kg)a$^$F7or8*qE_%7_Kg|&82eB! z3v|BMiTphKjytgt7M^!J@V@0Oa0*MI)G5gPFoW!pEPt{;$l7W~xGuX;|42QkT=Cwz zp)_Cskeg(5{H+5jLDXYBm>2`5_x4|e0o%<&b83+5WyrNM)HB=8qWYz3^z7}iy^%_l0;Ng;X4+o$n9w;IwNKTS6sO?3Mk;h4b))J~l^t!H zbSRC&lz-=^zl)zbtlaz{{$D&A9)D^8d!0P{*3uAgeB(glgN>+asRC6& zNt|qiMa_#fsM}Nrnjm6463h1pm7x&W6F}3-CWxo39V)Q{|DXM6TGj;5J07ycg;Cg+ z`OV&75qd5Jd76yaGclwdPC?}+CM->96vPuEg1dr-j6YL#4?eN`A1tYU6^WlEkl2{8 z^?w-UNci(Sgi`{j2qP*$3JuU8n5lOy~%DiOVWfkKn}HpmP)8f zNT8+V3Q|EpsYDXgQfNU)DOdjhHKK)5Xt-$#Dio>xgQE$BwjzZfg%YS}6QJQpkkV3i zv$xmwddK#7j{Y(0v7Pnq+Pn6i`Tmnx`(@_M$M5&v`yDfILVqVT843A=<;#~_Q*p5c zhCy2{#}ZwqEtex?nkbelH)dNy5Kskyq*vtmZ5n^wtRv_GM#R8W%EQb{OpEyT z`3OA|!uz(jR^5l95mZw^Zp-4lp&>S^D*od}iX{jfb-DP+?WS9j=#yo7C5facj(JXF zk%190$SujCZctIbu3`j@5$%+H3emfxxQ@6cdmB&>K!0asGF&k@NXRJd{Cge`@A!Ni za=TH-fl-k}$gItvo};3ktD-H}oUa5+py9;^!aKsq?US25Y6d7|nrulVSeeZ_pIL&y zey^8T{C>LR`c8A{uce9I8Y8>1aD71&2>w1u_|Y)pz{Gx(ngCj%XQhb%kN{kmN^xN-<(!~Xmia?4 z$YFQ&ovE4JkhnTQ{JJ3_;+baF|yWrRs}Lg@lwq%a#^`&jp>& zVn2?N_+Fx_^HdCI)8L>3M;8Ph4u|=0x=m_48PIfF6VZF42$nsJWf4 z=z5|H#_>$X1hip;%CaxzSoo;Uo9Fs?sehrNrX8MY37SC5H7&@;?A?9lj0}mbV^7+n z0t$spF1~mWfLu=CPaU%xPF`Y6WYN||h#C9V2d*2yY#iOxMg?@`l}Wr_dvm>ecLM_h zQ|LO>S*}l9gdYq8Ub19W8pU3QsmT9yS*KOMpt8Gf1d60(9biY&>Z|0n%r+l>IUbZL&(VUPIz_q}}j zX>9^fGf{zO1GxTY-`~)ALuA)xN3$byf223(v$&`Fu0%*mGLVWkYf!byXX0nvm3K0o)gIe-k37}I>u>lPYiF9_>J!Ljy zr0i1=G~3%(&lv@fqL|L=b@=e?b4Dj4AuZs0%U+PQWg7ZPg*}l2XywX+d4GNK$@~HY zpzJM}S@w&C<=z2QJpUXyGXDU1J}9i%#c6K`P)keU!_LmR<9AgmsjqO)#=?T@-~jUZ z>@_``70~sW6M&y8$=!uTNpCJQf)vFDq^dPY56?zvAP(QVv7pQd3It370h6Jjc?zh= z%#7v&AZJ{ieYT3(Rb)iYVc+oZipS^Q z3YM4AWSf@MTVUV-ipTB4V432i`J#MfNb!>I07@q9!%zjeqPZx`Z(+G?8!HPaZ(4r8 zNiaD7Amz;qaAMUI11b)!o__ks(BoWJF`%*|=)Ly}zR2p;^AAvVRe#~0%TCe<56*8u z>iR+)$Ws(*b^skaCez<k!Yj>XG8GVVi#Xhw-&@%nIVwzXB~Ri~D=i2s0knH}z&XDB^1-R<-?U3TyI@}aS4*?VC4k<3+e=@cy~JH|$sn$| zn=>rY&^PSjFfk*JsDFkn0c2UQYnNTc5seyb+&DCq{hSu1*QcHMkMf!V$2k%sVjC~M zSSWM3{`%^xLT8mk=)bLylMg)|D%Ye`AARIz|9*RztzMnwf(z#EC`w(DLK}|7`}g|s z92l8NE*l-Y>#nGCe9JAdxj7fPAhs*n-l26iHg7r0bKtnKMF>TuikfG1XUHe^B+DQyfs#F+3ajk?(;bYZnCS&gq%S5 zf$*qpM+NlRXEJMcHnRPzX>`S;`?2o&^mdA6Moc=N>csTg5sT1MA*8O+gM?#8=zrbl zB-FGO8vS>auYbatEwbDeoxkmL0%M+!Jx-O`Xc`-k4$VJocG4kj|ot(QI(+xnrcxx6Mj01fq9G2|YQ|ofu11=b0DWynyf{Vdwd);5Eg`YglYbMv-7+|_OvYRFSpxI^G#!F z-;9{_-`P*c3mq6y+bwssyJ*|gR`o-@c}?oEaP1V0t|z-n9>w+^@Dkk~#q;l}DnbUI12BUo`eME88(xl=?;Rh?9tHm^e&mkY?ltNTs92dVXGncJMe6(% zX2Znu{z6v3x8H|TPjr^&j2qP*$3HV3U*l_SZ?aCZgeJ5Ba;PP=R6x1Bwy~twahz3MEj{CP2fHAf=`3 zW^b>@>mA$UIr_)!j_s_yYwy~7Wb^&UWBcvQo6o-Ad+&FQ;eUiqXets$50)=qZcWF< z78nNYg#t@;o%TY3h-o5Q7QT_!3kI@?gCee!%YIcxUI$$G{^`U2y1&`o1mMV#BLaZb z>vnIc|H?Yhor$^n_mVdza!uepM#0JAi+$A0aPPY}Y6?1(0M} zoSDmUuB!5xLVsc0wuT^}3IeJqq6z}OcYGMF21d-ll*hs}A_ha34>5H45Moxuzt>OX znFzkOe9gF(#+sPNvN(Tun2oAR;FytO2?9slZa(yQ=yAE|mn8aJF4Cen?m3M`Hmt(Z z0_p}8^{XmI*cj7J-YXNoH;((Td#bkqB_`G|KxgK1Tz@q*M8qiX{JUN+Z~Ofm^mtGw zfKih~$gRzxo~xpsr=l&_oUa5+AoxO%==La5$JC6YVSpm0$reRnWj^nGW(flOd_G?ll!MM1kegiDIcIR0EiGt5bN3_l$kpXy z$rVeG-;e>YTo%2*>?L(Ws_t{u4Cq)mh9L0kXp~oEx$cWjhlGqkVoQSXb7AMRtnPKhErBE)_X1Hck3dLHSaux$Jt{-`Ddl+bmyD#c;>+m@D@pNuD=$c_lX zzXky?1NOviM#lFGnFx@}1zT=bNVVI0niqpX-tl@H)#qu~f?F5hd(ZaS3MUolexe&D z@Jz-9v|)qFvM&@^_^8es=lOXt7;I>Vr+-_5CXl!;fppZ~-RI8Cp==$0(yj_95;3{- z(jfo}1%W?z&TTlO5|bi}wk|@<*|$D;!ysl$byKSf=<2J}_ns~k zGU?*r!MVHVsa1i$1aKwo1xQ`5!hbPqQ5m2$Yw~zJ_R978>wc`JCxT{yERbKb9d0vZ zlKWDw?CUZ>D^?WayZiSyCrdCJCBIsXNT?gsvhT_OoqDPbXn0tptE=fLvsoi~uZ*DC z-oAQn6+p6VI;+>ALvzm=or;94fd5TpD~sm!@yGKE5PyKYr)XyR z&li__2T-h1zYL%VUhLkS(4xn^;evcq4U}3p!8>+d+{bUuuuQ{Wvx~#ToH(W$whWME z!Oop_6-PX7uyNz?bbt19MwDHjb>ctrt1=wpNQ`maUwENd=5phW^;d}X-zm$T@y zNzY?F&FSql%Z!#Yw1XD>Me~s$7LNS7doCx_;N? z1jf7{d7Ub=YJVCZkPdC%Z*$Ti%C!nZ-%)O;wxn3djb^DPhF(;DPoG1%xR8vhkp5V?PEl;!nU6g_pCB&9&p=?!1eJ_RSFQ%;gZ~E#qCM=(YzEr2Mq=3Fer?9+0 zc7I(KZJAcFeP~yR*!?kFJ&h?JY#N|ZnK6^})oIdKrOW21Qn~-@E-QTlFvBML620Ot zyc{e4+kWKTGJ%%^NS%${Ycw5Dsd8V;k@-f3%!L`u;NHnVVyA6P(%sT)8002ovPDHLkV1n=YCWHV0 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_33_-_one_colour_within_first_segment.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_33_-_one_colour_within_first_segment.png index 7c406aedca4badd7c940de93faddc1368b694c0d..4a57c86e1fa4079b2d09367f6ab71b50408570d8 100644 GIT binary patch delta 2637 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 ASO5S3 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_50_-_midway_through_first_segment.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_50_-_midway_through_first_segment.png index b760211496cebaabb62e5479126a25312fd58137..d4cd15cae26ee7ed9665b0bc75322f4e4c40d5c8 100644 GIT binary patch delta 2692 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 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_66_-_exactly_at_first_threshold_boundary.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_66_-_exactly_at_first_threshold_boundary.png index 685146c568ae1e8970c7200098b9b554270b8249..d7abc8e6717148f0cd8b39edb157bb8183060ec8 100644 GIT binary patch delta 2659 zcmV-p3Y_)J6o?g&Kz|A{Nkl|=KN_B?m?5q_U`w{Q2I{?9%4o^$TG3xC|^HswH#{?O9WVpOBU zdQHQ;bQ<$i74y?+cy%2;D^&d9xe;7lrXdjoD1`#s$AY=ZfQd1n1scpS1Lrv>xW91K z_?WnM?HUCD>{NGDr)g+a6f7PY!D2;$Ti3I$D^6h7!4x8)@$biQ7*OUb!2I0jF);?N zcU|C5@ZipHpnpcF+(s2-N(5pJ11n@1Ya|IDPp2*0URkMUiIWe_ry;Q^fyAZ+=oAg^ z3AWnQi9Pv_HhJR=C$ z7L67y4glPBmPWWjA{mr{#Kr`=-s!^d4~C%!N*|r0l(mX2RunuQjpj_e#WK`n;H4K- z4AmRxT&e?1#(c_!8D`M9r4jZ^_M-0{UByZghyt)7nZy^9Nff(=^sp@6cRDf1Fc269 z5t_yj!+$`fa-XCJbZE62)Fu_uY6)VS2(?iysy|wy5qct2w0lspKs10yV==5ur3&tB zkYR9+UY({{d;%8?Mo7<7;)kZgL6(N{^@k$ znys=c%P3--2;n;dlusxHwGZwI!oSa7P}`y%#eZVN>EJjV@On+nrjqc-dGIHBBpymY zcv65GvNpfSvk_1#1>dW_g8L|V`Pi69-?p@N1uM%EcM)6!5&|L3n)_uG6l( z%_Gl6kXl{%^iYsM%VZg!7e&i9F^0kJU=U}x>cmQup^_9D9&3PqzqJV&w1NJY`jK8> z?SJ8ql2@PxRmG#Rm}Q$dP2>4s5Z9+@t|=G;2>vvP(9RI(5%R0)ypF!t`=I;syAqZH zF$}O}XAJQMaxjpT!Z3L|s}}q*9!(E5fgL1IvFUIc{jc=reU_y_4?G}Z!6(zG z*{7jr9Sfb#o>w%A!rnjtJ!LN4Q-S@G9e<6FG=i4Nub8?wh1fQ0>mc6k_iNa=F$n;m zXrOtgirDvQh%_xQ~&&==j5NsPoKyC{uyft&@lklVu8Tyx}e7b51LD->m}^HD5>! z#=!9RhE1>K2vl3EVd+wGjC16O2eDXX(i7E$k|+dTB%7MpCSqh+?tACF>DsjtD9Su6 zi^Uk6IyHMtYck$9y|8tWeYM!vbAMN~EP*)Az^YYb^E!TIe3ins?yGWlS2vk?6As94Ix+fAeZhzjKdErE< zus2w+g!fSvC{V-WKGB#!R_1l-(#-dmi^VE;G_quTxgQf~9>J3YfGbyKZoE}0R%WKw zn~WfjhrFlN-962Do+-fIN&eEc`f)AB1oBxVkD}4p=Q-zt5y<(giMj2(0_i%1L}Km< zM8|1Ry1Cn;)tQVSuh(24L4TNg;s5}2fF{J0TF68omL+-6b5tOJ7~Pvr(j+h0Zm&w= z+4MkqHQMCIkD@?oow;kn433YikFG$rOmJ!jGgO2?9H+zKn0q2koM$FuDo`Xso{X)$ z?Agl1rF=3I_tVByxp9N+EjKq8woN;mP%JVRrX$EIoM>*Idm~6%O@GdunoiPPUF3Ae zJ@*t=e=}=ffYd5-g@@yV@$AKzKtn?eQYrI!sIE>$I6QaEOkOS%YarPH7WNOu1KBJ^ zE?*``xGPu6#A~w+EAw)_lbt8Z638m8I#&cT90rnKN)oU0C;6=3vjn<$(GE#6H?u%M z!}8^`U!?dWaj3O~ZGUE&2KTq@L6W70rcpS5o_ywE^X3E?X7)ObhRZ;7dz5(H^{xw; z zGYR1-0peP6m9G1+8@B(MM-s#c_0S;?@qr%#i0q0W^_jx!ji(D~tps5Qxq^&JQwaW? zT+l!i=)wgDPJdj@bsUEQ9KBU^)W5GE%Ho+jA1QY$7zEE>PoNVMZ>BdwD#&^};!=8PR=(fozxtbfRTBBO^{@@`qMT`X#USZmov zZBo&5xCipR)9-0VcSO;@uisJ}TBgzPjRvAPS#1mH)P2|u|Etz#Aasw8-UGc5o)Lg) z7)XZ0z`!pC5P3QRgHS>U0F*(YerG-G9~M?`Q1GT#aCZ>C1J=ig490-)Edkv}x-Bzy z6@>wkn}3t&KHiPw267NON^o}&t|ICziz@o5`B4pYkB-EH3CniMLIpiXdf+%H zOaNHv5RL-G4I+f61Sl=}zYI#F5d29HzF!r7Rt%+{SrwiZEPac~*sj~)KI(?+qzjIV z<<%O=pOzu6%bb;0n+i(>(@7ekogr{V)L52vsDBr?9I%1*0n63R4E81qu7iW)f&n#2cOIa` zx_|D1{h)kMQfBfCw}ru<;0w58Y6-j&KzL0Up0`RDXCf6VU1e{u2yG8Rov$LgHHzee zNlQ1sl28T({%syL&)0zIuk6XkDfBEkKOgX@F~{GWJYS#POS zYz`^}dAW?_<|L9ElF;3yk1A?}0{3?g{9K95d3oBRo}gT+D$qofyA&vQDo_?GP+Ape zH5zoc4&9?etICC9ZWhk-PEb;HQkkOy-R3ss@qdDZ%Cfp3 RWg!3n002ovPDHLkV1gzJ0Am0E delta 2598 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 IM6N<$f*=m$<^TWy diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_75_-_crossing_into_yellow_segment.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_75_-_crossing_into_yellow_segment.png index eade516c850066dce345a77c40a71697dbc77033..80d930a167764da68cd490f7d5e21e080419acb4 100644 GIT binary patch delta 2698 zcmV;53U&4B6t5MKL4R3EL_t(|ob8-@j8s=0$G`V6k2~*~g_+rfb$Nv4m5N{&ni?T! zVGVzfmI}3|jRLKN3T>%UjhZHg#DZc;D=pPpYml@}P2^G1SV}1_DvOe~U=hK(4ea}Q z=XLMQ+_?|`SoSeH_uRen*vD-6{I#?9+}C%`IlssGodIrhn}1@U2L6~cXO1==Hw-EY zW@a*&Da)wIWZ+R%wARTu_H-JbE>jQ>0F*?5?O?&QF<_z$Xr2bs%fNoo4(=Q`9*>Ep zrX~si*s;FRbVb2JNy3738Ve)|E>+DsF4=+IN0R6Z4SheFO@mY;0X4bTW1pc; z5B^Ai3X{Z+jd_@%D9MMxq+^jX%P7jq+A=C>H>IFFd;y}gT3RtdN3cj8m zB@^h2sT4NFpK=S0>>)?t=e-AAf91Xjb11r%j(1+#Q7LPcGB#3lb20#v@--cO@1}isiNbx4yfMIT?s>h7zWt9 zGlp20+$=>=I1mV+k6}i=*?8f6$BC+qRbacxQ*5R>gU**aN4=JzK&w}an7vd&#UTZ) zYgnjs_PnA{6khQA(ORVG9t*5jtf+jr5`T1x{EErDl8A0I9v#H9y6b;PS zDI@x08iKR`6b-Bq!Tb9y2!Q$1>@qZAD zvyRbC>SV$9q7SZj$X;~pp%~Rb-F!#0fW|(bA+R#uG}U;D4Sx)P#Bg>1DaGUCx`XT1ECUTU%Lt z@ImeStI$IBOzOE*H4Jk^^e-s|0}@4wY!Vnwu?UX}L(SH-A~MZTjJn zG+TmPJ*30nTQ6Ef9$rmN6C93%3={?EHpr`#D-2K0Uk%S~55D=VSu*av zI|+sv@`EIaLPNvUG4I2nV}CR#m4Zg+`hCjup&m3i#Fi{cWr>4Zp7o3wrT?7tf|j8MSBo#rA76ib^avPIceikm^U%V)AcPk(>>I) z1_f#(c$SlBv7)qC`h!zFm>wO0dV2Jg;5ZfZ)QK_%89vX9L|32)VSmB`fVp$ctwNR) z!%g`}hOeg$3UuQJ*;`(^R6l`V&PWT%p_OhAx^#)03|qR?d>u}?Ag&aN$8;y@k3KRI zXo~e921tIHoLaVhGNeV873=NQpNHnnlTclqU+LRqCUs|uSOdv+v9NwJ6v$@TC!-aM z2Oc1=m6aPt!^`nrwtog$0v$W%CZ4!yQ*26@d(EaH@%04pxbw6#>vPsCH#XYQ)1&V& zR#Yh1u%Y~WQS9LuVp?ZaGGM|axYv!^2Gf09ID-vthJvoh#XQ=T{@(}JJSLwQrx?mx! z+z<`-{`~<60)LHXE^t^fmN@Iz?-k=c5UN{6`{8!*Pw~J6tW;|@4PC$LLf@`FXcXDor!)$| zok3XtV}Gu`+jRID*c*WNu<8PNnwD*{Ry+DS_-O@Hw~F|E#zCu8E1~s7D;#GW@chBk9{`p*grfjqodEtx9@3oAzYI#D z5cqij-rt(vD~3Y%tn#~fL*HT~mKzqh-gd!p+J6Dtzly7KOsz;kSku2(UN$IK9uA$L z5!x97*I<6DR#BUJamxYgI?x5xk>AoZ+s1-xQ2Kyy@R`zZrgPVG1$Pm^F5Y`G% z{Q0>QmKzp?9}UCuzx*b~q6GqgOl=13ueO{1CP^?n13F5B;TdQatzQ&_dL^@P=Q)HP z3x9!*<)7&*i9ooOW8ru3h;EKTBa{%2H*60J{{cVTZ@Ga2YcB=_DmsZuiT#sbb$vCQ zr<^9v7+d@=`BA;P8tyj>7iTynD;;BRvIuPtL9USzc{GB=eF;N1zm!lK1?Nd8DxR$X z(^=YLf-(DnCDpGY_RSb#--s1GN0H$E$$!Cd$^qw}oy4-fbL<_l?~$%eV?O9BGjRX`TeBR)Vxpf>NPCb*WI@DwGNZbbz7mfwR$Z2;&`000000NkvXXt^-0~ Eg0N{2{{R30 delta 2628 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 S*w8fq0000k7Lf4|ixw?18*#%X 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 delta 2699 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 0 && value < 100) { + const capRadius = thicknessPx / 2 + + // Cap at position 0: colour of the first active threshold + const startColor = multiSegment ? Number(sorted[0].color) : singleActiveColor + const startAngle = posToAngle(0) + img.circle( + cx + arcRadius * Math.cos(startAngle), + cy + arcRadius * Math.sin(startAngle), + capRadius, + capRadius, + 0, + Math.PI * 2, + false, + parseColor(startColor) + ) + + // Cap at position=value: colour of whichever threshold the value falls in + const endThreshold = [...sorted].reverse().find((t) => Number(t.value) <= value) + const endColor = multiSegment + ? endThreshold + ? Number(endThreshold.color) + : Number(sorted[0].color) + : singleActiveColor + const endAngle = posToAngle(value) + img.circle( + cx + arcRadius * Math.cos(endAngle), + cy + arcRadius * Math.sin(endAngle), + capRadius, + capRadius, + 0, + Math.PI * 2, + false, + parseColor(endColor) + ) + } } else { for (let i = 0; i < sorted.length; i++) { const segStart = Number(sorted[i].value) diff --git a/shared-lib/lib/Model/StyleLayersModel.ts b/shared-lib/lib/Model/StyleLayersModel.ts index 554f8b22c3..cef2e5cd78 100644 --- a/shared-lib/lib/Model/StyleLayersModel.ts +++ b/shared-lib/lib/Model/StyleLayersModel.ts @@ -218,6 +218,7 @@ export interface ButtonGraphicsGaugeDrawElement value: number orientation: 'horizontal' | 'vertical' | 'ring' reverse: boolean + roundedEnds: boolean thickness: number multiSegment: boolean thresholds: Record[] @@ -231,6 +232,7 @@ export interface ButtonGraphicsGaugeElement value: ExpressionOrValue orientation: ExpressionOrValue<'horizontal' | 'vertical' | 'ring'> reverse: ExpressionOrValue + roundedEnds: ExpressionOrValue thickness: ExpressionOrValue multiSegment: ExpressionOrValue thresholds: ExpressionOrValue[]> From 700050d2073053674a5784727b01e8892dea69a8 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Sat, 6 Jun 2026 21:34:37 +0100 Subject: [PATCH 07/30] wip: fixes --- ...in_non-square_element_-_stays_circular.png | Bin 2172 -> 2155 bytes ..._false_value_75_-_single_colour_active.png | Bin 2793 -> 2768 bytes ...erse_true_value_75_-_counter-clockwise.png | Bin 2727 -> 2721 bytes ...roundedEnds_false_value_75_-_flat_ends.png | Bin 2666 -> 2671 bytes ..._gauge_element_ring_thick_thickness_40.png | Bin 2807 -> 2804 bytes ...er_gauge_element_ring_thin_thickness_8.png | Bin 2682 -> 2675 bytes ...tive_arc_only_dark_bg_makes_it_visible.png | Bin 2541 -> 2557 bytes ...e_33_-_one_colour_within_first_segment.png | Bin 2674 -> 2687 bytes ...alue_50_-_midway_through_first_segment.png | Bin 2730 -> 2736 bytes ..._-_exactly_at_first_threshold_boundary.png | Bin 2696 -> 2712 bytes ...alue_75_-_crossing_into_yellow_segment.png | Bin 2735 -> 2736 bytes ...g_value_90_-_crossing_into_red_segment.png | Bin 2774 -> 2774 bytes shared-lib/lib/Graphics/LayeredRenderer.ts | 57 ++++++++++++------ 13 files changed, 40 insertions(+), 17 deletions(-) diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_in_non-square_element_-_stays_circular.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_in_non-square_element_-_stays_circular.png index cc4a45005eee4f508d830b5cf436d04dc04ea265..81ab9fa16b5ee45118f608522ab8a9c674692bc9 100644 GIT binary patch delta 2113 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 delta 2756 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 KMNUMnLSTZr&pCMj diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_reverse_true_value_75_-_counter-clockwise.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_reverse_true_value_75_-_counter-clockwise.png index b8bee36a118a715fb9633baf4c20cb8ca44858ba..4623686bc53458301ba23adc66d8916b21369a0c 100644 GIT binary patch delta 2683 zcmV->3WW8i6`>W7L4Qk0L_t(|ob6nDa8%VD{_bnXBESp+GQ74&TGfiznxYZ6S1fx#Jo4orEQMqM|BA3VT< zlS?0SdM1NMdVBHf+FHEtaFjew$yzM+l@z*u(N)yN=p+r{paA|95AG}n?jsKDIrHC$ zH4M~g8h--14!@?MUeho~k`PkWqONHG*yHu$fFKmruPikmzblUL17X`2^ZwZjPpb#6 zf4GX8q#2r374t+9^Cby&nr6G--(4>J&gX;On=Dz)NAHayx*=My9i>t5?DN3?OF!6; zE2k{mSr+YHFZOu7n4~C}FN&BeNvP4S<=-N)-+!Pgh9neAP~W%uY>~tNm&b&S6X1BS z^u2EfXE+XLI1anLUfkK&hlQeO*}fE#Y>W8BZ3*-~+FP()@QEOrzS=bG2q0d-{aBGq zVt+;^V{6QU1Zi~|y0&-0pva5TdD7XxYEF;bsv+SF$Ki=U0I34@&$?I)^)`gO^(ZvZ z^{Xyue)2&4X&#NYHG&C`jF)W(5r)AwzaJTk?bcJv;?BF{DCCt`5){2R3TY162;@HE z(D>~}P?^d*iRI!Gj>FS_KlF5^JB4NRZm3BKEx)u_Gl@ z2z@sMI#Qk~Kb&~S;lOWlG*;BXpnD^OsSnDaKArZeRSEcfWD|jScaeu8Fc`Aj32X^~ zJIj?Z{gJ^@rxUwsYH;Q<4z3+K8W-nV;!dZ5wQKu`{Y?qV%+4VBWwI)Cz3GDQ`G3*V zR>k6PZZ{I^X*g@mfAD$pBwT&9x#MR_Q21f88$nAn>hGy9b*`g^UAt7`_&$-CPNR4$@AYS1(auf0Z& zOHG|>L6EqB94@H&eN83iR~0yN#0`UB;t>f_CM(cA=1auU<^bC!6M}IiXpo4Hf zO-%}%&MbR_1WBJKt9IvG&Pva5)Nz*J1<*7wZCZ8}WeJ-5|3(lwG?Tl{vRkZ0A$Ay# z1huu1{m4)#YcLLOM6K35=0nL8SPSE7tfITS-y2{U!0*@Kb{oiKC`2L*T&}Dv9=vnC z)?7d{(Xr=N*ny%@U|C?{LVpnsM^-3n8U<0z8pHhr8K5+nuOK>Bz44xM5elh! ztk@VUkaHaJK~`vj14pv1#{*n0^WBkaWe>7K3W8~M{l%5z4K9}fihq#n>*>jIF-VZ3 z&D^py7{k(t9ILqUO7cC$ay0kgJI=zgl&6$Y$yBA7W0CLs_~R@WS%NH5IMTJ{RghI9 zX3r+)=8qrGx>ngOW>JliT9&Hh+^PbzEfNz)kD3s~eaL}pH(y1XUxT!8{H&s@u2P6H z=)wgSk%);50svt@xqpr?^`-H%iq@_rXKr43#hv$mK0${p2)bo_5X5o7nl)sC_U+40 zOym*7ALqe#n};^kdKIag$397G)+7)Jm^a84}DdiXf#~LEr7pfvMB1%HDx(D`g9rZ%Z#$tj6?eE~B%w@E(6g<_^gE3)uxWvaS<*-2K-mJxaX&;6hi+nPmWLYNH;OpF`#dxYmCkmtXjtD+uwtcPPRDh`g}R5yS`7e{i3-|V+pEGxirpPU=X0IV zdGei83Wd;JAzLXeTS+hPpS9~BbLV|03i6& zARMRc9eDPX52<&fHElX$9Q;Ky%7E)fPapcp2{{H2E@f8(o54w-ITHv!WYlM zzs--@XKHOz{@ZDJ)`PFwywGySl><`{7?Byu1*;ss3Ns3fu99%`O?cF{I$5V>B9lxpnxlG zxB|gPgB2;=saWY4ca}rL-3`dh%Aj{sFEXFAcP6w_;Qzy;Zc`oj6P0;p#{eo`TgRW^ z(X^@wnK>CGzMepGc@nxXJcc9J!9mzBKy)}DfPcHP#0+5AAFyNy29nE@NZy)+cx_=r zt`OMsEIbE15cUfdUoE*9u_sVnjLeJ-GP5#}=15R4SD|}!=z`w=*TiJtPw{ZP>i~b6 p2Y-SGe{%Rp$;ALcmv9N?;eUP3w!_4*d>{Y-002ovPDHLkV1iMaEgAp- delta 2689 zcmV-{3V!vW6{i)DL4Q$6L_t(|ob6nDa8%VD{_bnt;)I=o- zo5#MA-Mx43vwy^7cawYW-hCw5q}Hafaj2!Lm5Rap+~3@#rSn z+S(|L2SK_|N9xKHl53I>FBhS^bzAjf^w|_*Dx*}Cn{cyd3Q!P3cAEI-vdvzTxl7tpX0sypg9A0<3an$LAN>PN? zibvTAk{8N|+#5k^WpTu0BXquj#d*p!DGF|l$FXr>0Dp74aZnJTl#!^i5v0{>2;Uz@ z;_5`Xnne9)_6I8HI<8M7aJeYrg{mqX%>Mpnw0SenjqxK=;~{FLu!Q5sfdr zL)$2si=d`qb2N&RJdfY_e2CM9w~>vK2vROo(7U4-QnOT2Czu!mgEIgfnEO7BnjQ*U zwzA;l;@e!9PUHT*K0H}njnfWC(d`ti#gf+~(fy0=f*wXEXb1-c@F#e1?HssIIIw5T ze&Y5MG@T+&0CJrD0p7>!2e4> z*iXx+EIU{h9bPZ?dA*peC}DKb^GJkQkbp*-$A8 zzxMg?TvZjcJT}l}qKNAf_DvC61W79-^!>z|9JZT9<8_Vj{mN(i;pN7gZZ{qY1Q5^T zet%q(NZ_J$+E$x+3DRmbbnoegL6Ik=^8@GLtT{P;vxbB=j>C>X0LeV|&uy_7YHbL4 z>sDx>`-yI7esV+nNgmTTP6rbnA1~VuA`FAwem~L{%Pmn)20EOE-=z}_s4l@w^ix6x?$z6$E|Zr^aj0DL~Oi9j4(hGf`9Mn ziPKhv;_q%Zy1&hWt1|z=TUsQ{pKtE?nGzJ-vkxiRS5J9(DY zNWAu%fX+^`ceTRN-`H*c&ED`Vv{SJKuAbyN?r&_IC5g14S8Fupl#wG6XIC|01ZZjqE06%c0B? zo23vdOhtm)E&Ry3x{Sd%yb!f&^Oz4MQ(!x$j{F%yPtTw?z%YQ{uR#zDq|+245e6<- zMivi`T(344&`fmlxfOPxD1Q_<4p^~5gu{^$%9=(YmC6{yg9I6%G?>pII##*yo>Ea? zujY^g01RWGxj7TB9vZ!u)W}qwK>&z>GJjTxd59pQ17= zoibla<;>WaEReGt@?lnJf&)jguBQTAF7wrq9TFL4g%kwSY6g=lr+*q;E&~){udlZ^ z!^JQ`jy7}C+h7b!BXY9hf(yuFie+i;;cuLU=`GJKA(N>>vBYA(@6%5+Tx1BcFukQq zO3NUtL}c4KBXR6l#Rrn?jsIdhxshh{2HVcQ)d=6 zH7P_HboMNZNW?@20e^t7pPa{+{Myu+MK|6^-nn_>4R_A}xda`wAn59;K@i6Q>(`SB zdimwtjfotB_+vcS9`n$KTB{;?)#N8>{rUj}0_KkS*|RL(8xi{&Wj+fB$g?PNUj&%M zzN^b+;O@I4#QUwSo>9U4f(TL?74+Xk&V;CRY}yn(~g=c)MWTC#{ZbjXblKOC*aO+D0_hiZroX+aIK&E&+YiUc>9O1N~Uf}QWW zOtU<8>hG1KXF2xcG5DLLsthwv5R8f9?l8 zKrW(AQxSSP1l=MDa-twFl+pEU7j!4NZ?3~G7*AQ}JAYXcM8{~<{h$t%PR1-WBxk_QPvpX^uo{RSF#kJCInH$U3e!cF?PJgdYr}oL*FpoO^t+7!X&ANUcsGd1ca4 z2#@T8|4~1xAFsCktdd_%ky?{N&m%nr%Ny4}TyP$BfGtFgAuG*Lul8*e^hMRRI5fd5Iaqn7?4j5DX;NCXl#3 z0r8UjhFm_dT`WAWdLZl<%064NF>Y_58j18pX{0YsLs}w1ouNYa=+Fgy@UMxmN(lb700000NkvXXu0mjf-Ax}q diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_roundedEnds_false_value_75_-_flat_ends.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_roundedEnds_false_value_75_-_flat_ends.png index eade516c850066dce345a77c40a71697dbc77033..f2ebdda5568f619c992e3b3cff26e6f8068a4881 100644 GIT binary patch delta 2633 zcmV-P3bys?6z>#}L4O%ZL_t(|ob6nDj8xSf{_bNQ_s)A3W`~sx3ISY*HBiZ#7};Qf zB{b236&h08DjEU>l~!BRKTI1E#g;;7eMKc&EYaAcfD%(Ef`zuM!6;Czg_dn`-_JXD zX6~JtJNMB)mVM05J$LWy%+Bm?`2LyQJ?EZt&o}4%-e-XuxPO6SKn?ydckW!H91}(~ z4Yk=UW~(aZWV7(;I(S_NXBglO18e$HpamLCj2ZY8uYdM2aDUZEX{b{a)MYZbRZ-y8b<6&`ONSoSp+;5Hc1C2tf5^jg!UOIM2P#9E_A3=8gdo;1 zuq>U%J+h3Ovwzu=j;#eWB-SU8Sf2o$qQRZv5I7isyUAVBc?MBZf}DndHIjsTWEq|@ zc1kIrb2_9(2~wj3{x3YLc2~i1#Zl5ZN=DGv(`jr>BoNZHQVm%UC&K-aNZ6j7JSl;nkqfk7Ead@F(E+9dpk{C^0&7y>QOMU7e1({+l1pZE5b zgdB;c@jr(H?h9_PoyC1`hSPxfkcgJ!El?IKMU6EsHBSNfW-^I;lSve+L)ust7u;_2 zFbo8SK|f8Smtmk%`6KBe9oh^HYK;ncxeTdMf;!7wyOBW4G^)2%7i=C3s`-Kmq5(WC zil|SeY=2MI!!S6{acFY8(ZLqCJCubAA<)X*J6rq(%wCj|C|6>>Z=8{XgkPYPo&!V2hxo=`_|zl4T!}Veniyj5Az$e5KK#k`$tkL=k+&`Un}c zfvy+2kiEs8g+C@iQB}oeQMBw6r)fMJ4&&Mc$r{@*fbg%vsNPWxIzwKX?$OctMkn;Z z*nif(B|!`Wd~aJET|u&_N>dc}hQsJ*m@y|S7d#($Q1d_y*dFqGXKS*0?35Mg}4n!W0fHKGmL)a=n50Z}> zk)TbR;&8ijO`}Qv@MUIIdih8sUcgWM~`|Tip7NxDgjES5ZX^ZYEq+w%+h?{ zE^nm`4P+~`t&PR$)1G49v!d|7?T4dg&k_9XaAdg#w19K^6%A zI!tVq8)RVADa;}0w%amrI?bJWQGcXy>C)uaT4TZUz6Z|B=BA16(~(}5tDJHOTDXuL zHaL5hM;YSqWkKNgWN%(xA?N&Viy*@QKCVKH25tAJh zCFH^}O$l;dAv@VxWZ0G&B8cP2G5a2ixXM%oBaq`)BV*em2r}2<=`=NM)cZ)#aT=6v z9tP27n(QE+H{U_2)b!C_InV)`pqaI56M{y^eg+{)Wy(%e03`2zq>vsGE)5MSxLo;X z>57FgoAMxuUE1WwSj>F8`G5V>Pyk(rI+IYv)-$}bhFBq4$^jdxktqqsfOBdFrq_g^ zc-)*Ij?+O;LpeQC&WwgBK{0~B0)Tn*N?T_*88A|nkH*M++7Ln4u92c$_){55 z9oe-q-9Z;Gl7nGOmQ=pHuGEm1%fvQQA^q8BRs>D69mD{syUC$t*MH~3TnrJ^-p-=0 z&s+}8pRb^{wy@H-$&BpZdEf{$L#H3__YY zO@sSeZXhj(Aj1F-AAct2uy^hh;BuAaQqY9J;0?sK#)$3whdeOKJjy#XM~{*Tibhpz z-fUe@GnpZ77NG^n24djN{B%$rL0^99#JO|icSEw>V zhui05acNGj@6!KNKT>zuuQ#p~Gwhq?u+EBj)V2a+N{YZw3Rvq zod-IhyU77R*FRkd{f1mr1v`c0k;fyTWpXz)qN45HHrti!<$%0OM*APyi6K>7kNHF& zj)F>AYnw3%Vp~|ucxHxWAGJnB+u=5(@2tGnAKM;7*MG}hM2&^(7IX9m(VVabv9Fy; zy@$OBzGi&~Lig$DIM@NiROts-M(EZzP^zZD4L6NO}%AgS05rOkx_Odm$-Oupu zFaigyuMruH0pW)NTHkB6jM$YF43NA(iPqz-NUkM2p@RtT4ud~szt*XsqiN}U3&N3=b?PS4pFkrh_ ru&pfIpSnTGo`3WRs8feM&31-AvZ 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 V(5Vh8xvu~K002ovPDHLkV1m>GRo?&r delta 2770 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 Y09!ZHTG(>uQvd(}07*qoM6N<$g87|%`Tzg` diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_thin_thickness_8.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_thin_thickness_8.png index d8d2a60477a011ec0cdd9feb6e94edffc36b0c51..bfe590dd27bf7cbe1e236bac6b634c064e3d3e92 100644 GIT binary patch delta 2637 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 delta 2502 zcmV;%2|4!t6YUd_L4TJ?L_t(|ob8-jkQ7%L$N#5K_jLDMdzM8gc8Q7@EBwIm!@<9@Bc_5kxD^+Uws=j!MRuN1YEu%@LAc~}t6lwvYRUwG5KyfKVaJePa zE;}|6|_Lj0PEjA;=_ zWDNJ4CbpK#_%fU(&Cu?ggJGUA8hh+T|DdYw@s6)46SK36JXTe*xx zjh+$(EXH7VnlL*}$me9F_N5@+jP`FHS!*9NO?;_XMAr2g8Z&@(p9O2J1@*X!W&RkHnF9J%0f~7n*9aDS=koHol(E*9^R2 zC45iPMU~Dpw{dpGBHL2mY8k*(_fNovKBf5@y=a8GCh|`LK zeoB#}6oVteIAm%|Uebe?_TaX-u-DlzJ59LR`h+7Efwo(#J3Sbk=Ia(n0PuLBfK7%G z`eOZ*;(vsy;+Ue~9kIDl?X)|vK4QV#Xri(_{0y1-O$O>)YUuMD5~v<=hA71eMZs}J z!4;|TE*^xr!GzwU!&&NtIzP2Pg~S_)Q0La2DAps+F;&Iy(rMpe6D7(?1{XkxvehpPv&d36)4cQZ6$oLvw}stUEJ&v z;R)4$6_*e^o6X{8lT-I}AYGG?{Yf^kC)ulPlpYEW9bADl%|%a-4geqm(6-A#;d=r~ ziQ$hh0DCkImt)hjijD&DmI&>6ZTMz6lz+NQ@Dlu?#uaGi&LK!rb<5fNUBsSac;~|+ zUQVacE6+^vsu|>78L0y)?r~4?pg*j0kNXO=a;1$mYy77|DZ}vYmqi?j#~ZodSt0Ry z0?NM??r~$Q0k_pZhw>GutEEQRXaPFS#EB6pI3G-I{yLiAJIQ6m-x(SUXFcs+?#{prGM( z?%dqZnCm5|c2rq0KHZNA)ZWgok6gGgch_5u5({`HH|^7dQYn79^2Uu>u7C4P13_MxKm zG(Pld3Z$wYWO?C&&E(L`wz2h*2Tt+^IxR$r_0<7np6nniva?;F2Tu@{CC(pb`(MB{7o8} z%uwp)+g;?-GBBxbIHkKBJH{7i(IPkSz5QIL)D>)XNgSGL-3{On=k^Uaue>tVz2SJk-kT!6v&co^s|D0;v7b!{Q20uLJGB!= zklO42O4?VT%a_PsVrpAELI%_a0lby(~9Q@iq(0`azg zqRd^PzWQnsrcLnTEfL?&dgz<9xYHlgVXvM08Mf77LGR`}$bX1SklfF=#c&0>eOts+ z2NT%4%z^7;@?(S{_stxfIlR)Z+*3jRYr#&|_}}87dA=nkxZwO+64F(Ez#jD**SiB} zCUBNJ$Ul__}FgHJ~J5FxjqwE9Tx69bEoPJ>15~- zMfTY&NDaqIO@D#t07d!_!Jb67#l_%v2Vt+C{g|Y0*O7ZN$DOe-k}+q0maQs`@d))! zM?I$2+#%zs9&%6Tp!eutvyg2wX?PEY{-_?Df-peaqd~k8&aD#K(~{bk0+SfZTltwZ zBMf@C4zt6A_G=AdK#eIf=fR z!JY5lLF`NniNlFuCq>kqiU8(D6Z#{1sQ5=1L2_RbiNA*5D~3ik^=4kkfZn0dxASug zjPxdJy#;H%1+ia*@-GE)uMFvb%{fKvdu>>q7R(Ld_pcI@K-;51Zouc%@OO7u9TxIW z_i0{_d7aHEwI}xYN zL2g$LF2}kT7NI0SC<`D(0*J$JJfm0;$1cX8J*Pq7)VP9(L?DdRSoG~W%3bAInZ5iF zh6JPzq@ey&1se2qNA~S6#3D%jF@^X);wXNh2!FG4cB_ubHmjmvnD$d> zyEV8=T$H*>Fg6>3X?~B0Dvf%2Ae(=m!i>_0Kcr(`H6 z6|SwfQ6h^$VX#-)Fg`i_r6Wvf{4&HLh+T+5IW?RzH2cLRLCmN?6R9k(z**)D|GUS5 zmp}5rR0dD+;H5kelYr(ZXpTb237~@%v_L@zD5U>Nk;Rr;j0$v@yEMoD0RxA)6*FdL Q!vFvP07*qoM6N<$f-Q&E1^@s6 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_33_-_one_colour_within_first_segment.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_33_-_one_colour_within_first_segment.png index 4a57c86e1fa4079b2d09367f6ab71b50408570d8..e7de590c9b01d5fbe681154f88c28b98eb5ca162 100644 GIT binary patch delta 2649 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 delta 2658 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 QApigX07*qoM6N<$g1?jiCjbBd diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_75_-_crossing_into_yellow_segment.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_75_-_crossing_into_yellow_segment.png index 80d930a167764da68cd490f7d5e21e080419acb4..288639623bcbccf1269b54f18881c933fb189854 100644 GIT binary patch delta 2699 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 Ef>)3b#sB~S diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_90_-_crossing_into_red_segment.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_90_-_crossing_into_red_segment.png index 90c56f5be32b96ea934e9ea47d31952277caed13..d202d56d57a8c63352ea619df085e6148788ebbb 100644 GIT binary patch delta 2721 zcmV;S3SRZr71kAyQGd+rF*Cc~!}jjlc;gV-K!Q{dBy~g~6g9zBfVMy>Ra*opNuxjr zkV0Bj6;i2GNKlhfD4`9ADn$ySRYOC?VM0n%E~{-+jwDSYl0xj*yS8_{$Ly|W=jb1f zkM+)*J+nTPPqMV@c{B6oo8SB0Z-H|-hiX8^J}g+Uz^KO!8-H{i?YSJnstTW`!K3RK zT$00U4`guSG7U*TKokP5p96cE1si9 zCW8fvf_7DfN7M3-D^B3MyE2FcC*F_YFrc(6Kzrf!*ft*MGC~Rm)evWm zpf*)Srz~S>Hh&A&pbO`bFY0;Z6ocl`ki030ALs&)F% z;lLovj(<-u4E75G{w4@WGu168>T!>gi*Zl@Efg`|`N`%ydWNA2)F?SF;bDMR|91m%L_7Bl)-6lzd~cZavE z`;|q|=2#5thKGxe8)RARal7%lC_*bbx^jrGixBsTNZyi!v{8Z@v|fI(2Vx*9foG?u zf00A0oA!hbnLo{AK`mL)-V+!4o;>oQn$T0!^CJaSVk zQJiJ*t=3i?v(+~KsRitZ>}a{I1x%Lyh?!*>#Mi|wuR}dM6wnYeFIPl;Jhk8@A&%1nv!h7<3myS}#HO(w7>QpeLgAPZfbFUkOeFA*jebnQ+TZgof?$?^CcRvi)z|c>I zOpg^16b`Fcxzb#E0ASlTKL!Ts?|(No)y{R=(6GA!wo`PA8^68KqU90fav8Yw)+F@;J9m1ZSD&VtKI8!ZFa6ZR>8sPg zl*N%p(8`r^f#n)QD#c*;?%69nV-Q~!VLxh~K-1kivP-jh|H~t2*)n<%_kaBJ4ajEC zZi%H{@H|hC=F`{Y>)$+ruDF6u&`U4P9xtUH-BrMd<+m=l~NpfALz-gdkc{Fqxct(lZ8HbZ@Gp zGZ}Mn<7-eOpRu6TAsCa7G=iMYx^(7E3sj0)VJD??#tF(&lG{8#M}G;jjRvP?U1OA=-Rg_0i zE(g5&syOw$actfkqXxCJ0ft~;cs;$WF1#sVY6n3iFMbN!EhS$-hN8B&Maj@{YO(WEYA1>_P zP!xhK-8?!jP!Qb~oz#Lp3qXK@Edx~3PILnQFU(8QrUV^4=s;K3gf2RC!j7eVHvG6F z1I1<_(~&`Z&427!YSMiYjdV4GmO+PM);k3%~Q?=N%bnY+<^?U=2ieM4-&zNME^F zLGJ8h^ZP!(kx$L=>_6V=xFkMNABWz&!!j|65u)z2u^o|4~0YyR5en8LR>6ZV5em zdn_|{H3bW#Zcd@+^&X^F6%VNWkNP3(FTGo(tg>n2&l;h-btG@3_b~#1vQR5!<@+uk9eG-%f#a{-|2>e_9@P9m0`d%?qx+PP(U$XQqro#5A4X&46 z5cUahScEa9Als3JymEA}yh>^KnhGYxAb3v@{DIQjc&gsin_iJdbbA!KP+sMgJI#TA zpNHc=4mdw>=4J1uVtiBYc`D>+lu&io1OWa$9>KeUU=ro`7u19xoXN3B8zjV6$6?rKMuCwV zj- zgHYR546hwV>c*6%n_o*1gFyVNh{i`6!GA_-yN~3o{lJp$(~*3%$=mbgECzUxy&>A_M bLv{E+QMuJywlO-A00000NkvXXu0mjfnyxlA delta 2721 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 bP#gXaVx`qsLX4W300000NkvXXu0mjfS@t6G diff --git a/shared-lib/lib/Graphics/LayeredRenderer.ts b/shared-lib/lib/Graphics/LayeredRenderer.ts index e85014f753..bc089f1081 100644 --- a/shared-lib/lib/Graphics/LayeredRenderer.ts +++ b/shared-lib/lib/Graphics/LayeredRenderer.ts @@ -103,7 +103,7 @@ export class GraphicsLayeredButtonRenderer { try { switch (element.type) { case 'group': { - await img.usingTemporaryLayer(element.opacity, async (img) => { + await img.usingAlpha(element.opacity, async () => { await img.usingRotation(drawBounds, element.rotation, async () => { elementBounds = await this.#drawGroupElement(img, drawBounds, element, skipDraw) @@ -122,7 +122,7 @@ export class GraphicsLayeredButtonRenderer { break } case 'reference': { - await img.usingTemporaryLayer(element.opacity, async (img) => { + await img.usingAlpha(element.opacity, async () => { await img.usingRotation(drawBounds, element.rotation, async () => { elementBounds = await this.#drawReferenceElement(img, drawBounds, element, skipDraw) @@ -247,8 +247,8 @@ export class GraphicsLayeredButtonRenderer { } if (imageDrawn === false) { - await img.usingRotation(drawBounds, element.rotation, async () => { - await img.usingTemporaryLayer(element.opacity, async (img) => { + await img.usingAlpha(element.opacity, async () => { + await img.usingRotation(drawBounds, element.rotation, async () => { const { x, y, width, height, maxX, maxY } = drawBounds // Orange background @@ -514,13 +514,48 @@ export class GraphicsLayeredButtonRenderer { // p=0 → top (−π/2); CW increases, CCW decreases const posToAngle = (p: number) => -Math.PI / 2 + (reverse ? -1 : 1) * (p / 100) * (Math.PI * 2) + // Pass 1: inactive arcs. + // Drawn into a temporary layer so that anti-aliased arc endpoints at threshold + // boundaries don't accumulate alpha and produce bright seams. Each arc is + // painted at full opacity on the temp layer; the layer is then composited onto + // the main canvas at the desired transparency in a single operation. + const drawInactiveArcs = (target: ImageBase) => { + for (let i = 0; i < sorted.length; i++) { + const segStart = Number(sorted[i].value) + const segEnd = i + 1 < sorted.length ? Number(sorted[i + 1].value) : 100 + const color = Number(sorted[i].color) + if (segStart >= segEnd) continue + + const inactiveStart = Math.max(segStart, value) + if (inactiveStart < segEnd) { + const [a1, a2] = reverse + ? [posToAngle(segEnd), posToAngle(inactiveStart)] + : [posToAngle(inactiveStart), posToAngle(segEnd)] + // Always use the base colour at full opacity on this layer — the + // transparency / darkening is applied when compositing the layer. + target.arcStroke(cx, cy, arcRadius, a1, a2, false, { + color: inactiveStyle === 'transparent' ? parseColor(color) : dimmedColor(color), + width: thicknessPx, + }) + } + } + } + + if (inactiveStyle === 'transparent') { + await img.usingTemporaryLayer(inactiveAmount / 100, async (layer) => { + drawInactiveArcs(layer) + }) + } else { + drawInactiveArcs(img) + } + + // Pass 2: active arcs (always fully opaque, drawn directly). for (let i = 0; i < sorted.length; i++) { const segStart = Number(sorted[i].value) const segEnd = i + 1 < sorted.length ? Number(sorted[i + 1].value) : 100 const color = Number(sorted[i].color) if (segStart >= segEnd) continue - // Active portion: segStart → min(segEnd, value) const activeEnd = Math.min(segEnd, value) if (activeEnd > segStart) { const activeColor = multiSegment ? color : singleActiveColor @@ -532,18 +567,6 @@ export class GraphicsLayeredButtonRenderer { width: thicknessPx, }) } - - // Inactive portion: max(segStart, value) → segEnd - const inactiveStart = Math.max(segStart, value) - if (inactiveStart < segEnd) { - const [a1, a2] = reverse - ? [posToAngle(segEnd), posToAngle(inactiveStart)] - : [posToAngle(inactiveStart), posToAngle(segEnd)] - img.arcStroke(cx, cy, arcRadius, a1, a2, false, { - color: dimmedColor(color), - width: thicknessPx, - }) - } } // Rounded end-caps on the active arc, except when value=100 (complete circle has no ends) From 1e3da23f06c02d3b9837d90af1904073758ea1b4 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Sat, 6 Jun 2026 21:53:20 +0100 Subject: [PATCH 08/30] wip: tests --- .../lib/Graphics/ConvertGraphicsElements.ts | 46 +++-- .../Graphics/ConvertGraphicsElements.test.ts | 184 ++++++++++++++++++ 2 files changed, 218 insertions(+), 12 deletions(-) diff --git a/companion/lib/Graphics/ConvertGraphicsElements.ts b/companion/lib/Graphics/ConvertGraphicsElements.ts index 0649216e71..f704d71d3d 100644 --- a/companion/lib/Graphics/ConvertGraphicsElements.ts +++ b/companion/lib/Graphics/ConvertGraphicsElements.ts @@ -1,7 +1,10 @@ import { createHash } from 'node:crypto' import type { JsonValue } from 'type-fest' import { formatLocation } from '@companion-app/shared/ControlId.js' -import { FONTSIZE_SHRINK_DEFAULT } from '@companion-app/shared/Graphics/ElementPropertiesSchemas.js' +import { + FONTSIZE_SHRINK_DEFAULT, + getElementSchemaProperty, +} from '@companion-app/shared/Graphics/ElementPropertiesSchemas.js' import type { ControlLocation } from '@companion-app/shared/Model/Common.js' import type { ExpressionOrValue } from '@companion-app/shared/Model/Options.js' import type { @@ -63,6 +66,29 @@ import { collectContentHashes, computeElementContentHash } from './ConvertGraphi import type { ElementConversionCache, ElementConversionCacheEntry } from './ElementConversionCache.js' import type { ImageResult } from './ImageResult.js' +/** Extract the valid choice IDs for a dropdown field directly from the element schema. */ +function dropdownChoices( + elementType: Parameters[0], + fieldId: string +): T[] { + const field = getElementSchemaProperty(elementType, fieldId) + if (field?.type !== 'dropdown') return [] + return field.choices.map((c) => String(c.id)) as T[] +} + +// All derived once at module load to avoid repeated schema lookups on every render. +const CANVAS_DECORATION_CHOICES = dropdownChoices('canvas', 'decoration') +const CANVAS_SHOW_STATUS_ICONS_CHOICES = dropdownChoices('canvas', 'showStatusIcons') +const IMAGE_FILL_MODE_CHOICES = dropdownChoices('image', 'fillMode') +const TEXT_FONT_CHOICES = dropdownChoices('text', 'font') +// borderPosition is shared across box/line/circle — all use the same choices from borderFields. +const BORDER_POSITION_CHOICES = dropdownChoices('box', 'borderPosition') +const GAUGE_ORIENTATION_CHOICES = dropdownChoices('gauge', 'orientation') +const GAUGE_INACTIVE_STYLE_CHOICES = dropdownChoices( + 'gauge', + 'inactiveStyle' +) + export async function ConvertSomeButtonGraphicsElementForDrawing( compositeElementStore: InstanceDefinitions, parser: VariablesAndExpressionParser, @@ -245,14 +271,10 @@ function convertCanvasElementForDrawing( type: 'canvas', usage: element.usage, // color, - decoration: helper.getEnum( - 'decoration', - Object.values(ButtonGraphicsDecorationType), - ButtonGraphicsDecorationType.FollowDefault - ), + decoration: helper.getEnum('decoration', CANVAS_DECORATION_CHOICES, ButtonGraphicsDecorationType.FollowDefault), showStatusIcons: helper.getEnum( 'showStatusIcons', - Object.values(ButtonGraphicsShowStatusIcons), + CANVAS_SHOW_STATUS_ICONS_CHOICES, ButtonGraphicsShowStatusIcons.FollowDefault ), contentHash: '', // Will be computed below @@ -643,7 +665,7 @@ async function convertImageElementForDrawing( base64Image, halign: helper.getHorizontalAlignment('halign'), valign: helper.getVerticalAlignment('valign'), - fillMode: helper.getEnum('fillMode', ['crop', 'fill', 'fit'], 'fit'), + fillMode: helper.getEnum('fillMode', IMAGE_FILL_MODE_CHOICES, 'fit'), contentHash: '', // Will be computed below } @@ -673,7 +695,7 @@ function convertTextElementForDrawing( text: helper.getParsedString('text', 'ERR') + '', fontsize: helper.getNumber('fontsize', FONTSIZE_SHRINK_DEFAULT), fontsizeAllowShrink: helper.getBoolean('fontsizeAllowShrink', false), - font: helper.getEnum('font', ['companion-sans', 'companion-mono'], 'companion-sans'), + font: helper.getEnum('font', TEXT_FONT_CHOICES, 'companion-sans'), color: helper.getNumber('color', 0), halign: helper.getHorizontalAlignment('halign'), valign: helper.getVerticalAlignment('valign'), @@ -796,8 +818,8 @@ function convertGaugeElementForDrawing( const enabled = helper.getBoolean('enabled', true) if (!enabled && context.onlyEnabled) return { drawElement: null, usedVariables, compositeElement: null } - const orientation = helper.getTolerantEnum('orientation', ['horizontal', 'vertical', 'ring'] as const, 'horizontal') - const inactiveStyle = helper.getTolerantEnum('inactiveStyle', ['transparent', 'dimmed'] as const, 'transparent') + const orientation = helper.getTolerantEnum('orientation', GAUGE_ORIENTATION_CHOICES, 'horizontal') + const inactiveStyle = helper.getTolerantEnum('inactiveStyle', GAUGE_INACTIVE_STYLE_CHOICES, 'transparent') const thresholdsRaw = (element.thresholds as ExpressionOrValue).value const thresholds: ButtonGraphicsGaugeDrawElement['thresholds'] = Array.isArray(thresholdsRaw) @@ -837,7 +859,7 @@ function convertBorderProperties( return { borderWidth: helper.getNumber('borderWidth', 0, 0.01), borderColor: helper.getNumber('borderColor', 0), - borderPosition: helper.getEnum('borderPosition', ['inside', 'center', 'outside'], 'inside'), + borderPosition: helper.getEnum('borderPosition', BORDER_POSITION_CHOICES, 'inside'), } } diff --git a/companion/test/Graphics/ConvertGraphicsElements.test.ts b/companion/test/Graphics/ConvertGraphicsElements.test.ts index 6a679af2b0..be1ac9dadf 100644 --- a/companion/test/Graphics/ConvertGraphicsElements.test.ts +++ b/companion/test/Graphics/ConvertGraphicsElements.test.ts @@ -6,6 +6,8 @@ import type { ButtonGraphicsBoxDrawElement, ButtonGraphicsBoxElement, ButtonGraphicsCanvasDrawElement, + ButtonGraphicsGaugeDrawElement, + ButtonGraphicsGaugeElement, ButtonGraphicsGroupDrawElement, ButtonGraphicsGroupElement, ButtonGraphicsImageElement, @@ -185,6 +187,36 @@ function makeGroupEl( } } +function makeGaugeEl(overrides: Partial = {}): ButtonGraphicsGaugeElement { + return { + id: 'gauge1', + name: '', + type: 'gauge', + usage: USAGE, + enabled: val(true), + opacity: val(100), + x: val(0), + y: val(0), + width: val(100), + height: val(100), + rotation: val(0), + value: val(50), + orientation: val('horizontal'), + reverse: val(false), + roundedEnds: val(true), + thickness: val(20), + multiSegment: val(true), + thresholds: val([ + { value: 0, color: 0x00ff00 }, + { value: 66, color: 0xffff00 }, + { value: 85, color: 0xff0000 }, + ]), + inactiveStyle: val('transparent'), + inactiveAmount: val(70), + ...overrides, + } +} + function makeReferenceEl(overrides: Partial = {}): ButtonGraphicsReferenceElement { return { id: 'ref1', @@ -2360,4 +2392,156 @@ describe('ConvertSomeButtonGraphicsElementForDrawing', () => { expect(result.cyclicLocations.size).toBe(1) }) }) + + describe('gauge element conversion', () => { + async function convertGauge( + element: ButtonGraphicsGaugeElement, + variableValues: Record> = {}, + onlyEnabled = true + ) { + return ConvertSomeButtonGraphicsElementForDrawing( + createMockInstanceDefinitions(), + createMockParser(variableValues), + mockDrawPixelBuffers, + [element], + new Map(), + onlyEnabled, + null, + null, + null + ) + } + + function gaugeDrawEl(result: Awaited>): ButtonGraphicsGaugeDrawElement { + return result.elements[0] as ButtonGraphicsGaugeDrawElement + } + + test('converts gauge element with all defaults', async () => { + const result = await convertGauge(makeGaugeEl()) + const el = gaugeDrawEl(result) + expect(el.type).toBe('gauge') + expect(el.id).toBe('gauge1') + expect(el.value).toBe(50) + expect(el.orientation).toBe('horizontal') + expect(el.reverse).toBe(false) + expect(el.roundedEnds).toBe(true) + expect(el.thickness).toBe(20) + expect(el.multiSegment).toBe(true) + expect(el.inactiveStyle).toBe('transparent') + expect(el.inactiveAmount).toBe(70) + expect(el.opacity).toBe(1) + }) + + test('value is clamped to 0–100', async () => { + expect(gaugeDrawEl(await convertGauge(makeGaugeEl({ value: val(150) }))).value).toBe(100) + expect(gaugeDrawEl(await convertGauge(makeGaugeEl({ value: val(-10) }))).value).toBe(0) + }) + + test('value is rounded to one decimal place', async () => { + expect(gaugeDrawEl(await convertGauge(makeGaugeEl({ value: val(33.333) }))).value).toBe(33.3) + expect(gaugeDrawEl(await convertGauge(makeGaugeEl({ value: val(66.666) }))).value).toBe(66.7) + }) + + test('value resolved from expression', async () => { + const el = gaugeDrawEl( + await convertGauge(makeGaugeEl({ value: expr('$(counter:level)') }), { counter: { level: 75 } }) + ) + expect(el.value).toBe(75) + }) + + test('orientation tolerant matching: leading whitespace and prefix', async () => { + // Deliberately out-of-spec raw strings exercising the tolerant parser — cast to bypass union check + expect( + gaugeDrawEl(await convertGauge(makeGaugeEl({ orientation: val(' horizontal') as any }))).orientation + ).toBe('horizontal') + expect(gaugeDrawEl(await convertGauge(makeGaugeEl({ orientation: val('v') as any }))).orientation).toBe( + 'vertical' + ) + expect(gaugeDrawEl(await convertGauge(makeGaugeEl({ orientation: val('r') as any }))).orientation).toBe('ring') + }) + + test('orientation: unknown value falls back to default', async () => { + expect(gaugeDrawEl(await convertGauge(makeGaugeEl({ orientation: val('diagonal') as any }))).orientation).toBe( + 'horizontal' + ) + }) + + test('all three orientations pass through', async () => { + for (const o of ['horizontal', 'vertical', 'ring'] as const) { + expect(gaugeDrawEl(await convertGauge(makeGaugeEl({ orientation: val(o) }))).orientation).toBe(o) + } + }) + + test('inactiveStyle tolerant matching', async () => { + expect( + gaugeDrawEl(await convertGauge(makeGaugeEl({ inactiveStyle: val(' transparent') as any }))).inactiveStyle + ).toBe('transparent') + expect(gaugeDrawEl(await convertGauge(makeGaugeEl({ inactiveStyle: val('d') as any }))).inactiveStyle).toBe( + 'dimmed' + ) + }) + + test('thickness clamped to 1–50', async () => { + expect(gaugeDrawEl(await convertGauge(makeGaugeEl({ thickness: val(80) }))).thickness).toBe(50) + expect(gaugeDrawEl(await convertGauge(makeGaugeEl({ thickness: val(0) }))).thickness).toBe(1) + }) + + test('thresholds parsed from table rows', async () => { + const el = gaugeDrawEl(await convertGauge(makeGaugeEl())) + expect(el.thresholds).toEqual([ + { value: 0, color: 0x00ff00 }, + { value: 66, color: 0xffff00 }, + { value: 85, color: 0xff0000 }, + ]) + }) + + test('threshold values clamped to 0–100', async () => { + const el = gaugeDrawEl( + await convertGauge( + makeGaugeEl({ + thresholds: val([ + { value: -10, color: 0xff0000 }, + { value: 200, color: 0x00ff00 }, + ]), + }) + ) + ) + expect(el.thresholds[0]!.value).toBe(0) + expect(el.thresholds[1]!.value).toBe(100) + }) + + test('empty thresholds produce empty array', async () => { + expect(gaugeDrawEl(await convertGauge(makeGaugeEl({ thresholds: val([]) }))).thresholds).toEqual([]) + }) + + test('enabled=false with onlyEnabled=true filters element out', async () => { + const result = await convertGauge(makeGaugeEl({ enabled: val(false) }), {}, true) + expect(result.elements).toHaveLength(0) + }) + + test('enabled=false with onlyEnabled=false produces disabled draw element', async () => { + const result = await convertGauge(makeGaugeEl({ enabled: val(false) }), {}, false) + expect(gaugeDrawEl(result).enabled).toBe(false) + }) + + test('opacity scaled from percentage to 0–1', async () => { + expect(gaugeDrawEl(await convertGauge(makeGaugeEl({ opacity: val(50) }))).opacity).toBeCloseTo(0.5) + }) + + test('roundedEnds=false passes through', async () => { + expect(gaugeDrawEl(await convertGauge(makeGaugeEl({ roundedEnds: val(false) }))).roundedEnds).toBe(false) + }) + + test('contentHash changes when value changes', async () => { + const hash50 = gaugeDrawEl(await convertGauge(makeGaugeEl({ value: val(50) }))).contentHash + const hash75 = gaugeDrawEl(await convertGauge(makeGaugeEl({ value: val(75) }))).contentHash + expect(hash50).not.toBe(hash75) + }) + + test('contentHash is stable for identical inputs', async () => { + const a = gaugeDrawEl(await convertGauge(makeGaugeEl())).contentHash + const b = gaugeDrawEl(await convertGauge(makeGaugeEl())).contentHash + expect(a).toBe(b) + }) + }) }) From 415ac517fe6883f80ce8c0eec47a0af5bcbd35d0 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Sat, 6 Jun 2026 22:30:36 +0100 Subject: [PATCH 09/30] wip: fix --- .../ConvertGraphicsElements/Helper.ts | 8 +-- ..._semi-transparent_over_another_element.png | Bin 296 -> 294 bytes ...in_non-square_element_-_stays_circular.png | Bin 2155 -> 2146 bytes ..._false_value_75_-_single_colour_active.png | Bin 2768 -> 2756 bytes ...erse_true_value_75_-_counter-clockwise.png | Bin 2721 -> 2733 bytes ...roundedEnds_false_value_75_-_flat_ends.png | Bin 2671 -> 2630 bytes ..._gauge_element_ring_thick_thickness_40.png | Bin 2804 -> 2779 bytes ...er_gauge_element_ring_thin_thickness_8.png | Bin 2675 -> 2644 bytes ..._element_ring_value_100_-_fully_active.png | Bin 2670 -> 2637 bytes ...e_33_-_one_colour_within_first_segment.png | Bin 2687 -> 2672 bytes ...alue_50_-_midway_through_first_segment.png | Bin 2736 -> 2709 bytes ..._-_exactly_at_first_threshold_boundary.png | Bin 2712 -> 2685 bytes ...alue_75_-_crossing_into_yellow_segment.png | Bin 2736 -> 2712 bytes ...inactive_-_both_halves_clearly_visible.png | Bin 2727 -> 2707 bytes ...g_value_90_-_crossing_into_red_segment.png | Bin 2774 -> 2744 bytes ...r_group_properties_group_with_rotation.png | Bin 1292 -> 1631 bytes shared-lib/lib/Graphics/ImageBase.ts | 2 +- shared-lib/lib/Graphics/LayeredRenderer.ts | 61 +++++++++++------- 18 files changed, 43 insertions(+), 28 deletions(-) diff --git a/companion/lib/Graphics/ConvertGraphicsElements/Helper.ts b/companion/lib/Graphics/ConvertGraphicsElements/Helper.ts index a8fff05159..a169ce2a89 100644 --- a/companion/lib/Graphics/ConvertGraphicsElements/Helper.ts +++ b/companion/lib/Graphics/ConvertGraphicsElements/Helper.ts @@ -158,15 +158,15 @@ export class ElementExpressionHelper { } /** - * Like getEnum, but tolerant of leading whitespace and case differences. - * Matches from the first non-whitespace character, case-insensitively. + * Like getEnum, but compares the string value to the enum values in a tolerant way: + * Matches only the first non-whitespace character, case-insensitively. */ getTolerantEnum(propertyName: keyof T, values: readonly TVal[], defaultValue: TVal): TVal { const raw = this.getString(propertyName, defaultValue) const trimmed = String(raw ?? '') - .trimStart() + .trim() .toLowerCase() - return values.find((v) => v.toLowerCase().startsWith(trimmed)) ?? defaultValue + return values.find((v) => v.toLowerCase().startsWith(trimmed[0])) ?? defaultValue } getBoolean(propertyName: keyof T, defaultValue: boolean): boolean { diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_box_properties_box_with_opacity_-_semi-transparent_over_another_element.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_box_properties_box_with_opacity_-_semi-transparent_over_another_element.png index 5ef6f16d4c29c56c40051c76b6710393a0693a3a..da4f336d1810598d7fd3d720c31ecfc1cc583114 100644 GIT binary patch delta 163 zcmV;U09^m50;U3xL1fTLL_t(|obB5o4#O}I1<~)S%pive&*8&;m;mDx7PV8Utz$3` z87x?hWM(JQH_G#Po>wk1pHh*_d_A%QGwr9(c6?6$6p5Mv=e@G5FHHr>12cv&~>V6=(cV_mUs#K4KtXAYdTruO5bT4{^}3 R5W@ff002ovPDHLkV1lr0NUZ<> delta 155 zcmV;M0A&BB0;mFzL1EHKL_t(|obB5o62dSP2H`&pE~0lSzK5@;cd3Xj5(Uc)kVfpM zBA$4&GjT~GeWN^&$8qK&<0&OcE@!VXIs*X%0Rw?64NTdyWbLy}XKR-1ncmJ|pqiFB z+b{PT#yvNYp;s@ZX3N}Xo6cG)&iJA3B|p@C#6ZA6z(CSpJphJt4{^)I;A{W@002ov JPDHLkV1iq>L<;}_ diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_in_non-square_element_-_stays_circular.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_in_non-square_element_-_stays_circular.png index 81ab9fa16b5ee45118f608522ab8a9c674692bc9..b2093363680313136a65b08435380076575fc66a 100644 GIT binary patch delta 2104 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 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_roundedEnds_false_value_75_-_flat_ends.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_roundedEnds_false_value_75_-_flat_ends.png index f2ebdda5568f619c992e3b3cff26e6f8068a4881..ea15b2c551431615a09698d9b47d196f3a0fc3f9 100644 GIT binary patch delta 2593 zcmV++3f}ea6vh;gKz|AFNkl!#?nD^Wcy1pmLOXM~5&e z1hIyJ8)X?QB!3B?$>$v%TMy|-uT3MpHVrCEfj`P4xHkx{YjReWqXfAP1FJ<5Dpn%L*GZ5E^2)-0V_g+$Ln^DFACS8Qhl1pj;c$$Fex-_2MGK zAkHv|(KH4b1}arNk`~sW)oDWgX0{BF0U8;Y-OuMS)@Q(Ev{;t0S`;n&BpC)f1OZ36>cmQygU--s*w}#Zo)RKN1JS3V z$S<%bmL&;lP*rS5CN2A6ry3Ws*))RzZL|i40t~Dpz#}xpk(q&%C={bxIbYz4w0ZxNXPBBrvU&I z4J>+C!N7I~arf|BF#zldhw+Kq+2GJgxPL!!BeEqjJXrz~n-b8&WKkm$bk990c)f)q zH{Ps*dq&6EtK9fwFo=^L&jhDlJ@AKk2)hJgyYAN!-x4Rbn-X;0bvZ0vESp~7tuc_g zpT-fsv`L*V2tO5u?=2s(J$Y{u>Rj^>#+0DD?@kj3lVu7ozZ{&%Jf{@(KdJ|vAb$u9 z0}TCm$h4{Giuv>NXlfz{g!}jVkxW*U1eg*~5`~)GHN_FC;j4n+ zOJpM>t&j@7+aky?fOl;~$}8Ct9zb9@mVtTqL-9Twu?eE9?A%Jk;hf z9!8(Kt&J>R-Q80hrB4E^a!2wP#?1XFL00-_HWI}Bl`Ljj{g|m9C8)t-w|`(Z5@fXl ztJa%{7{`%A-isHfUpk{wFmZCOXnbya96{zbT$ZU>W8TMuPSF72xlBcj=gn78HamN) zR{?Z@Ntv%AQxUs7_cH=ftkUkp6o8JKzx?qcHmY2fEtBB!6qEGv$I~}sSTP7{*PzB4 zMk!1XawWmwEL1?3pw*d2-hb@im{A&KB}I_OU~mS9?-npv1A4&x$St2%j{x!zMC_(4XvltpOw|`_;U#+0IxxCW1 z=?r;^41*ARG6Re`K>%>%2suZxZe4N4f2KlON?tv_gN3^`1qAKe=O%uPHcQ{etqm`E!i$S{D;PVy?+ zwk>WNugw$~yn*-wmu>wTOm$=S~5VMB^v#dGY!Ad7PeCST-B`K@8cO?AIGl6{M9C z;t!G)Go@0fd4gOPLnNrDhsEc97s4;vW$0#>p8=wKqEN1#zL9>eEr;lC>q^$pb0N#q zMV77~^<)h^r+>&X+DN@|xhHU{0i{*Jg+E+?LCCcOfcrzYWl+D1JOH;3t;+x}$ z?un8u)+LX*@xDgOLh}*Wimv>f{*otT^ni~3z5R%7i+{mjtM6?a9s|+eL=oQ_w{}4c zAo5TI?tk0ct+5?`*8Z{7e~IuhW^ul z+Q(}VdVj|LSusrX%xZjV9P!O$-{Nw(K6SzWx*z;O9-e<~s@l@W0_l z?SGD1Fi~euK2F*{Skl58()Xs3`g*Ej9YuooxEH$r0+~a zTq8pFO?=%`%5YNMcfbe!Aa7Y`SShMX(0C|q3Y3Kklxq|yZ3?tn4Z2^49?+o)8fcLQ z)6anIXTe@zhd-k%*j^Ui_q~>Zvy&Jh=o$*Hpfda)c{Zn4skoBl00000NkvXXu0mjf DF5Jvn delta 2634 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 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_thick_thickness_40.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_thick_thickness_40.png index 3d4ce6dbe667e2799f9656c1d0a4d7a9afad72a9..bb01a9023ad417ae8133dfec1cd0ed0fb10b038a 100644 GIT binary patch delta 2742 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 delta 2767 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 V(5Vh8xvu~K002ovPDHLkV1lJkRl@)P diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_thin_thickness_8.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_thin_thickness_8.png index bfe590dd27bf7cbe1e236bac6b634c064e3d3e92..d6d84cb1e9c8fab0ee43bd93727289d4204927c4 100644 GIT binary patch delta 2607 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 KMNUMnLSTYAU*F>Z delta 2633 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 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_33_-_one_colour_within_first_segment.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_33_-_one_colour_within_first_segment.png index e7de590c9b01d5fbe681154f88c28b98eb5ca162..bb97b2a4a7da32cad041d05f7e7edb5c4664c4ac 100644 GIT binary patch delta 2634 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

hR^iwrN2^J9iQxG*< zYCw|J{qnKujtWT6650gGu$kfdGW}1;;+aYGGpkMHd0` zjd|#eI`n28dei?bSB(L6i3+2}fW=v06&Bu4yzpG|fa~GFB{^`HIru;JS53E+!jKia dj_Vj3{tJ`Y(Z#-J;tK!(002ovPDHLkV1iGG2ju_& delta 2699 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 FV1ljzFslFn delta 2674 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 diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_75_-_crossing_into_yellow_segment.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_gauge_element_ring_value_75_-_crossing_into_yellow_segment.png index 288639623bcbccf1269b54f18881c933fb189854..0176da26d962caef33cc0d8e6d9f656dc9f0887e 100644 GIT binary patch delta 2674 zcmV-&3XS!!6_^!}L4QI?L_t(|ob6nDa8%VD{_bNR_wF+rl1(BZKm{b$QW2VtRvah- zBQsjmN=N%}fYypw3PtO5+RoJJNYPq`u~s_OYEfIK{t>8;QJ~;JI}Jle9<5L)(1DPI z4awepN9}uc7rx8E0-Wkg764DHPaCEZ8m6yfzzKiU@|65u5_cy#2M6ms}7zMo}!-}v|=M7hyt*@uMch z!$4pd#D8fTJq!bl${$G&>(Fa-Xbl=N3o;OwiO}k_qUNI{3UzDiigpi1Ob`uVRWgZ| zOvdqKml+0!ISy@JFS^*$bb~TWfwWM9xLibkbHC%~YBtp%&>C>ue?$ZoLr#*W@lUT8 zhdB;sT`tEjRt&_YBH}-YL;Z}sD=~=3FCz%Q6n}Qqw`fPP7;@S;4u1#+Eybps@O|im zZ?6yOuci_IUL1PVUMa^n#X)HlLca?+p2M;ESdmT_BuA$xydDbSl~8EZ$r+jy2)q?Q zO0wP2M zv40n1P^UQ)%Yp>eX&P21leT?QG>vB>5u6_-UE>%g5P2?w=;Kk_7|q}fbiLXIBUIQu zu_cIMfN!o%Ar>a9Dw(42t4IWKhABANSi$$c4-NM>fVoV5C#_LK?4?-2v1|!ixKP53 z`3fe#rsC9XZs>IGx1v)NwpCT3qeRm^8h`Md@Sy(d^`K<(PRg`oka#3vI}VYcP{_bN z_oM*;6b;OJNQ-67%xjmWu>eyMGI8r?s+H zr>W2yhj_@Yy1AJwUgyqTtZ)oO>h$O%is9<1d|2Dj0#BD;l2L&aQmwR28@0 zn!)hAm!eQOc#xYg=6yKm6n_mE*3qb?h|QVPk1LacZntS0msKQt*G@8^2 zeLC68u2hi33nYThpC^0EbLToQGpQ8x&nB!6 z(Q)s+rAGB!YSu`VT z*cMCi`*?Ui&i4f^)$Y?zSLc1EsuZ>y;xKQTiter5D7wV{YJvi~w{(-QHyA+mGx=)< ziJ()b+<5Q3Azk#zITz-2v3RN_1BEpqx5!AWu`gX4PY^Z;kQS3od!RJ{*Z-_TEu!Im z?zspgiN;S3d4KV_CKbo0lVu%E_x3l={ zZ$tQbiwwic@-skedko4A<2TapZ|+BIn|&o~=+zL|82Qx3*7c))T?NlEa*Q@uZ(QsR zoUwp1TfwE@UxMi+yCv=uZrh?NI4LBLu8)F}$QoI%)qm0XcBkXY^|64sSVYI$9R+hf z^{eVZW$dj@L4w$J7BxSuvF)QZXy|C|K<0~AUndFc1jM$-Y%kun3o2jM8X z^0)d6-jFc@2D)~1A-*vVlO20++w_=-y%2-2L9lm0Od$Gr6z=~xtM7IkepdgY8o?a} zs#_T#@qbtX9q)EPnLS3zn*q}IrP2OjJH)%}FWlhMb@2O~*E$t-=sxLZlVAi4^sVT# z?WfF8aPcn}!SCY{dOb7{0G4}GQ9$2aeGtAcK%MSD|LH*Wv(*T_?EJ16MtWye*dRby zNA@kQgzJI}{&)P~_wexitF$^txkZL}r#NtbwSV53^`u1_(RER92b^EkD(O-$K6Sui zTVpW!;|k;L9EybZvB?lA}nJg&+*(Sk?(hu1dmm zjen0^5!1teNQO^Np;_DANW1IZJlAM z7*m3VLupo^%ut}rRG>5~(5rPAegj6pfF997i!_)n25c9b{h2N{#)7@b!uzq;HgGN{ g1_`=`YbXu>2XuVPd2M)~-T(jq07*qoM6N<$g0giW9RL6T delta 2699 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 bO?~_ig`Bhv^(o1e00000NkvXXu0mjf49O+& delta 2689 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 N002ovPDHLkV1him1l#}s delta 2737 zcmV;i3QqO771kAyL4SZrL_t(|ob6nBY#h}c{@%>&F*Cc~!}jjlc;gV-K!Q{dBy~g~ z6g9zBfVMy>Ra*opNuxjrkV0Bj6;i2GNKlhfD4`9ADn$ySRYOC?VM0n%E~{-+jwDSY zl0xj*yS8_{$Ly|W=jb1fkM+)*J+nTPPqMV@c{B6oo8SB0Z-0SvIEQLL#y%`qu)wIt z4I6YF?YSJnstTW`!K3RKT$00U4`guSG7U*TKokP5p96cE1si9CW8fvf_7DfN7M3-D^B3MyE2FcC*F_YFrc(6Kzrf!*ft*MGJiq}2h|W~ji5GFMW-xdX*LVipbO`bFY0;Z6ocl`ki030ALs&)F%;lLovj(<-u4E75G{w4@WGu168>T!>gi*Zhxl}s)eMK1N%`s>__eJKJA6vDMR|9 z1m%L_7Bl)-6lzd~cZavE`;|q|=2#5thKGxe8)RARal7%lC_*bbx^jrGixBsTNZyi! zv{8Z@v|fI(2Vx*9foG?u12V-SKaQN&l<+7O$hf9eI2A_%Ps!TYrJ5;7PAktZX_ zonM+uQk0oQn$T0!^CJaSVkQJiJ*t=3i?v(+~KsRitZ>}a{I1x%Lyh?!*>#Mi|wuR}dM z6wnYeFIPl;Jhk8@A&%1nv!h7<3myS}#HO(tnp4 zm7pzK2HLgAPZfbFUkOeFA*jebnQ+TZgof?$?^CcRvi)z|c>IOpg^16n_q@Sh><%dH`VCHa`Xi>hI@N3z8=AJx*V0a;J=u z%L-$=0)kerj)P&$eV#~!!>(No)y{R=(6GA!wo`PA8^68KqU90fav8Yw)+F@;J9m1Z zSD&VtKI8!ZFa6ZR>8sPgl*N%p(8`r^f#n)QD#c*;?%69nV-Q~!VLxh~K!4NSI5JXxTD)5cmA^4ajECZi%H{@H|hC=F`{Y>)$+ruDF6u&`U4P9xtUH- zBrMd<+m=l~NpfALz- zgdkc{Fqxct(lZ8HbZ@GpGZ}Mn<7-eOpRu6TAsCa7G=iMYx^(7E3x8CKT45)pbH)kE zQj*&|KSv3&jRvP?U1OA=-Rg_0iE(g5&syOw$actfkqXxCJ0ft~;cz->;tS-DMU}^_gL40Ag z%-Y&=SiRc1pJpZ_erFt-m+l~XeqUG)Dj=x4+lh}pqWeUhUr#{`O>3iFMbN!EhS$-h zN8B&Maj@{YO(WEYA1>_PP!xhK-8?!jP!Qb~oz#Lp3qXK@Edx~3PILnQFU(8QrUV^4 z=s;K3gf2RC!hepXeK!2KBLl@|Ak&dSe9i1xYSMiYjdV4GmO+PM);k3%~Q?=N%bn zY+<^?U=2ieM4-&zNME^FLGpc*dw_!(kx$L=>_6V=xFkMNABWz&!!j|65u)z2u^o z|4~0YyR5en8LR>6ZV5emdn_|{H3bW#Zcd@+^&X^F6%VNWkNP3(FTGo(tg>n2&l;h- zbtG@3_b~#1vQR5!<@_#BB(tQ$?1;t+m(Fpuo{qQ_f`d%?q zx+PP(U$XQqro#5A4X&465cUahScEa9Als3JymEA}yh>^KnhGYxAb3v@{DIQjc&gsi zn_iJdbbA!KP+sMgJI#TApNHc=4mdw>=4J1uVtiBYc`D>+lu&io1OWa$9>KeUU=ro`7u19x zoXN3B8zjV6$6?rKMuCwVj-gHYR546hwV>VL+RrJG+%5Q9MctBA%&8o@?tyN~3o{lJp$ z(~^T*C&-ZM$kE&Lv{E+QMuJywlO-A00000NkvXXu0mjf<+MHJ diff --git a/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_group_properties_group_with_rotation.png b/companion/test/Graphics/__snapshots__/GraphicsLayeredButtonRenderer_group_properties_group_with_rotation.png index 858443ea5a3a40c7f1847d94cde543eec057f9a1..366d17216b2a1789309dd288cbdd73c3450f926d 100644 GIT binary patch delta 1586 zcmV-22F>}53f~NnKz{}aNkl%U2IfU5XXOW+tLaJ6i{jXKy9k2DPmd?Bp~1i zer~J}py7q6hzUl7LW}xf8WLc)P(TtfZBWAtF%TnUMGc~e5CXl zO~j@#UBT6~G7Sh_WN8~p6T%k=VVTb-Hg(vuF;y%B&=|9|W3!EheIVwWZ4|Rve@?1-Lw$M&|$w04AUmqm*ih zttO%lr0T$=B1l8@2POg=P`hvA39{D|@{fS9yu@F&4@mYgUx+fpXXgNvAUpxQ04&rX z57<5+;XhE9G6a{&xU8}*#uLIZ6HPlz+BMlRn*dsaLqAG!S%Y%B4FT3sen4mhJ~e6g zqZf0D6@Tdg6wtwxjCBrxFbo(56r+sPARo|&uvGF-a^~Vh-zJY^l+nQk;L2nU^MO|L zTCm%^7Hmz_K>QJOoI*O8aS1sGb2l6V6ayt1wjsU<5(>?2{Zgk0p$I6(Wt1*v6Mt=lrH0N}8!j3_x`eB6nToR9x?F!b z4KxC~Oxo>;wx~x3={mAF#Bf~J0wpPv>Mw^)A^*%2^6&op%bp!n$8~h_BFfy9%k-ap z2#vUGMhVL+{7Fv^(t7%Gf>9_Nf&OWg>K|&-t`Ubisb+udB7NH2w|b9rFadK1(1LUo z?tiG4Rb9*#L>O(p5BEMC*3!XAVm{{oKacffc#kYTm+Gz>bU_4ZEkl6->VHSU2ikFH zAn$Bn?i>ecJvn4i0KGJNga#b;%JZB$^P_W9tc5}%JPq9Fhd!VAo^0MC|J<%_cLeF{ z(5MgS%b!7As8UA_gM_aF~Qr z4&0nJnSLSEkOxpU4hy7`gYhj-0u3?I6!KA~kb9j? zOc#HULj{9qlBZ~mXG8J>RLi)Gc7FmW51X_bo~*63AuJ?^H>8qZ;@{#C4RXDC+TDXv zZhGUHhwv2+(>P7D)ZoUy%@SHm8AY1$}6iS+pf%Agx1~TFaxRke8dQ zOKK5br4OrR4u_LBk!Ex$Lx>PSnPi+)oT8(4lf`tYqS=cP-^O?~M2U%}@urXuwR(WV zGi0+tDmmuGjNfz4Xf5MReSaElQm?NCv+v^YvQ)F*t0CVV#F>WW2?A*MXO$5i%(de1 z7=N= z0dkm3KFzlNoiigHbOsG^uPNkrnnIp?kuCGdV~xz_H(MGq+Cg+hQ!76r1b~OkCAc)| kLSY+O%#?Y2YxDen0bBm8BmU_c)&Kwi07*qoM6N<$f(}y2wEzGB delta 1244 zcmV<21S9+342%kpKz{_bNkl%O=w(I6vuyOCM7UXv4OUmS}X|>C^3vMK? zR0IvUQ9)9vre8^BqBJ7KblyyoCedJmpCF3lX)}mvFH%%-A zn{QVP4uI0dI*Xd-yjtuI!3SfANn<)fJ9!p>W)IzdC&pX!^O*>0(jOC;&7eGE9)kn0 zpCX@NaaFT)81=^h#xh7VbWvnBs2Si(;+&Cw{xIZPAdCb^FB0b#*MW8u_JDtYb0m0Q zdRa4M(-&r@*ne%_G#DI!2Y@U&e$h0~7_{q|z!{{I>?Oxd2yr!1F0+ec(#KM0V>l2z zgL0e;H8=o5CwYF-G~+r(OQ>UQ8#(~?vVI+in{%lue*r@z`A~#n*n4h}#I z@BtPJnxQl7!F*?srs)R0GV6{nzC}1GNq!5%xUT@|B!A81xB(nA?~*TafW;n0q=%eI z!~UjNj#Ci|4nP8Um%mxkG~KG^Ho%?@X_|w;&!Mke!I%enB*nF$hP-ExkcoJ306KsL zwV2T?`+^wqltG%Mg91MW8L$p)lr0^M%ai22c6J>gyWYBD|2mDx5;0E60CQeCxu0UU zd98ti1Aov)ky*8v)e+j1ZOysGLP zhpHLmQ$}qxt`Vn4`k60%Q9XnF$*7ISQ3_nuG+(I2fvqnpW{@Enwb9rpTM}5(%bH^b z=|$oc8Lh2vkstx|kz+p6w5eGv45J#sL^Ga1cx>c3(99uEo9W_lH7Wd zvMqz`dh1FZl)@x1nXRA6F<2B$z1CrcmR6ChhgZRC0@2p>xyi=_|$50FmM z3|KXHD;lhl;B2`UcQfc;1|ue;)(&AvdRZy|>aL7>V+WKJsE- zhJWpC_Rb>48K>Y-y3DJ^0hDpz`IPczj4UrxWEeIIxBCjgk|e2W&774qp(q1Gsye0JcOj5%EHI6*y9DSue%hJ!wU-co89k@{lRJ1{stT zX|J7(-Lw}$CB@e~NE?v0H)2s?nFJU7bPVmgp<}eNP9_p%i}0Kz`OaV0kg>~_X@78k zVZbooOUnP=EsVcI-GdBpnGPNU-mtIm){+8EoDO23rc1Mq(ZV`Iz;XKmLD7Zsp{{~e;Qd}azVc;FZ zYUUyj^N|Tdktq*sHS@fYvL(jTURCcRIuV%wZDN-#GXd1hsxqn%^ z8{|ZZn(4l55ynjhhp1*yDLO_gcbKZ8W-cQfsdj7CEB^r+L`KJjZHmzV0000 Promise): Promise { const oldAlpha = this.context2d.globalAlpha - this.context2d.globalAlpha = alpha + this.context2d.globalAlpha *= alpha try { await fcn() diff --git a/shared-lib/lib/Graphics/LayeredRenderer.ts b/shared-lib/lib/Graphics/LayeredRenderer.ts index bc089f1081..a428d5ce9d 100644 --- a/shared-lib/lib/Graphics/LayeredRenderer.ts +++ b/shared-lib/lib/Graphics/LayeredRenderer.ts @@ -103,7 +103,7 @@ export class GraphicsLayeredButtonRenderer { try { switch (element.type) { case 'group': { - await img.usingAlpha(element.opacity, async () => { + await img.usingTemporaryLayer(element.opacity, async (img) => { await img.usingRotation(drawBounds, element.rotation, async () => { elementBounds = await this.#drawGroupElement(img, drawBounds, element, skipDraw) @@ -122,7 +122,7 @@ export class GraphicsLayeredButtonRenderer { break } case 'reference': { - await img.usingAlpha(element.opacity, async () => { + await img.usingTemporaryLayer(element.opacity, async (img) => { await img.usingRotation(drawBounds, element.rotation, async () => { elementBounds = await this.#drawReferenceElement(img, drawBounds, element, skipDraw) @@ -247,7 +247,7 @@ export class GraphicsLayeredButtonRenderer { } if (imageDrawn === false) { - await img.usingAlpha(element.opacity, async () => { + await img.usingTemporaryLayer(element.opacity, async (img) => { await img.usingRotation(drawBounds, element.rotation, async () => { const { x, y, width, height, maxX, maxY } = drawBounds @@ -319,7 +319,7 @@ export class GraphicsLayeredButtonRenderer { const marginX = 2 * marginScale * drawBounds.width const marginY = 1 * marginScale * drawBounds.height - await img.usingAlpha(element.opacity, async () => { + await img.usingTemporaryLayer(element.opacity, async (img) => { await img.usingRotation(drawBounds, element.rotation, async () => { img.drawAlignedText( drawBounds.x + marginX, @@ -358,7 +358,7 @@ export class GraphicsLayeredButtonRenderer { // Calculate a pixel width, relative to the parent bounds const borderWidth = Math.max(0, parentBounds.width, parentBounds.height) * element.borderWidth - await img.usingAlpha(element.opacity, async () => { + await img.usingTemporaryLayer(element.opacity, async (img) => { await img.usingRotation(drawBounds, element.rotation, async () => { img.box( drawBounds.x, @@ -424,7 +424,7 @@ export class GraphicsLayeredButtonRenderer { // Calculate a pixel width, relative to the parent bounds const borderWidth = Math.max(0, parentBounds.width, parentBounds.height) * element.borderWidth - await img.usingAlpha(element.opacity, async () => { + await img.usingTemporaryLayer(element.opacity, async (img) => { const midX = drawBounds.x + drawBounds.width / 2 const midY = drawBounds.y + drawBounds.height / 2 const radiusX = drawBounds.width / 2 @@ -467,13 +467,30 @@ export class GraphicsLayeredButtonRenderer { if (sorted.length === 0) return drawBounds const { x, y, width, height, maxX, maxY } = drawBounds - const { value, orientation, reverse, multiSegment, inactiveStyle, inactiveAmount } = element + const { orientation, reverse, multiSegment, inactiveStyle } = element + + // Clamp gauge-level numbers to valid finite ranges so downstream math never sees NaN. + const value = Number.isFinite(element.value) ? Math.max(0, Math.min(100, element.value)) : 0 + const inactiveAmount = Number.isFinite(element.inactiveAmount) + ? Math.max(0, Math.min(100, element.inactiveAmount)) + : 0 + + // Sanitize a threshold numeric field: coerce to finite, clamp to 0–100. + const safeThreshVal = (v: unknown): number => { + const n = Number(v) + return Number.isFinite(n) ? Math.max(0, Math.min(100, n)) : 0 + } + // Sanitize a color field: coerce to finite, fall back to black. + const safeColor = (v: unknown): number => { + const n = Number(v) + return Number.isFinite(n) ? n : 0 + } // For single-color mode, find the highest threshold whose start <= current value - let singleActiveColor = Number(sorted[0].color) + let singleActiveColor = safeColor(sorted[0].color) if (!multiSegment) { for (const t of sorted) { - if (Number(t.value) <= value) singleActiveColor = Number(t.color) + if (safeThreshVal(t.value) <= value) singleActiveColor = safeColor(t.color) } } @@ -502,7 +519,7 @@ export class GraphicsLayeredButtonRenderer { } } - await img.usingAlpha(element.opacity, async () => { + await img.usingTemporaryLayer(element.opacity, async (img) => { await img.usingRotation(drawBounds, element.rotation, async () => { if (orientation === 'ring') { const cx = x + width / 2 @@ -521,9 +538,9 @@ export class GraphicsLayeredButtonRenderer { // the main canvas at the desired transparency in a single operation. const drawInactiveArcs = (target: ImageBase) => { for (let i = 0; i < sorted.length; i++) { - const segStart = Number(sorted[i].value) - const segEnd = i + 1 < sorted.length ? Number(sorted[i + 1].value) : 100 - const color = Number(sorted[i].color) + const segStart = safeThreshVal(sorted[i].value) + const segEnd = i + 1 < sorted.length ? safeThreshVal(sorted[i + 1].value) : 100 + const color = safeColor(sorted[i].color) if (segStart >= segEnd) continue const inactiveStart = Math.max(segStart, value) @@ -551,9 +568,9 @@ export class GraphicsLayeredButtonRenderer { // Pass 2: active arcs (always fully opaque, drawn directly). for (let i = 0; i < sorted.length; i++) { - const segStart = Number(sorted[i].value) - const segEnd = i + 1 < sorted.length ? Number(sorted[i + 1].value) : 100 - const color = Number(sorted[i].color) + const segStart = safeThreshVal(sorted[i].value) + const segEnd = i + 1 < sorted.length ? safeThreshVal(sorted[i + 1].value) : 100 + const color = safeColor(sorted[i].color) if (segStart >= segEnd) continue const activeEnd = Math.min(segEnd, value) @@ -608,9 +625,9 @@ export class GraphicsLayeredButtonRenderer { } } else { for (let i = 0; i < sorted.length; i++) { - const segStart = Number(sorted[i].value) - const segEnd = i + 1 < sorted.length ? Number(sorted[i + 1].value) : 100 - const color = Number(sorted[i].color) + const segStart = safeThreshVal(sorted[i].value) + const segEnd = i + 1 < sorted.length ? safeThreshVal(sorted[i + 1].value) : 100 + const color = safeColor(sorted[i].color) if (segStart >= segEnd) continue @@ -618,15 +635,13 @@ export class GraphicsLayeredButtonRenderer { const activeEnd = Math.min(segEnd, value) if (activeEnd > segStart) { const activeColor = multiSegment ? color : singleActiveColor - const [ax1, ay1, ax2, ay2] = segmentBox(segStart, activeEnd) - img.box(ax1, ay1, ax2, ay2, parseColor(activeColor)) + img.box(...segmentBox(segStart, activeEnd), parseColor(activeColor)) } // Inactive portion: max(segStart, value) → segEnd const inactiveStart = Math.max(segStart, value) if (inactiveStart < segEnd) { - const [ix1, iy1, ix2, iy2] = segmentBox(inactiveStart, segEnd) - img.box(ix1, iy1, ix2, iy2, dimmedColor(color)) + img.box(...segmentBox(inactiveStart, segEnd), dimmedColor(color)) } } } From ecf2566a19141386f3b9abff953535309ca5403c Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Sun, 7 Jun 2026 12:58:59 +0100 Subject: [PATCH 10/30] wip: list input --- .../ControlTypes/Button/LayerDefaults.ts | 6 +- .../lib/Graphics/ConvertGraphicsElements.ts | 18 +- .../lib/Graphics/ElementPropertiesSchemas.ts | 17 +- shared-lib/lib/Model/Options.ts | 9 + shared-lib/lib/ValidateInputValue.ts | 54 +++- .../ElementPropertiesEditor.tsx | 56 +++- .../src/Components/ListInputField.stories.tsx | 138 ++++++++++ webui/src/Components/ListInputField.tsx | 252 ++++++++++++++++++ webui/src/Controls/OptionsInputField.tsx | 17 ++ 9 files changed, 553 insertions(+), 14 deletions(-) create mode 100644 webui/src/Components/ListInputField.stories.tsx create mode 100644 webui/src/Components/ListInputField.tsx diff --git a/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts b/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts index 040c3a9818..ba190924b0 100644 --- a/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts +++ b/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts @@ -140,9 +140,9 @@ export function CreateElementOfType(type: SomeButtonGraphicsElement['type']): So multiSegment: { value: true, isExpression: false }, thresholds: { value: [ - { value: 0, color: 0x00ff00 }, - { value: 66, color: 0xffff00 }, - { value: 85, color: 0xff0000 }, + { value: { value: 0, isExpression: false }, color: { value: 0x00ff00, isExpression: false } }, + { value: { value: 66, isExpression: false }, color: { value: 0xffff00, isExpression: false } }, + { value: { value: 85, isExpression: false }, color: { value: 0xff0000, isExpression: false } }, ], isExpression: false, }, diff --git a/companion/lib/Graphics/ConvertGraphicsElements.ts b/companion/lib/Graphics/ConvertGraphicsElements.ts index f704d71d3d..1617ce80ed 100644 --- a/companion/lib/Graphics/ConvertGraphicsElements.ts +++ b/companion/lib/Graphics/ConvertGraphicsElements.ts @@ -6,7 +6,7 @@ import { getElementSchemaProperty, } from '@companion-app/shared/Graphics/ElementPropertiesSchemas.js' import type { ControlLocation } from '@companion-app/shared/Model/Common.js' -import type { ExpressionOrValue } from '@companion-app/shared/Model/Options.js' +import { isExpressionOrValue, type ExpressionOrValue } from '@companion-app/shared/Model/Options.js' import type { ButtonGraphicsBorder, ButtonGraphicsBounds, @@ -537,6 +537,7 @@ function parseCompositeElementChildOptions( case 'internal:trigger': case 'internal:trigger_collection': case 'internal:variable': + case 'internal:list': case 'secret-text': case 'static-text': case 'custom-variable': @@ -822,10 +823,21 @@ function convertGaugeElementForDrawing( const inactiveStyle = helper.getTolerantEnum('inactiveStyle', GAUGE_INACTIVE_STYLE_CHOICES, 'transparent') const thresholdsRaw = (element.thresholds as ExpressionOrValue).value + // Resolve a cell that may be a plain JsonValue (old internal:table) or ExpressionOrValue (internal:list) + const resolveThresholdCell = (raw: unknown): number => { + if (isExpressionOrValue(raw)) { + if (raw.isExpression) { + const r = helper.executeExpressionAndTrackVariables(raw.value, 'number') + return r.ok ? Number(r.value) : 0 + } + return Number(raw.value ?? 0) + } + return Number(raw ?? 0) + } const thresholds: ButtonGraphicsGaugeDrawElement['thresholds'] = Array.isArray(thresholdsRaw) ? thresholdsRaw.map((row) => ({ - value: Math.max(0, Math.min(100, Number((row as any)?.value ?? 0))), - color: Number((row as any)?.color ?? 0), + value: Math.max(0, Math.min(100, resolveThresholdCell((row as any)?.value))), + color: resolveThresholdCell((row as any)?.color), })) : [] diff --git a/shared-lib/lib/Graphics/ElementPropertiesSchemas.ts b/shared-lib/lib/Graphics/ElementPropertiesSchemas.ts index 63a1e51969..0f89d787b3 100644 --- a/shared-lib/lib/Graphics/ElementPropertiesSchemas.ts +++ b/shared-lib/lib/Graphics/ElementPropertiesSchemas.ts @@ -539,6 +539,12 @@ export const gaugeElementSchema: ElementSchemaSection[] = [ max: 50, step: 1, }, + ], + }, + { + id: 'thresholds', + label: 'Colour Thresholds', + fields: [ { type: 'checkbox', id: 'multiSegment', @@ -548,12 +554,13 @@ export const gaugeElementSchema: ElementSchemaSection[] = [ default: true, }, { - type: 'internal:table', + type: 'internal:list', id: 'thresholds', - label: 'Colour thresholds', - tooltip: 'Define colour stops for the gauge. Each row specifies the value (0-100) at which that colour starts.', - disableAutoExpression: true, - columns: [ + label: 'Thresholds', + tooltip: + 'Define colour stops for the gauge. Each threshold specifies the value (0-100) at which that colour starts.', + addLabel: 'Add threshold', + fields: [ { id: 'value', type: 'number', label: 'Value', min: 0, max: 100, step: 1, default: 0 }, { id: 'color', diff --git a/shared-lib/lib/Model/Options.ts b/shared-lib/lib/Model/Options.ts index c9a8743cb7..6480902c64 100644 --- a/shared-lib/lib/Model/Options.ts +++ b/shared-lib/lib/Model/Options.ts @@ -75,6 +75,7 @@ export interface CompanionInputFieldBaseExtended { | 'internal:vertical-alignment' | 'internal:image-file' | 'internal:table' + | 'internal:list' /** The label of the field */ label: string /** A hover tooltip for this field */ @@ -177,6 +178,13 @@ export interface InternalInputFieldTable extends CompanionInputFieldBaseExtended default: Record[] } +export interface InternalInputFieldList extends CompanionInputFieldBaseExtended { + type: 'internal:list' + fields: SomeCompanionInputField[] + addLabel?: string + default: Record[] +} + export type InternalInputField = | InternalInputFieldTime | InternalInputFieldDate @@ -193,6 +201,7 @@ export type InternalInputField = | InternalInputFieldVerticalAlignment | InternalInputFieldPngImage | InternalInputFieldTable + | InternalInputFieldList export interface CompanionInputFieldStaticTextExtended extends CompanionInputFieldBaseExtended { type: 'static-text' diff --git a/shared-lib/lib/ValidateInputValue.ts b/shared-lib/lib/ValidateInputValue.ts index 7573c6cda3..e5abee7d1d 100644 --- a/shared-lib/lib/ValidateInputValue.ts +++ b/shared-lib/lib/ValidateInputValue.ts @@ -1,7 +1,7 @@ import isEqual from 'fast-deep-equal' import type { JsonValue } from 'type-fest' import { ParseExpression } from './Expression/ExpressionParse.js' -import type { SomeCompanionInputField } from './Model/Options.js' +import { isExpressionOrValue, type SomeCompanionInputField } from './Model/Options.js' import { stringifyVariableValue } from './Model/Variables.js' import { assertNever } from './Util.js' @@ -338,6 +338,58 @@ export function validateInputValue( return { sanitisedValue: sanitisedRows, validationError: undefined, validationWarnings } } + case 'internal:list': { + if (!Array.isArray(value)) { + return { sanitisedValue: value, validationError: 'Value must be an array', validationWarnings } + } + + const sanitisedRows: JsonValue[] = [] + for (let rowIndex = 0; rowIndex < value.length; rowIndex++) { + const row = value[rowIndex] + if (typeof row !== 'object' || row === null || Array.isArray(row)) { + return { + sanitisedValue: value, + validationError: `Row ${rowIndex} must be an object`, + validationWarnings, + } + } + + const sanitisedRow: Record = {} + for (const field of definition.fields) { + const cellRaw = (row as Record)[field.id] + // Auto-wrap bare JsonValue for saved data predating expression support + const cell = isExpressionOrValue(cellRaw) ? cellRaw : { isExpression: false, value: cellRaw } + + if (cell.isExpression) { + if (typeof cell.value !== 'string') { + return { + sanitisedValue: value, + validationError: `Row ${rowIndex}, field "${field.label}": Expression must be a string`, + validationWarnings, + } + } + sanitisedRow[field.id] = cell as unknown as JsonValue + } else { + const result = validateInputValue(field, cell.value as JsonValue | undefined, options) + if (result.validationError) { + return { + sanitisedValue: value, + validationError: `Row ${rowIndex}, field "${field.label}": ${result.validationError}`, + validationWarnings, + } + } + validationWarnings.push( + ...result.validationWarnings.map((w) => `Row ${rowIndex}, field "${field.label}": ${w}`) + ) + sanitisedRow[field.id] = { isExpression: false, value: result.sanitisedValue } as unknown as JsonValue + } + } + sanitisedRows.push(sanitisedRow) + } + + return { sanitisedValue: sanitisedRows, validationError: undefined, validationWarnings } + } + case 'internal:connection_id': case 'internal:connection_collection': case 'internal:custom_variable': diff --git a/webui/src/Buttons/EditButton/LayeredButtonEditor/ElementPropertiesEditor.tsx b/webui/src/Buttons/EditButton/LayeredButtonEditor/ElementPropertiesEditor.tsx index 009f4527fc..77810f84f8 100644 --- a/webui/src/Buttons/EditButton/LayeredButtonEditor/ElementPropertiesEditor.tsx +++ b/webui/src/Buttons/EditButton/LayeredButtonEditor/ElementPropertiesEditor.tsx @@ -1,14 +1,16 @@ import { observer } from 'mobx-react-lite' -import { useContext } from 'react' +import { useCallback, useContext } from 'react' import type { JsonValue } from 'type-fest' import { elementSchemas, elementSimpleModeFields } from '@companion-app/shared/Graphics/ElementPropertiesSchemas.js' import { EntityModelType } from '@companion-app/shared/Model/EntityModel.js' -import type { SomeCompanionInputField } from '@companion-app/shared/Model/Options.js' +import type { ExpressionOrValue, InternalInputFieldList, SomeCompanionInputField } from '@companion-app/shared/Model/Options.js' import type { SomeButtonGraphicsElement } from '@companion-app/shared/Model/StyleLayersModel.js' import { Accordion } from '~/Components/Accordion.js' import { Form } from '~/Components/Form.js' +import { ListInputField } from '~/Components/ListInputField.js' import type { LocalVariablesStore } from '~/Controls/LocalVariablesStore.js' import { getInputFeatures, OptionsInputControl } from '~/Controls/OptionsInputField.js' +import { trpc, useMutationExt } from '~/Resources/TRPC.js' import { PreventDefaultHandler } from '~/Resources/util.js' import { RootAppStoreContext } from '~/Stores/RootAppStore.js' import { ElementCommonProperties } from './ElementCommonProperties.js' @@ -151,6 +153,16 @@ const SchemaFieldWrapper = observer(function SchemaFieldWrapper({ }) { const features = getInputFeatures(field) + if (field.type === 'internal:list') { + return ( + + ) + } + return ( ) }) + +const ListSchemaFieldWrapper = observer(function ListSchemaFieldWrapper({ + field, + elementProps, + localVariablesStore, +}: { + field: InternalInputFieldList + elementProps: SomeButtonGraphicsElement + localVariablesStore: LocalVariablesStore +}) { + const { controlId } = useElementPropertiesContext() + const updateOptionMutation = useMutationExt(trpc.controls.styles.updateOption.mutationOptions()) + + const rawValue = (elementProps as unknown as Record)[field.id] as + | ExpressionOrValue + | undefined + const value = rawValue?.isExpression ? undefined : (rawValue?.value as Record[] | undefined) + + const setValue = useCallback( + (newValue: Record[]) => { + updateOptionMutation + .mutateAsync({ controlId, elementId: elementProps.id, key: field.id, value: { isExpression: false, value: newValue } }) + .catch((e) => console.error('Failed to update element', e)) + }, + [updateOptionMutation, controlId, elementProps.id, field.id] + ) + + return ( + + ) +}) diff --git a/webui/src/Components/ListInputField.stories.tsx b/webui/src/Components/ListInputField.stories.tsx new file mode 100644 index 0000000000..01bae1d9d7 --- /dev/null +++ b/webui/src/Components/ListInputField.stories.tsx @@ -0,0 +1,138 @@ +import type { Decorator, Meta, StoryObj } from '@storybook/react' +import { useState } from 'react' +import type { ExpressionOrValue } from '@companion-app/shared/Model/Options.js' +import type { InternalInputFieldList } from '@companion-app/shared/Model/Options.js' +import type { JsonValue } from 'type-fest' +import { MenuPortalContext } from './MenuPortalContext.js' +import { ListInputField } from './ListInputField.js' + +const withPortal: Decorator = (Story) => ( + +

+ +
+ +) + +const gaugeThresholdDefinition: InternalInputFieldList = { + id: 'thresholds', + type: 'internal:list', + label: 'Colour thresholds', + tooltip: 'Define colour stops for the gauge.', + addLabel: 'Add threshold', + fields: [ + { id: 'value', type: 'number', label: 'Value', min: 0, max: 100, step: 1, default: 0 }, + { id: 'color', type: 'colorpicker', label: 'Colour', default: 0x00ff00, enableAlpha: false, returnType: 'number' }, + ], + default: [ + { value: 0, color: 0x00ff00 }, + { value: 66, color: 0xffff00 }, + { value: 85, color: 0xff0000 }, + ], +} + +function exprVal(v: T): ExpressionOrValue { + return { isExpression: false, value: v } +} + +function StatefulList({ + definition, + initialValue, + disabled, + fieldSupportsExpression, +}: { + definition: InternalInputFieldList + initialValue: Record>[] + disabled?: boolean + fieldSupportsExpression?: boolean +}): React.JSX.Element { + const [value, setValue] = useState(initialValue) + return ( + <> + +
+
{JSON.stringify(value, null, 2)}
+
+ + ) +} + +const meta = { + title: 'Components/ListInputField', + decorators: [withPortal], + render: (args) => , +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + definition: gaugeThresholdDefinition, + initialValue: [ + { value: exprVal(0), color: exprVal(0x00ff00) }, + { value: exprVal(66), color: exprVal(0xffff00) }, + { value: exprVal(85), color: exprVal(0xff0000) }, + ], + }, +} + +export const WithExpressions: Story = { + args: { + definition: gaugeThresholdDefinition, + initialValue: [ + { value: exprVal(0), color: exprVal(0x00ff00) }, + { value: { isExpression: true, value: '$(internal:custom_var)' }, color: exprVal(0xffff00) }, + ], + fieldSupportsExpression: true, + }, +} + +export const Empty: Story = { + args: { + definition: gaugeThresholdDefinition, + initialValue: [], + }, +} + +export const Disabled: Story = { + args: { + definition: gaugeThresholdDefinition, + initialValue: [ + { value: exprVal(0), color: exprVal(0x00ff00) }, + { value: exprVal(66), color: exprVal(0xffff00) }, + ], + disabled: true, + }, +} + +export const TextColumn: Story = { + args: { + definition: { + id: 'labels', + type: 'internal:list', + label: 'Labels', + addLabel: 'Add label', + fields: [ + { id: 'value', type: 'number', label: 'Position', min: 0, max: 100, step: 1, default: 0 }, + { id: 'label', type: 'textinput', label: 'Text', default: '' }, + ], + default: [], + } satisfies InternalInputFieldList, + initialValue: [ + { value: exprVal(0), label: exprVal('Low') }, + { value: exprVal(50), label: exprVal('Mid') }, + { value: exprVal(100), label: exprVal('High') }, + ], + fieldSupportsExpression: true, + }, +} diff --git a/webui/src/Components/ListInputField.tsx b/webui/src/Components/ListInputField.tsx new file mode 100644 index 0000000000..ca12ec4a1d --- /dev/null +++ b/webui/src/Components/ListInputField.tsx @@ -0,0 +1,252 @@ +import { faArrowDown, faArrowUp, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import classNames from 'classnames' +import { observer } from 'mobx-react-lite' +import { Fragment, useCallback, useId } from 'react' +import type { JsonValue } from 'type-fest' +import type { EntityModelType } from '@companion-app/shared/Model/EntityModel.js' +import { + isExpressionOrValue, + type ExpressionOrValue, + type InternalInputFieldList, + type SomeCompanionInputField, +} from '@companion-app/shared/Model/Options.js' +import type { LocalVariablesStore } from '~/Controls/LocalVariablesStore.js' +import { Button } from './Button.js' +import { ColorInputField } from './ColorInputField.js' +import { FieldOrExpression } from './FieldOrExpression.js' +import { FormLabel } from './Form.js' +import { Grid } from './Grid.js' +import { InlineHelpIcon } from './InlineHelp.js' +import { NumberInputField } from './NumberInputField.js' +import { TextInputFieldSimple } from './TextInputField.js' + +function fieldDefault(field: SomeCompanionInputField): JsonValue { + if ('default' in field && field.default !== undefined) return field.default as JsonValue + return null +} + +function newRow(fields: SomeCompanionInputField[]): Record> { + const row: Record> = {} + for (const field of fields) row[field.id] = { isExpression: false, value: fieldDefault(field) } + return row +} + +function normaliseCell(raw: JsonValue | undefined): ExpressionOrValue { + if (isExpressionOrValue(raw)) return raw + return { isExpression: false, value: raw } +} + +interface ListCellProps { + field: SomeCompanionInputField + value: JsonValue | undefined + setValue: (v: JsonValue) => void + disabled?: boolean + inputId: string +} + +function ListCell({ field, value, setValue, disabled, inputId }: ListCellProps): React.JSX.Element { + switch (field.type) { + case 'number': + return ( + + ) + case 'colorpicker': + return ( + + id={inputId} + value={(value as number | undefined) ?? 0} + setValue={setValue} + enableAlpha={field.enableAlpha ?? false} + returnType={field.returnType ?? 'number'} + disabled={disabled} + /> + ) + case 'textinput': + return ( + + ) + default: + return Unsupported field type + } +} + +export interface ListInputFieldProps { + definition: InternalInputFieldList + value: Record>[] | undefined + setValue: (rows: Record>[]) => void + disabled?: boolean + localVariablesStore: LocalVariablesStore | null + entityType: EntityModelType | null + isLocatedInGrid: boolean + fieldSupportsExpression: boolean + visibility?: boolean +} + +export const ListInputField = observer(function ListInputField({ + definition, + value, + setValue, + disabled, + localVariablesStore, + entityType, + isLocatedInGrid, + fieldSupportsExpression, + visibility = true, +}: ListInputFieldProps): React.JSX.Element { + const baseId = useId() + const rows = value ?? [] + + const addRow = useCallback(() => { + setValue([...rows, newRow(definition.fields)]) + }, [rows, definition.fields, setValue]) + + const removeRow = useCallback( + (rowIndex: number) => { + setValue(rows.filter((_, i) => i !== rowIndex)) + }, + [rows, setValue] + ) + + const moveRow = useCallback( + (rowIndex: number, direction: -1 | 1) => { + const next = rowIndex + direction + if (next < 0 || next >= rows.length) return + const updated = [...rows] + ;[updated[rowIndex], updated[next]] = [updated[next], updated[rowIndex]] + setValue(updated) + }, + [rows, setValue] + ) + + const updateCell = useCallback( + (rowIndex: number, fieldId: string, cellValue: ExpressionOrValue) => { + setValue( + rows.map((row, i) => + i === rowIndex ? { ...row, [fieldId]: cellValue as ExpressionOrValue } : row + ) + ) + }, + [rows, setValue] + ) + + const hidden = !visibility + + return ( + <> + + + + {rows.map((row, rowIndex) => ( + + + + + {definition.fields.map((field) => { + const cellRaw = row[field.id] + const cell = normaliseCell(cellRaw as JsonValue | undefined) + const inputId = `${baseId}-${rowIndex}-${field.id}` + const canExpression = fieldSupportsExpression && !field.disableAutoExpression + + const setCell = (newCell: ExpressionOrValue) => + updateCell(rowIndex, field.id, newCell) + const setCellValue = (v: JsonValue) => setCell({ isExpression: false, value: v }) + + const input = ( + + ) + + return ( + + + + + ) + })} + + ))} + + ) +}) diff --git a/webui/src/Controls/OptionsInputField.tsx b/webui/src/Controls/OptionsInputField.tsx index 646969ff85..d899d74c30 100644 --- a/webui/src/Controls/OptionsInputField.tsx +++ b/webui/src/Controls/OptionsInputField.tsx @@ -24,6 +24,7 @@ import { InlineHelpCustom, InlineHelpIcon } from '~/Components/InlineHelp.js' import { MultiDropdownInputField } from '~/Components/MultiDropdownInputField.js' import { NumberInputField } from '~/Components/NumberInputField.js' import { SwitchInputField } from '~/Components/SwitchInputField.js' +import { ListInputField } from '~/Components/ListInputField.js' import { TableInputField } from '~/Components/TableInputField.js' import { TextInputField } from '~/Components/TextInputField.js' import { InternalCustomVariableDropdown, InternalModuleField } from './InternalModuleField.js' @@ -88,6 +89,22 @@ export const OptionsInputField = observer(function OptionsInputField({ const inputId = useId() + if (option.type === 'internal:list') { + return ( + [] | undefined} + setValue={(val) => setValue(option.id, { isExpression: false, value: val as any })} + disabled={!!readonly} + localVariablesStore={localVariablesStore} + entityType={entityType} + isLocatedInGrid={isLocatedInGrid} + fieldSupportsExpression={fieldSupportsExpression && !option.disableAutoExpression} + visibility={visibility} + /> + ) + } + let control = ( Date: Sun, 7 Jun 2026 14:28:28 +0100 Subject: [PATCH 11/30] wip: list polish --- .../ControlTypes/Button/LayerDefaults.ts | 2 +- .../lib/Graphics/ConvertGraphicsElements.ts | 14 ++++++------ .../lib/Graphics/ElementPropertiesSchemas.ts | 15 +++++++------ shared-lib/lib/Graphics/LayeredRenderer.ts | 2 +- shared-lib/lib/Model/Options.ts | 1 + shared-lib/lib/Model/StyleLayersModel.ts | 4 ++-- tools/generate_graphics_types.mts | 1 + .../ElementPropertiesEditor.tsx | 19 ++++++++++------ .../src/Components/ListInputField.stories.tsx | 8 +++---- webui/src/Components/ListInputField.tsx | 22 ++++++++++--------- webui/src/Controls/OptionsInputField.tsx | 4 ++-- 11 files changed, 51 insertions(+), 41 deletions(-) diff --git a/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts b/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts index ba190924b0..579f256444 100644 --- a/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts +++ b/companion/lib/Controls/ControlTypes/Button/LayerDefaults.ts @@ -138,7 +138,7 @@ export function CreateElementOfType(type: SomeButtonGraphicsElement['type']): So roundedEnds: { value: true, isExpression: false }, thickness: { value: 20, isExpression: false }, multiSegment: { value: true, isExpression: false }, - thresholds: { + segments: { value: [ { value: { value: 0, isExpression: false }, color: { value: 0x00ff00, isExpression: false } }, { value: { value: 66, isExpression: false }, color: { value: 0xffff00, isExpression: false } }, diff --git a/companion/lib/Graphics/ConvertGraphicsElements.ts b/companion/lib/Graphics/ConvertGraphicsElements.ts index 1617ce80ed..d92318549b 100644 --- a/companion/lib/Graphics/ConvertGraphicsElements.ts +++ b/companion/lib/Graphics/ConvertGraphicsElements.ts @@ -822,9 +822,9 @@ function convertGaugeElementForDrawing( const orientation = helper.getTolerantEnum('orientation', GAUGE_ORIENTATION_CHOICES, 'horizontal') const inactiveStyle = helper.getTolerantEnum('inactiveStyle', GAUGE_INACTIVE_STYLE_CHOICES, 'transparent') - const thresholdsRaw = (element.thresholds as ExpressionOrValue).value + const segmentsRaw = (element.segments as ExpressionOrValue).value // Resolve a cell that may be a plain JsonValue (old internal:table) or ExpressionOrValue (internal:list) - const resolveThresholdCell = (raw: unknown): number => { + const resolveSegmentCell = (raw: unknown): number => { if (isExpressionOrValue(raw)) { if (raw.isExpression) { const r = helper.executeExpressionAndTrackVariables(raw.value, 'number') @@ -834,10 +834,10 @@ function convertGaugeElementForDrawing( } return Number(raw ?? 0) } - const thresholds: ButtonGraphicsGaugeDrawElement['thresholds'] = Array.isArray(thresholdsRaw) - ? thresholdsRaw.map((row) => ({ - value: Math.max(0, Math.min(100, resolveThresholdCell((row as any)?.value))), - color: resolveThresholdCell((row as any)?.color), + const thresholds: ButtonGraphicsGaugeDrawElement['segments'] = Array.isArray(segmentsRaw) + ? segmentsRaw.map((row) => ({ + value: Math.max(0, Math.min(100, resolveSegmentCell((row as any)?.value))), + color: resolveSegmentCell((row as any)?.color), })) : [] @@ -855,7 +855,7 @@ function convertGaugeElementForDrawing( roundedEnds: helper.getBoolean('roundedEnds', true), thickness: Math.max(1, Math.min(50, helper.getNumber('thickness', 20))), multiSegment: helper.getBoolean('multiSegment', true), - thresholds, + segments: thresholds, inactiveStyle, inactiveAmount: helper.getNumber('inactiveAmount', 70), contentHash: '', diff --git a/shared-lib/lib/Graphics/ElementPropertiesSchemas.ts b/shared-lib/lib/Graphics/ElementPropertiesSchemas.ts index 0f89d787b3..57686a6535 100644 --- a/shared-lib/lib/Graphics/ElementPropertiesSchemas.ts +++ b/shared-lib/lib/Graphics/ElementPropertiesSchemas.ts @@ -542,24 +542,25 @@ export const gaugeElementSchema: ElementSchemaSection[] = [ ], }, { - id: 'thresholds', - label: 'Colour Thresholds', + id: 'segments', + label: 'Colour Segments', fields: [ { type: 'checkbox', id: 'multiSegment', label: 'Multi-segment colours', tooltip: - 'When enabled, each colour threshold segment is visible in the filled portion. When disabled, only the active threshold colour is used for the entire filled area.', + 'When enabled, each colour segment is visible in the filled portion. When disabled, only the active segment colour is used for the entire filled area.', default: true, }, { type: 'internal:list', - id: 'thresholds', - label: 'Thresholds', + id: 'segments', + label: 'Segments', tooltip: - 'Define colour stops for the gauge. Each threshold specifies the value (0-100) at which that colour starts.', - addLabel: 'Add threshold', + 'Define colour stops for the gauge. Each segment specifies the value (0-100) at which that colour starts.', + addLabel: 'Add segment', + minItems: 1, fields: [ { id: 'value', type: 'number', label: 'Value', min: 0, max: 100, step: 1, default: 0 }, { diff --git a/shared-lib/lib/Graphics/LayeredRenderer.ts b/shared-lib/lib/Graphics/LayeredRenderer.ts index a428d5ce9d..cfd74b683d 100644 --- a/shared-lib/lib/Graphics/LayeredRenderer.ts +++ b/shared-lib/lib/Graphics/LayeredRenderer.ts @@ -463,7 +463,7 @@ export class GraphicsLayeredButtonRenderer { const drawBounds = parentBounds.compose(element.x, element.y, element.width, element.height) if (skipDraw) return drawBounds - const sorted = [...element.thresholds].sort((a, b) => Number(a.value) - Number(b.value)) + const sorted = [...element.segments].sort((a, b) => Number(a.value) - Number(b.value)) if (sorted.length === 0) return drawBounds const { x, y, width, height, maxX, maxY } = drawBounds diff --git a/shared-lib/lib/Model/Options.ts b/shared-lib/lib/Model/Options.ts index 6480902c64..08bfaa1511 100644 --- a/shared-lib/lib/Model/Options.ts +++ b/shared-lib/lib/Model/Options.ts @@ -182,6 +182,7 @@ export interface InternalInputFieldList extends CompanionInputFieldBaseExtended type: 'internal:list' fields: SomeCompanionInputField[] addLabel?: string + minItems?: number default: Record[] } diff --git a/shared-lib/lib/Model/StyleLayersModel.ts b/shared-lib/lib/Model/StyleLayersModel.ts index cef2e5cd78..7297860484 100644 --- a/shared-lib/lib/Model/StyleLayersModel.ts +++ b/shared-lib/lib/Model/StyleLayersModel.ts @@ -221,7 +221,7 @@ export interface ButtonGraphicsGaugeDrawElement roundedEnds: boolean thickness: number multiSegment: boolean - thresholds: Record[] + segments: Record[] inactiveStyle: 'transparent' | 'dimmed' inactiveAmount: number } @@ -235,7 +235,7 @@ export interface ButtonGraphicsGaugeElement roundedEnds: ExpressionOrValue thickness: ExpressionOrValue multiSegment: ExpressionOrValue - thresholds: ExpressionOrValue[]> + segments: ExpressionOrValue[]> inactiveStyle: ExpressionOrValue<'transparent' | 'dimmed'> inactiveAmount: ExpressionOrValue } diff --git a/tools/generate_graphics_types.mts b/tools/generate_graphics_types.mts index d2993ba7e6..9c042d2760 100644 --- a/tools/generate_graphics_types.mts +++ b/tools/generate_graphics_types.mts @@ -49,6 +49,7 @@ function convertFieldType(field: SomeCompanionInputField, isExpressionable: bool tsType = 'VerticalAlignment' break case 'internal:table': + case 'internal:list': tsType = 'Record[]' break default: diff --git a/webui/src/Buttons/EditButton/LayeredButtonEditor/ElementPropertiesEditor.tsx b/webui/src/Buttons/EditButton/LayeredButtonEditor/ElementPropertiesEditor.tsx index 77810f84f8..97a02c0e53 100644 --- a/webui/src/Buttons/EditButton/LayeredButtonEditor/ElementPropertiesEditor.tsx +++ b/webui/src/Buttons/EditButton/LayeredButtonEditor/ElementPropertiesEditor.tsx @@ -3,7 +3,11 @@ import { useCallback, useContext } from 'react' import type { JsonValue } from 'type-fest' import { elementSchemas, elementSimpleModeFields } from '@companion-app/shared/Graphics/ElementPropertiesSchemas.js' import { EntityModelType } from '@companion-app/shared/Model/EntityModel.js' -import type { ExpressionOrValue, InternalInputFieldList, SomeCompanionInputField } from '@companion-app/shared/Model/Options.js' +import type { + ExpressionOrValue, + InternalInputFieldList, + SomeCompanionInputField, +} from '@companion-app/shared/Model/Options.js' import type { SomeButtonGraphicsElement } from '@companion-app/shared/Model/StyleLayersModel.js' import { Accordion } from '~/Components/Accordion.js' import { Form } from '~/Components/Form.js' @@ -155,11 +159,7 @@ const SchemaFieldWrapper = observer(function SchemaFieldWrapper({ if (field.type === 'internal:list') { return ( - + ) } @@ -210,7 +210,12 @@ const ListSchemaFieldWrapper = observer(function ListSchemaFieldWrapper({ const setValue = useCallback( (newValue: Record[]) => { updateOptionMutation - .mutateAsync({ controlId, elementId: elementProps.id, key: field.id, value: { isExpression: false, value: newValue } }) + .mutateAsync({ + controlId, + elementId: elementProps.id, + key: field.id, + value: { isExpression: false, value: newValue }, + }) .catch((e) => console.error('Failed to update element', e)) }, [updateOptionMutation, controlId, elementProps.id, field.id] diff --git a/webui/src/Components/ListInputField.stories.tsx b/webui/src/Components/ListInputField.stories.tsx index 01bae1d9d7..9084d6a5bd 100644 --- a/webui/src/Components/ListInputField.stories.tsx +++ b/webui/src/Components/ListInputField.stories.tsx @@ -1,10 +1,10 @@ import type { Decorator, Meta, StoryObj } from '@storybook/react' import { useState } from 'react' -import type { ExpressionOrValue } from '@companion-app/shared/Model/Options.js' -import type { InternalInputFieldList } from '@companion-app/shared/Model/Options.js' import type { JsonValue } from 'type-fest' -import { MenuPortalContext } from './MenuPortalContext.js' +import type { ExpressionOrValue, InternalInputFieldList } from '@companion-app/shared/Model/Options.js' +import { withMockStore } from '../../.storybook/mockRootAppStore.js' import { ListInputField } from './ListInputField.js' +import { MenuPortalContext } from './MenuPortalContext.js' const withPortal: Decorator = (Story) => ( @@ -68,7 +68,7 @@ function StatefulList({ const meta = { title: 'Components/ListInputField', - decorators: [withPortal], + decorators: [withMockStore, withPortal], render: (args) => , } satisfies Meta diff --git a/webui/src/Components/ListInputField.tsx b/webui/src/Components/ListInputField.tsx index ca12ec4a1d..8db55fba2e 100644 --- a/webui/src/Components/ListInputField.tsx +++ b/webui/src/Components/ListInputField.tsx @@ -2,7 +2,7 @@ import { faArrowDown, faArrowUp, faPlus, faTrash } from '@fortawesome/free-solid import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import classNames from 'classnames' import { observer } from 'mobx-react-lite' -import { Fragment, useCallback, useId } from 'react' +import { Fragment, useCallback, useId, useMemo } from 'react' import type { JsonValue } from 'type-fest' import type { EntityModelType } from '@companion-app/shared/Model/EntityModel.js' import { @@ -22,7 +22,7 @@ import { NumberInputField } from './NumberInputField.js' import { TextInputFieldSimple } from './TextInputField.js' function fieldDefault(field: SomeCompanionInputField): JsonValue { - if ('default' in field && field.default !== undefined) return field.default as JsonValue + if ('default' in field && field.default !== undefined) return field.default return null } @@ -108,7 +108,7 @@ export const ListInputField = observer(function ListInputField({ visibility = true, }: ListInputFieldProps): React.JSX.Element { const baseId = useId() - const rows = value ?? [] + const rows = useMemo(() => value ?? [], [value]) const addRow = useCallback(() => { setValue([...rows, newRow(definition.fields)]) @@ -135,19 +135,21 @@ export const ListInputField = observer(function ListInputField({ const updateCell = useCallback( (rowIndex: number, fieldId: string, cellValue: ExpressionOrValue) => { setValue( - rows.map((row, i) => - i === rowIndex ? { ...row, [fieldId]: cellValue as ExpressionOrValue } : row - ) + rows.map((row, i) => (i === rowIndex ? { ...row, [fieldId]: cellValue as ExpressionOrValue } : row)) ) }, [rows, setValue] ) const hidden = !visibility + const atMinimum = definition.minItems !== undefined && rows.length <= definition.minItems return ( <> -