Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic/advanced mode transfer function editor #294

Merged
merged 47 commits into from
Jul 18, 2024
Merged

Conversation

frasercl
Copy link
Contributor

@frasercl frasercl commented Jul 11, 2024

Review time: medium-large (~45min)

Resolves #80 and potentially #131: implements the final piece of @lynwilhelm's channel settings editor redesign by adding a "basic" mode to the transfer function editor, in which only a min and max of a ramp may be set, and hiding the current control points editor as "advanced" mode.

  • Adds two new fields to ViewerState: ramp: [number, number] for the min and max of the ramp set by "basic" mode, and useControlPoints: boolean to indicate whether to set the transfer function based on ramp or the existing controlPoints field.
  • Adds an "advanced" checkbox to the transfer function editor, which controls useControlPoints.
  • In TfEditor's new "basic" mode:
    • Control points are replaced by two purple sliders for the min and max
    • The horizontal axis labels get pushed down out of the way of the min slider
    • Two additional numeric inputs appear for the min and max
  • When a time or region change causes the transfer function to be remapped, the viewer will also remap whichever state is not currently being used to set the transfer function (controlPoints in "basic" mode, ramp in "advanced" mode) so that it doesn't get out of sync.

Screenshots

image
image

frasercl added 30 commits June 21, 2024 12:05
@frasercl frasercl requested a review from a team as a code owner July 11, 2024 21:15
@frasercl frasercl requested review from toloudis and ShrimpCryptid and removed request for a team July 11, 2024 21:15
}

// TODO: this creates a redundant Uint8Array and algorithmically fills it twice. Can we avoid this?
const remapLut = new Lut().createFromControlPoints(controlPoints);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can just call remapControlPoints which is an exported function out of Lut.ts in the volume-viewer, without constructing this temporary remapLut. We could export it at the top level of the volume-viewer package, or make it some kind of class method on Lut...

Copy link
Contributor Author

@frasercl frasercl Jul 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not currently exported from volume-viewer in a form we can use, but will be once allen-cell-animated/vole-core#227 merges.

const { controlPoints, ramp, useControlPoints } = channelState;
useImageEffect(
(currentImage) => {
if (useControlPoints && controlPoints.length < 2) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this if condition checking for? is it for before any channel data has arrived, or something like that? I see it was there in the previous implementation too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this triggers only when the user drags a control point all the way to the left, then all the way to the right, thereby deleting all but one point. The effect is that the transfer function is left in its last valid state rather than getting zeroed out when there's only one control point. Does this behavior make sense?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess? It kinda sounds like something that could be handled inside the tfeditor but at least I get the scenario where it could happen.

@@ -128,6 +134,8 @@ const DEFAULT_CHANNEL_STATE: ChannelState = {
isovalue: 128,
opacity: 1.0,
color: [226, 205, 179] as ColorArray,
useControlPoints: false,
ramp: [0, TFEDITOR_MAX_BIN],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this would make anything cleaner but maybe we could have the control points default to the equivalent of [[0, 0], [TFEDITOR_MAX_BIN, 1]]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, rather than a bin index, we probably should think of the "x" domain of the transfer function (aka LUT) as intensities. It's a function of input intensity to output intensity.

If that incurs a crazy amount of changes then we should save it for a different PR and possibly wait for the full dynamic range modifications. ( also related to putting intensity numbers along the x axis in the UI.)

const onOpacityChange = ([newValue]: number[]): void => _onOpacityChange(newValue / ISOSURFACE_OPACITY_SLIDER_MAX);
const onIsovalueChange = ([newValue]: number[]): void => changeSettingForThisChannel("isovalue", newValue);
const onOpacityChange = ([newValue]: number[]): void =>
changeSettingForThisChannel("opacity", newValue / ISOSURFACE_OPACITY_SLIDER_MAX);
Copy link
Contributor

@toloudis toloudis Jul 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this replace all the other usages of props.changeChannelSetting in this component, like the ones above for volumeEnabled and isosurfaceEnabled?

// | |
// \ /
// *
// width: 0.65 * height; height of rectangle: 0.6 * height; height of triangle: 0.4 * height
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ love this -- but semi-wondering if a svg does any better here.

Copy link
Contributor Author

@frasercl frasercl Jul 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an svg! The context works like a canvas, but D3 turns it into an svg data string.

const dragRampSlider = (handle: TfEditorRampSliderHandle, x: number): void => {
if (handle === TfEditorRampSliderHandle.Min) {
const max = rampRef.current[1];
setRamp([Math.min(x, max), max]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you drag the min handle past max and vice versa?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope - this line ensures that each slider stops at the other slider's position rather than dragging past it.

setSelectedPointIdx(draggedPointIdxRef.current);
} else if (idxRight > draggedIdx + 1) {
newControlPoints.splice(draggedIdx + 1, idxRight - draggedIdx - 1);
if (typeof draggedPointIdxRef.current === "number") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the other type that's not a "number"? should you just check props.useControlPoints here instead? or maybe draggedPointIdxRef.current as number vs draggedPointIdxRef.current as minmaxsliderenumthingy in the lines below - for readability

Copy link
Contributor Author

@frasercl frasercl Jul 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that props.useControlPoints would make more semantic sense here. But it also wouldn't cause TypeScript to narrow this type for the following lines, where draggedPointIdxRef.current is passed either to a function that takes a number or to one that takes a TfEditorRampSliderHandle.

@frasercl
Copy link
Contributor Author

The above 3 commits additionally resolve #281. The fix is quick, and that area of code is changed enough by this PR that fixing on main and merging would have been way more work.

Comment on lines 76 to 80
if (controlPoints.length < 3) {
return [0, TFEDITOR_MAX_BIN];
}
return [controlPoints[1].x, controlPoints[2].x];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it mean if there are only two (or fewer) control points? Is one control point always at (0, 0.0), and the other at (255, 1.0)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took this callout as an opportunity to make this function more robust (I knew I wasn't quite dotting all my i's the first time around) but yeah: all the lut generator functions in volume-viewer add those two points at the ends.

Copy link
Contributor

@ShrimpCryptid ShrimpCryptid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few questions but otherwise LGTM!

controlPointsRef.current = props.controlPoints;
}

if (typeof draggedPointIdxRef.current !== "string") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would this be a string value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can either be a number, representing a control point, in "advanced" mode; or either "min" or "max" representing a slider handle in basic mode. See my response to @toloudis, above:

...props.useControlPoints would make more semantic sense here. But it also wouldn't cause TypeScript to narrow this type for the following lines...

Since this has now tripped up two people, it might need a comment

@toloudis
Copy link
Contributor

The above 3 commits additionally resolve #281. The fix is quick, and that area of code is changed enough by this PR that fixing on main and merging would have been way more work.

For future, the way to do it with a pr is with a branch off of this branch, and a PR that uses this branch as its base. Then you just have to be careful not to merge the new pr before the older one.

@frasercl
Copy link
Contributor Author

For future, the way to do it with a pr is with a branch off of this branch, and a PR that uses this branch as its base. Then you just have to be careful not to merge the new pr before the older one.

This occurred to me mere moments after pushing the commits onto this branch 🙃

@frasercl frasercl merged commit ca27665 into main Jul 18, 2024
3 checks passed
@frasercl frasercl deleted the feature/basic-tf-editor branch July 18, 2024 00:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Tighten up design of channel settings
3 participants