Skip to content

Commit

Permalink
feat(itk-view-controls-shoelace): color map swatches in menu
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulHax committed Aug 19, 2024
1 parent 3931696 commit 25321e3
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 102 deletions.
5 changes: 0 additions & 5 deletions packages/element/src/itk-camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ const bindCamera = (
let scale = false;

const onPointerDown = (e: PointerEvent) => {
e.preventDefault();

if (e.button === 0) {
rotate = true;
} else if (e.button === 1) {
Expand All @@ -51,7 +49,6 @@ const bindCamera = (
viewport.addEventListener('pointerdown', onPointerDown);

const onPointerUp = (e: PointerEvent) => {
e.preventDefault();
if (e.button === 0) {
rotate = false;
} else if (e.button === 1) {
Expand Down Expand Up @@ -98,8 +95,6 @@ const bindCamera = (
viewport.addEventListener('pointermove', onPointerMove);

const onWheel = (e: WheelEvent) => {
e.preventDefault();

camera.zoom(ZOOM_SPEED * camera.distance * e.deltaY);
updateView();
};
Expand Down
203 changes: 123 additions & 80 deletions packages/element/src/itk-view-controls-shoelace.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { LitElement, PropertyValues, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { LitElement, PropertyValues, css, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { ViewControls, ViewActor } from './view-controls-controller.js';
import { ref } from 'lit/directives/ref.js';

const spacesToUnderscores = (s: string) => s.replace(/\s/g, '_');

@customElement('itk-view-controls-shoelace')
export class ViewControlsShoelace extends LitElement {
@property({ type: String })
Expand All @@ -13,6 +11,9 @@ export class ViewControlsShoelace extends LitElement {
actor: ViewActor | undefined;
private controls = new ViewControls(this);

@state()
selectedComponent = 0;

setActor(actor: ViewActor) {
this.actor = actor;
this.controls.setActor(actor);
Expand All @@ -38,95 +39,109 @@ export class ViewControlsShoelace extends LitElement {
{ length: scaleCount },
(_, i) => i,
).reverse();
const colorMapOptions = this.controls.colorMapsOptions?.value ?? [];
const colorMapValuesToOptions = Object.fromEntries(
colorMapOptions?.map((option) => [spacesToUnderscores(option), option]) ??
[],
);
const colorMapOptions = this.controls.colorMapsOptions?.value ?? {};

const componentCount = this.controls.componentCount?.value ?? 1;
const showComponentSelector = componentCount > 1;
const components = Array.from({ length: componentCount }, (_, i) => i);
const colorMap =
this.controls.colorMaps?.value[this.controls.selectedComponent] ?? '';
this.controls.colorMaps?.value[this.selectedComponent] ?? '';

const isImage3D = imageDimension >= 3;
const showScale = scaleCount >= 2;
const tfEditorHeight = this.view === '2d' ? '2rem' : '8rem';

return html`
<sl-card>
${isImage3D && this.view === '2d'
? html`
<sl-range
value=${Number(slice)}
@sl-change="${this.controls.onSlice}"
min="0"
max="1"
step=".01"
label="Slice"
style="min-width: 8rem;"
></sl-range>
<sl-radio-group
label="Slice Axis"
value=${axis}
@sl-change="${this.controls.onAxis}"
>
<sl-radio-button value="I">X</sl-radio-button>
<sl-radio-button value="J">Y</sl-radio-button>
<sl-radio-button value="K">Z</sl-radio-button>
</sl-radio-group>
`
: ''}
${showScale
? html`
<sl-radio-group
label="Image Scale"
value=${scale}
@sl-change="${this.controls.onScale}"
>
${scaleOptions.map(
(option) =>
html`<sl-radio-button value=${option}>
${option}
</sl-radio-button>`,
)}
</sl-radio-group>
`
: ''}
${showComponentSelector
? html`
<sl-tab-group
style="max-width: 18rem"
value=${this.controls.selectedComponent}
@sl-tab-show="${(e: CustomEvent) => {
this.controls.onSelectedComponent(Number(e.detail.name));
this.requestUpdate(); // trigger re-render to update color map value
}}"
>
${components.map(
(option) =>
html`<sl-tab panel="${option}" slot="nav">
Component ${option}
</sl-tab>`,
)}
</sl-tab-group>
`
: ''}
<sl-select
style="padding-top: .4rem;"
label="Color Map"
value=${spacesToUnderscores(colorMap)}
@sl-input=${(e: InputEvent) =>
this.controls.onColorMap(
colorMapValuesToOptions[(e.target as HTMLInputElement).value],
)}
${
isImage3D && this.view === '2d'
? html`
<sl-range
value=${Number(slice)}
@sl-change="${this.controls.onSlice}"
min="0"
max="1"
step=".01"
label="Slice"
style="min-width: 8rem;"
></sl-range>
<sl-radio-group
label="Slice Axis"
value=${axis}
@sl-change="${this.controls.onAxis}"
>
<sl-radio-button value="I">X</sl-radio-button>
<sl-radio-button value="J">Y</sl-radio-button>
<sl-radio-button value="K">Z</sl-radio-button>
</sl-radio-group>
`
: ''
}
${
showScale
? html`
<sl-radio-group
label="Image Scale"
value=${scale}
@sl-change="${this.controls.onScale}"
>
${scaleOptions.map(
(option) =>
html`<sl-radio-button value=${option}>
${option}
</sl-radio-button>`,
)}
</sl-radio-group>
`
: ''
}
${
showComponentSelector
? html`
<sl-tab-group
style="max-width: 18rem"
value=${this.controls.selectedComponent}
@sl-tab-show="${(e: CustomEvent) => {
const component = Number(e.detail.name);
this.controls.onSelectedComponent(component);
// trigger re-render to update color map value
this.selectedComponent = Number(e.detail.name);
}}"
>
${components.map(
(option) =>
html`<sl-tab panel="${option}" slot="nav">
Component ${option}
</sl-tab>`,
)}
</sl-tab-group>
`
: ''
}
<sl-dropdown
style="padding-top: .4rem; width: 100%"
>
${Object.entries(colorMapValuesToOptions).map(
([value, readable]) =>
html`<sl-option value=${value}>${readable}</sl-option>`,
)}
</sl-select>
<sl-button slot="trigger" caret class="color-button">
<sl-tooltip content=${colorMap}>
<img src=${colorMapOptions[colorMap] ?? ''} class='selected-swatch'></img>
</sl-tooltip>
</sl-button>
<sl-menu
@sl-select=${(e: CustomEvent) => {
this.controls.onColorMap(e.detail.item.value);
}}
class="color-button"
>
${Object.entries(colorMapOptions).map(
([value, icon]) =>
html`<sl-menu-item value=${value}>
<div class='swatch'> <img src=${icon}></img> ${value} </div>
</sl-menu-item>`,
)}
</sl-menu>
</sl-dropdown>
<div style="padding-top: 0.4rem;">
${this.view === '2d' ? 'Color Range' : 'Opacity and Color'}
</div>
Expand All @@ -137,6 +152,34 @@ export class ViewControlsShoelace extends LitElement {
</sl-card>
`;
}

static styles = css`
.color-button {
width: 100%;
}
.color-button::part(label) {
width: 100%;
padding: 0;
padding-right: var(--sl-spacing-medium);
}
.selected-swatch {
width: 100%;
height: 100%;
}
.swatch {
height: 100%;
display: flex;
}
.swatch img {
width: 8rem;
height: 100%;
padding-right: var(--sl-spacing-small);
}
`;
}

declare global {
Expand Down
11 changes: 7 additions & 4 deletions packages/element/src/view-controls-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,16 @@ export class ViewControls implements ReactiveController {
slice: SelectorController<View2dActor, number> | undefined;
axis: SelectorController<View2dActor, AxisType> | undefined;
imageDimension: SelectorController<View2dActor, number> | undefined;
colorMapsOptions: SelectorController<RenderingActor, string[]> | undefined;
colorMapsOptions:
| SelectorController<RenderingActor, Record<string, string>>
| undefined;
colorMaps: SelectorController<Image, string[]> | undefined;
componentCount: SelectorController<Image, number> | undefined;

selectedComponent = 0;

transferFunctionEditor: TransferFunctionEditor | undefined;
view: '2d' | '3d' = '2d';
selectedComponent = 0;
colorTransferFunctions = new Map<number, ColorTransferFunction>(); // component -> colorTransferFunction

constructor(host: ReactiveControllerHost) {
Expand Down Expand Up @@ -199,11 +202,11 @@ export class ViewControls implements ReactiveController {
this.colorMapsOptions = new SelectorController(
this.host,
renderer,
(state) => state.context.colorMapOptions ?? [],
(state) => state.context.colorMapOptions ?? {},
);
this.rendererSubscription = renderer.on(
'colorTransferFunctionApplied',
this.onColorTransferFunction.bind(this),
this.onColorTransferFunction,
);
}
};
Expand Down
7 changes: 5 additions & 2 deletions packages/viewer/src/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,11 @@ export const image = setup({
actions: [
assign({
colorMaps: ({ context, event }) => {
context.colorMaps[event.component] = event.colorMap;
return context.colorMaps;
return [
...context.colorMaps.slice(0, event.component),
event.colorMap,
...context.colorMaps.slice(event.component + 1),
];
},
}),
],
Expand Down
1 change: 1 addition & 0 deletions packages/vtkjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@itk-viewer/viewer": "workspace:^",
"@kitware/vtk.js": "^30.4.1",
"gl-matrix": "^3.4.3",
"itk-viewer-color-maps": "^1.2.0",
"xstate": "5.17.1"
}
}
6 changes: 3 additions & 3 deletions packages/vtkjs/src/view-2d-vtkjs.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Camera, Pose } from '@itk-viewer/viewer/camera.js';
import { Axis, AxisType } from '@itk-viewer/viewer/slice-utils.js';
import { Image, ImageSnapshot } from '@itk-viewer/viewer/image.js';

import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
import { ColorMapIcons } from 'itk-viewer-color-maps';
import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction';

export type Context = {
Expand All @@ -24,7 +24,7 @@ export type Context = {
sliceIndex?: number;
imageActor?: Image;
imageSubscription?: Subscription;
colorMapOptions: string[];
colorMapOptions: Record<string, string>;
};

export type SetContainerEvent = {
Expand Down Expand Up @@ -96,7 +96,7 @@ export const view2dLogic = setup({
camera: undefined,
axis: Axis.K,
parent,
colorMapOptions: vtkColorMaps.rgbPresetNames,
colorMapOptions: Object.fromEntries(ColorMapIcons.entries()),
};
},
id: 'view2dVtkjs',
Expand Down
6 changes: 3 additions & 3 deletions packages/vtkjs/src/view-2d-vtkjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import vtkImageMapper from '@kitware/vtk.js/Rendering/Core/ImageMapper.js';
import { SlicingMode } from '@kitware/vtk.js/Rendering/Core/ImageMapper/Constants.js';
import vtkImageSlice from '@kitware/vtk.js/Rendering/Core/ImageSlice.js';
import vtkImageProperty from '@kitware/vtk.js/Rendering/Core/ImageProperty.js';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer.js';
import vtkRenderWindow from '@kitware/vtk.js/Rendering/Core/RenderWindow.js';
import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper.js';
import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction.js';
import { getColorMap } from 'itk-viewer-color-maps';

import { Pose, toMat4 } from '@itk-viewer/viewer/camera.js';
import {
Expand All @@ -23,7 +24,6 @@ import {
import { AxisType } from '@itk-viewer/viewer/slice-utils.js';
import { ImageSnapshot } from '@itk-viewer/viewer/image.js';
import { BuiltImage } from '@itk-viewer/io/MultiscaleSpatialImage.js';
import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction.js';

const axisToSliceMode = {
I: SlicingMode.I,
Expand Down Expand Up @@ -218,7 +218,7 @@ const createImplementation = () => {

colorMaps.forEach((colorMap, component) => {
const colorFunc = getRGBTransferFunction(actorProperty, component);
const preset = vtkColorMaps.getPresetByName(colorMap);
const preset = getColorMap(colorMap, component);
if (!preset) throw new Error(`Color map '${colorMap}' not found`);
colorFunc.applyColorMap(preset);
colorFunc.modified(); // applyColorMap does not always trigger modified()
Expand Down
Loading

0 comments on commit 25321e3

Please sign in to comment.