-
Notifications
You must be signed in to change notification settings - Fork 4
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
Conversation
} | ||
|
||
// TODO: this creates a redundant Uint8Array and algorithmically fills it twice. Can we avoid this? | ||
const remapLut = new Lut().createFromControlPoints(controlPoints); |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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], |
There was a problem hiding this comment.
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]]
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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") { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
.
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. |
if (controlPoints.length < 3) { | ||
return [0, TFEDITOR_MAX_BIN]; | ||
} | ||
return [controlPoints[1].x, controlPoints[2].x]; | ||
} |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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.
There was a problem hiding this 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") { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
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 🙃 |
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.
ViewerState
:ramp: [number, number]
for the min and max of the ramp set by "basic" mode, anduseControlPoints: boolean
to indicate whether to set the transfer function based onramp
or the existingcontrolPoints
field.useControlPoints
.TfEditor
's new "basic" mode:controlPoints
in "basic" mode,ramp
in "advanced" mode) so that it doesn't get out of sync.Screenshots