Skip to content

Commit

Permalink
Feature/color module (#352)
Browse files Browse the repository at this point in the history
* change color handling module

* organize and cleanup

* update example

* update tests

* fix file name

* name functions more consistently

* remove setGeom

* move renderer out of colorHandler

* remove import

* fix typo

* live modes stop correctly

* fix tests

* add type for colorinfo

* use type for ColorInfo

* remove redundant function

* rename for clarity
  • Loading branch information
meganrm authored Dec 5, 2023
1 parent a6fd9fd commit 572439e
Show file tree
Hide file tree
Showing 15 changed files with 397 additions and 472 deletions.
19 changes: 9 additions & 10 deletions examples/ColorPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,14 @@ const ColorPicker = ({
if (selectedSubagent === "<unmodified>") {
subAgent = [""];
}
const entry: SelectionEntry[] = [
{
name: selectedAgent,
tags: subAgent,
},
];
setColorSelectionInfo([{
agents: entry,
const entry: SelectionEntry = {
name: selectedAgent,
tags: subAgent,
};
setColorSelectionInfo({
agent: entry,
color: selectedColor,
}]);
});
}
};

Expand Down Expand Up @@ -110,7 +108,8 @@ const ColorPicker = ({
type="text"
placeholder="add Hex Color"
onChange={(event) => {
setColorToAppend(event.target.value)}}
setColorToAppend(event.target.value);
}}
></input>
<button onClick={() => addColorToColorArray(colorToAppend)}>
Add color to color array
Expand Down
8 changes: 4 additions & 4 deletions examples/Viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ const initialState: ViewerState = {
selectionStateInfo: {
highlightedAgents: [],
hiddenAgents: [],
colorChanges: [{ agents: [], color: "" }],
colorChange: null,
},
filePending: null,
simulariumFile: null,
Expand Down Expand Up @@ -619,14 +619,14 @@ class Viewer extends React.Component<InputParams, ViewerState> {
this.setState({ agentColors });
};

public setColorSelectionInfo = (colorChanges) => {
public setColorSelectionInfo = (colorChange) => {
this.setState({
...this.state,
selectionStateInfo: {
hiddenAgents: this.state.selectionStateInfo.hiddenAgents,
highlightedAgents:
this.state.selectionStateInfo.highlightedAgents,
colorChanges: colorChanges,
colorChange: colorChange,
},
});
};
Expand All @@ -646,7 +646,7 @@ class Viewer extends React.Component<InputParams, ViewerState> {
<div className="container" style={{ height: "90%", width: "75%" }}>
<select
onChange={(event) => {
simulariumController.stop();
simulariumController.pause();
playbackFile = event.target.value;
this.configureAndLoad();
}}
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export type {
SelectionStateInfo,
UIDisplayData,
VisDataFrame,
ColorChanges,
ColorChange,
SelectionEntry,
TrajectoryFileInfo,
NetConnectionParams,
Expand Down
33 changes: 7 additions & 26 deletions src/simularium/SelectionInterface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { filter, find, uniq } from "lodash";
import { EncodedTypeMapping } from "./types";
import { convertColorNumberToString } from "../visGeometry/color-utils";
import { convertColorNumberToString } from "../visGeometry/ColorHandler";

// An individual entry parsed from an encoded name
// The encoded names can be just a name or a name plus a
Expand All @@ -17,15 +17,15 @@ export interface SelectionEntry {
tags: string[];
}

export interface ColorChanges {
agents: SelectionEntry[];
export interface ColorChange {
agent: SelectionEntry;
color: string;
}

export interface SelectionStateInfo {
highlightedAgents: SelectionEntry[];
hiddenAgents: SelectionEntry[];
colorChanges: ColorChanges[];
colorChange: ColorChange | null;
}

interface DisplayStateEntry {
Expand Down Expand Up @@ -284,29 +284,10 @@ class SelectionInterface {
});
}

public updateAgentColors(
agentIds: number[],
colorChanges: ColorChanges,
uiDisplayData: UIDisplayData
): void {
colorChanges.agents.forEach((agentToUpdate) => {
for (const group of uiDisplayData) {
if (group.name === agentToUpdate.name) {
this.updateUiDataColor(
group.name,
agentIds,
colorChanges.color
);
break;
}
}
});
}

public setInitialAgentColors(
uiDisplayData: UIDisplayData,
colors: (string | number)[],
setColorForIds: (ids: number[], colorIndex: number) => void
setColorForIds: (ids: number[], color: string | number) => void
): (string | number)[] {
let defaultColorIndex = 0;
uiDisplayData.forEach((group) => {
Expand All @@ -322,7 +303,7 @@ class SelectionInterface {
if (!hasNewColors) {
// if no colors have been set by the user for this name,
// just give all states of this agent name the same color
setColorForIds(ids, defaultColorIndex);
setColorForIds(ids, colors[defaultColorIndex]);
this.updateUiDataColor(
group.name,
ids,
Expand Down Expand Up @@ -360,7 +341,7 @@ class SelectionInterface {
} else {
groupColorIndex = -1;
}
setColorForIds([ids[index]], agentColorIndex);
setColorForIds([ids[index]], colors[agentColorIndex]);
});
}
if (groupColorIndex > -1) {
Expand Down
2 changes: 1 addition & 1 deletion src/simularium/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type {
SelectionStateInfo,
UIDisplayData,
SelectionEntry,
ColorChanges,
ColorChange,
} from "./SelectionInterface";
export { ErrorLevel, FrontEndError } from "./FrontEndError";
export { NetMessageEnum } from "./WebsocketClient";
Expand Down
101 changes: 101 additions & 0 deletions src/test/ColorHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* NOTE: this file is testing many private
methods and properties of VisGeometry.ts
which we do by using (visGeometry as any)
so we need to disable the eslint rule here
*/

import ColorHandler from "../visGeometry/ColorHandler";
import { convertColorStringToNumber } from "../visGeometry/ColorHandler";
import { Color } from "three";

const initialColorData = ["#000000", "#000001", "#000002", "#000003"];
const colorHandler = new ColorHandler();

describe("ColorHandler", () => {
beforeEach(() => {
(colorHandler as any).updateColorArray(initialColorData);
});
describe("updateColorArray", () => {
test("sets colorsData with 4 values for each color", () => {
expect((colorHandler as any).colorsData).toHaveLength(
initialColorData.length * 4
);
expect((colorHandler as any).numberOfColors).toBe(
initialColorData.length
);
});
});
describe("convertDataColorIndexToId", () => {
test("it returns the index into the shorter, color as string, array", () => {
expect((colorHandler as any).convertDataColorIndexToId(0)).toBe(0);
expect((colorHandler as any).convertDataColorIndexToId(4)).toBe(1);
expect((colorHandler as any).convertDataColorIndexToId(8)).toBe(2);
expect((colorHandler as any).convertDataColorIndexToId(12)).toBe(3);
});
test("if the index is outside the the length of colorsData, it loops to the beginning", () => {
expect((colorHandler as any).convertDataColorIndexToId(16)).toBe(0);
expect((colorHandler as any).convertDataColorIndexToId(20)).toBe(1);
expect((colorHandler as any).convertDataColorIndexToId(24)).toBe(2);
expect((colorHandler as any).convertDataColorIndexToId(28)).toBe(3);
});
test("if the colorDataIndex is not divisible by 4, returns -1", () => {
expect((colorHandler as any).convertDataColorIndexToId(1)).toBe(-1);
expect((colorHandler as any).convertDataColorIndexToId(2)).toBe(-1);
expect((colorHandler as any).convertDataColorIndexToId(3)).toBe(-1);
});
});
describe("getColorDataIndex", () => {
test("it returns the index into the colorData array of an existing color", () => {
for (let i = 0; i < initialColorData.length; i++) {
const colorNumber = convertColorStringToNumber(
initialColorData[i]
);
const newColor = [
((colorNumber & 0x00ff0000) >> 16) / 255.0,
((colorNumber & 0x0000ff00) >> 8) / 255.0,
((colorNumber & 0x000000ff) >> 0) / 255.0,
1.0,
];
expect((colorHandler as any).getColorDataIndex(newColor)).toBe(
i * 4
);
}
});
test("it returns -1 if the color is not found", () => {
const colorNumber = convertColorStringToNumber("#000004");
const newColor = [
((colorNumber & 0x00ff0000) >> 16) / 255.0,
((colorNumber & 0x0000ff00) >> 8) / 255.0,
((colorNumber & 0x000000ff) >> 0) / 255.0,
1.0,
];
expect((colorHandler as any).getColorDataIndex(newColor)).toBe(-1);
});
});

describe("getColorById", () => {
test("it returns the color as a string for a valid colorId", () => {
for (let i = 0; i < initialColorData.length; i++) {
const expectedColor = new Color(initialColorData[i]);
const actualColor = (colorHandler as any).getColorById(i);
expect(actualColor.r).toBeCloseTo(expectedColor.r, 0.0001);
expect(actualColor.g).toBeCloseTo(expectedColor.g, 0.0001);
expect(actualColor.b).toBeCloseTo(expectedColor.b, 0.0001);
}
});
test("it returns the first color for an invalid colorId", () => {
const actualColor = (colorHandler as any).getColorById(-1);

expect(actualColor).toEqual((colorHandler as any).getColorById(0));
});
test("it loops around if the id is out of range", () => {
expect((colorHandler as any).getColorById(4)).toEqual(
(colorHandler as any).getColorById(0)
);
expect((colorHandler as any).getColorById(5)).toEqual(
(colorHandler as any).getColorById(1)
);
});
});
});
53 changes: 14 additions & 39 deletions src/test/SelectionInterface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,7 @@ const idMapping = {
};

const color = "";
const initialColorChanges = [
{
agents: [],
color,
},
];
const initialColorChanges = null;

describe("SelectionInterface module", () => {
describe("decode", () => {
Expand Down Expand Up @@ -237,7 +232,7 @@ describe("SelectionInterface module", () => {
{ name: "D", tags: [] },
],
hiddenAgents: [],
colorChanges: initialColorChanges,
colorChange: initialColorChanges,
};
const ids = si.getHighlightedIds(selectionStateHighlight);
const allAs = [0, 1, 2, 3];
Expand All @@ -257,7 +252,7 @@ describe("SelectionInterface module", () => {
{ name: "D", tags: [""] },
],
hiddenAgents: [],
colorChanges: initialColorChanges,
colorChange: initialColorChanges,
};
const ids = si.getHighlightedIds(selectionStateHighlight);

Expand All @@ -274,7 +269,7 @@ describe("SelectionInterface module", () => {
{ name: "E", tags: ["t1000"] },
],
hiddenAgents: [],
colorChanges: initialColorChanges,
colorChange: initialColorChanges,
};
const ids = si.getHighlightedIds(selectionStateHighlight);

Expand All @@ -287,7 +282,7 @@ describe("SelectionInterface module", () => {
const selectionStateHighlight = {
highlightedAgents: [{ name: "E", tags: [""] }],
hiddenAgents: [],
colorChanges: initialColorChanges,
colorChange: initialColorChanges,
};
const ids = si.getHighlightedIds(selectionStateHighlight);

Expand All @@ -305,7 +300,7 @@ describe("SelectionInterface module", () => {
{ name: "A", tags: [] },
{ name: "C", tags: [] },
],
colorChanges: initialColorChanges,
colorChange: initialColorChanges,
};
const ids = si.getHiddenIds(selectionStateHide);

Expand All @@ -321,7 +316,7 @@ describe("SelectionInterface module", () => {
{ name: "A", tags: ["t1", "t2"] },
{ name: "B", tags: ["t1"] },
],
colorChanges: initialColorChanges,
colorChange: initialColorChanges,
};
const ids = si.getHiddenIds(selectionStateHide);
expect(ids).toEqual([1, 2, 3, 5, 7]);
Expand All @@ -337,7 +332,7 @@ describe("SelectionInterface module", () => {
{ name: "A", tags: [""] },
{ name: "C", tags: ["", "t1", "t2"] },
],
colorChanges: initialColorChanges,
colorChange: initialColorChanges,
};
const ids = si.getHiddenIds(selectionStateHide);

Expand Down Expand Up @@ -419,25 +414,6 @@ describe("SelectionInterface module", () => {
});
});

describe("updateAgentColors", () => {
test("it will update the entries with a new color", () => {
const agentIds = [0];
const newColor = "#111111";
const colorChanges = {
agents: [{ name: "A", tags: [] }],
color: newColor,
};
const si = new SelectionInterface();
si.parse(idMapping);
const uiDisplayData = si.getUIDisplayData();
const oldColors = si.getColorsForName("A");
expect(oldColors).not.toContain(newColor);
si.updateAgentColors(agentIds, colorChanges, uiDisplayData);
const colors = si.getColorsForName("A");
expect(colors).toContain(newColor);
});
});

describe("setInitialAgentColors", () => {
const defaultColor = "#0";
const defaultColorListLength = 6;
Expand Down Expand Up @@ -539,11 +515,11 @@ describe("SelectionInterface module", () => {
expect(uiDisplayDataForE?.color).toEqual("");
si.setInitialAgentColors(uiDisplayData, colorList, setColorForIds);
expect(uiDisplayDataForE?.color).toEqual("#00");
expect(setColorForIds).toHaveBeenCalledWith([13], 0);
expect(setColorForIds).toHaveBeenCalledWith([13], "#00");
});
test("If no user colors are provided all the ids for an entry will get a default color", () => {
si.setInitialAgentColors(uiDisplayData, colorList, setColorForIds);
expect(setColorForIds).toHaveBeenCalledWith([13], 0);
expect(setColorForIds).toHaveBeenCalledWith([13], "#00");
});
test("if all the colors are the same, the parent entry will also get that color, even if no unmodified color set", () => {
si.setInitialAgentColors(uiDisplayData, colorList, setColorForIds);
Expand All @@ -556,12 +532,11 @@ describe("SelectionInterface module", () => {
test("If user colors are provided each id will be set with the new color", () => {
si.setInitialAgentColors(uiDisplayData, colorList, setColorForIds);
// the first new user color will be appended to the end of the list
const indexOfColorForA = defaultColorListLength;
// these are all the agent A ids, each should get the first new color assigned
expect(setColorForIds).toHaveBeenCalledWith([0], indexOfColorForA);
expect(setColorForIds).toHaveBeenCalledWith([1], indexOfColorForA);
expect(setColorForIds).toHaveBeenCalledWith([2], indexOfColorForA);
expect(setColorForIds).toHaveBeenCalledWith([3], indexOfColorForA);
expect(setColorForIds).toHaveBeenCalledWith([0], agentColors.A);
expect(setColorForIds).toHaveBeenCalledWith([1], agentColors.A);
expect(setColorForIds).toHaveBeenCalledWith([2], agentColors.A);
expect(setColorForIds).toHaveBeenCalledWith([3], agentColors.A);
});
});
});
Loading

0 comments on commit 572439e

Please sign in to comment.