@weasel-js/labkit 1.3.0-pre.0 → 1.3.0
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/dist/_dts/{index-DgcNdEdh.d.ts → types-BttTIent.d.ts} +2 -2
- package/dist/_dts/{types-AHlQxBNN.d.ts → types-DCGEcMO_.d.ts} +1 -1
- package/dist/canvas/index.d.ts +2 -2
- package/dist/canvas/index.js +3 -2
- package/dist/chrome/index.d.ts +3 -3
- package/dist/chrome/index.js +6 -5
- package/dist/chunk-3XXPU73K.js +138 -0
- package/dist/chunk-3XXPU73K.js.map +1 -0
- package/dist/{chunk-PO6L3NSH.js → chunk-6X5RBC6G.js} +17 -17
- package/dist/chunk-6X5RBC6G.js.map +1 -0
- package/dist/{chunk-NS54R52R.js → chunk-BDJXIBRZ.js} +5 -17
- package/dist/chunk-BDJXIBRZ.js.map +1 -0
- package/dist/{chunk-V7PRSIRQ.js → chunk-BMW4TDP5.js} +4 -4
- package/dist/chunk-BMW4TDP5.js.map +1 -0
- package/dist/{chunk-O7NVSCLN.js → chunk-JDROYQM3.js} +8 -8
- package/dist/chunk-JDROYQM3.js.map +1 -0
- package/dist/{chunk-HFDVGF4X.js → chunk-LGKRIUSW.js} +3 -3
- package/dist/{chunk-HFDVGF4X.js.map → chunk-LGKRIUSW.js.map} +1 -1
- package/dist/{chunk-Z6HJ5FEM.js → chunk-QCICIHEO.js} +19 -7
- package/dist/{chunk-Z6HJ5FEM.js.map → chunk-QCICIHEO.js.map} +1 -1
- package/dist/{chunk-RVNN4CHQ.js → chunk-QRYPXSGP.js} +5 -5
- package/dist/{chunk-RVNN4CHQ.js.map → chunk-QRYPXSGP.js.map} +1 -1
- package/dist/chunk-RGT4EJIB.js +6961 -0
- package/dist/chunk-RGT4EJIB.js.map +1 -0
- package/dist/chunk-SBXNQW4G.js +3 -0
- package/dist/chunk-SBXNQW4G.js.map +1 -0
- package/dist/chunk-T66HU3HX.js +17 -0
- package/dist/chunk-T66HU3HX.js.map +1 -0
- package/dist/chunk-WTI26YTM.js +266 -0
- package/dist/chunk-WTI26YTM.js.map +1 -0
- package/dist/controls/index.d.ts +2 -2
- package/dist/controls/index.js +3 -3
- package/dist/dragdrop/index.d.ts +44 -3
- package/dist/dragdrop/index.js +3 -1
- package/dist/index.d.ts +571 -541
- package/dist/index.js +69 -541
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +3 -3
- package/dist/layers/index.js +4 -2
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +15 -2
- package/dist/passthrough/weasel-ui.js +3 -2
- package/dist/primitives/index.js +5 -4
- package/dist/styles.css +1 -703
- package/dist/surface/index.js +2 -2
- package/dist/ui/layers/index.d.ts +8 -2
- package/dist/ui/layers/index.js +2 -4
- package/dist/undo/index.d.ts +2 -2
- package/package.json +5 -5
- package/src/config/builder.test.ts +7 -1
- package/src/config/builder.ts +1 -5
- package/src/config/resolve.test.ts +1 -2
- package/src/controls/ControlPanel.stories.tsx +41 -2
- package/src/controls/ControlPanel.test.tsx +31 -9
- package/src/controls/ControlPanel.tsx +56 -22
- package/src/dragdrop/index.ts +11 -0
- package/src/index.ts +49 -2
- package/src/lab/index.ts +1 -1
- package/src/layers/AGENTS.md +1 -1
- package/src/layers/LayerList.tsx +1 -1
- package/src/passthrough/weasel-ui.ts +22 -17
- package/src/styles.less +0 -4
- package/src/theme/Interstellar.stories.tsx +1 -1
- package/src/trial/Trial.config.test.tsx +3 -8
- package/src/ui/layers/index.ts +5 -1
- package/dist/chunk-NS54R52R.js.map +0 -1
- package/dist/chunk-O2BULFHX.js +0 -152
- package/dist/chunk-O2BULFHX.js.map +0 -1
- package/dist/chunk-O7NVSCLN.js.map +0 -1
- package/dist/chunk-PO6L3NSH.js.map +0 -1
- package/dist/chunk-PQJ5B2U2.js +0 -27
- package/dist/chunk-PQJ5B2U2.js.map +0 -1
- package/dist/chunk-TLGRYALP.js +0 -6151
- package/dist/chunk-TLGRYALP.js.map +0 -1
- package/dist/chunk-V7PRSIRQ.js.map +0 -1
- package/dist/chunk-ZZAYVX4X.js +0 -505
- package/dist/chunk-ZZAYVX4X.js.map +0 -1
- package/src/primitives/DragHandleGlyph.tsx +0 -29
- package/src/ui/format.test.ts +0 -33
- package/src/ui/format.ts +0 -30
- package/src/ui/layers/LayerStack.less +0 -143
- package/src/ui/layers/LayerStack.stories.tsx +0 -45
- package/src/ui/layers/LayerStack.test.tsx +0 -170
- package/src/ui/layers/LayerStack.tsx +0 -208
- package/src/ui/properties/CheckboxRow.stories.tsx +0 -27
- package/src/ui/properties/ColorRow.stories.tsx +0 -106
- package/src/ui/properties/CurveField.less +0 -48
- package/src/ui/properties/CurveField.stories.tsx +0 -20
- package/src/ui/properties/CurveField.test.tsx +0 -127
- package/src/ui/properties/CurveField.tsx +0 -184
- package/src/ui/properties/EffectCard.tsx +0 -324
- package/src/ui/properties/Gallery.stories.tsx +0 -178
- package/src/ui/properties/NumberRow.stories.tsx +0 -65
- package/src/ui/properties/PropertyGroup.less +0 -50
- package/src/ui/properties/PropertyGroup.stories.tsx +0 -34
- package/src/ui/properties/PropertyGroup.test.tsx +0 -33
- package/src/ui/properties/PropertyGroup.tsx +0 -45
- package/src/ui/properties/PropertyList.stories.tsx +0 -83
- package/src/ui/properties/PropertyPanel.less +0 -606
- package/src/ui/properties/PropertyPanel.stories.tsx +0 -190
- package/src/ui/properties/PropertyPanel.test.tsx +0 -193
- package/src/ui/properties/PropertyPanel.tsx +0 -469
- package/src/ui/properties/PropertyRow.stories.tsx +0 -53
- package/src/ui/properties/SelectRow.stories.tsx +0 -58
- package/src/ui/properties/SliderRow.stories.tsx +0 -74
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +0 -398
- package/src/ui/properties/TextRow.stories.tsx +0 -62
- package/src/ui/properties/ToggleRow.stories.tsx +0 -57
- package/src/ui/properties/index.ts +0 -39
- package/src/ui/properties/storyLayouts.tsx +0 -36
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { ColorRow, PropertyList } from './PropertyPanel';
|
|
4
|
-
|
|
5
|
-
const meta: Meta<typeof ColorRow> = {
|
|
6
|
-
title: 'labkit/UI/Properties/Rows/ColorRow',
|
|
7
|
-
component: ColorRow,
|
|
8
|
-
};
|
|
9
|
-
export default meta;
|
|
10
|
-
type Story = StoryObj<typeof ColorRow>;
|
|
11
|
-
|
|
12
|
-
function One({ initial, label }: { initial: string; label: string }) {
|
|
13
|
-
const [value, setValue] = useState(initial);
|
|
14
|
-
return (
|
|
15
|
-
<div style={{ width: 280 }}>
|
|
16
|
-
<ColorRow label={label} value={value} onChange={setValue} />
|
|
17
|
-
</div>
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const Single: Story = {
|
|
22
|
-
render: () => <One initial="#b08adb" label="Fill" />,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
function Pair() {
|
|
26
|
-
const [fill, setFill] = useState('#b08adb');
|
|
27
|
-
const [stroke, setStroke] = useState('#1a1428');
|
|
28
|
-
return (
|
|
29
|
-
<div style={{ width: 320 }}>
|
|
30
|
-
<PropertyList>
|
|
31
|
-
<ColorRow label="Fill" value={fill} onChange={setFill} />
|
|
32
|
-
<ColorRow label="Stroke" value={stroke} onChange={setStroke} />
|
|
33
|
-
</PropertyList>
|
|
34
|
-
</div>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const PairInList: Story = {
|
|
39
|
-
render: () => <Pair />,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
function OneWithAlpha({
|
|
43
|
-
initial,
|
|
44
|
-
initialAlpha,
|
|
45
|
-
label,
|
|
46
|
-
alphaDisabled,
|
|
47
|
-
}: {
|
|
48
|
-
initial: string;
|
|
49
|
-
initialAlpha: number;
|
|
50
|
-
label: string;
|
|
51
|
-
alphaDisabled?: boolean;
|
|
52
|
-
}) {
|
|
53
|
-
const [value, setValue] = useState(initial);
|
|
54
|
-
const [alpha, setAlpha] = useState(initialAlpha);
|
|
55
|
-
return (
|
|
56
|
-
<div style={{ width: 280 }}>
|
|
57
|
-
<ColorRow
|
|
58
|
-
label={label}
|
|
59
|
-
value={value}
|
|
60
|
-
onChange={setValue}
|
|
61
|
-
alpha={alpha}
|
|
62
|
-
onAlphaChange={setAlpha}
|
|
63
|
-
alphaDisabled={alphaDisabled}
|
|
64
|
-
/>
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export const WithAlpha: Story = {
|
|
70
|
-
render: () => <OneWithAlpha initial="#7ec8e3" initialAlpha={0.65} label="Fill" />,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export const AlphaDisabled: Story = {
|
|
74
|
-
render: () => <OneWithAlpha initial="#7ec8e3" initialAlpha={1} label="Fill" alphaDisabled />,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
function AlphaPair() {
|
|
78
|
-
const [fill, setFill] = useState('#b08adb');
|
|
79
|
-
const [fillA, setFillA] = useState(0.8);
|
|
80
|
-
const [stroke, setStroke] = useState('#1a1428');
|
|
81
|
-
const [strokeA, setStrokeA] = useState(0.4);
|
|
82
|
-
return (
|
|
83
|
-
<div style={{ width: 320 }}>
|
|
84
|
-
<PropertyList>
|
|
85
|
-
<ColorRow
|
|
86
|
-
label="Fill"
|
|
87
|
-
value={fill}
|
|
88
|
-
onChange={setFill}
|
|
89
|
-
alpha={fillA}
|
|
90
|
-
onAlphaChange={setFillA}
|
|
91
|
-
/>
|
|
92
|
-
<ColorRow
|
|
93
|
-
label="Stroke"
|
|
94
|
-
value={stroke}
|
|
95
|
-
onChange={setStroke}
|
|
96
|
-
alpha={strokeA}
|
|
97
|
-
onAlphaChange={setStrokeA}
|
|
98
|
-
/>
|
|
99
|
-
</PropertyList>
|
|
100
|
-
</div>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export const AlphaPairInList: Story = {
|
|
105
|
-
render: () => <AlphaPair />,
|
|
106
|
-
};
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
.lk-curve-field {
|
|
2
|
-
display: grid;
|
|
3
|
-
gap: 6px;
|
|
4
|
-
grid-column: 1 / -1;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
.lk-curve-field__plot {
|
|
8
|
-
position: relative;
|
|
9
|
-
width: 100%;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
.lk-curve-field__marks {
|
|
13
|
-
position: absolute;
|
|
14
|
-
inset: 0;
|
|
15
|
-
pointer-events: none;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.lk-curve-field__readouts {
|
|
19
|
-
display: flex;
|
|
20
|
-
justify-content: space-between;
|
|
21
|
-
font-variant-numeric: tabular-nums;
|
|
22
|
-
font-size: var(--wzl-font-size);
|
|
23
|
-
color: var(--wzl-fg-subtle);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.lk-curve-field__readout {
|
|
27
|
-
font-style: normal;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
.lk-curve-field__actions {
|
|
31
|
-
display: flex;
|
|
32
|
-
gap: 6px;
|
|
33
|
-
justify-self: start;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
.lk-curve-field__action {
|
|
37
|
-
background: transparent;
|
|
38
|
-
border: 1px solid var(--wzl-border);
|
|
39
|
-
border-radius: var(--wzl-radius-sm);
|
|
40
|
-
color: var(--wzl-fg);
|
|
41
|
-
padding: 4px 10px;
|
|
42
|
-
font-size: var(--wzl-font-size-sm);
|
|
43
|
-
cursor: pointer;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.lk-curve-field__action:hover {
|
|
47
|
-
background: var(--wzl-surface-raised);
|
|
48
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { CurveField } from './CurveField';
|
|
4
|
-
|
|
5
|
-
const meta: Meta<typeof CurveField> = {
|
|
6
|
-
title: 'labkit/UI/Properties/CurveField',
|
|
7
|
-
component: CurveField,
|
|
8
|
-
};
|
|
9
|
-
export default meta;
|
|
10
|
-
|
|
11
|
-
export const DomeContour: StoryObj<typeof CurveField> = {
|
|
12
|
-
render: () => {
|
|
13
|
-
const [v, setV] = useState<number[]>([0, -1, 0.5, 0.5, 1, 0.7]);
|
|
14
|
-
return (
|
|
15
|
-
<div style={{ width: 280 }}>
|
|
16
|
-
<CurveField values={v} min={-1} max={1} step={0.02} width={280} onChange={setV} />
|
|
17
|
-
</div>
|
|
18
|
-
);
|
|
19
|
-
},
|
|
20
|
-
};
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
-
import { assert, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
// CurveEditor uses weasel's own bundled React and fails in jsdom.
|
|
5
|
-
// Mock it out — these tests only assert on readouts and the flip button.
|
|
6
|
-
vi.mock('../../passthrough/weasel-ui', async (importOriginal) => {
|
|
7
|
-
const real = await importOriginal<typeof import('../../passthrough/weasel-ui')>();
|
|
8
|
-
return { ...real, CurveEditor: () => null };
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
import { CurveField } from './CurveField';
|
|
12
|
-
|
|
13
|
-
describe('CurveField', () => {
|
|
14
|
-
it('flip button mirrors x → 1-x and resorts', () => {
|
|
15
|
-
const onChange = vi.fn();
|
|
16
|
-
render(
|
|
17
|
-
<CurveField
|
|
18
|
-
values={[0, -1, 0.3, 0.4, 1, 0.7]}
|
|
19
|
-
min={-1}
|
|
20
|
-
max={1}
|
|
21
|
-
step={0.02}
|
|
22
|
-
width={200}
|
|
23
|
-
height={110}
|
|
24
|
-
onChange={onChange}
|
|
25
|
-
/>,
|
|
26
|
-
);
|
|
27
|
-
screen.getByRole('button', { name: /flip horizontally/i }).click();
|
|
28
|
-
expect(onChange).toHaveBeenCalledTimes(1);
|
|
29
|
-
// After flip: (0,-1) → (1,-1), (0.3,0.4) → (0.7,0.4), (1,0.7) → (0,0.7).
|
|
30
|
-
// Sort by x ascending: (0,0.7), (0.7,0.4), (1,-1).
|
|
31
|
-
expect(onChange.mock.calls[0][0]).toEqual([0, 0.7, 0.7, 0.4, 1, -1]);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('flip vertically mirrors y through (min+max)/2 and keeps x order', () => {
|
|
35
|
-
const onChange = vi.fn();
|
|
36
|
-
render(
|
|
37
|
-
<CurveField
|
|
38
|
-
values={[0, -1, 0.3, 0.4, 1, 0.7]}
|
|
39
|
-
min={-1}
|
|
40
|
-
max={1}
|
|
41
|
-
step={0.02}
|
|
42
|
-
width={200}
|
|
43
|
-
height={110}
|
|
44
|
-
onChange={onChange}
|
|
45
|
-
/>,
|
|
46
|
-
);
|
|
47
|
-
screen.getByRole('button', { name: /flip vertically/i }).click();
|
|
48
|
-
expect(onChange).toHaveBeenCalledTimes(1);
|
|
49
|
-
// (min+max)/2 = 0. -1 → 1, 0.4 → -0.4, 0.7 → -0.7. x order preserved.
|
|
50
|
-
expect(onChange.mock.calls[0][0]).toEqual([0, 1, 0.3, -0.4, 1, -0.7]);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('flip vertically with [0,1] range mirrors through 0.5', () => {
|
|
54
|
-
const onChange = vi.fn();
|
|
55
|
-
render(
|
|
56
|
-
<CurveField
|
|
57
|
-
values={[0, 0, 0.5, 0.25, 1, 1]}
|
|
58
|
-
min={0}
|
|
59
|
-
max={1}
|
|
60
|
-
step={0.01}
|
|
61
|
-
width={200}
|
|
62
|
-
height={110}
|
|
63
|
-
onChange={onChange}
|
|
64
|
-
/>,
|
|
65
|
-
);
|
|
66
|
-
screen.getByRole('button', { name: /flip vertically/i }).click();
|
|
67
|
-
expect(onChange.mock.calls[0][0]).toEqual([0, 1, 0.5, 0.75, 1, 0]);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('renders a band mark as a positioned overlay rect', () => {
|
|
71
|
-
const { container } = render(
|
|
72
|
-
<CurveField
|
|
73
|
-
values={[0, 0, 1, 1]}
|
|
74
|
-
min={0}
|
|
75
|
-
max={1}
|
|
76
|
-
step={0.01}
|
|
77
|
-
width={200}
|
|
78
|
-
height={110}
|
|
79
|
-
marks={[{ kind: 'band', x: [0.2, 0.4], color: '#ffcc00' }]}
|
|
80
|
-
onChange={() => {}}
|
|
81
|
-
/>,
|
|
82
|
-
);
|
|
83
|
-
const overlay = container.querySelector('.lk-curve-field__marks');
|
|
84
|
-
assert(overlay);
|
|
85
|
-
const rect = overlay.querySelector('rect');
|
|
86
|
-
assert(rect);
|
|
87
|
-
// x = 0.2 * 200 = 40; width = (0.4 - 0.2) * 200 = 40.
|
|
88
|
-
expect(rect.getAttribute('x')).toBe('40');
|
|
89
|
-
expect(rect.getAttribute('width')).toBe('40');
|
|
90
|
-
expect(rect.getAttribute('fill')).toBe('#ffcc00');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('renders a line mark as a positioned line', () => {
|
|
94
|
-
const { container } = render(
|
|
95
|
-
<CurveField
|
|
96
|
-
values={[0, 0, 1, 1]}
|
|
97
|
-
min={0}
|
|
98
|
-
max={1}
|
|
99
|
-
step={0.01}
|
|
100
|
-
width={200}
|
|
101
|
-
height={110}
|
|
102
|
-
marks={[{ kind: 'line', x: 0.7, color: '#ffcc00' }]}
|
|
103
|
-
onChange={() => {}}
|
|
104
|
-
/>,
|
|
105
|
-
);
|
|
106
|
-
const line = container.querySelector('.lk-curve-field__marks line');
|
|
107
|
-
assert(line);
|
|
108
|
-
expect(line.getAttribute('x1')).toBe('140');
|
|
109
|
-
expect(line.getAttribute('x2')).toBe('140');
|
|
110
|
-
expect(line.getAttribute('stroke')).toBe('#ffcc00');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('omits the marks overlay when marks is undefined', () => {
|
|
114
|
-
const { container } = render(
|
|
115
|
-
<CurveField
|
|
116
|
-
values={[0, 0, 1, 1]}
|
|
117
|
-
min={0}
|
|
118
|
-
max={1}
|
|
119
|
-
step={0.01}
|
|
120
|
-
width={200}
|
|
121
|
-
height={110}
|
|
122
|
-
onChange={() => {}}
|
|
123
|
-
/>,
|
|
124
|
-
);
|
|
125
|
-
expect(container.querySelector('.lk-curve-field__marks')).toBeNull();
|
|
126
|
-
});
|
|
127
|
-
});
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { useCallback, useMemo } from 'react';
|
|
2
|
-
import { type ControlPoint, CurveEditor, dlog } from '../../passthrough/weasel-ui';
|
|
3
|
-
|
|
4
|
-
/** An annotation drawn over a curve plot: a shaded band across a range of x,
|
|
5
|
-
* or a vertical line at one x. */
|
|
6
|
-
export type CurveMark =
|
|
7
|
-
| { kind: 'band'; x: [number, number]; color?: string }
|
|
8
|
-
| { kind: 'line'; x: number; color?: string };
|
|
9
|
-
|
|
10
|
-
/** Props for `<CurveField>`. */
|
|
11
|
-
export interface CurveFieldProps {
|
|
12
|
-
/** Flat [x0, y0, x1, y1, …] — matches how curve-as-array configs
|
|
13
|
-
* serialize in JSON snapshots. */
|
|
14
|
-
values: number[];
|
|
15
|
-
min: number;
|
|
16
|
-
max: number;
|
|
17
|
-
step: number;
|
|
18
|
-
/** Plot width in CSS px. Caller sizes (typically uses a ResizeObserver). */
|
|
19
|
-
width: number;
|
|
20
|
-
/** Plot height in CSS px. Default 110. */
|
|
21
|
-
height?: number;
|
|
22
|
-
/** Flat default curve. When provided, a Reset button restores it. */
|
|
23
|
-
defaults?: readonly number[];
|
|
24
|
-
onChange: (next: number[]) => void;
|
|
25
|
-
/** Optional vertical marks overlaid on the plot (e.g. landmarks). */
|
|
26
|
-
marks?: readonly CurveMark[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* A function-domain (y = f(x), x ∈ [0,1]) curve editor with per-stop
|
|
31
|
-
* numeric readouts and flip buttons. Wraps weasel-ui's
|
|
32
|
-
* `CurveEditor`; consumers wanting the raw editor (2D paths,
|
|
33
|
-
* custom anchors) should import it from `@weasel-js/labkit/weasel-ui`.
|
|
34
|
-
*/
|
|
35
|
-
export function CurveField({
|
|
36
|
-
values,
|
|
37
|
-
min,
|
|
38
|
-
max,
|
|
39
|
-
step,
|
|
40
|
-
width,
|
|
41
|
-
height = 110,
|
|
42
|
-
defaults,
|
|
43
|
-
onChange,
|
|
44
|
-
marks,
|
|
45
|
-
}: CurveFieldProps) {
|
|
46
|
-
const points: ControlPoint[] = useMemo(() => {
|
|
47
|
-
const out: ControlPoint[] = [];
|
|
48
|
-
for (let i = 0; i + 1 < values.length; i += 2) out.push({ x: values[i], y: values[i + 1] });
|
|
49
|
-
return out;
|
|
50
|
-
}, [values]);
|
|
51
|
-
|
|
52
|
-
const handleChange = useCallback(
|
|
53
|
-
(next: ControlPoint[]) => {
|
|
54
|
-
const flat: number[] = new Array(next.length * 2);
|
|
55
|
-
for (let i = 0; i < next.length; i++) {
|
|
56
|
-
const ySnap = Math.round(next[i].y / step) * step;
|
|
57
|
-
const y = Math.max(min, Math.min(max, ySnap));
|
|
58
|
-
flat[i * 2] = next[i].x;
|
|
59
|
-
flat[i * 2 + 1] = y;
|
|
60
|
-
}
|
|
61
|
-
dlog('curve-field', 'handleChange', {
|
|
62
|
-
nLen: next.length,
|
|
63
|
-
flatLen: flat.length,
|
|
64
|
-
sample: flat.slice(0, 4),
|
|
65
|
-
});
|
|
66
|
-
onChange(flat);
|
|
67
|
-
},
|
|
68
|
-
[onChange, min, max, step],
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
const handleFlip = useCallback(() => {
|
|
72
|
-
const flipped: Array<{ x: number; y: number }> = [];
|
|
73
|
-
for (let i = 0; i + 1 < values.length; i += 2) {
|
|
74
|
-
flipped.push({ x: 1 - values[i], y: values[i + 1] });
|
|
75
|
-
}
|
|
76
|
-
flipped.sort((a, b) => a.x - b.x);
|
|
77
|
-
const out: number[] = [];
|
|
78
|
-
for (const p of flipped) out.push(p.x, p.y);
|
|
79
|
-
onChange(out);
|
|
80
|
-
}, [values, onChange]);
|
|
81
|
-
|
|
82
|
-
const handleFlipVertical = useCallback(() => {
|
|
83
|
-
const mid = (min + max) / 2;
|
|
84
|
-
const flipped: number[] = new Array(values.length);
|
|
85
|
-
for (let i = 0; i + 1 < values.length; i += 2) {
|
|
86
|
-
flipped[i] = values[i];
|
|
87
|
-
flipped[i + 1] = 2 * mid - values[i + 1];
|
|
88
|
-
}
|
|
89
|
-
onChange(flipped);
|
|
90
|
-
}, [values, min, max, onChange]);
|
|
91
|
-
|
|
92
|
-
return (
|
|
93
|
-
<div className="lk-curve-field">
|
|
94
|
-
<div className="lk-curve-field__plot">
|
|
95
|
-
<CurveEditor
|
|
96
|
-
value={points}
|
|
97
|
-
onInput={handleChange}
|
|
98
|
-
domain="1d"
|
|
99
|
-
constrain="function"
|
|
100
|
-
xRange={[0, 1]}
|
|
101
|
-
yRange={[min, max]}
|
|
102
|
-
width={width}
|
|
103
|
-
height={height}
|
|
104
|
-
endpoints="pinned-x"
|
|
105
|
-
addPointMode="click-curve"
|
|
106
|
-
minPoints={2}
|
|
107
|
-
grid={{}}
|
|
108
|
-
history={false}
|
|
109
|
-
/>
|
|
110
|
-
{marks && marks.length > 0 && (
|
|
111
|
-
<svg
|
|
112
|
-
className="lk-curve-field__marks"
|
|
113
|
-
width={width}
|
|
114
|
-
height={height}
|
|
115
|
-
viewBox={`0 0 ${width} ${height}`}
|
|
116
|
-
aria-hidden="true"
|
|
117
|
-
>
|
|
118
|
-
{marks.map((m) => {
|
|
119
|
-
const color = m.color ?? '#ffcc00';
|
|
120
|
-
if (m.kind === 'band') {
|
|
121
|
-
const x0 = Math.max(0, Math.min(1, m.x[0])) * width;
|
|
122
|
-
const x1 = Math.max(0, Math.min(1, m.x[1])) * width;
|
|
123
|
-
const left = Math.min(x0, x1);
|
|
124
|
-
const w = Math.abs(x1 - x0);
|
|
125
|
-
return (
|
|
126
|
-
<g key={`band-${m.x[0]}-${m.x[1]}-${color}`}>
|
|
127
|
-
<rect x={left} y={0} width={w} height={height} fill={color} fillOpacity={0.4} />
|
|
128
|
-
<line
|
|
129
|
-
x1={left}
|
|
130
|
-
y1={0}
|
|
131
|
-
x2={left}
|
|
132
|
-
y2={height}
|
|
133
|
-
stroke={color}
|
|
134
|
-
strokeOpacity={0.85}
|
|
135
|
-
strokeWidth={1}
|
|
136
|
-
/>
|
|
137
|
-
<line
|
|
138
|
-
x1={left + w}
|
|
139
|
-
y1={0}
|
|
140
|
-
x2={left + w}
|
|
141
|
-
y2={height}
|
|
142
|
-
stroke={color}
|
|
143
|
-
strokeOpacity={0.85}
|
|
144
|
-
strokeWidth={1}
|
|
145
|
-
/>
|
|
146
|
-
</g>
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
const x = Math.max(0, Math.min(1, m.x)) * width;
|
|
150
|
-
return (
|
|
151
|
-
<line
|
|
152
|
-
key={`mark-${m.x}-${color}`}
|
|
153
|
-
x1={x}
|
|
154
|
-
y1={0}
|
|
155
|
-
x2={x}
|
|
156
|
-
y2={height}
|
|
157
|
-
stroke={color}
|
|
158
|
-
strokeWidth={1}
|
|
159
|
-
/>
|
|
160
|
-
);
|
|
161
|
-
})}
|
|
162
|
-
</svg>
|
|
163
|
-
)}
|
|
164
|
-
</div>
|
|
165
|
-
<div className="lk-curve-field__actions">
|
|
166
|
-
<button type="button" className="lk-curve-field__action" onClick={handleFlip}>
|
|
167
|
-
Flip horizontally
|
|
168
|
-
</button>
|
|
169
|
-
<button type="button" className="lk-curve-field__action" onClick={handleFlipVertical}>
|
|
170
|
-
Flip vertically
|
|
171
|
-
</button>
|
|
172
|
-
{defaults && (
|
|
173
|
-
<button
|
|
174
|
-
type="button"
|
|
175
|
-
className="lk-curve-field__action"
|
|
176
|
-
onClick={() => onChange([...defaults])}
|
|
177
|
-
>
|
|
178
|
-
Reset
|
|
179
|
-
</button>
|
|
180
|
-
)}
|
|
181
|
-
</div>
|
|
182
|
-
</div>
|
|
183
|
-
);
|
|
184
|
-
}
|