zds-pickers 4.0.5 → 4.0.6
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/.editorconfig +19 -0
- package/.github/workflows/codeql-analysis.yml +67 -0
- package/.nvmrc +1 -0
- package/.prettierignore +0 -0
- package/.prettierrc +13 -0
- package/.storybook/main.ts +42 -0
- package/.storybook/manager.ts +12 -0
- package/.storybook/preview.tsx +26 -0
- package/.vscode/extensions.json +32 -0
- package/.vscode/javascript.code-snippets +229 -0
- package/.vscode/settings.json +358 -0
- package/biome.jsonc +158 -0
- package/dist/index.es.js +3 -1
- package/dist/types/index.d.ts +266 -2
- package/dist/types/pickers/CCPicker.d.ts +2 -1
- package/dist/types/pickers/ChannelMappingPicker.d.ts +2 -1
- package/dist/types/pickers/ChannelPicker.d.ts +5 -3
- package/dist/types/pickers/LatchPicker.d.ts +5 -3
- package/dist/types/pickers/MappingPicker.d.ts +2 -1
- package/dist/types/pickers/NotePicker.d.ts +2 -1
- package/dist/types/pickers/PolarityPicker.d.ts +6 -4
- package/dist/types/pickers/ResponseCurve.d.ts +14 -5
- package/dist/types/pickers/ResponseCurvePicker.d.ts +6 -4
- package/dist/types/pickers/StatusPicker.d.ts +2 -1
- package/dist/types/pickers/ValuePicker.d.ts +2 -1
- package/lib/@types/nebiru-react-rotary-knob.d.ts +20 -0
- package/lib/@types/react-svgmt.d.ts +74 -0
- package/lib/@types/zds-mappings.d.ts +55 -0
- package/lib/@types/zds-react-piano.d.ts +17 -0
- package/lib/hooks/useComponentSize.ts +52 -0
- package/lib/hooks/useStateWithDynamicDefault.ts +13 -0
- package/lib/main.ts +34 -0
- package/lib/midi/ccValues.ts +139 -0
- package/lib/midi/export.ts +54 -0
- package/lib/other/DefaultTooltip.ts +14 -0
- package/lib/other/OctavePlayer.tsx +141 -0
- package/lib/other/SVGText.tsx +221 -0
- package/lib/other/SoundFontProvider.tsx +130 -0
- package/lib/pickers/CCPicker.tsx +28 -0
- package/lib/pickers/ChannelMappingPicker.tsx +135 -0
- package/lib/pickers/ChannelPicker.tsx +76 -0
- package/lib/pickers/Knob.tsx +93 -0
- package/lib/pickers/KnobPicker.tsx +93 -0
- package/lib/pickers/LatchPicker.tsx +33 -0
- package/lib/pickers/MappingPicker.tsx +68 -0
- package/lib/pickers/NotePicker.tsx +95 -0
- package/lib/pickers/PianoPicker.tsx +77 -0
- package/lib/pickers/PolarityPicker.tsx +39 -0
- package/lib/pickers/ResponseCurve.tsx +304 -0
- package/lib/pickers/ResponseCurvePicker.tsx +74 -0
- package/lib/pickers/Select.tsx +248 -0
- package/lib/pickers/StatusPicker.tsx +28 -0
- package/lib/pickers/ValuePicker.tsx +67 -0
- package/lib/pickers/knobSkin10.tsx +94 -0
- package/lib/rotaryKnob/InternalInput.tsx +43 -0
- package/lib/rotaryKnob/Types.ts +6 -0
- package/lib/rotaryKnob/helpers/DrawCircle.tsx +41 -0
- package/lib/rotaryKnob/helpers/DrawLine.tsx +32 -0
- package/lib/rotaryKnob/helpers/HelpersOverlay.tsx +43 -0
- package/lib/rotaryKnob/helpers/KnobVisualHelpers.tsx +138 -0
- package/lib/rotaryKnob/index.tsx +417 -0
- package/lib/rotaryKnob/knobdefaultskin.ts +56 -0
- package/lib/rotaryKnob/utils.ts +112 -0
- package/lib/temp/acoustic_grand_piano-mp3.js +94 -0
- package/lib/utils.ts +41 -0
- package/package.json +2 -6
- package/stories/assets/sampleStyles.css +349 -0
- package/stories/assets/utils.js +7 -0
- package/stories/picker.stories.tsx +313 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +34 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +45 -0
- package/dist/index.d.ts +0 -244
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { forwardRef, useCallback, useMemo } from 'react'
|
|
2
|
+
import type { GroupBase, SelectInstance } from 'react-select'
|
|
3
|
+
import { Midi } from 'tonal'
|
|
4
|
+
import { type MapItem, type Mapping, emptyMapping } from 'zds-mappings'
|
|
5
|
+
import useStateWithDynamicDefault from '../hooks/useStateWithDynamicDefault'
|
|
6
|
+
import { assertRange } from '../utils'
|
|
7
|
+
import Select from './Select'
|
|
8
|
+
import type { Option, SelectProps } from './Select'
|
|
9
|
+
|
|
10
|
+
const { midiToNoteName } = Midi
|
|
11
|
+
|
|
12
|
+
const formattedMapEntry = ({ note, name }: MapItem) =>
|
|
13
|
+
`${note} ${name.length ? '-' : ''} ${name}`
|
|
14
|
+
const formattedListEntry = (label: string, idx: number) => ({
|
|
15
|
+
label,
|
|
16
|
+
value: idx + 1,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
type NotePickerProps = SelectProps<number> & {
|
|
20
|
+
channel: number
|
|
21
|
+
disabled?: boolean
|
|
22
|
+
isMelodicMode?: boolean
|
|
23
|
+
mapping?: Mapping
|
|
24
|
+
onChange: (value: number) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type NotePickerRef = SelectInstance<
|
|
28
|
+
Option<number>,
|
|
29
|
+
false,
|
|
30
|
+
GroupBase<Option<number>>
|
|
31
|
+
>
|
|
32
|
+
|
|
33
|
+
const NotePicker = forwardRef<
|
|
34
|
+
SelectInstance<Option<number>, false, GroupBase<Option<number>>>,
|
|
35
|
+
NotePickerProps
|
|
36
|
+
>((props, ref) => {
|
|
37
|
+
const {
|
|
38
|
+
channel,
|
|
39
|
+
disabled = false,
|
|
40
|
+
isMelodicMode = false,
|
|
41
|
+
mapping,
|
|
42
|
+
onChange,
|
|
43
|
+
value: initialValue,
|
|
44
|
+
...rest
|
|
45
|
+
} = props
|
|
46
|
+
|
|
47
|
+
const options = useMemo(() => {
|
|
48
|
+
if (isMelodicMode) {
|
|
49
|
+
return emptyMapping()
|
|
50
|
+
.map(({ note }) => {
|
|
51
|
+
const midiNoteName = midiToNoteName(note, { sharps: false })
|
|
52
|
+
.replace('b', '♭')
|
|
53
|
+
.replace('#', '♯')
|
|
54
|
+
return `${midiNoteName} (#${note})`
|
|
55
|
+
})
|
|
56
|
+
.map(formattedListEntry)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (mapping || emptyMapping())
|
|
60
|
+
.map(formattedMapEntry)
|
|
61
|
+
.map(formattedListEntry)
|
|
62
|
+
}, [isMelodicMode, mapping])
|
|
63
|
+
|
|
64
|
+
const [value, setValue] = useStateWithDynamicDefault(initialValue)
|
|
65
|
+
|
|
66
|
+
const handleChange = useCallback(
|
|
67
|
+
(v: number | string) => {
|
|
68
|
+
if (typeof v === 'string') return
|
|
69
|
+
|
|
70
|
+
const possibleNoteNumber = assertRange(v, 128, 0)
|
|
71
|
+
|
|
72
|
+
if (possibleNoteNumber > 0) {
|
|
73
|
+
onChange(possibleNoteNumber)
|
|
74
|
+
}
|
|
75
|
+
setValue(v)
|
|
76
|
+
},
|
|
77
|
+
[onChange, setValue],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Select
|
|
82
|
+
{...rest}
|
|
83
|
+
disabled={disabled}
|
|
84
|
+
// isDisabled={disabled}
|
|
85
|
+
onChange={handleChange}
|
|
86
|
+
options={options}
|
|
87
|
+
ref={ref}
|
|
88
|
+
value={value}
|
|
89
|
+
/>
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
export default NotePicker
|
|
94
|
+
|
|
95
|
+
export type { NotePickerProps, NotePickerRef }
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Piano as ReactPiano } from 'zds-react-piano'
|
|
2
|
+
import type { WithProviderProps } from '../other/OctavePlayer'
|
|
3
|
+
import SoundfontProvider from '../other/SoundFontProvider'
|
|
4
|
+
|
|
5
|
+
const firstNote = 21
|
|
6
|
+
const lastNote = 108
|
|
7
|
+
const numNotes = lastNote - firstNote - 1
|
|
8
|
+
|
|
9
|
+
const whiteNotes = numNotes * 0.7 // approx
|
|
10
|
+
|
|
11
|
+
const audioContext = new window.AudioContext()
|
|
12
|
+
|
|
13
|
+
type PianoProps = {
|
|
14
|
+
disabled: boolean
|
|
15
|
+
height: number
|
|
16
|
+
width: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PianoPicker = ({ height, width, ...rest }: PianoProps) => {
|
|
20
|
+
const keyWidth = width / whiteNotes + 2
|
|
21
|
+
const keyWidthToHeight = keyWidth / height
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div>
|
|
25
|
+
<ReactPiano
|
|
26
|
+
keyWidthToHeight={keyWidthToHeight}
|
|
27
|
+
noteRange={{ first: firstNote, last: lastNote }}
|
|
28
|
+
width={width}
|
|
29
|
+
{...rest}
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const WithProvider = ({
|
|
36
|
+
format,
|
|
37
|
+
height,
|
|
38
|
+
hostname,
|
|
39
|
+
instrumentName,
|
|
40
|
+
onChange,
|
|
41
|
+
onClick,
|
|
42
|
+
onDoubleClick,
|
|
43
|
+
onKeyMouseEnter,
|
|
44
|
+
onKeyMouseLeave,
|
|
45
|
+
soundfont,
|
|
46
|
+
width,
|
|
47
|
+
}: WithProviderProps) => (
|
|
48
|
+
<SoundfontProvider
|
|
49
|
+
{...{
|
|
50
|
+
audioContext,
|
|
51
|
+
format,
|
|
52
|
+
hostname,
|
|
53
|
+
instrumentName,
|
|
54
|
+
onChange,
|
|
55
|
+
soundfont,
|
|
56
|
+
}}
|
|
57
|
+
render={({ isLoading, playNote, stopNote }) => (
|
|
58
|
+
<PianoPicker
|
|
59
|
+
disabled={isLoading}
|
|
60
|
+
{...{
|
|
61
|
+
height,
|
|
62
|
+
onClick,
|
|
63
|
+
onDoubleClick,
|
|
64
|
+
onKeyMouseEnter,
|
|
65
|
+
onKeyMouseLeave,
|
|
66
|
+
playNote,
|
|
67
|
+
stopNote,
|
|
68
|
+
width,
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
/>
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
export default WithProvider
|
|
76
|
+
|
|
77
|
+
export type { PianoProps }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { forwardRef } from 'react'
|
|
2
|
+
import type { GroupBase, SelectInstance } from 'react-select'
|
|
3
|
+
import Select from './Select'
|
|
4
|
+
import type { Option, SelectProps } from './Select'
|
|
5
|
+
|
|
6
|
+
type Polarity = 0 | 1
|
|
7
|
+
|
|
8
|
+
type PolarityPickerProps = SelectProps<Polarity> & {
|
|
9
|
+
labelOff?: string
|
|
10
|
+
labelOn?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type PolarityPickerRef = SelectInstance<
|
|
14
|
+
Option<Polarity>,
|
|
15
|
+
false,
|
|
16
|
+
GroupBase<Option<Polarity>>
|
|
17
|
+
>
|
|
18
|
+
|
|
19
|
+
const PolarityPicker = forwardRef<
|
|
20
|
+
SelectInstance<Option<Polarity>, false, GroupBase<Option<Polarity>>>,
|
|
21
|
+
PolarityPickerProps
|
|
22
|
+
>(({ labelOff = 'Normally Off', labelOn = 'Normally On', ...rest }, ref) => {
|
|
23
|
+
const options = [
|
|
24
|
+
{ value: 0, label: labelOff },
|
|
25
|
+
{ value: 1, label: labelOn },
|
|
26
|
+
] as Option<Polarity>[]
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Select
|
|
30
|
+
{...rest}
|
|
31
|
+
options={options}
|
|
32
|
+
ref={ref}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export default PolarityPicker
|
|
38
|
+
|
|
39
|
+
export type { Polarity, PolarityPickerProps, PolarityPickerRef }
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
2
|
+
import useComponentSize from '../hooks/useComponentSize'
|
|
3
|
+
import DefaultTooltip, { type TooltipProps } from '../other/DefaultTooltip'
|
|
4
|
+
import SVGText from '../other/SVGText'
|
|
5
|
+
import type { Option } from './Select'
|
|
6
|
+
|
|
7
|
+
const RESPONSE_CURVE_0 = 0
|
|
8
|
+
const RESPONSE_CURVE_1 = 1
|
|
9
|
+
const RESPONSE_CURVE_2 = 2
|
|
10
|
+
const RESPONSE_CURVE_3 = 3
|
|
11
|
+
const RESPONSE_CURVE_4 = 4
|
|
12
|
+
const RESPONSE_CURVE_5 = 5
|
|
13
|
+
const RESPONSE_CURVE_6 = 6
|
|
14
|
+
const RESPONSE_CURVE_7 = 7
|
|
15
|
+
|
|
16
|
+
type Curve =
|
|
17
|
+
| typeof RESPONSE_CURVE_0
|
|
18
|
+
| typeof RESPONSE_CURVE_1
|
|
19
|
+
| typeof RESPONSE_CURVE_2
|
|
20
|
+
| typeof RESPONSE_CURVE_3
|
|
21
|
+
| typeof RESPONSE_CURVE_4
|
|
22
|
+
| typeof RESPONSE_CURVE_5
|
|
23
|
+
| typeof RESPONSE_CURVE_6
|
|
24
|
+
| typeof RESPONSE_CURVE_7
|
|
25
|
+
|
|
26
|
+
type ResponseCurve = {
|
|
27
|
+
value: Curve
|
|
28
|
+
label: React.ReactNode
|
|
29
|
+
c1: number
|
|
30
|
+
c2: number
|
|
31
|
+
x1: number
|
|
32
|
+
y1: number
|
|
33
|
+
x2: number
|
|
34
|
+
y2: number
|
|
35
|
+
labelX: number
|
|
36
|
+
labelY: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const RESPONSE_CURVES: Option<Curve>[] = [
|
|
40
|
+
{
|
|
41
|
+
value: RESPONSE_CURVE_7,
|
|
42
|
+
label: 'Always maxed out.',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
value: RESPONSE_CURVE_6,
|
|
46
|
+
label: 'Most sensitive.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
value: RESPONSE_CURVE_5,
|
|
50
|
+
label: 'Moderately sensitive.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
value: RESPONSE_CURVE_4,
|
|
54
|
+
label: 'A little sensitive.',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
value: RESPONSE_CURVE_0,
|
|
58
|
+
label: 'Linear, no change.',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
value: RESPONSE_CURVE_1,
|
|
62
|
+
label: 'A little less sensitive.',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
value: RESPONSE_CURVE_2,
|
|
66
|
+
label: 'Even less sensitive.',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
value: RESPONSE_CURVE_3,
|
|
70
|
+
label: 'Least sensitive.',
|
|
71
|
+
},
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
// const width = 218
|
|
75
|
+
// const height = 150
|
|
76
|
+
const axisOffset = 20
|
|
77
|
+
const buttonSize = 15
|
|
78
|
+
|
|
79
|
+
const axisX1 = axisOffset
|
|
80
|
+
const axisY1 = axisOffset
|
|
81
|
+
|
|
82
|
+
type ResponseCurveProps = {
|
|
83
|
+
autosize: boolean
|
|
84
|
+
disabled?: boolean
|
|
85
|
+
inverted: boolean
|
|
86
|
+
onChange: (event: { target: { value: Curve } }) => void
|
|
87
|
+
Tooltip: React.FC<TooltipProps>
|
|
88
|
+
value: Curve
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const ResponseCurve = (props: ResponseCurveProps) => {
|
|
92
|
+
const {
|
|
93
|
+
Tooltip = DefaultTooltip,
|
|
94
|
+
autosize,
|
|
95
|
+
disabled,
|
|
96
|
+
inverted,
|
|
97
|
+
onChange,
|
|
98
|
+
value: responseCurve,
|
|
99
|
+
} = props
|
|
100
|
+
|
|
101
|
+
const ref = useRef(null)
|
|
102
|
+
const svgRef = useRef(null)
|
|
103
|
+
const { height: ourHeight, width: ourWidth } = useComponentSize(ref)
|
|
104
|
+
|
|
105
|
+
const width = autosize ? ourWidth || 218 : 218
|
|
106
|
+
const height = autosize ? ourHeight || 150 : 150
|
|
107
|
+
|
|
108
|
+
const axisX2 = width - axisOffset
|
|
109
|
+
const axisY2 = height - axisOffset
|
|
110
|
+
|
|
111
|
+
const xMid = width / 2
|
|
112
|
+
const yMid = height / 2
|
|
113
|
+
|
|
114
|
+
const svgProps = { ref: svgRef }
|
|
115
|
+
|
|
116
|
+
const responseCurves = RESPONSE_CURVES.map(
|
|
117
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
|
|
118
|
+
({ value, label }): ResponseCurve => {
|
|
119
|
+
let c1 = 0
|
|
120
|
+
let c2 = 0
|
|
121
|
+
let x1 = axisX1
|
|
122
|
+
let y1 = inverted ? axisY1 : axisY2
|
|
123
|
+
let x2 = axisX2
|
|
124
|
+
let y2 = inverted ? axisY2 : axisY1
|
|
125
|
+
let labelX = xMid
|
|
126
|
+
let labelY = yMid
|
|
127
|
+
switch (value) {
|
|
128
|
+
case RESPONSE_CURVE_0:
|
|
129
|
+
c1 = xMid
|
|
130
|
+
c2 = yMid
|
|
131
|
+
labelY += 3
|
|
132
|
+
|
|
133
|
+
break
|
|
134
|
+
case RESPONSE_CURVE_1:
|
|
135
|
+
c1 = inverted ? xMid + width * 0.13 : xMid - width * 0.13
|
|
136
|
+
c2 = yMid - height * 0.13
|
|
137
|
+
labelX = inverted ? xMid + width * 0.1 : xMid - width * 0.1
|
|
138
|
+
labelY = yMid - height * 0.09
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
case RESPONSE_CURVE_2:
|
|
142
|
+
c1 = inverted ? xMid + width * 0.25 : xMid - width * 0.25
|
|
143
|
+
c2 = yMid - height * 0.25
|
|
144
|
+
labelX = inverted ? xMid + width * 0.2 : xMid - width * 0.2
|
|
145
|
+
labelY = yMid - height * 0.17
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
case RESPONSE_CURVE_3:
|
|
149
|
+
c1 = inverted ? xMid + width * 0.35 : xMid - width * 0.35
|
|
150
|
+
c2 = yMid - height * 0.35
|
|
151
|
+
labelX = inverted ? xMid + width * 0.28 : xMid - width * 0.28
|
|
152
|
+
labelY = yMid - height * 0.25
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
case RESPONSE_CURVE_4:
|
|
156
|
+
c1 = inverted ? xMid - width * 0.13 : xMid + width * 0.13
|
|
157
|
+
c2 = yMid + height * 0.13
|
|
158
|
+
labelX = inverted ? xMid - width * 0.1 : xMid + width * 0.1
|
|
159
|
+
labelY = yMid + height * 0.1
|
|
160
|
+
break
|
|
161
|
+
|
|
162
|
+
case RESPONSE_CURVE_5:
|
|
163
|
+
c1 = inverted ? xMid - width * 0.25 : xMid + width * 0.25
|
|
164
|
+
c2 = yMid + height * 0.25
|
|
165
|
+
labelX = inverted ? xMid - width * 0.2 : xMid + width * 0.2
|
|
166
|
+
labelY = yMid + height * 0.18
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
case RESPONSE_CURVE_6:
|
|
170
|
+
c1 = inverted ? xMid - width * 0.35 : xMid + width * 0.35
|
|
171
|
+
c2 = yMid + height * 0.35
|
|
172
|
+
labelX = inverted ? xMid - width * 0.28 : xMid + width * 0.28
|
|
173
|
+
labelY = yMid + height * 0.25
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
case RESPONSE_CURVE_7:
|
|
177
|
+
x1 = inverted ? axisX1 : axisX2
|
|
178
|
+
y1 = axisY1
|
|
179
|
+
x2 = x1
|
|
180
|
+
y2 = axisY2
|
|
181
|
+
c1 = x1
|
|
182
|
+
c2 = yMid
|
|
183
|
+
labelX = x1 - 5
|
|
184
|
+
labelY = height - 30
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
default:
|
|
188
|
+
break
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
value,
|
|
192
|
+
label,
|
|
193
|
+
c1,
|
|
194
|
+
c2,
|
|
195
|
+
x1,
|
|
196
|
+
y1,
|
|
197
|
+
x2,
|
|
198
|
+
y2,
|
|
199
|
+
labelX,
|
|
200
|
+
labelY,
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div
|
|
207
|
+
className="response-curve-picker"
|
|
208
|
+
ref={ref}>
|
|
209
|
+
<svg {...svgProps}>
|
|
210
|
+
<rect
|
|
211
|
+
className="container"
|
|
212
|
+
height={height}
|
|
213
|
+
rx={3}
|
|
214
|
+
ry={3}
|
|
215
|
+
width={width}
|
|
216
|
+
x={0}
|
|
217
|
+
y={0}
|
|
218
|
+
/>
|
|
219
|
+
|
|
220
|
+
<SVGText
|
|
221
|
+
className="axis"
|
|
222
|
+
textAnchor="middle"
|
|
223
|
+
transform={`translate(${axisX1 - 14} ${height / 2}) rotate(90)`}
|
|
224
|
+
x={0}
|
|
225
|
+
y={0}
|
|
226
|
+
text="Applied force"
|
|
227
|
+
/>
|
|
228
|
+
|
|
229
|
+
<SVGText
|
|
230
|
+
className="axis"
|
|
231
|
+
textAnchor="middle"
|
|
232
|
+
transform={`translate(${width / 2} ${axisY2 + 13})`}
|
|
233
|
+
x={0}
|
|
234
|
+
y={0}
|
|
235
|
+
text="Output (velocity)"
|
|
236
|
+
/>
|
|
237
|
+
|
|
238
|
+
<line
|
|
239
|
+
className="axis"
|
|
240
|
+
x1={axisX1}
|
|
241
|
+
x2={axisX1}
|
|
242
|
+
y1={axisY1}
|
|
243
|
+
y2={axisY2}
|
|
244
|
+
/>
|
|
245
|
+
<line
|
|
246
|
+
className="axis"
|
|
247
|
+
x1={axisX1}
|
|
248
|
+
x2={axisX2}
|
|
249
|
+
y1={axisY2}
|
|
250
|
+
y2={axisY2}
|
|
251
|
+
/>
|
|
252
|
+
|
|
253
|
+
{responseCurves.map(
|
|
254
|
+
({ value, label, x1, y1, x2, y2, c1, c2, labelX, labelY }) => (
|
|
255
|
+
<g key={value}>
|
|
256
|
+
<path
|
|
257
|
+
className={value === responseCurve ? 'curve-selected' : 'curve'}
|
|
258
|
+
d={`M${x1} ${y1} C ${c1} ${c2}, ${c1} ${c2}, ${x2} ${y2}`}
|
|
259
|
+
/>
|
|
260
|
+
|
|
261
|
+
<Tooltip
|
|
262
|
+
placement="left"
|
|
263
|
+
title={label}>
|
|
264
|
+
<rect
|
|
265
|
+
className={
|
|
266
|
+
value === responseCurve ? 'button-selected' : 'button'
|
|
267
|
+
}
|
|
268
|
+
height={buttonSize}
|
|
269
|
+
onClick={
|
|
270
|
+
disabled
|
|
271
|
+
? f => f
|
|
272
|
+
: () =>
|
|
273
|
+
onChange({
|
|
274
|
+
target: {
|
|
275
|
+
value: Number.parseInt(String(value)) as Curve,
|
|
276
|
+
},
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
transform={`translate(${labelX + 3} ${labelY - buttonSize / 1.05}) rotate(45)`}
|
|
280
|
+
width={buttonSize}
|
|
281
|
+
/>
|
|
282
|
+
</Tooltip>
|
|
283
|
+
|
|
284
|
+
<text
|
|
285
|
+
className={
|
|
286
|
+
value === responseCurve ? 'button-selected' : 'button'
|
|
287
|
+
}
|
|
288
|
+
transform={`translate(${labelX} ${labelY})`}>
|
|
289
|
+
{value}
|
|
290
|
+
<title>{label}</title>
|
|
291
|
+
</text>
|
|
292
|
+
</g>
|
|
293
|
+
),
|
|
294
|
+
)}
|
|
295
|
+
</svg>
|
|
296
|
+
</div>
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export default ResponseCurve
|
|
301
|
+
|
|
302
|
+
export { RESPONSE_CURVES }
|
|
303
|
+
|
|
304
|
+
export type { Curve, ResponseCurve, ResponseCurveProps }
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { forwardRef, useCallback } from 'react'
|
|
2
|
+
import type { GroupBase, SelectInstance } from 'react-select'
|
|
3
|
+
import DefaultTooltip, { type TooltipProps } from '../other/DefaultTooltip'
|
|
4
|
+
import { findObj } from '../utils'
|
|
5
|
+
import ResponseCurve, { type Curve, RESPONSE_CURVES } from './ResponseCurve'
|
|
6
|
+
import Select from './Select'
|
|
7
|
+
import type { Option, SelectProps } from './Select'
|
|
8
|
+
|
|
9
|
+
type ResponseCurvePickerProps = SelectProps<Curve> & {
|
|
10
|
+
inverted?: boolean
|
|
11
|
+
Tooltip?: React.FC<TooltipProps>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type ResponseCurvePickerRef = SelectInstance<
|
|
15
|
+
Option<Curve>,
|
|
16
|
+
false,
|
|
17
|
+
GroupBase<Option<Curve>>
|
|
18
|
+
>
|
|
19
|
+
|
|
20
|
+
const ResponseCurvePicker = forwardRef<
|
|
21
|
+
SelectInstance<Option<Curve>, false, GroupBase<Option<Curve>>>,
|
|
22
|
+
ResponseCurvePickerProps
|
|
23
|
+
>((props, ref) => {
|
|
24
|
+
const {
|
|
25
|
+
Tooltip = DefaultTooltip,
|
|
26
|
+
inverted = false,
|
|
27
|
+
onChange,
|
|
28
|
+
value,
|
|
29
|
+
...rest
|
|
30
|
+
} = props
|
|
31
|
+
|
|
32
|
+
const Placeholder = useCallback(
|
|
33
|
+
() => (
|
|
34
|
+
<div className="singleValue">
|
|
35
|
+
{(findObj('value', value)(RESPONSE_CURVES) as Option<Curve>)?.label}
|
|
36
|
+
</div>
|
|
37
|
+
),
|
|
38
|
+
[value],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const MenuList = useCallback(
|
|
42
|
+
({ setValue }: { setValue: (v: Curve) => void }) => (
|
|
43
|
+
<ResponseCurve
|
|
44
|
+
autosize
|
|
45
|
+
inverted={inverted}
|
|
46
|
+
onChange={({ target: { value: v } }) => {
|
|
47
|
+
setValue(v)
|
|
48
|
+
onChange(v)
|
|
49
|
+
}}
|
|
50
|
+
Tooltip={Tooltip}
|
|
51
|
+
value={Number.parseInt(String(value)) as Curve}
|
|
52
|
+
/>
|
|
53
|
+
),
|
|
54
|
+
[Tooltip, inverted, onChange, value],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Select
|
|
59
|
+
{...rest}
|
|
60
|
+
components={{
|
|
61
|
+
Placeholder,
|
|
62
|
+
MenuList,
|
|
63
|
+
}}
|
|
64
|
+
isSearchable={false}
|
|
65
|
+
onChange={f => f}
|
|
66
|
+
ref={ref}
|
|
67
|
+
value={value}
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
export default ResponseCurvePicker
|
|
73
|
+
|
|
74
|
+
export type { ResponseCurvePickerProps, ResponseCurvePickerRef }
|