Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/162.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix scalar variables not applying after re-selecting a part colored by a vector component
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,11 @@ export class AggregateVariableInfo {

for (let i = 0; i < actorNodes.length; i++) {
const node = actorNodes[i];
const variable = node.variableCollection.getVariable(variableInfo.id);
let thisComponentId: number | null = node.variableComponent;

if (variable != null) {
if (variable.getRangeInfo(node.variableComponent) == null) {
// Default to the first component option.
thisComponentId = variable.componentOptions[0].id;
}
}
const thisComponentId = AggregateVariableInfo.#resolveNodeComponentId(
node,
variableInfo,
obj.#componentOptions
);

if (i === 0) {
obj.#displayComponentId = thisComponentId;
Expand All @@ -117,6 +113,37 @@ export class AggregateVariableInfo {
return obj;
}

/**
* The node's component when it names an option of this variable and has a range, else the variable's first option.
*
* A node holds one component, belonging to the variable it is coloured by,
* and it is read here for every variable in its collection. A component
* with a range but no option (component 0 of a one-component variable,
* whose only option is the magnitude) is therefore replaced too.
*
* @param node - Scene node whose configured component is read.
* @param variableInfo - Variable the component is resolved for.
* @param componentOptions - The variable's options, keyed by component ID string.
* @returns The component ID to display for this node.
*/
static #resolveNodeComponentId(
node: VisorSceneNodeExtended,
variableInfo: VisorVariableInfo,
componentOptions: ReadonlyMap<string, AggregateVariableComponentInfo>
): number | null {
const componentId: number | null = node.variableComponent;
const variable = node.variableCollection.getVariable(variableInfo.id);
const firstOptionId = (variable ?? variableInfo).componentOptions[0].id;

if (componentId == null || !componentOptions.has(componentId.toString())) {
return firstOptionId;
}
if (variable != null && variable.getRangeInfo(componentId) == null) {
return firstOptionId;
}
return componentId;
}

/**
* Gets the variable's unique identifier.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ describe('AggregateVariableInfo', () => {
expect(component.displayVariableMax).toBe(5);
});

test('warns when a displayed component has range data but no component option', async () => {
test('falls back to the first option when a displayed component has range data but no component option', async () => {
const warningSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});

const variable = createVariable({
Expand All @@ -280,9 +280,9 @@ describe('AggregateVariableInfo', () => {

const result = selection.currentVariableInfo!;

expect(result.displayComponentId).toBe(999);
expect(result.currentComponentInfo).toBeUndefined();
expect(warningSpy).toHaveBeenCalledWith("invalid componentId: '999'");
expect(result.displayComponentId).toBe(-1);
expect(result.currentComponentInfo).toBe(result.componentOptions.get('-1'));
expect(warningSpy).not.toHaveBeenCalled();
});

test('setDisplayComponentId selects a component by number', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -670,3 +670,73 @@ describe('the range trigger is sent for an edit and never for a delivery', () =>
expect(rangeSends(triggerSender)).toEqual([]);
});
});

/** Sets a panel `<select>` to a value and lets its change handler settle. */
async function chooseOption(select: HTMLSelectElement, value: string): Promise<void> {
select.value = value;
await act(async () => {
fireEvent.change(select);
await new Promise((resolve) => setTimeout(resolve, 0));
});
}

/** The panel `<select>` holding an option with the given value. */
function selectHolding(container: HTMLElement, value: string): HTMLSelectElement {
return container.querySelector(`select option[value="${value}"]`)!
.parentElement as HTMLSelectElement;
}

describe('re-selection carries the node component only where it names an option', () => {
test('a scalar is applied after re-selecting a part coloured by a vector component X', async () => {
const { frontend, renderer } = makeFrontend();
const part = frontend.sceneGraph.descendantActorNodesOrSelfDictionary[PART_A_ID];
frontend.setTreeViewUtil(makeTreeViewUtilDouble([part]));
await frontend.setAppStateAsync(
{
scene: {
variableStates: {
[VELOCITY_ID]: velocityRecord(),
[PRESSURE_ID]: pressureRecord(),
},
},
},
false
);
await part.setColorVariableAsync(VELOCITY_ID, -1);

let container: HTMLElement = null!;
await act(async () => {
container = render(
<Panel_TopRight visorState={frontend} onLoad={() => {}} />
).container;
});
const util = await frontend.panelTopRightUtilPromise;
const warningSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
try {
await chooseOption(selectHolding(container, '0'), '0');
await act(async () => {
await util.refreshSelectionAsync();
});
renderer.sendPartColorVariableAsync.mockClear();

await chooseOption(selectHolding(container, PRESSURE_ID), PRESSURE_ID);

expect(renderer.sendPartColorVariableAsync.mock.calls).toEqual([
[
1,
{
variableId: 'POINT::pressure::1',
variableType: 'POINT',
variableName: 'pressure',
component: -1,
min: 2,
max: 8,
},
],
]);
expect(warningSpy).not.toHaveBeenCalledWith("invalid componentId: '0'");
} finally {
warningSpy.mockRestore();
}
});
});
Loading