Skip to content

Commit

Permalink
Support any number of points in SelectionTool
Browse files Browse the repository at this point in the history
This adds a capability to define selections that need different number of
points:
  1. Can specify minimum number of points
  2. Can specify maximum number of points, can be -1 for no limit
  3. When number of points is at least minimum
   - set status boolean
   - end selection on enter/return key press
   - pointer down on same position sets last point (within maxMovement)

Also make SVG selections compatible using new type, add SvgPolyline and
use in new story
  • Loading branch information
PeterC-DLS committed Apr 26, 2023
1 parent 1d86771 commit e09002d
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 57 deletions.
94 changes: 90 additions & 4 deletions apps/storybook/src/SelectionTool.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import type { Rect, Selection } from '@h5web/lib';
import type { Points, Rect, Selection } from '@h5web/lib';
import {
Box,
DataToHtml,
Pan,
ResetZoomButton,
SelectionTool,
SvgCircle,
SvgElement,
SvgLine,
SvgPolyline,
SvgRect,
VisCanvas,
Zoom,
} from '@h5web/lib';
import { useThrottledState } from '@react-hookz/web';
import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import { Vector3 } from 'three';

import FillHeight from './decorators/FillHeight';
import { getTitleForSelection } from './utils';
Expand Down Expand Up @@ -110,7 +113,9 @@ export const PersistedDataSelection = {
<SelectionTool
validate={({ html }) => Box.fromPoints(...html).hasMinSize(50)}
onSelectionStart={() => setPersistedDataSelection(undefined)}
onValidSelection={({ data }) => setPersistedDataSelection(data)}
onValidSelection={({ data }) =>
setPersistedDataSelection(data as Rect)
}
>
{({ html: htmlSelection }, _, isValid) => (
<SvgElement>
Expand All @@ -137,6 +142,87 @@ export const PersistedDataSelection = {
},
} satisfies Story;

export const PersistedPolylineSelection = {
args: {
minPoints: 3,
},

render: (args) => {
const [persistedPolylineSelection, setPersistedPolylineSelection] =
useState<Points>();

const { maxPoints } = { ...args };
return (
<VisCanvas
title={getTitleForSelection(persistedPolylineSelection)}
abscissaConfig={{ visDomain: [-10, 0], showGrid: true }}
ordinateConfig={{ visDomain: [50, 100], showGrid: true }}
>
<Pan modifierKey="Control" />
<Zoom />
<ResetZoomButton />

<SelectionTool
{...args}
validate={({ html }) => {
return maxPoints === 1 && html.length === 1
? true
: Box.fromPoints(...html).hasMinSize(50);
}}
onSelectionStart={() => setPersistedPolylineSelection(undefined)}
onValidSelection={({ data }) => setPersistedPolylineSelection(data)}
>
{({ html: htmlSelection }, _, isValid, isComplete) =>
maxPoints === 1 ? (
<SvgElement>
<SvgCircle
coords={[
htmlSelection[0],
htmlSelection[0].clone().add(new Vector3(3)),
]}
fill="teal"
fillOpacity="0.3"
/>
</SvgElement>
) : (
<SvgElement>
<SvgPolyline
coords={htmlSelection}
stroke={isValid ? 'teal' : 'orangered'}
strokeDasharray={isComplete ? '4 0' : '4'}
/>
</SvgElement>
)
}
</SelectionTool>

{persistedPolylineSelection && (
<DataToHtml points={persistedPolylineSelection}>
{(...htmlSelection) =>
maxPoints === 1 ? (
<SvgElement>
<SvgCircle
coords={[
htmlSelection[0],
htmlSelection[0].clone().add(new Vector3(3)),
]}
fill="teal"
fillOpacity="0.3"
/>
</SvgElement>
) : (
<SvgElement>
<SvgPolyline coords={htmlSelection} stroke="teal" />
</SvgElement>
)
}
</DataToHtml>
)}
</VisCanvas>
);
},
} satisfies Story;

export const LineWithLengthValidation = {
render: () => {
const [isValid, setValid] = useThrottledState<boolean | undefined>(
Expand Down Expand Up @@ -199,8 +285,8 @@ export const RectWithTransform = {
box.expandBySize(-box.size.width / 2, 1); // shrink width of selection by two (equally on both side)

const html = box.toRect();
const world = html.map((pt) => htmlToWorld(camera, pt)) as Rect;
const data = world.map(worldToData) as Rect;
const world = html.map((pt) => htmlToWorld(camera, pt));
const data = world.map(worldToData);
return { html, world, data };
}}
>
Expand Down
7 changes: 4 additions & 3 deletions apps/storybook/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type { Rect } from '@h5web/lib';
import type { Points } from '@h5web/lib';
import { format } from 'd3-format';

export const formatCoord = format('.2f');

export function getTitleForSelection(selection: Rect | undefined) {
export function getTitleForSelection(selection: Points | undefined) {
if (!selection) {
return 'No selection';
}

const [start, end] = selection;
const start = selection[0];
const end = selection[selection.length - 1];
return `Selection from (${formatCoord(start.x)}, ${formatCoord(
start.y
)}) to (${formatCoord(end.x)}, ${formatCoord(end.y)})`;
Expand Down
3 changes: 3 additions & 0 deletions packages/lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@ export type { DefaultInteractionsConfig } from './interactions/DefaultInteractio
// SVG
export { default as SvgElement } from './interactions/svg/SvgElement';
export { default as SvgLine } from './interactions/svg/SvgLine';
export { default as SvgPolyline } from './interactions/svg/SvgPolyline';
export { default as SvgRect } from './interactions/svg/SvgRect';
export { default as SvgCircle } from './interactions/svg/SvgCircle';
export type { SvgElementProps } from './interactions/svg/SvgElement';
export type { SvgLineProps } from './interactions/svg/SvgLine';
export type { SvgPolygonProps } from './interactions/svg/SvgPolygon';
export type { SvgRectProps } from './interactions/svg/SvgRect';
export type { SvgCircleProps } from './interactions/svg/SvgCircle';

Expand Down Expand Up @@ -126,6 +128,7 @@ export type {
InteractionInfo,
ModifierKey,
Selection,
Points,
Rect,
CanvasEvent,
CanvasEventCallbacks,
Expand Down
Loading

0 comments on commit e09002d

Please sign in to comment.