@dxos/react-ui-canvas 0.7.5-main.499c70c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +8 -0
- package/README.md +3 -0
- package/dist/lib/browser/index.mjs +525 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/node/index.cjs +559 -0
- package/dist/lib/node/index.cjs.map +7 -0
- package/dist/lib/node/meta.json +1 -0
- package/dist/lib/node-esm/index.mjs +527 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/types/src/components/Canvas/Canvas.d.ts +15 -0
- package/dist/types/src/components/Canvas/Canvas.d.ts.map +1 -0
- package/dist/types/src/components/Canvas/Canvas.stories.d.ts +8 -0
- package/dist/types/src/components/Canvas/Canvas.stories.d.ts.map +1 -0
- package/dist/types/src/components/Canvas/index.d.ts +2 -0
- package/dist/types/src/components/Canvas/index.d.ts.map +1 -0
- package/dist/types/src/components/FPS.d.ts +9 -0
- package/dist/types/src/components/FPS.d.ts.map +1 -0
- package/dist/types/src/components/Grid/Grid.d.ts +18 -0
- package/dist/types/src/components/Grid/Grid.d.ts.map +1 -0
- package/dist/types/src/components/Grid/Grid.stories.d.ts +8 -0
- package/dist/types/src/components/Grid/Grid.stories.d.ts.map +1 -0
- package/dist/types/src/components/Grid/index.d.ts +2 -0
- package/dist/types/src/components/Grid/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +4 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +4 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/projection.d.ts +58 -0
- package/dist/types/src/hooks/projection.d.ts.map +1 -0
- package/dist/types/src/hooks/useProjection.d.ts +12 -0
- package/dist/types/src/hooks/useProjection.d.ts.map +1 -0
- package/dist/types/src/hooks/useWheel.d.ts +7 -0
- package/dist/types/src/hooks/useWheel.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +5 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +20 -0
- package/dist/types/src/types.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +3 -0
- package/dist/types/src/util/index.d.ts.map +1 -0
- package/dist/types/src/util/svg.d.ts +33 -0
- package/dist/types/src/util/svg.d.ts.map +1 -0
- package/dist/types/src/util/svg.stories.d.ts +6 -0
- package/dist/types/src/util/svg.stories.d.ts.map +1 -0
- package/dist/types/src/util/util.d.ts +16 -0
- package/dist/types/src/util/util.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +61 -0
- package/src/components/Canvas/Canvas.stories.tsx +82 -0
- package/src/components/Canvas/Canvas.tsx +83 -0
- package/src/components/Canvas/index.ts +5 -0
- package/src/components/FPS.tsx +98 -0
- package/src/components/Grid/Grid.stories.tsx +43 -0
- package/src/components/Grid/Grid.tsx +61 -0
- package/src/components/Grid/index.ts +5 -0
- package/src/components/index.ts +7 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/projection.tsx +149 -0
- package/src/hooks/useProjection.tsx +28 -0
- package/src/hooks/useWheel.tsx +55 -0
- package/src/index.ts +8 -0
- package/src/types.ts +13 -0
- package/src/util/index.ts +6 -0
- package/src/util/svg.stories.tsx +45 -0
- package/src/util/svg.tsx +131 -0
- package/src/util/util.ts +39 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxos-theme';
|
|
6
|
+
|
|
7
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
8
|
+
import React, { useRef, useState } from 'react';
|
|
9
|
+
|
|
10
|
+
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
11
|
+
|
|
12
|
+
import { Grid, type GridProps } from './Grid';
|
|
13
|
+
import { type ProjectionState } from '../../hooks';
|
|
14
|
+
import { useWheel } from '../../hooks';
|
|
15
|
+
|
|
16
|
+
const Render = (props: GridProps) => {
|
|
17
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
18
|
+
const [{ scale, offset }, setProjection] = useState<ProjectionState>({ scale: 1, offset: { x: 0, y: 0 } });
|
|
19
|
+
useWheel(ref.current, setProjection);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div ref={ref} className='grow'>
|
|
23
|
+
<Grid scale={scale} offset={offset} {...props} />
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const meta: Meta<GridProps> = {
|
|
29
|
+
title: 'ui/react-ui-canvas/Grid',
|
|
30
|
+
component: Grid,
|
|
31
|
+
render: Render,
|
|
32
|
+
decorators: [withTheme, withLayout({ fullscreen: true })],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default meta;
|
|
36
|
+
|
|
37
|
+
type Story = StoryObj<GridProps>;
|
|
38
|
+
|
|
39
|
+
export const Default: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
size: 16,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { forwardRef, useMemo } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
8
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
9
|
+
|
|
10
|
+
import { type Point } from '../../types';
|
|
11
|
+
import { GridPattern, testId } from '../../util';
|
|
12
|
+
|
|
13
|
+
const gridRatios = [1 / 4, 1, 4, 16];
|
|
14
|
+
|
|
15
|
+
const defaultOffset: Point = { x: 0, y: 0 };
|
|
16
|
+
|
|
17
|
+
const createId = (parent: string, grid: number) => `dx-canvas-grid-${parent}-${grid}`;
|
|
18
|
+
|
|
19
|
+
export type GridProps = ThemedClassName<{ id: string; size: number; scale?: number; offset?: Point }>;
|
|
20
|
+
|
|
21
|
+
export const Grid = forwardRef<SVGSVGElement, GridProps>(
|
|
22
|
+
({ id: parentId, size: gridSize, scale = 1, offset = defaultOffset, classNames }, forwardedRef) => {
|
|
23
|
+
const grids = useMemo(
|
|
24
|
+
() =>
|
|
25
|
+
gridRatios
|
|
26
|
+
.map((ratio) => ({ id: ratio, size: ratio * gridSize * scale }))
|
|
27
|
+
.filter(({ size }) => size >= gridSize && size <= 256),
|
|
28
|
+
[gridSize, scale],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<svg
|
|
33
|
+
{...testId('dx-canvas-grid')}
|
|
34
|
+
ref={forwardedRef}
|
|
35
|
+
className={mx(
|
|
36
|
+
'absolute inset-0 w-full h-full pointer-events-none touch-none select-none',
|
|
37
|
+
'stroke-neutral-500',
|
|
38
|
+
classNames,
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
{/* NOTE: The pattern is offset so that the middle of the pattern aligns with the grid. */}
|
|
42
|
+
<defs>
|
|
43
|
+
{grids.map(({ id, size }) => (
|
|
44
|
+
<GridPattern key={id} id={createId(parentId, id)} offset={offset} size={size} />
|
|
45
|
+
))}
|
|
46
|
+
</defs>
|
|
47
|
+
<g>
|
|
48
|
+
{grids.map(({ id }, i) => (
|
|
49
|
+
<rect
|
|
50
|
+
key={id}
|
|
51
|
+
opacity={0.1 + i * 0.05}
|
|
52
|
+
fill={`url(#${createId(parentId, id)})`}
|
|
53
|
+
width='100%'
|
|
54
|
+
height='100%'
|
|
55
|
+
/>
|
|
56
|
+
))}
|
|
57
|
+
</g>
|
|
58
|
+
</svg>
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as d3 from 'd3';
|
|
6
|
+
import {
|
|
7
|
+
type Matrix,
|
|
8
|
+
applyToPoints,
|
|
9
|
+
compose,
|
|
10
|
+
inverse,
|
|
11
|
+
translate as translateMatrix,
|
|
12
|
+
identity,
|
|
13
|
+
scale as scaleMatrix,
|
|
14
|
+
} from 'transformation-matrix';
|
|
15
|
+
|
|
16
|
+
import { type Point, type Dimension } from '../types';
|
|
17
|
+
|
|
18
|
+
export const defaultOffset: Point = { x: 0, y: 0 };
|
|
19
|
+
|
|
20
|
+
// TODO(burdon): Rotation also?
|
|
21
|
+
export type ProjectionState = {
|
|
22
|
+
scale: number;
|
|
23
|
+
offset: Point;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// TODO(burdon): Tradeoff between stable ProjectionMapping object that can be used with live values within a closure,
|
|
27
|
+
// vs. a reactive object that can trigger updates?
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Maps between screen and model coordinates.
|
|
31
|
+
*/
|
|
32
|
+
export interface Projection {
|
|
33
|
+
get bounds(): Dimension;
|
|
34
|
+
get scale(): number;
|
|
35
|
+
get offset(): Point;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Maps the model space to the screen offset (from the top-left of the element).
|
|
39
|
+
*/
|
|
40
|
+
toScreen(points: Point[]): Point[];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Maps the pointer coordinate (from the top-left of the element) to the model space.
|
|
44
|
+
*/
|
|
45
|
+
toModel(points: Point[]): Point[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class ProjectionMapper implements Projection {
|
|
49
|
+
private _bounds: Dimension = { width: 0, height: 0 };
|
|
50
|
+
private _scale: number = 1;
|
|
51
|
+
private _offset: Point = defaultOffset;
|
|
52
|
+
private _toScreen: Matrix = identity();
|
|
53
|
+
private _toModel: Matrix = identity();
|
|
54
|
+
|
|
55
|
+
constructor(bounds?: Dimension, scale?: number, offset?: Point) {
|
|
56
|
+
if (bounds && scale && offset) {
|
|
57
|
+
this.update(bounds, scale, offset);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
update(bounds: Dimension, scale: number, offset: Point) {
|
|
62
|
+
this._bounds = bounds;
|
|
63
|
+
this._scale = scale;
|
|
64
|
+
this._offset = offset;
|
|
65
|
+
this._toScreen = compose(translateMatrix(this._offset.x, this._offset.y), scaleMatrix(this._scale));
|
|
66
|
+
this._toModel = inverse(this._toScreen);
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get bounds() {
|
|
71
|
+
return this._bounds;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get scale() {
|
|
75
|
+
return this._scale;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get offset() {
|
|
79
|
+
return this._offset;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
toScreen(points: Point[]): Point[] {
|
|
83
|
+
return applyToPoints(this._toScreen, points);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
toModel(points: Point[]): Point[] {
|
|
87
|
+
return applyToPoints(this._toModel, points);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Maintain position while zooming.
|
|
93
|
+
*/
|
|
94
|
+
export const getZoomTransform = ({
|
|
95
|
+
scale,
|
|
96
|
+
offset,
|
|
97
|
+
pos,
|
|
98
|
+
newScale,
|
|
99
|
+
}: ProjectionState & { pos: Point; newScale: number }): ProjectionState => {
|
|
100
|
+
return {
|
|
101
|
+
scale: newScale,
|
|
102
|
+
offset: {
|
|
103
|
+
x: pos.x - (pos.x - offset.x) * (newScale / scale),
|
|
104
|
+
y: pos.y - (pos.y - offset.y) * (newScale / scale),
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Zoom while keeping the specified position in place.
|
|
111
|
+
*/
|
|
112
|
+
// TODO(burdon): Convert to object.
|
|
113
|
+
export const zoomInPlace = (
|
|
114
|
+
setTransform: (state: ProjectionState) => void,
|
|
115
|
+
pos: Point,
|
|
116
|
+
offset: Point,
|
|
117
|
+
current: number,
|
|
118
|
+
next: number,
|
|
119
|
+
delay = 200,
|
|
120
|
+
) => {
|
|
121
|
+
const is = d3.interpolate(current, next);
|
|
122
|
+
d3.transition()
|
|
123
|
+
.ease(d3.easeSinOut)
|
|
124
|
+
.duration(delay)
|
|
125
|
+
.tween('zoom', () => (t) => {
|
|
126
|
+
const newScale = is(t);
|
|
127
|
+
setTransform(getZoomTransform({ scale: current, newScale, offset, pos }));
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Zoom to new scale and position.
|
|
133
|
+
*/
|
|
134
|
+
// TODO(burdon): Convert to object.
|
|
135
|
+
export const zoomTo = (
|
|
136
|
+
setTransform: (state: ProjectionState) => void,
|
|
137
|
+
current: ProjectionState,
|
|
138
|
+
next: ProjectionState,
|
|
139
|
+
delay = 200,
|
|
140
|
+
) => {
|
|
141
|
+
const is = d3.interpolateObject({ scale: current.scale, ...current.offset }, { scale: next.scale, ...next.offset });
|
|
142
|
+
d3.transition()
|
|
143
|
+
.ease(d3.easeSinOut)
|
|
144
|
+
.duration(delay)
|
|
145
|
+
.tween('zoom', () => (t) => {
|
|
146
|
+
const { scale, x, y } = is(t);
|
|
147
|
+
setTransform({ scale, offset: { x, y } });
|
|
148
|
+
});
|
|
149
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type CSSProperties, type Dispatch, type SetStateAction, createContext, useContext } from 'react';
|
|
6
|
+
|
|
7
|
+
import { raise } from '@dxos/debug';
|
|
8
|
+
|
|
9
|
+
import { type Projection, type ProjectionState } from './projection';
|
|
10
|
+
|
|
11
|
+
export type CanvasContext = ProjectionState & {
|
|
12
|
+
root: HTMLDivElement;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
styles: CSSProperties;
|
|
16
|
+
projection: Projection;
|
|
17
|
+
setProjection: Dispatch<SetStateAction<ProjectionState>>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
// TODO(burdon): Use radix?
|
|
24
|
+
export const CanvasContext = createContext<CanvasContext | null>(null);
|
|
25
|
+
|
|
26
|
+
export const useProjection = (): CanvasContext => {
|
|
27
|
+
return useContext(CanvasContext) ?? raise(new Error('Missing CanvasContext'));
|
|
28
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { bindAll } from 'bind-event-listener';
|
|
6
|
+
import { type Dispatch, type SetStateAction, useEffect } from 'react';
|
|
7
|
+
|
|
8
|
+
import { getZoomTransform, type ProjectionState } from './projection';
|
|
9
|
+
import { getRelativePoint } from '../util';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Handle wheel events to update the transform state (zoom and offset).
|
|
13
|
+
*/
|
|
14
|
+
export const useWheel = (el: HTMLDivElement | null, setProjection: Dispatch<SetStateAction<ProjectionState>>) => {
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!el) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return bindAll(el, [
|
|
21
|
+
{
|
|
22
|
+
type: 'wheel',
|
|
23
|
+
options: { capture: true, passive: false },
|
|
24
|
+
listener: (ev: WheelEvent) => {
|
|
25
|
+
ev.preventDefault();
|
|
26
|
+
|
|
27
|
+
// Zoom or pan.
|
|
28
|
+
if (ev.ctrlKey) {
|
|
29
|
+
if (!el) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Keep centered while zooming.
|
|
34
|
+
setProjection(({ scale, offset }) => {
|
|
35
|
+
const pos = getRelativePoint(el, ev);
|
|
36
|
+
const scaleSensitivity = 0.01;
|
|
37
|
+
const newScale = scale * Math.exp(-ev.deltaY * scaleSensitivity);
|
|
38
|
+
return getZoomTransform({ scale, offset, newScale, pos });
|
|
39
|
+
});
|
|
40
|
+
} else {
|
|
41
|
+
setProjection(({ scale, offset: { x, y } }) => {
|
|
42
|
+
return {
|
|
43
|
+
scale,
|
|
44
|
+
offset: {
|
|
45
|
+
x: x - ev.deltaX,
|
|
46
|
+
y: y - ev.deltaY,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
}, [el, setProjection]);
|
|
55
|
+
};
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { S } from '@dxos/effect';
|
|
6
|
+
|
|
7
|
+
export const Point = S.Struct({ x: S.Number, y: S.Number });
|
|
8
|
+
export const Dimension = S.Struct({ width: S.Number, height: S.Number });
|
|
9
|
+
export const Rect = S.extend(Point, Dimension);
|
|
10
|
+
|
|
11
|
+
export type Point = S.Schema.Type<typeof Point>;
|
|
12
|
+
export type Dimension = S.Schema.Type<typeof Dimension>;
|
|
13
|
+
export type Rect = S.Schema.Type<typeof Rect>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxos-theme';
|
|
6
|
+
|
|
7
|
+
import type { Meta } from '@storybook/react';
|
|
8
|
+
import React from 'react';
|
|
9
|
+
|
|
10
|
+
import { withTheme } from '@dxos/storybook-utils';
|
|
11
|
+
|
|
12
|
+
import { Arrow, createPath } from './svg';
|
|
13
|
+
import { testId } from './util';
|
|
14
|
+
|
|
15
|
+
const Render = () => (
|
|
16
|
+
<svg className='border border-neutral-500 w-[400px] h-[400px]'>
|
|
17
|
+
<defs>
|
|
18
|
+
<Arrow id='arrow-start' classNames='fill-none stroke-red-500' dir='start' />
|
|
19
|
+
<Arrow id='arrow-end' classNames='fill-none stroke-red-500' dir='end' />
|
|
20
|
+
</defs>
|
|
21
|
+
<path
|
|
22
|
+
{...testId('dx-storybook', true)}
|
|
23
|
+
className={'stroke-red-500'}
|
|
24
|
+
d={createPath([
|
|
25
|
+
{ x: 100, y: 300 },
|
|
26
|
+
{ x: 300, y: 100 },
|
|
27
|
+
])}
|
|
28
|
+
markerStart={'url(#arrow-start)'}
|
|
29
|
+
markerEnd={'url(#arrow-end)'}
|
|
30
|
+
/>
|
|
31
|
+
</svg>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const meta: Meta = {
|
|
35
|
+
title: 'ui/react-ui-canvas/svg',
|
|
36
|
+
render: Render,
|
|
37
|
+
decorators: [withTheme],
|
|
38
|
+
parameters: {
|
|
39
|
+
layout: 'centered',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default meta;
|
|
44
|
+
|
|
45
|
+
export const Default = {};
|
package/src/util/svg.tsx
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type PropsWithChildren, type SVGProps } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
8
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
9
|
+
|
|
10
|
+
import { type Dimension, type Point } from '../types';
|
|
11
|
+
|
|
12
|
+
// Refs
|
|
13
|
+
// - https://airbnb.io/visx/gallery
|
|
14
|
+
// - https://github.com/tldraw/tldraw/blob/main/packages/editor/src/lib/primitives/Vec.ts
|
|
15
|
+
|
|
16
|
+
export const createPath = (points: Point[], join = false) => {
|
|
17
|
+
return ['M', points.map(({ x, y }) => `${x},${y}`).join(' L '), join ? 'Z' : ''].join(' ');
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths
|
|
22
|
+
* NOTE: Leave space around shape for line width.
|
|
23
|
+
*/
|
|
24
|
+
export const Markers = ({ id = 'dx-marker', classNames }: ThemedClassName<{ id?: string }>) => {
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<Arrow id={`${id}-arrow-start`} dir='start' classNames={classNames} />
|
|
28
|
+
<Arrow id={`${id}-arrow-end`} dir='end' classNames={classNames} />
|
|
29
|
+
<Arrow id={`${id}-triangle-start`} dir='start' closed classNames={classNames} />
|
|
30
|
+
<Arrow id={`${id}-triangle-end`} dir='end' closed classNames={classNames} />
|
|
31
|
+
<Marker id={`${id}-circle`} pos={{ x: 6, y: 6 }} size={{ width: 12, height: 12 }}>
|
|
32
|
+
<circle cx={6} cy={6} r={5} stroke={'context-stroke'} className={mx(classNames)} />
|
|
33
|
+
</Marker>
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type MarkerProps = SVGProps<SVGMarkerElement> &
|
|
39
|
+
PropsWithChildren<
|
|
40
|
+
ThemedClassName<{
|
|
41
|
+
id: string;
|
|
42
|
+
pos: Point;
|
|
43
|
+
size: Dimension;
|
|
44
|
+
fill?: boolean;
|
|
45
|
+
}>
|
|
46
|
+
>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* https://www.w3.org/TR/SVG2/painting.html#Markers
|
|
50
|
+
*/
|
|
51
|
+
export const Marker = ({
|
|
52
|
+
id,
|
|
53
|
+
className,
|
|
54
|
+
children,
|
|
55
|
+
pos: { x: refX, y: refY },
|
|
56
|
+
size: { width: markerWidth, height: markerHeight },
|
|
57
|
+
fill,
|
|
58
|
+
...rest
|
|
59
|
+
}: MarkerProps) => (
|
|
60
|
+
<marker
|
|
61
|
+
id={id}
|
|
62
|
+
className={className}
|
|
63
|
+
{...{
|
|
64
|
+
refX,
|
|
65
|
+
refY,
|
|
66
|
+
markerWidth,
|
|
67
|
+
markerHeight,
|
|
68
|
+
markerUnits: 'strokeWidth',
|
|
69
|
+
orient: 'auto',
|
|
70
|
+
...rest,
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
</marker>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
export const Arrow = ({
|
|
78
|
+
classNames,
|
|
79
|
+
id,
|
|
80
|
+
size = 16,
|
|
81
|
+
dir = 'end',
|
|
82
|
+
closed = false,
|
|
83
|
+
}: ThemedClassName<{ id: string; size?: number; dir?: 'start' | 'end'; closed?: boolean }>) => (
|
|
84
|
+
<Marker
|
|
85
|
+
id={id}
|
|
86
|
+
size={{ width: size, height: size }}
|
|
87
|
+
pos={dir === 'end' ? { x: size, y: size / 2 } : { x: 0, y: size / 2 }}
|
|
88
|
+
>
|
|
89
|
+
<path
|
|
90
|
+
fill={closed ? undefined : 'none'}
|
|
91
|
+
stroke={'context-stroke'}
|
|
92
|
+
className={mx(classNames)}
|
|
93
|
+
d={createPath(
|
|
94
|
+
dir === 'end'
|
|
95
|
+
? [
|
|
96
|
+
{ x: 1, y: 1 },
|
|
97
|
+
{ x: size, y: size / 2 },
|
|
98
|
+
{ x: 1, y: size - 1 },
|
|
99
|
+
]
|
|
100
|
+
: [
|
|
101
|
+
{ x: size - 1, y: 1 },
|
|
102
|
+
{ x: 0, y: size / 2 },
|
|
103
|
+
{ x: size - 1, y: size - 1 },
|
|
104
|
+
],
|
|
105
|
+
closed,
|
|
106
|
+
)}
|
|
107
|
+
/>
|
|
108
|
+
</Marker>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
export const GridPattern = ({
|
|
112
|
+
classNames,
|
|
113
|
+
id,
|
|
114
|
+
size,
|
|
115
|
+
offset,
|
|
116
|
+
}: ThemedClassName<{ id: string; size: number; offset: Point }>) => (
|
|
117
|
+
<pattern
|
|
118
|
+
id={id}
|
|
119
|
+
x={(size / 2 + offset.x) % size}
|
|
120
|
+
y={(size / 2 + offset.y) % size}
|
|
121
|
+
width={size}
|
|
122
|
+
height={size}
|
|
123
|
+
patternUnits='userSpaceOnUse'
|
|
124
|
+
>
|
|
125
|
+
{/* TODO(burdon): vars. */}
|
|
126
|
+
<g className={mx(classNames)}>
|
|
127
|
+
<line x1={0} y1={size / 2} x2={size} y2={size / 2} />
|
|
128
|
+
<line x1={size / 2} y1={0} x2={size / 2} y2={size} />
|
|
129
|
+
</g>
|
|
130
|
+
</pattern>
|
|
131
|
+
);
|
package/src/util/util.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
let logged = false;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get the relative point of the cursor.
|
|
9
|
+
* NOTE: ev.offset returns the position relative to the target.
|
|
10
|
+
*/
|
|
11
|
+
export const getRelativePoint = (el: HTMLElement, ev: MouseEvent) => {
|
|
12
|
+
const rect = el.getBoundingClientRect();
|
|
13
|
+
return { x: ev.clientX - rect.x, y: ev.clientY - rect.top };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
*
|
|
18
|
+
*/
|
|
19
|
+
// TODO(burdon): Factor out.
|
|
20
|
+
export const testId = <ID = string>(id: ID, inspect = false) => {
|
|
21
|
+
if (inspect) {
|
|
22
|
+
if (!logged) {
|
|
23
|
+
// eslint-disable-next-line no-console
|
|
24
|
+
console.log('Open storybook in expanded window;\nthen run INSPECT()');
|
|
25
|
+
logged = true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
(window as any).INSPECT = () => {
|
|
29
|
+
const el = document.querySelector(`[data-test-id="${id}"]`);
|
|
30
|
+
(window as any).inspect(el);
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.log(el);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { [DATA_TEST_ID]: id };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const DATA_TEST_ID = 'data-test-id';
|