Command bar: add extrude command, nonlinear editing, etc (#1204)
* Tweak toaster look and feel * Add icons, tweak plus icon names * Rename commandBarMeta to commandBarConfig * Refactor command bar, add support for icons * Create a tailwind plugin for aria-pressed button state * Remove overlay from behind command bar * Clean up toolbar * Button and other style tweaks * Icon tweaks follow-up: make old icons work with new sizing * Delete unused static icons * More CSS tweaks * Small CSS tweak to project sidebar * Add command bar E2E test * fumpt * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * fix typo in a comment * Fix icon padding (built version only) * Update onboarding and warning banner icons padding * Misc minor style fixes * Get Extrude opening and canceling from command bar * Iconography tweaks * Get extrude kind of working * Refactor command bar config types and organization * Move command bar configs to be co-located with each other * Start building a state machine for the command bar * Start converting command bar to state machine * Add support for multiple args, confirmation step * Submission behavior, hotkeys, code organization * Add new test for extruding from command bar * Polish step back and selection hotkeys, CSS tweaks * Loading style tweaks * Validate selection inputs, polish UX of args re-editing * Prevent submission with multiple selection on singlular arg * Remove stray console logs * Tweak test, CSS nit, remove extrude "result" argument * Fix linting warnings * Show Ctrl+/ instead of ⌘K on all platforms but Mac * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * Add "Enter sketch" to command bar * fix command bar test * Fix flaky cmd bar extrude test by waiting for engine select response * Cover both button labels '⌘K' and 'Ctrl+/' in test --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -8,275 +8,275 @@ property float z
|
|||||||
element face 68
|
element face 68
|
||||||
property list uchar uint vertex_indices
|
property list uchar uint vertex_indices
|
||||||
end_header
|
end_header
|
||||||
0 0 4
|
0 0 4
|
||||||
0 0 0
|
0 0 0
|
||||||
0 -1 4
|
0 -1 4
|
||||||
0 -1 4
|
0 -1 4
|
||||||
0 0 0
|
0 0 0
|
||||||
0 -1 0
|
0 -1 0
|
||||||
0 -1 4
|
0 -1 4
|
||||||
0 -1 0
|
0 -1 0
|
||||||
3.0950184 -1 4
|
3.0950184 -1 4
|
||||||
3.0950184 -1 4
|
3.0950184 -1 4
|
||||||
0 -1 0
|
0 -1 0
|
||||||
3.0950184 -1 0
|
3.0950184 -1 0
|
||||||
3.0950184 -1 4
|
3.0950184 -1 4
|
||||||
3.0950184 -1 0
|
3.0950184 -1 0
|
||||||
5.9513144 -3 4
|
5.9513144 -3 4
|
||||||
5.9513144 -3 4
|
5.9513144 -3 4
|
||||||
3.0950184 -1 0
|
3.0950184 -1 0
|
||||||
5.9513144 -3 0
|
5.9513144 -3 0
|
||||||
5.9513144 -3 4
|
5.9513144 -3 4
|
||||||
5.9513144 -3 0
|
5.9513144 -3 0
|
||||||
9.5 -3 4
|
9.5 -3 4
|
||||||
9.5 -3 4
|
9.5 -3 4
|
||||||
5.9513144 -3 0
|
5.9513144 -3 0
|
||||||
9.5 -3 0
|
9.5 -3 0
|
||||||
9.5 -3 4
|
9.5 -3 4
|
||||||
9.5 -3 0
|
9.5 -3 0
|
||||||
9.5 -2.5 4
|
9.5 -2.5 4
|
||||||
9.5 -2.5 4
|
9.5 -2.5 4
|
||||||
9.5 -3 0
|
9.5 -3 0
|
||||||
9.5 -2.5 0
|
9.5 -2.5 0
|
||||||
9.5 -2.5 4
|
9.5 -2.5 4
|
||||||
9.5 -2.5 0
|
9.5 -2.5 0
|
||||||
6.108964 -2.5 4
|
6.108964 -2.5 4
|
||||||
6.108964 -2.5 4
|
6.108964 -2.5 4
|
||||||
9.5 -2.5 0
|
9.5 -2.5 0
|
||||||
6.108964 -2.5 0
|
6.108964 -2.5 0
|
||||||
3.4311862 -0.625 4
|
3.4311862 -0.625 4
|
||||||
4.323779 -1.25 4
|
4.323779 -1.25 4
|
||||||
4.323779 -1.25 0
|
4.323779 -1.25 0
|
||||||
4.323779 -1.25 4
|
4.323779 -1.25 4
|
||||||
6.108964 -2.5 4
|
6.108964 -2.5 4
|
||||||
6.108964 -2.5 0
|
6.108964 -2.5 0
|
||||||
3.4311862 -0.625 0
|
3.4311862 -0.625 0
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
3.4311862 -0.625 4
|
3.4311862 -0.625 4
|
||||||
3.4311862 -0.625 0
|
3.4311862 -0.625 0
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
4.323779 -1.25 4
|
4.323779 -1.25 4
|
||||||
6.108964 -2.5 0
|
6.108964 -2.5 0
|
||||||
4.323779 -1.25 0
|
4.323779 -1.25 0
|
||||||
3.4311862 -0.625 0
|
3.4311862 -0.625 0
|
||||||
3.4311862 -0.625 4
|
3.4311862 -0.625 4
|
||||||
4.323779 -1.25 0
|
4.323779 -1.25 0
|
||||||
3.342784 0.375 4
|
3.342784 0.375 4
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
4.146974 0.75 4
|
4.146974 0.75 4
|
||||||
3.342784 0.375 4
|
3.342784 0.375 4
|
||||||
3.342784 0.375 0
|
3.342784 0.375 0
|
||||||
3.342784 0.375 0
|
3.342784 0.375 0
|
||||||
4.146974 0.75 0
|
4.146974 0.75 0
|
||||||
4.146974 0.75 4
|
4.146974 0.75 4
|
||||||
4.146974 0.75 0
|
4.146974 0.75 0
|
||||||
5.755354 1.5 0
|
5.755354 1.5 0
|
||||||
5.755354 1.5 4
|
5.755354 1.5 4
|
||||||
3.342784 0.375 4
|
3.342784 0.375 4
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
3.342784 0.375 0
|
3.342784 0.375 0
|
||||||
5.755354 1.5 4
|
5.755354 1.5 4
|
||||||
4.146974 0.75 4
|
4.146974 0.75 4
|
||||||
4.146974 0.75 0
|
4.146974 0.75 0
|
||||||
5.755354 1.5 4
|
5.755354 1.5 4
|
||||||
5.755354 1.5 0
|
5.755354 1.5 0
|
||||||
9.5 1.5 4
|
9.5 1.5 4
|
||||||
9.5 1.5 4
|
9.5 1.5 4
|
||||||
5.755354 1.5 0
|
5.755354 1.5 0
|
||||||
9.5 1.5 0
|
9.5 1.5 0
|
||||||
9.5 1.5 4
|
9.5 1.5 4
|
||||||
9.5 1.5 0
|
9.5 1.5 0
|
||||||
9.5 2 4
|
9.5 2 4
|
||||||
9.5 2 4
|
9.5 2 4
|
||||||
9.5 1.5 0
|
9.5 1.5 0
|
||||||
9.5 2 0
|
9.5 2 0
|
||||||
9.5 2 4
|
9.5 2 4
|
||||||
9.5 2 0
|
9.5 2 0
|
||||||
5.644507 2 4
|
5.644507 2 4
|
||||||
5.644507 2 4
|
5.644507 2 4
|
||||||
9.5 2 0
|
9.5 2 0
|
||||||
5.644507 2 0
|
5.644507 2 0
|
||||||
5.644507 2 4
|
5.644507 2 4
|
||||||
5.644507 2 0
|
5.644507 2 0
|
||||||
3.5 1 4
|
3.5 1 4
|
||||||
3.5 1 4
|
3.5 1 4
|
||||||
5.644507 2 0
|
5.644507 2 0
|
||||||
3.5 1 0
|
3.5 1 0
|
||||||
3.5 1 4
|
3.5 1 4
|
||||||
3.5 1 0
|
3.5 1 0
|
||||||
0 1 4
|
0 1 4
|
||||||
0 1 4
|
0 1 4
|
||||||
3.5 1 0
|
3.5 1 0
|
||||||
0 1 0
|
0 1 0
|
||||||
0 1 4
|
0 1 4
|
||||||
0 1 0
|
0 1 0
|
||||||
0 0 4
|
0 0 4
|
||||||
0 0 4
|
0 0 4
|
||||||
0 1 0
|
0 1 0
|
||||||
0 0 0
|
0 0 0
|
||||||
3.342784 0.375 0
|
3.342784 0.375 0
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
3.5 1 0
|
3.5 1 0
|
||||||
3.4311862 -0.625 0
|
3.4311862 -0.625 0
|
||||||
4.323779 -1.25 0
|
4.323779 -1.25 0
|
||||||
3.0950184 -1 0
|
3.0950184 -1 0
|
||||||
3.342784 0.375 0
|
3.342784 0.375 0
|
||||||
3.5 1 0
|
3.5 1 0
|
||||||
4.146974 0.75 0
|
4.146974 0.75 0
|
||||||
4.323779 -1.25 0
|
4.323779 -1.25 0
|
||||||
5.9513144 -3 0
|
5.9513144 -3 0
|
||||||
3.0950184 -1 0
|
3.0950184 -1 0
|
||||||
0 -1 0
|
0 -1 0
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
3.0950184 -1 0
|
3.0950184 -1 0
|
||||||
0 -1 0
|
0 -1 0
|
||||||
0 0 0
|
0 0 0
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
9.5 -3 0
|
9.5 -3 0
|
||||||
6.108964 -2.5 0
|
6.108964 -2.5 0
|
||||||
9.5 -2.5 0
|
9.5 -2.5 0
|
||||||
9.5 -3 0
|
9.5 -3 0
|
||||||
5.9513144 -3 0
|
5.9513144 -3 0
|
||||||
6.108964 -2.5 0
|
6.108964 -2.5 0
|
||||||
5.9513144 -3 0
|
5.9513144 -3 0
|
||||||
4.323779 -1.25 0
|
4.323779 -1.25 0
|
||||||
6.108964 -2.5 0
|
6.108964 -2.5 0
|
||||||
5.644507 2 0
|
5.644507 2 0
|
||||||
5.755354 1.5 0
|
5.755354 1.5 0
|
||||||
4.146974 0.75 0
|
4.146974 0.75 0
|
||||||
3.0950184 -1 0
|
3.0950184 -1 0
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
3.4311862 -0.625 0
|
3.4311862 -0.625 0
|
||||||
4.146974 0.75 0
|
4.146974 0.75 0
|
||||||
3.5 1 0
|
3.5 1 0
|
||||||
5.644507 2 0
|
5.644507 2 0
|
||||||
9.5 1.5 0
|
9.5 1.5 0
|
||||||
5.755354 1.5 0
|
5.755354 1.5 0
|
||||||
9.5 2 0
|
9.5 2 0
|
||||||
5.755354 1.5 0
|
5.755354 1.5 0
|
||||||
5.644507 2 0
|
5.644507 2 0
|
||||||
9.5 2 0
|
9.5 2 0
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
0 0 0
|
0 0 0
|
||||||
0 1 0
|
0 1 0
|
||||||
3.5 1 0
|
3.5 1 0
|
||||||
2.5385938 0 0
|
2.5385938 0 0
|
||||||
0 1 0
|
0 1 0
|
||||||
3.342784 0.375 4
|
3.342784 0.375 4
|
||||||
3.5 1 4
|
3.5 1 4
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
4.146974 0.75 4
|
4.146974 0.75 4
|
||||||
3.5 1 4
|
3.5 1 4
|
||||||
3.342784 0.375 4
|
3.342784 0.375 4
|
||||||
3.4311862 -0.625 4
|
3.4311862 -0.625 4
|
||||||
3.0950184 -1 4
|
3.0950184 -1 4
|
||||||
4.323779 -1.25 4
|
4.323779 -1.25 4
|
||||||
4.146974 0.75 4
|
4.146974 0.75 4
|
||||||
5.755354 1.5 4
|
5.755354 1.5 4
|
||||||
5.644507 2 4
|
5.644507 2 4
|
||||||
0 1 4
|
0 1 4
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
3.5 1 4
|
3.5 1 4
|
||||||
0 1 4
|
0 1 4
|
||||||
0 0 4
|
0 0 4
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
5.644507 2 4
|
5.644507 2 4
|
||||||
5.755354 1.5 4
|
5.755354 1.5 4
|
||||||
9.5 2 4
|
9.5 2 4
|
||||||
9.5 2 4
|
9.5 2 4
|
||||||
5.755354 1.5 4
|
5.755354 1.5 4
|
||||||
9.5 1.5 4
|
9.5 1.5 4
|
||||||
4.146974 0.75 4
|
4.146974 0.75 4
|
||||||
5.644507 2 4
|
5.644507 2 4
|
||||||
3.5 1 4
|
3.5 1 4
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
3.0950184 -1 4
|
3.0950184 -1 4
|
||||||
3.4311862 -0.625 4
|
3.4311862 -0.625 4
|
||||||
4.323779 -1.25 4
|
4.323779 -1.25 4
|
||||||
3.0950184 -1 4
|
3.0950184 -1 4
|
||||||
5.9513144 -3 4
|
5.9513144 -3 4
|
||||||
6.108964 -2.5 4
|
6.108964 -2.5 4
|
||||||
4.323779 -1.25 4
|
4.323779 -1.25 4
|
||||||
5.9513144 -3 4
|
5.9513144 -3 4
|
||||||
9.5 -2.5 4
|
9.5 -2.5 4
|
||||||
6.108964 -2.5 4
|
6.108964 -2.5 4
|
||||||
9.5 -3 4
|
9.5 -3 4
|
||||||
6.108964 -2.5 4
|
6.108964 -2.5 4
|
||||||
5.9513144 -3 4
|
5.9513144 -3 4
|
||||||
9.5 -3 4
|
9.5 -3 4
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
0 -1 4
|
0 -1 4
|
||||||
3.0950184 -1 4
|
3.0950184 -1 4
|
||||||
0 -1 4
|
0 -1 4
|
||||||
2.5385938 0 4
|
2.5385938 0 4
|
||||||
0 0 4
|
0 0 4
|
||||||
3 0 1 2
|
3 0 1 2
|
||||||
3 3 4 5
|
3 3 4 5
|
||||||
3 6 7 8
|
3 6 7 8
|
||||||
3 9 10 11
|
3 9 10 11
|
||||||
3 12 13 14
|
3 12 13 14
|
||||||
3 15 16 17
|
3 15 16 17
|
||||||
3 18 19 20
|
3 18 19 20
|
||||||
3 21 22 23
|
3 21 22 23
|
||||||
3 24 25 26
|
3 24 25 26
|
||||||
3 27 28 29
|
3 27 28 29
|
||||||
3 30 31 32
|
3 30 31 32
|
||||||
3 33 34 35
|
3 33 34 35
|
||||||
3 36 37 38
|
3 36 37 38
|
||||||
3 39 40 41
|
3 39 40 41
|
||||||
3 42 43 44
|
3 42 43 44
|
||||||
3 45 46 47
|
3 45 46 47
|
||||||
3 48 49 50
|
3 48 49 50
|
||||||
3 51 52 53
|
3 51 52 53
|
||||||
3 54 55 56
|
3 54 55 56
|
||||||
3 57 58 59
|
3 57 58 59
|
||||||
3 60 61 62
|
3 60 61 62
|
||||||
3 63 64 65
|
3 63 64 65
|
||||||
3 66 67 68
|
3 66 67 68
|
||||||
3 69 70 71
|
3 69 70 71
|
||||||
3 72 73 74
|
3 72 73 74
|
||||||
3 75 76 77
|
3 75 76 77
|
||||||
3 78 79 80
|
3 78 79 80
|
||||||
3 81 82 83
|
3 81 82 83
|
||||||
3 84 85 86
|
3 84 85 86
|
||||||
3 87 88 89
|
3 87 88 89
|
||||||
3 90 91 92
|
3 90 91 92
|
||||||
3 93 94 95
|
3 93 94 95
|
||||||
3 96 97 98
|
3 96 97 98
|
||||||
3 99 100 101
|
3 99 100 101
|
||||||
3 102 103 104
|
3 102 103 104
|
||||||
3 105 106 107
|
3 105 106 107
|
||||||
3 108 109 110
|
3 108 109 110
|
||||||
3 111 112 113
|
3 111 112 113
|
||||||
3 114 115 116
|
3 114 115 116
|
||||||
3 117 118 119
|
3 117 118 119
|
||||||
3 120 121 122
|
3 120 121 122
|
||||||
3 123 124 125
|
3 123 124 125
|
||||||
3 126 127 128
|
3 126 127 128
|
||||||
3 129 130 131
|
3 129 130 131
|
||||||
3 132 133 134
|
3 132 133 134
|
||||||
3 135 136 137
|
3 135 136 137
|
||||||
3 138 139 140
|
3 138 139 140
|
||||||
3 141 142 143
|
3 141 142 143
|
||||||
3 144 145 146
|
3 144 145 146
|
||||||
3 147 148 149
|
3 147 148 149
|
||||||
3 150 151 152
|
3 150 151 152
|
||||||
3 153 154 155
|
3 153 154 155
|
||||||
3 156 157 158
|
3 156 157 158
|
||||||
3 159 160 161
|
3 159 160 161
|
||||||
3 162 163 164
|
3 162 163 164
|
||||||
3 165 166 167
|
3 165 166 167
|
||||||
3 168 169 170
|
3 168 169 170
|
||||||
3 171 172 173
|
3 171 172 173
|
||||||
3 174 175 176
|
3 174 175 176
|
||||||
3 177 178 179
|
3 177 178 179
|
||||||
3 180 181 182
|
3 180 181 182
|
||||||
3 183 184 185
|
3 183 184 185
|
||||||
3 186 187 188
|
3 186 187 188
|
||||||
3 189 190 191
|
3 189 190 191
|
||||||
3 192 193 194
|
3 192 193 194
|
||||||
3 195 196 197
|
3 195 196 197
|
||||||
3 198 199 200
|
3 198 199 200
|
||||||
3 201 202 203
|
3 201 202 203
|
||||||
|
Binary file not shown.
Binary file not shown.
@ -5,6 +5,7 @@ import { v4 as uuidv4 } from 'uuid'
|
|||||||
import { getUtils } from './test-utils'
|
import { getUtils } from './test-utils'
|
||||||
import waitOn from 'wait-on'
|
import waitOn from 'wait-on'
|
||||||
import { Themes } from '../../src/lib/theme'
|
import { Themes } from '../../src/lib/theme'
|
||||||
|
import { platform } from '@tauri-apps/api/os'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
|
||||||
@ -643,7 +644,11 @@ test('Command bar works and can change a setting', async ({ page }) => {
|
|||||||
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||||
|
|
||||||
// First try opening the command bar and closing it
|
// First try opening the command bar and closing it
|
||||||
await page.getByRole('button', { name: '⌘K' }).click()
|
// It has a different label on mac and windows/linux, "Meta+K" and "Ctrl+/" respectively
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'Ctrl+/' })
|
||||||
|
.or(page.getByRole('button', { name: '⌘K' }))
|
||||||
|
.click()
|
||||||
await expect(cmdSearchBar).toBeVisible()
|
await expect(cmdSearchBar).toBeVisible()
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
await expect(cmdSearchBar).not.toBeVisible()
|
await expect(cmdSearchBar).not.toBeVisible()
|
||||||
@ -658,12 +663,12 @@ test('Command bar works and can change a setting', async ({ page }) => {
|
|||||||
const themeOption = page.getByRole('option', { name: 'Set Theme' })
|
const themeOption = page.getByRole('option', { name: 'Set Theme' })
|
||||||
await expect(themeOption).toBeVisible()
|
await expect(themeOption).toBeVisible()
|
||||||
await themeOption.click()
|
await themeOption.click()
|
||||||
const themeInput = page.getByPlaceholder(Themes.System)
|
const themeInput = page.getByPlaceholder('Select an option')
|
||||||
await expect(themeInput).toBeVisible()
|
await expect(themeInput).toBeVisible()
|
||||||
await expect(themeInput).toBeFocused()
|
await expect(themeInput).toBeFocused()
|
||||||
// Select dark theme
|
// Select dark theme
|
||||||
await page.keyboard.press('ArrowDown')
|
await page.keyboard.press('ArrowDown')
|
||||||
await page.keyboard.press('ArrowUp')
|
await page.keyboard.press('ArrowDown')
|
||||||
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
|
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
|
||||||
'data-headlessui-state',
|
'data-headlessui-state',
|
||||||
'active'
|
'active'
|
||||||
@ -675,3 +680,59 @@ test('Command bar works and can change a setting', async ({ page }) => {
|
|||||||
// Check that the theme changed
|
// Check that the theme changed
|
||||||
await expect(page.locator('body')).toHaveClass(`body-bg ${Themes.Dark}`)
|
await expect(page.locator('body')).toHaveClass(`body-bg ${Themes.Dark}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Can extrude from the command bar', async ({ page, context }) => {
|
||||||
|
await context.addInitScript(async (token) => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([-6.95, 4.98], %)
|
||||||
|
|> line([25.1, 0.41], %)
|
||||||
|
|> line([0.73, -14.93], %)
|
||||||
|
|> line([-23.44, 0.52], %)
|
||||||
|
|> close(%)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const u = getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
let cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||||
|
await page.keyboard.press('Meta+K')
|
||||||
|
await expect(cmdSearchBar).toBeVisible()
|
||||||
|
|
||||||
|
// Search for extrude command and choose it
|
||||||
|
await page.getByRole('option', { name: 'Extrude' }).click()
|
||||||
|
await expect(page.locator('#arg-form > label')).toContainText(
|
||||||
|
'Please select one face'
|
||||||
|
)
|
||||||
|
await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled()
|
||||||
|
|
||||||
|
// Click to select face and set distance
|
||||||
|
await u.openAndClearDebugPanel()
|
||||||
|
await page.getByText('|> line([25.1, 0.41], %)').click()
|
||||||
|
await u.waitForCmdReceive('select_add')
|
||||||
|
await u.closeDebugPanel()
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click()
|
||||||
|
await expect(page.getByRole('button', { name: 'distance' })).toBeDisabled()
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
|
// Review step and argument hotkeys
|
||||||
|
await page.keyboard.press('2')
|
||||||
|
await expect(page.getByRole('button', { name: '5' })).toBeDisabled()
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
|
||||||
|
// Check that the code was updated
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
|
`const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([-6.95, 4.98], %)
|
||||||
|
|> line([25.1, 0.41], %)
|
||||||
|
|> line([0.73, -14.93], %)
|
||||||
|
|> line([-23.44, 0.52], %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(5, %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 113 KiB |
Binary file not shown.
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Binary file not shown.
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 54 KiB |
@ -8,7 +8,7 @@ import {
|
|||||||
createRoutesFromElements,
|
createRoutesFromElements,
|
||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
import { GlobalStateProvider } from './components/GlobalStateProvider'
|
||||||
import CommandBarProvider from 'components/CommandBar'
|
import CommandBarProvider from 'components/CommandBar/CommandBar'
|
||||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||||
import { BROWSER_FILE_NAME } from 'Router'
|
import { BROWSER_FILE_NAME } from 'Router'
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ import {
|
|||||||
settingsMachine,
|
settingsMachine,
|
||||||
} from './machines/settingsMachine'
|
} from './machines/settingsMachine'
|
||||||
import { ContextFrom } from 'xstate'
|
import { ContextFrom } from 'xstate'
|
||||||
import CommandBarProvider from 'components/CommandBar'
|
import CommandBarProvider from 'components/CommandBar/CommandBar'
|
||||||
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
||||||
import * as Sentry from '@sentry/react'
|
import * as Sentry from '@sentry/react'
|
||||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||||
|
@ -4,9 +4,11 @@ import { engineCommandManager } from './lang/std/engineConnection'
|
|||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
|
import usePlatform from 'hooks/usePlatform'
|
||||||
|
|
||||||
export const Toolbar = () => {
|
export const Toolbar = () => {
|
||||||
const { setCommandBarOpen } = useCommandsContext()
|
const platform = usePlatform()
|
||||||
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { state, send, context } = useModelingContext()
|
const { state, send, context } = useModelingContext()
|
||||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||||
const bgClassName =
|
const bgClassName =
|
||||||
@ -177,10 +179,15 @@ export const Toolbar = () => {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
onClick={() => send('extrude intent')}
|
onClick={() =>
|
||||||
disabled={!state.can('extrude intent')}
|
commandBarSend({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: { name: 'Extrude', ownerMachine: 'modeling' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={!state.can('Extrude')}
|
||||||
title={
|
title={
|
||||||
state.can('extrude intent')
|
state.can('Extrude')
|
||||||
? 'extrude'
|
? 'extrude'
|
||||||
: 'sketches need to be closed, or not already extruded'
|
: 'sketches need to be closed, or not already extruded'
|
||||||
}
|
}
|
||||||
@ -204,10 +211,10 @@ export const Toolbar = () => {
|
|||||||
</menu>
|
</menu>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => setCommandBarOpen(true)}
|
onClick={() => commandBarSend({ type: 'Open' })}
|
||||||
className="rounded-r-full pr-4 self-stretch border-energy-10 hover:border-energy-10 dark:border-chalkboard-80 bg-energy-10/50 hover:bg-energy-10 dark:bg-chalkboard-80 dark:text-energy-10"
|
className="rounded-r-full pr-4 self-stretch border-energy-10 hover:border-energy-10 dark:border-chalkboard-80 bg-energy-10/50 hover:bg-energy-10 dark:bg-chalkboard-80 dark:text-energy-10"
|
||||||
>
|
>
|
||||||
⌘K
|
{platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -7,6 +7,7 @@ import styles from './AppHeader.module.css'
|
|||||||
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
|
import { NetworkHealthIndicator } from './NetworkHealthIndicator'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
|
import usePlatform from 'hooks/usePlatform'
|
||||||
|
|
||||||
interface AppHeaderProps extends React.PropsWithChildren {
|
interface AppHeaderProps extends React.PropsWithChildren {
|
||||||
showToolbar?: boolean
|
showToolbar?: boolean
|
||||||
@ -22,7 +23,8 @@ export const AppHeader = ({
|
|||||||
className = '',
|
className = '',
|
||||||
enableMenu = false,
|
enableMenu = false,
|
||||||
}: AppHeaderProps) => {
|
}: AppHeaderProps) => {
|
||||||
const { setCommandBarOpen } = useCommandsContext()
|
const platform = usePlatform()
|
||||||
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { auth } = useGlobalStateContext()
|
const { auth } = useGlobalStateContext()
|
||||||
const user = auth?.context?.user
|
const user = auth?.context?.user
|
||||||
|
|
||||||
@ -47,12 +49,12 @@ export const AppHeader = ({
|
|||||||
) : (
|
) : (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => setCommandBarOpen(true)}
|
onClick={() => commandBarSend({ type: 'Open' })}
|
||||||
className="text-sm self-center flex items-center w-fit gap-3"
|
className="text-sm self-center flex items-center w-fit gap-3"
|
||||||
>
|
>
|
||||||
Command Palette{' '}
|
Command Palette{' '}
|
||||||
<kbd className="bg-energy-10/50 dark:bg-chalkboard-100 dark:text-energy-10 inline-block px-1 py-0.5 border-energy-10 dark:border-chalkboard-90">
|
<kbd className="bg-energy-10/50 dark:bg-chalkboard-100 dark:text-energy-10 inline-block px-1 py-0.5 border-energy-10 dark:border-chalkboard-90">
|
||||||
⌘K
|
{platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
|
||||||
</kbd>
|
</kbd>
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)}
|
)}
|
||||||
|
@ -24,13 +24,13 @@ export const PanelHeader = ({
|
|||||||
}: CollapsiblePanelProps) => {
|
}: CollapsiblePanelProps) => {
|
||||||
return (
|
return (
|
||||||
<summary className={styles.header}>
|
<summary className={styles.header}>
|
||||||
<div className="flex gap-2 align-center items-center flex-1">
|
<div className="flex gap-2 items-center flex-1">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon={icon}
|
icon={icon}
|
||||||
className="p-1"
|
className="p-1"
|
||||||
size="sm"
|
size="sm"
|
||||||
bgClassName={
|
bgClassName={
|
||||||
'dark:!bg-chalkboard-100 group-open:bg-chalkboard-80 dark:group-open:!bg-chalkboard-90 group-open:border dark:group-open:border-chalkboard-60 rounded-sm ' +
|
'dark:!bg-chalkboard-100 group-open:bg-chalkboard-80 dark:group-open:!bg-chalkboard-90 border border-transparent dark:group-open:border-chalkboard-60 rounded-sm ' +
|
||||||
(iconClassNames?.bg || '')
|
(iconClassNames?.bg || '')
|
||||||
}
|
}
|
||||||
iconClassName={
|
iconClassName={
|
||||||
|
@ -1,404 +0,0 @@
|
|||||||
import { Combobox, Dialog, Transition } from '@headlessui/react'
|
|
||||||
import {
|
|
||||||
Dispatch,
|
|
||||||
Fragment,
|
|
||||||
SetStateAction,
|
|
||||||
createContext,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
|
||||||
import Fuse from 'fuse.js'
|
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandArgument,
|
|
||||||
CommandArgumentOption,
|
|
||||||
} from '../lib/commands'
|
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { CustomIcon } from './CustomIcon'
|
|
||||||
|
|
||||||
type ComboboxOption = Command | CommandArgumentOption
|
|
||||||
type CommandArgumentData = [string, any]
|
|
||||||
|
|
||||||
export const CommandsContext = createContext(
|
|
||||||
{} as {
|
|
||||||
commands: Command[]
|
|
||||||
addCommands: (commands: Command[]) => void
|
|
||||||
removeCommands: (commands: Command[]) => void
|
|
||||||
commandBarOpen: boolean
|
|
||||||
setCommandBarOpen: Dispatch<SetStateAction<boolean>>
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const CommandBarProvider = ({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) => {
|
|
||||||
const [commands, internalSetCommands] = useState([] as Command[])
|
|
||||||
const [commandBarOpen, setCommandBarOpen] = useState(false)
|
|
||||||
|
|
||||||
function sortCommands(a: Command, b: Command) {
|
|
||||||
if (b.owner === 'auth') return -1
|
|
||||||
if (a.owner === 'auth') return 1
|
|
||||||
return a.name.localeCompare(b.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => console.log('commands updated', commands), [commands])
|
|
||||||
|
|
||||||
const addCommands = (newCommands: Command[]) => {
|
|
||||||
internalSetCommands((prevCommands) =>
|
|
||||||
[...newCommands, ...prevCommands].sort(sortCommands)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const removeCommands = (newCommands: Command[]) => {
|
|
||||||
internalSetCommands((prevCommands) =>
|
|
||||||
prevCommands
|
|
||||||
.filter((command) => !newCommands.includes(command))
|
|
||||||
.sort(sortCommands)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CommandsContext.Provider
|
|
||||||
value={{
|
|
||||||
commands,
|
|
||||||
addCommands,
|
|
||||||
removeCommands,
|
|
||||||
commandBarOpen,
|
|
||||||
setCommandBarOpen,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<CommandBar />
|
|
||||||
</CommandsContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CommandBar = () => {
|
|
||||||
const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
|
|
||||||
useHotkeys(['meta+k', 'meta+/'], () => {
|
|
||||||
if (commands?.length === 0) return
|
|
||||||
setCommandBarOpen(!commandBarOpen)
|
|
||||||
})
|
|
||||||
|
|
||||||
const [selectedCommand, setSelectedCommand] = useState<Command>()
|
|
||||||
const [commandArguments, setCommandArguments] = useState<CommandArgument[]>(
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
const [commandArgumentData, setCommandArgumentData] = useState<
|
|
||||||
CommandArgumentData[]
|
|
||||||
>([])
|
|
||||||
const [commandArgumentIndex, setCommandArgumentIndex] = useState<number>(0)
|
|
||||||
|
|
||||||
function clearState() {
|
|
||||||
setCommandBarOpen(false)
|
|
||||||
setSelectedCommand(undefined)
|
|
||||||
setCommandArguments([])
|
|
||||||
setCommandArgumentData([])
|
|
||||||
setCommandArgumentIndex(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectCommand(command: Command) {
|
|
||||||
console.log('selecting command', command)
|
|
||||||
if (!('args' in command && command.args?.length)) {
|
|
||||||
submitCommand({ command })
|
|
||||||
} else {
|
|
||||||
setCommandArguments(command.args)
|
|
||||||
setSelectedCommand(command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stepBack() {
|
|
||||||
if (!selectedCommand) {
|
|
||||||
clearState()
|
|
||||||
} else {
|
|
||||||
if (commandArgumentIndex === 0) {
|
|
||||||
setSelectedCommand(undefined)
|
|
||||||
} else {
|
|
||||||
setCommandArgumentIndex((prevIndex) => Math.max(0, prevIndex - 1))
|
|
||||||
}
|
|
||||||
if (commandArgumentData.length > 0) {
|
|
||||||
setCommandArgumentData((prevData) => prevData.slice(0, -1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendCommandArgumentData(data: { name: any }) {
|
|
||||||
const transformedData = [
|
|
||||||
commandArguments[commandArgumentIndex].name,
|
|
||||||
data.name,
|
|
||||||
]
|
|
||||||
if (commandArgumentIndex + 1 === commandArguments.length) {
|
|
||||||
submitCommand({
|
|
||||||
dataArr: [
|
|
||||||
...commandArgumentData,
|
|
||||||
transformedData,
|
|
||||||
] as CommandArgumentData[],
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setCommandArgumentData(
|
|
||||||
(prevData) => [...prevData, transformedData] as CommandArgumentData[]
|
|
||||||
)
|
|
||||||
setCommandArgumentIndex((prevIndex) => prevIndex + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitCommand({
|
|
||||||
command = selectedCommand,
|
|
||||||
dataArr = commandArgumentData,
|
|
||||||
}) {
|
|
||||||
console.log('submitting command', command, dataArr)
|
|
||||||
if (dataArr.length === 0) {
|
|
||||||
command?.callback()
|
|
||||||
} else {
|
|
||||||
const data = Object.fromEntries(dataArr)
|
|
||||||
console.log('submitting data', data)
|
|
||||||
command?.callback(data)
|
|
||||||
}
|
|
||||||
setCommandBarOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDisplayValue(command: Command) {
|
|
||||||
if (
|
|
||||||
'args' in command &&
|
|
||||||
command.args &&
|
|
||||||
command.args?.length > 0 &&
|
|
||||||
'formatFunction' in command &&
|
|
||||||
command.formatFunction
|
|
||||||
) {
|
|
||||||
command.formatFunction(
|
|
||||||
command.args.map((c, i) =>
|
|
||||||
commandArgumentData[i] ? commandArgumentData[i][0] : `<${c.name}>`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return command.name
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root
|
|
||||||
show={commandBarOpen || false}
|
|
||||||
afterLeave={() => clearState()}
|
|
||||||
as={Fragment}
|
|
||||||
>
|
|
||||||
<Dialog
|
|
||||||
onClose={() => {
|
|
||||||
setCommandBarOpen(false)
|
|
||||||
}}
|
|
||||||
className="fixed inset-0 z-40 overflow-y-auto pb-4 pt-1"
|
|
||||||
>
|
|
||||||
<Transition.Child
|
|
||||||
enter="duration-100 ease-out"
|
|
||||||
enterFrom="opacity-0 scale-95"
|
|
||||||
enterTo="opacity-100 scale-100"
|
|
||||||
leave="duration-75 ease-in"
|
|
||||||
leaveFrom="opacity-100 scale-100"
|
|
||||||
leaveTo="opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Dialog.Panel
|
|
||||||
className="relative w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
|
|
||||||
as="div"
|
|
||||||
>
|
|
||||||
{!(
|
|
||||||
commandArguments &&
|
|
||||||
commandArguments.length &&
|
|
||||||
selectedCommand
|
|
||||||
) ? (
|
|
||||||
<CommandComboBox
|
|
||||||
options={commands}
|
|
||||||
handleSelection={selectCommand}
|
|
||||||
stepBack={stepBack}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="px-4 text-sm flex flex-wrap gap-2">
|
|
||||||
<p className="pr-4 flex gap-2 items-center">
|
|
||||||
{selectedCommand &&
|
|
||||||
'icon' in selectedCommand &&
|
|
||||||
selectedCommand.icon && (
|
|
||||||
<CustomIcon
|
|
||||||
name={selectedCommand.icon}
|
|
||||||
className="w-5 h-5"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{getDisplayValue(selectedCommand)}
|
|
||||||
</p>
|
|
||||||
{commandArguments.map((arg, i) => (
|
|
||||||
<p
|
|
||||||
key={arg.name}
|
|
||||||
className={`w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
|
|
||||||
i === commandArgumentIndex
|
|
||||||
? 'bg-energy-10/50 dark:bg-energy-10/20 border-energy-10 dark:border-energy-10'
|
|
||||||
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{commandArgumentIndex >= i && commandArgumentData[i] ? (
|
|
||||||
commandArgumentData[i][1]
|
|
||||||
) : arg.defaultValue ? (
|
|
||||||
arg.defaultValue
|
|
||||||
) : (
|
|
||||||
<em>{arg.name}</em>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="block w-full my-2 h-[1px] bg-chalkboard-20 dark:bg-chalkboard-80" />
|
|
||||||
<Argument
|
|
||||||
arg={commandArguments[commandArgumentIndex]}
|
|
||||||
appendCommandArgumentData={appendCommandArgumentData}
|
|
||||||
stepBack={stepBack}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Argument({
|
|
||||||
arg,
|
|
||||||
appendCommandArgumentData,
|
|
||||||
stepBack,
|
|
||||||
}: {
|
|
||||||
arg: CommandArgument
|
|
||||||
appendCommandArgumentData: Dispatch<SetStateAction<any>>
|
|
||||||
stepBack: () => void
|
|
||||||
}) {
|
|
||||||
const { setCommandBarOpen } = useCommandsContext()
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (inputRef.current) {
|
|
||||||
inputRef.current.focus()
|
|
||||||
inputRef.current.select()
|
|
||||||
}
|
|
||||||
}, [arg, inputRef])
|
|
||||||
|
|
||||||
return arg.type === 'select' ? (
|
|
||||||
<CommandComboBox
|
|
||||||
options={arg.options}
|
|
||||||
handleSelection={appendCommandArgumentData}
|
|
||||||
stepBack={stepBack}
|
|
||||||
placeholder="Select an option"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<form
|
|
||||||
onSubmit={(event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
appendCommandArgumentData({ name: inputRef.current?.value })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label className="flex items-center mx-4 my-4">
|
|
||||||
<span className="px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80">
|
|
||||||
{arg.name}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
|
||||||
placeholder="Enter a value"
|
|
||||||
defaultValue={arg.defaultValue}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.metaKey && event.key === 'k') setCommandBarOpen(false)
|
|
||||||
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
|
||||||
stepBack()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CommandBarProvider
|
|
||||||
|
|
||||||
function CommandComboBox({
|
|
||||||
options,
|
|
||||||
handleSelection,
|
|
||||||
stepBack,
|
|
||||||
placeholder,
|
|
||||||
}: {
|
|
||||||
options: ComboboxOption[]
|
|
||||||
handleSelection: Dispatch<SetStateAction<any>>
|
|
||||||
stepBack: () => void
|
|
||||||
placeholder?: string
|
|
||||||
}) {
|
|
||||||
const { setCommandBarOpen } = useCommandsContext()
|
|
||||||
const [query, setQuery] = useState('')
|
|
||||||
const [filteredOptions, setFilteredOptions] = useState<ComboboxOption[]>()
|
|
||||||
|
|
||||||
const defaultOption =
|
|
||||||
options.find((o) => 'isCurrent' in o && o.isCurrent) || null
|
|
||||||
|
|
||||||
const fuse = new Fuse(options, {
|
|
||||||
keys: ['name', 'description'],
|
|
||||||
threshold: 0.3,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const results = fuse.search(query).map((result) => result.item)
|
|
||||||
setFilteredOptions(query.length > 0 ? results : options)
|
|
||||||
}, [query])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Combobox defaultValue={defaultOption} onChange={handleSelection}>
|
|
||||||
<div className="flex items-center gap-2 px-4 pb-2 border-solid border-0 border-b border-b-chalkboard-20 dark:border-b-chalkboard-80">
|
|
||||||
<CustomIcon
|
|
||||||
name="search"
|
|
||||||
className="w-5 h-5 bg-energy-10/50 dark:bg-chalkboard-90 dark:text-energy-10"
|
|
||||||
/>
|
|
||||||
<Combobox.Input
|
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
|
||||||
className="w-full bg-transparent focus:outline-none selection:bg-energy-10/50 dark:selection:bg-energy-10/20 dark:focus:outline-none"
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.metaKey && event.key === 'k') setCommandBarOpen(false)
|
|
||||||
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
|
||||||
stepBack()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={
|
|
||||||
(defaultOption && defaultOption.name) ||
|
|
||||||
placeholder ||
|
|
||||||
'Search commands'
|
|
||||||
}
|
|
||||||
autoCapitalize="off"
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
spellCheck="false"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Combobox.Options
|
|
||||||
static
|
|
||||||
className="overflow-y-auto max-h-96 cursor-pointer"
|
|
||||||
>
|
|
||||||
{filteredOptions?.map((option) => (
|
|
||||||
<Combobox.Option
|
|
||||||
key={option.name}
|
|
||||||
value={option}
|
|
||||||
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
|
|
||||||
>
|
|
||||||
{'icon' in option && option.icon && (
|
|
||||||
<CustomIcon
|
|
||||||
name={option.icon}
|
|
||||||
className="w-5 h-5 dark:text-energy-10"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<p className="flex-grow">{option.name} </p>
|
|
||||||
{'isCurrent' in option && option.isCurrent && (
|
|
||||||
<small className="text-chalkboard-70 dark:text-chalkboard-50">
|
|
||||||
current
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</Combobox.Option>
|
|
||||||
))}
|
|
||||||
</Combobox.Options>
|
|
||||||
</Combobox>
|
|
||||||
)
|
|
||||||
}
|
|
114
src/components/CommandBar/CommandArgOptionInput.tsx
Normal file
114
src/components/CommandBar/CommandArgOptionInput.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { Combobox } from '@headlessui/react'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { CommandArgumentOption } from 'lib/commandTypes'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
function CommandArgOptionInput({
|
||||||
|
options,
|
||||||
|
argName,
|
||||||
|
stepBack,
|
||||||
|
onSubmit,
|
||||||
|
placeholder,
|
||||||
|
}: {
|
||||||
|
options: CommandArgumentOption<unknown>[]
|
||||||
|
argName: string
|
||||||
|
stepBack: () => void
|
||||||
|
onSubmit: (data: unknown) => void
|
||||||
|
placeholder?: string
|
||||||
|
}) {
|
||||||
|
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const formRef = useRef<HTMLFormElement>(null)
|
||||||
|
const [argValue, setArgValue] = useState<(typeof options)[number]['value']>(
|
||||||
|
options.find((o) => 'isCurrent' in o && o.isCurrent)?.value ||
|
||||||
|
commandBarState.context.argumentsToSubmit[argName] ||
|
||||||
|
options[0].value
|
||||||
|
)
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
|
||||||
|
|
||||||
|
const fuse = new Fuse(options, {
|
||||||
|
keys: ['name', 'description'],
|
||||||
|
threshold: 0.3,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
inputRef.current?.focus()
|
||||||
|
inputRef.current?.select()
|
||||||
|
}, [inputRef])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const results = fuse.search(query).map((result) => result.item)
|
||||||
|
setFilteredOptions(query.length > 0 ? results : options)
|
||||||
|
}, [query])
|
||||||
|
|
||||||
|
function handleSelectOption(option: CommandArgumentOption<unknown>) {
|
||||||
|
setArgValue(option)
|
||||||
|
onSubmit(option.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
|
onSubmit(argValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form id="arg-form" onSubmit={handleSubmit} ref={formRef}>
|
||||||
|
<Combobox value={argValue} onChange={handleSelectOption} name="options">
|
||||||
|
<div className="flex items-center mx-4 mt-4 mb-2">
|
||||||
|
<label
|
||||||
|
htmlFor="option-input"
|
||||||
|
className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
|
||||||
|
>
|
||||||
|
{argName}
|
||||||
|
</label>
|
||||||
|
<Combobox.Input
|
||||||
|
id="option-input"
|
||||||
|
ref={inputRef}
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.metaKey && event.key === 'k')
|
||||||
|
commandBarSend({ type: 'Close' })
|
||||||
|
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
||||||
|
stepBack()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={
|
||||||
|
(argValue as CommandArgumentOption<unknown>)?.name ||
|
||||||
|
placeholder ||
|
||||||
|
'Select an option for ' + argName
|
||||||
|
}
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
spellCheck="false"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Combobox.Options
|
||||||
|
static
|
||||||
|
className="overflow-y-auto max-h-96 cursor-pointer"
|
||||||
|
>
|
||||||
|
{filteredOptions?.map((option) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={option.name}
|
||||||
|
value={option}
|
||||||
|
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
|
||||||
|
>
|
||||||
|
<p className="flex-grow">{option.name} </p>
|
||||||
|
{'isCurrent' in option && option.isCurrent && (
|
||||||
|
<small className="text-chalkboard-70 dark:text-chalkboard-50">
|
||||||
|
current
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
</Combobox>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandArgOptionInput
|
166
src/components/CommandBar/CommandBar.tsx
Normal file
166
src/components/CommandBar/CommandBar.tsx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import { Dialog, Popover, Transition } from '@headlessui/react'
|
||||||
|
import { Fragment, createContext, useEffect } from 'react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { useMachine } from '@xstate/react'
|
||||||
|
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||||
|
import { EventFrom, StateFrom } from 'xstate'
|
||||||
|
import CommandBarArgument from './CommandBarArgument'
|
||||||
|
import CommandComboBox from '../CommandComboBox'
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
|
import CommandBarReview from './CommandBarReview'
|
||||||
|
|
||||||
|
type CommandsContextType = {
|
||||||
|
commandBarState: StateFrom<typeof commandBarMachine>
|
||||||
|
commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommandsContext = createContext<CommandsContextType>({
|
||||||
|
commandBarState: commandBarMachine.initialState,
|
||||||
|
commandBarSend: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CommandBarProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) => {
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
|
||||||
|
guards: {
|
||||||
|
'Arguments are ready': (context, _) => {
|
||||||
|
return context.selectedCommand?.args
|
||||||
|
? context.argumentsToSubmit.length ===
|
||||||
|
Object.keys(context.selectedCommand.args)?.length
|
||||||
|
: false
|
||||||
|
},
|
||||||
|
'Command has no arguments': (context, _event) => {
|
||||||
|
return (
|
||||||
|
!context.selectedCommand?.args ||
|
||||||
|
Object.keys(context.selectedCommand?.args).length === 0
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Close the command bar when navigating
|
||||||
|
useEffect(() => {
|
||||||
|
commandBarSend({ type: 'Close' })
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandsContext.Provider
|
||||||
|
value={{
|
||||||
|
commandBarState,
|
||||||
|
commandBarSend,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<CommandBar />
|
||||||
|
</CommandsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommandBar = () => {
|
||||||
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
|
const {
|
||||||
|
context: { selectedCommand, currentArgument, commands },
|
||||||
|
} = commandBarState
|
||||||
|
const isSelectionArgument = currentArgument?.inputType === 'selection'
|
||||||
|
const WrapperComponent = isSelectionArgument ? Popover : Dialog
|
||||||
|
|
||||||
|
useHotkeys(['mod+k', 'mod+/'], () => {
|
||||||
|
if (commandBarState.context.commands.length === 0) return
|
||||||
|
if (commandBarState.matches('Closed')) {
|
||||||
|
commandBarSend({ type: 'Open' })
|
||||||
|
} else {
|
||||||
|
commandBarSend({ type: 'Close' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function stepBack() {
|
||||||
|
if (!currentArgument) {
|
||||||
|
if (commandBarState.matches('Review')) {
|
||||||
|
const entries = Object.entries(selectedCommand?.args || {})
|
||||||
|
|
||||||
|
commandBarSend({
|
||||||
|
type: commandBarState.matches('Review')
|
||||||
|
? 'Edit argument'
|
||||||
|
: 'Change current argument',
|
||||||
|
data: {
|
||||||
|
arg: {
|
||||||
|
name: entries[entries.length - 1][0],
|
||||||
|
...entries[entries.length - 1][1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
commandBarSend({ type: 'Deselect command' })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const entries = Object.entries(selectedCommand?.args || {})
|
||||||
|
const index = entries.findIndex(
|
||||||
|
([key, _]) => key === currentArgument.name
|
||||||
|
)
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
commandBarSend({ type: 'Deselect command' })
|
||||||
|
} else {
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Change current argument',
|
||||||
|
data: {
|
||||||
|
arg: { name: entries[index - 1][0], ...entries[index - 1][1] },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root
|
||||||
|
show={!commandBarState.matches('Closed') || false}
|
||||||
|
afterLeave={() => {
|
||||||
|
if (selectedCommand?.onCancel) selectedCommand.onCancel()
|
||||||
|
commandBarSend({ type: 'Clear' })
|
||||||
|
}}
|
||||||
|
as={Fragment}
|
||||||
|
>
|
||||||
|
<WrapperComponent
|
||||||
|
open={!commandBarState.matches('Closed') || isSelectionArgument}
|
||||||
|
onClose={() => {
|
||||||
|
commandBarSend({ type: 'Close' })
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
|
||||||
|
(isSelectionArgument ? 'pointer-events-none' : '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Transition.Child
|
||||||
|
enter="duration-100 ease-out"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="duration-75 ease-in"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<WrapperComponent.Panel
|
||||||
|
className="relative z-50 pointer-events-auto w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
|
||||||
|
as="div"
|
||||||
|
>
|
||||||
|
{commandBarState.matches('Selecting command') ? (
|
||||||
|
<CommandComboBox options={commands} />
|
||||||
|
) : commandBarState.matches('Gathering arguments') ? (
|
||||||
|
<CommandBarArgument stepBack={stepBack} />
|
||||||
|
) : (
|
||||||
|
commandBarState.matches('Review') && (
|
||||||
|
<CommandBarReview stepBack={stepBack} />
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</WrapperComponent.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</WrapperComponent>
|
||||||
|
</Transition.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandBarProvider
|
80
src/components/CommandBar/CommandBarArgument.tsx
Normal file
80
src/components/CommandBar/CommandBarArgument.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import CommandArgOptionInput from './CommandArgOptionInput'
|
||||||
|
import CommandBarBasicInput from './CommandBarBasicInput'
|
||||||
|
import CommandBarSelectionInput from './CommandBarSelectionInput'
|
||||||
|
import { CommandArgument } from 'lib/commandTypes'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import CommandBarHeader from './CommandBarHeader'
|
||||||
|
|
||||||
|
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
|
||||||
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
|
const {
|
||||||
|
context: { currentArgument },
|
||||||
|
} = commandBarState
|
||||||
|
|
||||||
|
function onSubmit(data: unknown) {
|
||||||
|
if (!currentArgument) return
|
||||||
|
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Submit argument',
|
||||||
|
data: {
|
||||||
|
[currentArgument.name]:
|
||||||
|
currentArgument.inputType === 'number'
|
||||||
|
? parseFloat((data as string) || '0')
|
||||||
|
: data,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
currentArgument && (
|
||||||
|
<CommandBarHeader>
|
||||||
|
<ArgumentInput
|
||||||
|
arg={currentArgument}
|
||||||
|
stepBack={stepBack}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
|
</CommandBarHeader>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandBarArgument
|
||||||
|
|
||||||
|
function ArgumentInput({
|
||||||
|
arg,
|
||||||
|
stepBack,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
arg: CommandArgument<unknown> & { name: string }
|
||||||
|
stepBack: () => void
|
||||||
|
onSubmit: (event: any) => void
|
||||||
|
}) {
|
||||||
|
switch (arg.inputType) {
|
||||||
|
case 'options':
|
||||||
|
return (
|
||||||
|
<CommandArgOptionInput
|
||||||
|
options={arg.options}
|
||||||
|
argName={arg.name}
|
||||||
|
stepBack={stepBack}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
placeholder="Select an option"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case 'selection':
|
||||||
|
return (
|
||||||
|
<CommandBarSelectionInput
|
||||||
|
arg={arg}
|
||||||
|
stepBack={stepBack}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<CommandBarBasicInput
|
||||||
|
arg={arg}
|
||||||
|
stepBack={stepBack}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
66
src/components/CommandBar/CommandBarBasicInput.tsx
Normal file
66
src/components/CommandBar/CommandBarBasicInput.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { CommandArgument } from 'lib/commandTypes'
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
|
function CommandBarBasicInput({
|
||||||
|
arg,
|
||||||
|
stepBack,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
arg: CommandArgument<unknown> & {
|
||||||
|
inputType: 'number' | 'string'
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
stepBack: () => void
|
||||||
|
onSubmit: (event: unknown) => void
|
||||||
|
}) {
|
||||||
|
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||||
|
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const inputType = arg.inputType === 'number' ? 'number' : 'text'
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.focus()
|
||||||
|
inputRef.current.select()
|
||||||
|
}
|
||||||
|
}, [arg, inputRef])
|
||||||
|
|
||||||
|
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
|
onSubmit(inputRef.current?.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form id="arg-form" onSubmit={handleSubmit}>
|
||||||
|
<label className="flex items-center mx-4 my-4">
|
||||||
|
<span className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80">
|
||||||
|
{arg.name}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
id="arg-form"
|
||||||
|
name={inputType}
|
||||||
|
ref={inputRef}
|
||||||
|
type={inputType}
|
||||||
|
required
|
||||||
|
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
||||||
|
placeholder="Enter a value"
|
||||||
|
defaultValue={
|
||||||
|
(commandBarState.context.argumentsToSubmit[arg.name] as
|
||||||
|
| string
|
||||||
|
| undefined) || (arg.defaultValue as string)
|
||||||
|
}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
||||||
|
stepBack()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandBarBasicInput
|
171
src/components/CommandBar/CommandBarHeader.tsx
Normal file
171
src/components/CommandBar/CommandBarHeader.tsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { CustomIcon } from '../CustomIcon'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { ActionButton } from '../ActionButton'
|
||||||
|
import { Selections, getSelectionTypeDisplayText } from 'lib/selections'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
|
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||||
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
|
const {
|
||||||
|
context: { selectedCommand, currentArgument, argumentsToSubmit },
|
||||||
|
} = commandBarState
|
||||||
|
const isReviewing = commandBarState.matches('Review')
|
||||||
|
const [showShortcuts, setShowShortcuts] = useState(false)
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'alt',
|
||||||
|
() => setShowShortcuts(true),
|
||||||
|
{ enableOnFormTags: true, enableOnContentEditable: true },
|
||||||
|
[showShortcuts]
|
||||||
|
)
|
||||||
|
useHotkeys(
|
||||||
|
'alt',
|
||||||
|
() => setShowShortcuts(false),
|
||||||
|
{ keyup: true, enableOnFormTags: true, enableOnContentEditable: true },
|
||||||
|
[showShortcuts]
|
||||||
|
)
|
||||||
|
useHotkeys(
|
||||||
|
[
|
||||||
|
'alt+1',
|
||||||
|
'alt+2',
|
||||||
|
'alt+3',
|
||||||
|
'alt+4',
|
||||||
|
'alt+5',
|
||||||
|
'alt+6',
|
||||||
|
'alt+7',
|
||||||
|
'alt+8',
|
||||||
|
'alt+9',
|
||||||
|
'alt+0',
|
||||||
|
],
|
||||||
|
(_, b) => {
|
||||||
|
if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
|
||||||
|
if (!selectedCommand?.args) return
|
||||||
|
const argName = Object.keys(selectedCommand.args)[
|
||||||
|
parseInt(b.keys[0], 10) - 1
|
||||||
|
]
|
||||||
|
const arg = selectedCommand?.args[argName]
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Change current argument',
|
||||||
|
data: { arg: { ...arg, name: argName } },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ keyup: true, enableOnFormTags: true, enableOnContentEditable: true },
|
||||||
|
[argumentsToSubmit, selectedCommand]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
selectedCommand &&
|
||||||
|
argumentsToSubmit && (
|
||||||
|
<>
|
||||||
|
<div className="px-4 text-sm flex gap-4 items-start">
|
||||||
|
<div className="flex flex-1 flex-wrap gap-2">
|
||||||
|
<p
|
||||||
|
data-command-name={selectedCommand?.name}
|
||||||
|
className="pr-4 flex gap-2 items-center"
|
||||||
|
>
|
||||||
|
{selectedCommand &&
|
||||||
|
'icon' in selectedCommand &&
|
||||||
|
selectedCommand.icon && (
|
||||||
|
<CustomIcon name={selectedCommand.icon} className="w-5 h-5" />
|
||||||
|
)}
|
||||||
|
{selectedCommand?.name}
|
||||||
|
</p>
|
||||||
|
{Object.entries(selectedCommand?.args || {}).map(
|
||||||
|
([argName, arg], i) => (
|
||||||
|
<button
|
||||||
|
disabled={!isReviewing && currentArgument?.name === argName}
|
||||||
|
onClick={() => {
|
||||||
|
commandBarSend({
|
||||||
|
type: isReviewing
|
||||||
|
? 'Edit argument'
|
||||||
|
: 'Change current argument',
|
||||||
|
data: { arg: { ...arg, name: argName } },
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
key={argName}
|
||||||
|
className={`relative w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
|
||||||
|
argName === currentArgument?.name
|
||||||
|
? 'disabled:bg-energy-10/50 dark:disabled:bg-energy-10/20 disabled:border-energy-10 dark:disabled:border-energy-10 disabled:text-chalkboard-100 dark:disabled:text-chalkboard-10'
|
||||||
|
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{argumentsToSubmit[argName] ? (
|
||||||
|
arg.inputType === 'selection' ? (
|
||||||
|
getSelectionTypeDisplayText(
|
||||||
|
argumentsToSubmit[argName] as Selections
|
||||||
|
)
|
||||||
|
) : typeof argumentsToSubmit[argName] === 'object' ? (
|
||||||
|
JSON.stringify(argumentsToSubmit[argName])
|
||||||
|
) : (
|
||||||
|
argumentsToSubmit[argName]
|
||||||
|
)
|
||||||
|
) : arg.payload ? (
|
||||||
|
arg.inputType === 'selection' ? (
|
||||||
|
getSelectionTypeDisplayText(arg.payload as Selections)
|
||||||
|
) : typeof arg.payload === 'object' ? (
|
||||||
|
JSON.stringify(arg.payload)
|
||||||
|
) : (
|
||||||
|
arg.payload
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<em>{argName}</em>
|
||||||
|
)}
|
||||||
|
{showShortcuts && (
|
||||||
|
<small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100">
|
||||||
|
<span className="sr-only">Hotkey: </span>
|
||||||
|
{i + 1}
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isReviewing ? <ReviewingButton /> : <GatheringArgsButton />}
|
||||||
|
</div>
|
||||||
|
<div className="block w-full my-2 h-[1px] bg-chalkboard-20 dark:bg-chalkboard-80" />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReviewingButton() {
|
||||||
|
return (
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
autoFocus
|
||||||
|
type="submit"
|
||||||
|
form="review-form"
|
||||||
|
className="w-fit !p-0 rounded-sm border !border-chalkboard-100 dark:!border-energy-10 hover:shadow"
|
||||||
|
icon={{
|
||||||
|
icon: 'checkmark',
|
||||||
|
bgClassName:
|
||||||
|
'p-1 rounded-sm !bg-chalkboard-100 hover:!bg-chalkboard-110 dark:!bg-energy-20 dark:hover:!bg-energy-10',
|
||||||
|
iconClassName: '!text-energy-10 dark:!text-chalkboard-100',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Submit command</span>
|
||||||
|
</ActionButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function GatheringArgsButton() {
|
||||||
|
return (
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
type="submit"
|
||||||
|
form="arg-form"
|
||||||
|
className="w-fit !p-0 rounded-sm"
|
||||||
|
icon={{
|
||||||
|
icon: 'arrowRight',
|
||||||
|
bgClassName: 'p-1 rounded-sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Continue</span>
|
||||||
|
</ActionButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandBarHeader
|
81
src/components/CommandBar/CommandBarReview.tsx
Normal file
81
src/components/CommandBar/CommandBarReview.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import CommandBarHeader from './CommandBarHeader'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
|
function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
||||||
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
|
const {
|
||||||
|
context: { argumentsToSubmit, selectedCommand },
|
||||||
|
} = commandBarState
|
||||||
|
|
||||||
|
useHotkeys('backspace', stepBack, {
|
||||||
|
enableOnFormTags: true,
|
||||||
|
enableOnContentEditable: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'1, 2, 3, 4, 5, 6, 7, 8, 9, 0',
|
||||||
|
(_, b) => {
|
||||||
|
if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
|
||||||
|
if (!selectedCommand?.args) return
|
||||||
|
const argName = Object.keys(selectedCommand.args)[
|
||||||
|
parseInt(b.keys[0], 10) - 1
|
||||||
|
]
|
||||||
|
const arg = selectedCommand?.args[argName]
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Edit argument',
|
||||||
|
data: { arg: { ...arg, name: argName } },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ keyup: true, enableOnFormTags: true, enableOnContentEditable: true },
|
||||||
|
[argumentsToSubmit, selectedCommand]
|
||||||
|
)
|
||||||
|
|
||||||
|
Object.keys(argumentsToSubmit).forEach((key, i) => {
|
||||||
|
const arg = selectedCommand?.args ? selectedCommand?.args[key] : undefined
|
||||||
|
if (!arg) return
|
||||||
|
})
|
||||||
|
|
||||||
|
function submitCommand() {
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Submit command',
|
||||||
|
data: argumentsToSubmit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandBarHeader>
|
||||||
|
<p className="px-4">Confirm {selectedCommand?.name}</p>
|
||||||
|
<form
|
||||||
|
id="review-form"
|
||||||
|
className="absolute opacity-0 inset-0 pointer-events-none"
|
||||||
|
onSubmit={submitCommand}
|
||||||
|
>
|
||||||
|
{Object.entries(argumentsToSubmit).map(([key, value], i) => {
|
||||||
|
const arg = selectedCommand?.args
|
||||||
|
? selectedCommand?.args[key]
|
||||||
|
: undefined
|
||||||
|
if (!arg) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
id={key}
|
||||||
|
name={key}
|
||||||
|
key={key}
|
||||||
|
type="text"
|
||||||
|
defaultValue={
|
||||||
|
typeof value === 'object'
|
||||||
|
? JSON.stringify(value)
|
||||||
|
: (value as string)
|
||||||
|
}
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</form>
|
||||||
|
</CommandBarHeader>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandBarReview
|
114
src/components/CommandBar/CommandBarSelectionInput.tsx
Normal file
114
src/components/CommandBar/CommandBarSelectionInput.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { useSelector } from '@xstate/react'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { useKclContext } from 'lang/KclSinglton'
|
||||||
|
import { CommandArgument } from 'lib/commandTypes'
|
||||||
|
import {
|
||||||
|
ResolvedSelectionType,
|
||||||
|
canSubmitSelectionArg,
|
||||||
|
getSelectionType,
|
||||||
|
getSelectionTypeDisplayText,
|
||||||
|
} from 'lib/selections'
|
||||||
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import { StateFrom } from 'xstate'
|
||||||
|
|
||||||
|
const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) =>
|
||||||
|
snapshot.context.selectionRanges
|
||||||
|
|
||||||
|
function CommandBarSelectionInput({
|
||||||
|
arg,
|
||||||
|
stepBack,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
arg: CommandArgument<unknown> & { inputType: 'selection'; name: string }
|
||||||
|
stepBack: () => void
|
||||||
|
onSubmit: (data: unknown) => void
|
||||||
|
}) {
|
||||||
|
const { code } = useKclContext()
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||||
|
const selection = useSelector(arg.actor, selectionSelector)
|
||||||
|
const [selectionsByType, setSelectionsByType] = useState<
|
||||||
|
'none' | ResolvedSelectionType[]
|
||||||
|
>(
|
||||||
|
selection.codeBasedSelections[0]?.range[1] === code.length
|
||||||
|
? 'none'
|
||||||
|
: getSelectionType(selection)
|
||||||
|
)
|
||||||
|
const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>(
|
||||||
|
canSubmitSelectionArg(selectionsByType, arg)
|
||||||
|
)
|
||||||
|
|
||||||
|
useHotkeys('tab', () => onSubmit(selection), {
|
||||||
|
enableOnFormTags: true,
|
||||||
|
enableOnContentEditable: true,
|
||||||
|
keyup: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
inputRef.current?.focus()
|
||||||
|
}, [selection, inputRef])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectionsByType(
|
||||||
|
selection.codeBasedSelections[0]?.range[1] === code.length
|
||||||
|
? 'none'
|
||||||
|
: getSelectionType(selection)
|
||||||
|
)
|
||||||
|
}, [selection])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
|
||||||
|
}, [selectionsByType, arg])
|
||||||
|
|
||||||
|
function handleChange() {
|
||||||
|
inputRef.current?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!canSubmitSelection) {
|
||||||
|
setHasSubmitted(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form id="arg-form" onSubmit={handleSubmit}>
|
||||||
|
<label
|
||||||
|
className={
|
||||||
|
'relative flex items-center mx-4 my-4 ' +
|
||||||
|
(!hasSubmitted || canSubmitSelection || 'text-destroy-50')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{canSubmitSelection
|
||||||
|
? getSelectionTypeDisplayText(selection) + ' selected'
|
||||||
|
: `Please select ${arg.multiple ? 'one or more faces' : 'one face'}`}
|
||||||
|
<input
|
||||||
|
id="selection"
|
||||||
|
name="selection"
|
||||||
|
ref={inputRef}
|
||||||
|
required
|
||||||
|
placeholder="Select an entity with your mouse"
|
||||||
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Backspace') {
|
||||||
|
stepBack()
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
commandBarSend({ type: 'Close' })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={handleChange}
|
||||||
|
value={JSON.stringify(selection || {})}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandBarSelectionInput
|
90
src/components/CommandComboBox.tsx
Normal file
90
src/components/CommandComboBox.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { Combobox } from '@headlessui/react'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { Command } from 'lib/commandTypes'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { CustomIcon } from './CustomIcon'
|
||||||
|
|
||||||
|
function CommandComboBox({
|
||||||
|
options,
|
||||||
|
placeholder,
|
||||||
|
}: {
|
||||||
|
options: Command[]
|
||||||
|
placeholder?: string
|
||||||
|
}) {
|
||||||
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
|
||||||
|
|
||||||
|
const defaultOption =
|
||||||
|
options.find((o) => 'isCurrent' in o && o.isCurrent) || null
|
||||||
|
|
||||||
|
const fuse = new Fuse(options, {
|
||||||
|
keys: ['name', 'description'],
|
||||||
|
threshold: 0.3,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const results = fuse.search(query).map((result) => result.item)
|
||||||
|
setFilteredOptions(query.length > 0 ? results : options)
|
||||||
|
}, [query])
|
||||||
|
|
||||||
|
function handleSelection(command: Command) {
|
||||||
|
commandBarSend({ type: 'Select command', data: { command } })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Combobox defaultValue={defaultOption} onChange={handleSelection}>
|
||||||
|
<div className="flex items-center gap-2 px-4 pb-2 border-solid border-0 border-b border-b-chalkboard-20 dark:border-b-chalkboard-80">
|
||||||
|
<CustomIcon
|
||||||
|
name="search"
|
||||||
|
className="w-5 h-5 bg-energy-10/50 dark:bg-chalkboard-90 dark:text-energy-10"
|
||||||
|
/>
|
||||||
|
<Combobox.Input
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
className="w-full bg-transparent focus:outline-none selection:bg-energy-10/50 dark:selection:bg-energy-10/20 dark:focus:outline-none"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (
|
||||||
|
(event.metaKey && event.key === 'k') ||
|
||||||
|
(event.key === 'Backspace' && !event.currentTarget.value)
|
||||||
|
) {
|
||||||
|
commandBarSend({ type: 'Close' })
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={
|
||||||
|
(defaultOption && defaultOption.name) ||
|
||||||
|
placeholder ||
|
||||||
|
'Search commands'
|
||||||
|
}
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
spellCheck="false"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Combobox.Options
|
||||||
|
static
|
||||||
|
className="overflow-y-auto max-h-96 cursor-pointer"
|
||||||
|
>
|
||||||
|
{filteredOptions?.map((option) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={option.name}
|
||||||
|
value={option}
|
||||||
|
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
|
||||||
|
>
|
||||||
|
{'icon' in option && option.icon && (
|
||||||
|
<CustomIcon
|
||||||
|
name={option.icon}
|
||||||
|
className="w-5 h-5 dark:text-energy-10"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<p className="flex-grow">{option.name} </p>
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
</Combobox>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandComboBox
|
@ -3,6 +3,7 @@ export type CustomIconName =
|
|||||||
| 'arrowLeft'
|
| 'arrowLeft'
|
||||||
| 'arrowRight'
|
| 'arrowRight'
|
||||||
| 'arrowUp'
|
| 'arrowUp'
|
||||||
|
| 'checkmark'
|
||||||
| 'close'
|
| 'close'
|
||||||
| 'equal'
|
| 'equal'
|
||||||
| 'extrude'
|
| 'extrude'
|
||||||
@ -90,6 +91,22 @@ export const CustomIcon = ({
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
case 'checkmark':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M8.29956 13.5388L13.9537 6L14.7537 6.6L8.75367 14.6L8.00012 14.6536L5 11.6536L5.70709 10.9465L8.29956 13.5388Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
case 'close':
|
case 'close':
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
@ -40,7 +40,7 @@ export const FileMachineProvider = ({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { setCommandBarOpen } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { project } = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
const { project } = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
||||||
|
|
||||||
const [state, send] = useMachine(fileMachine, {
|
const [state, send] = useMachine(fileMachine, {
|
||||||
@ -54,7 +54,7 @@ export const FileMachineProvider = ({
|
|||||||
event: EventFrom<typeof fileMachine>
|
event: EventFrom<typeof fileMachine>
|
||||||
) => {
|
) => {
|
||||||
if (event.data && 'name' in event.data) {
|
if (event.data && 'name' in event.data) {
|
||||||
setCommandBarOpen(false)
|
commandBarSend({ type: 'Close' })
|
||||||
navigate(
|
navigate(
|
||||||
`${paths.FILE}/${encodeURIComponent(
|
`${paths.FILE}/${encodeURIComponent(
|
||||||
context.selectedDirectory + sep + event.data.name
|
context.selectedDirectory + sep + event.data.name
|
||||||
|
@ -1,19 +1,11 @@
|
|||||||
import { useMachine } from '@xstate/react'
|
import { useMachine } from '@xstate/react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { paths } from '../Router'
|
import { paths } from '../Router'
|
||||||
import {
|
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
||||||
authCommandBarConfig,
|
|
||||||
authMachine,
|
|
||||||
TOKEN_PERSIST_KEY,
|
|
||||||
} from '../machines/authMachine'
|
|
||||||
import withBaseUrl from '../lib/withBaseURL'
|
import withBaseUrl from '../lib/withBaseURL'
|
||||||
import React, { createContext, useEffect, useRef } from 'react'
|
import React, { createContext, useEffect, useRef } from 'react'
|
||||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||||
import {
|
import { SETTINGS_PERSIST_KEY, settingsMachine } from 'machines/settingsMachine'
|
||||||
SETTINGS_PERSIST_KEY,
|
|
||||||
settingsCommandBarConfig,
|
|
||||||
settingsMachine,
|
|
||||||
} from 'machines/settingsMachine'
|
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { setThemeClass, Themes } from 'lib/theme'
|
import { setThemeClass, Themes } from 'lib/theme'
|
||||||
import {
|
import {
|
||||||
@ -23,8 +15,9 @@ import {
|
|||||||
Prop,
|
Prop,
|
||||||
StateFrom,
|
StateFrom,
|
||||||
} from 'xstate'
|
} from 'xstate'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig'
|
||||||
|
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -45,7 +38,6 @@ export const GlobalStateProvider = ({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { commands } = useCommandsContext()
|
|
||||||
|
|
||||||
// Settings machine setup
|
// Settings machine setup
|
||||||
const retrievedSettings = useRef(
|
const retrievedSettings = useRef(
|
||||||
@ -81,10 +73,9 @@ export const GlobalStateProvider = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
useStateMachineCommands({
|
useStateMachineCommands({
|
||||||
|
machineId: 'settings',
|
||||||
state: settingsState,
|
state: settingsState,
|
||||||
send: settingsSend,
|
send: settingsSend,
|
||||||
commands,
|
|
||||||
owner: 'settings',
|
|
||||||
commandBarConfig: settingsCommandBarConfig,
|
commandBarConfig: settingsCommandBarConfig,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -121,11 +112,10 @@ export const GlobalStateProvider = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
useStateMachineCommands({
|
useStateMachineCommands({
|
||||||
|
machineId: 'auth',
|
||||||
state: authState,
|
state: authState,
|
||||||
send: authSend,
|
send: authSend,
|
||||||
commands,
|
|
||||||
commandBarConfig: authCommandBarConfig,
|
commandBarConfig: authCommandBarConfig,
|
||||||
owner: 'auth',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -15,23 +15,23 @@ const Loading = ({ children }: React.PropsWithChildren) => {
|
|||||||
data-testid="loading"
|
data-testid="loading"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 10 10" className="w-8 h-8">
|
<svg viewBox="0 0 10 10" className="w-8 h-8">
|
||||||
<circle cx="5" cy="5" r="4" stroke="var(--liquid-20)" fill="none" />
|
<circle cx="5" cy="5" r="4" stroke="var(--energy-50)" fill="none" />
|
||||||
<circle
|
<circle
|
||||||
cx="5"
|
cx="5"
|
||||||
cy="5"
|
cy="5"
|
||||||
r="4"
|
r="4"
|
||||||
stroke="var(--liquid-10)"
|
stroke="var(--energy-10)"
|
||||||
fill="none"
|
fill="none"
|
||||||
strokeDasharray="4, 4"
|
strokeDasharray="4, 4"
|
||||||
className="animate-spin origin-center"
|
className="animate-spin origin-center"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<p className="text-base mt-4 text-liquid-80 dark:text-liquid-20">
|
<p className="text-base mt-4 text-energy-80 dark:text-energy-30">
|
||||||
{children || 'Loading'}
|
{children || 'Loading'}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className={
|
className={
|
||||||
'text-sm mt-4 text-liquid-90 dark:text-liquid-10 transition-opacity duration-500' +
|
'text-sm mt-4 text-energy-70 dark:text-energy-50 transition-opacity duration-500' +
|
||||||
(hasLongLoadTime ? ' opacity-100' : ' opacity-0')
|
(hasLongLoadTime ? ' opacity-100' : ' opacity-0')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -29,19 +29,26 @@ import {
|
|||||||
addNewSketchLn,
|
addNewSketchLn,
|
||||||
compareVec2Epsilon,
|
compareVec2Epsilon,
|
||||||
} from 'lang/std/sketch'
|
} from 'lang/std/sketch'
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
||||||
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
||||||
import {
|
import {
|
||||||
angleBetweenInfo,
|
angleBetweenInfo,
|
||||||
applyConstraintAngleBetween,
|
applyConstraintAngleBetween,
|
||||||
} from './Toolbar/SetAngleBetween'
|
} from './Toolbar/SetAngleBetween'
|
||||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import { pathMapToSelections } from 'lang/util'
|
import { pathMapToSelections } from 'lang/util'
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
import { handleSelectionBatch, handleSelectionWithShift } from 'lib/selections'
|
import {
|
||||||
|
canExtrudeSelection,
|
||||||
|
handleSelectionBatch,
|
||||||
|
handleSelectionWithShift,
|
||||||
|
isSelectionLastLine,
|
||||||
|
isSketchPipe,
|
||||||
|
} from 'lib/selections'
|
||||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||||
|
import useStateMachineCommands from 'hooks/useStateMachineCommands'
|
||||||
|
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -59,6 +66,7 @@ export const ModelingMachineProvider = ({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const { auth } = useGlobalStateContext()
|
const { auth } = useGlobalStateContext()
|
||||||
|
const { code } = useKclContext()
|
||||||
const token = auth?.context?.token
|
const token = auth?.context?.token
|
||||||
const streamRef = useRef<HTMLDivElement>(null)
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
useSetupEngineManager(streamRef, token)
|
useSetupEngineManager(streamRef, token)
|
||||||
@ -68,8 +76,6 @@ export const ModelingMachineProvider = ({
|
|||||||
editorView: s.editorView,
|
editorView: s.editorView,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// const { commands } = useCommandsContext()
|
|
||||||
|
|
||||||
// Settings machine setup
|
// Settings machine setup
|
||||||
// const retrievedSettings = useRef(
|
// const retrievedSettings = useRef(
|
||||||
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
|
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
|
||||||
@ -83,148 +89,85 @@ export const ModelingMachineProvider = ({
|
|||||||
// >
|
// >
|
||||||
// )
|
// )
|
||||||
|
|
||||||
const [modelingState, modelingSend] = useMachine(modelingMachine, {
|
const [modelingState, modelingSend, modelingActor] = useMachine(
|
||||||
// context: persistedSettings,
|
modelingMachine,
|
||||||
actions: {
|
{
|
||||||
'Modify AST': () => {},
|
// context: persistedSettings,
|
||||||
'Update code selection cursors': () => {},
|
actions: {
|
||||||
'show default planes': () => {
|
'Modify AST': () => {},
|
||||||
kclManager.showPlanes()
|
'Update code selection cursors': () => {},
|
||||||
},
|
'show default planes': () => {
|
||||||
'create path': assign({
|
kclManager.showPlanes()
|
||||||
sketchEnginePathId: () => {
|
|
||||||
const sketchUuid = uuidv4()
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: sketchUuid,
|
|
||||||
cmd: {
|
|
||||||
type: 'start_path',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'edit_mode_enter',
|
|
||||||
target: sketchUuid,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return sketchUuid
|
|
||||||
},
|
},
|
||||||
}),
|
'create path': assign({
|
||||||
'AST start new sketch': assign(
|
sketchEnginePathId: () => {
|
||||||
({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => {
|
const sketchUuid = uuidv4()
|
||||||
if (!axis) {
|
engineCommandManager.sendSceneCommand({
|
||||||
// Something really weird must have happened for this to happen.
|
type: 'modeling_cmd_req',
|
||||||
console.error('axis is undefined for starting a new sketch')
|
cmd_id: sketchUuid,
|
||||||
return {}
|
cmd: {
|
||||||
}
|
type: 'start_path',
|
||||||
if (!segmentId) {
|
},
|
||||||
// Something really weird must have happened for this to happen.
|
})
|
||||||
console.error('segmentId is undefined for starting a new sketch')
|
engineCommandManager.sendSceneCommand({
|
||||||
return {}
|
type: 'modeling_cmd_req',
|
||||||
}
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
const _addStartSketch = addStartSketch(
|
type: 'edit_mode_enter',
|
||||||
kclManager.ast,
|
target: sketchUuid,
|
||||||
axis,
|
},
|
||||||
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
})
|
||||||
[
|
return sketchUuid
|
||||||
roundOff(coords[1].x - coords[0].x),
|
},
|
||||||
roundOff(coords[1].y - coords[0].y),
|
}),
|
||||||
]
|
'AST start new sketch': assign(
|
||||||
)
|
({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => {
|
||||||
const _modifiedAst = _addStartSketch.modifiedAst
|
if (!axis) {
|
||||||
const _pathToNode = _addStartSketch.pathToNode
|
// Something really weird must have happened for this to happen.
|
||||||
const newCode = recast(_modifiedAst)
|
console.error('axis is undefined for starting a new sketch')
|
||||||
const astWithUpdatedSource = parse(newCode)
|
return {}
|
||||||
const updatedPipeNode = getNodeFromPath<PipeExpression>(
|
|
||||||
astWithUpdatedSource,
|
|
||||||
_pathToNode
|
|
||||||
).node
|
|
||||||
const startProfileAtCallExp = updatedPipeNode.body.find(
|
|
||||||
(exp) =>
|
|
||||||
exp.type === 'CallExpression' &&
|
|
||||||
exp.callee.name === 'startProfileAt'
|
|
||||||
)
|
|
||||||
if (startProfileAtCallExp)
|
|
||||||
engineCommandManager.artifactMap[sketchEnginePathId] = {
|
|
||||||
type: 'result',
|
|
||||||
range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
|
|
||||||
commandType: 'start_path',
|
|
||||||
data: null,
|
|
||||||
raw: {} as any,
|
|
||||||
}
|
}
|
||||||
const lineCallExp = updatedPipeNode.body.find(
|
if (!segmentId) {
|
||||||
(exp) => exp.type === 'CallExpression' && exp.callee.name === 'line'
|
// Something really weird must have happened for this to happen.
|
||||||
)
|
console.error('segmentId is undefined for starting a new sketch')
|
||||||
if (lineCallExp)
|
return {}
|
||||||
engineCommandManager.artifactMap[segmentId] = {
|
|
||||||
type: 'result',
|
|
||||||
range: [lineCallExp.start, lineCallExp.end],
|
|
||||||
commandType: 'extend_path',
|
|
||||||
parentId: sketchEnginePathId,
|
|
||||||
data: null,
|
|
||||||
raw: {} as any,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kclManager.executeAstMock(astWithUpdatedSource, true)
|
const _addStartSketch = addStartSketch(
|
||||||
|
|
||||||
return {
|
|
||||||
sketchPathToNode: _pathToNode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
'AST add line segment': async (
|
|
||||||
{ sketchPathToNode, sketchEnginePathId },
|
|
||||||
{ data: { coords, segmentId } }
|
|
||||||
) => {
|
|
||||||
if (!sketchPathToNode) return
|
|
||||||
const lastCoord = coords[coords.length - 1]
|
|
||||||
|
|
||||||
const pathInfo = await engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'path_get_info',
|
|
||||||
path_id: sketchEnginePathId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const firstSegment = pathInfo?.data?.data?.segments.find(
|
|
||||||
(seg: any) => seg.command === 'line_to'
|
|
||||||
)
|
|
||||||
const firstSegCoords = await engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'curve_get_control_points',
|
|
||||||
curve_id: firstSegment.command_id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const startPathCoord = firstSegCoords?.data?.data?.control_points[0]
|
|
||||||
|
|
||||||
const isClose = compareVec2Epsilon(
|
|
||||||
[startPathCoord.x, startPathCoord.y],
|
|
||||||
[lastCoord.x, lastCoord.y]
|
|
||||||
)
|
|
||||||
|
|
||||||
let _modifiedAst: Program
|
|
||||||
if (!isClose) {
|
|
||||||
const newSketchLn = addNewSketchLn({
|
|
||||||
node: kclManager.ast,
|
|
||||||
programMemory: kclManager.programMemory,
|
|
||||||
to: [lastCoord.x, lastCoord.y],
|
|
||||||
from: [coords[0].x, coords[0].y],
|
|
||||||
fnName: 'line',
|
|
||||||
pathToNode: sketchPathToNode,
|
|
||||||
})
|
|
||||||
const _modifiedAst = newSketchLn.modifiedAst
|
|
||||||
kclManager.executeAstMock(_modifiedAst, true).then(() => {
|
|
||||||
const lineCallExp = getNodeFromPath<CallExpression>(
|
|
||||||
kclManager.ast,
|
kclManager.ast,
|
||||||
newSketchLn.pathToNode
|
axis,
|
||||||
|
[roundOff(coords[0].x), roundOff(coords[0].y)],
|
||||||
|
[
|
||||||
|
roundOff(coords[1].x - coords[0].x),
|
||||||
|
roundOff(coords[1].y - coords[0].y),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
const _modifiedAst = _addStartSketch.modifiedAst
|
||||||
|
const _pathToNode = _addStartSketch.pathToNode
|
||||||
|
const newCode = recast(_modifiedAst)
|
||||||
|
const astWithUpdatedSource = parse(newCode)
|
||||||
|
const updatedPipeNode = getNodeFromPath<PipeExpression>(
|
||||||
|
astWithUpdatedSource,
|
||||||
|
_pathToNode
|
||||||
).node
|
).node
|
||||||
if (segmentId)
|
const startProfileAtCallExp = updatedPipeNode.body.find(
|
||||||
|
(exp) =>
|
||||||
|
exp.type === 'CallExpression' &&
|
||||||
|
exp.callee.name === 'startProfileAt'
|
||||||
|
)
|
||||||
|
if (startProfileAtCallExp)
|
||||||
|
engineCommandManager.artifactMap[sketchEnginePathId] = {
|
||||||
|
type: 'result',
|
||||||
|
range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
|
||||||
|
commandType: 'start_path',
|
||||||
|
data: null,
|
||||||
|
raw: {} as any,
|
||||||
|
}
|
||||||
|
const lineCallExp = updatedPipeNode.body.find(
|
||||||
|
(exp) =>
|
||||||
|
exp.type === 'CallExpression' && exp.callee.name === 'line'
|
||||||
|
)
|
||||||
|
if (lineCallExp)
|
||||||
engineCommandManager.artifactMap[segmentId] = {
|
engineCommandManager.artifactMap[segmentId] = {
|
||||||
type: 'result',
|
type: 'result',
|
||||||
range: [lineCallExp.start, lineCallExp.end],
|
range: [lineCallExp.start, lineCallExp.end],
|
||||||
@ -233,120 +176,189 @@ export const ModelingMachineProvider = ({
|
|||||||
data: null,
|
data: null,
|
||||||
raw: {} as any,
|
raw: {} as any,
|
||||||
}
|
}
|
||||||
})
|
|
||||||
} else {
|
|
||||||
_modifiedAst = addCloseToPipe({
|
|
||||||
node: kclManager.ast,
|
|
||||||
programMemory: kclManager.programMemory,
|
|
||||||
pathToNode: sketchPathToNode,
|
|
||||||
})
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: { type: 'edit_mode_exit' },
|
|
||||||
})
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: { type: 'default_camera_disable_sketch_mode' },
|
|
||||||
})
|
|
||||||
kclManager.executeAstMock(_modifiedAst, true)
|
|
||||||
// updateAst(_modifiedAst, true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'sketch exit execute': () => {
|
|
||||||
kclManager.executeAst()
|
|
||||||
},
|
|
||||||
'set tool': () => {}, // TODO
|
|
||||||
'toast extrude failed': () => {
|
|
||||||
toast.error(
|
|
||||||
'Extrude failed, sketches need to be closed, or not already extruded'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
'Set selection': assign(({ selectionRanges }, event) => {
|
|
||||||
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
|
|
||||||
const setSelections = event.data
|
|
||||||
if (!editorView) return {}
|
|
||||||
if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
|
|
||||||
return { selectionRanges: setSelections.selection }
|
|
||||||
else if (setSelections.selectionType === 'otherSelection') {
|
|
||||||
// TODO KittyCAD/engine/issues/1620: send axis highlight when it's working (if that's what we settle on)
|
|
||||||
// const axisAddCmd: EngineCommand = {
|
|
||||||
// type: 'modeling_cmd_req',
|
|
||||||
// cmd: {
|
|
||||||
// type: 'highlight_set_entities',
|
|
||||||
// entities: [
|
|
||||||
// setSelections.selection === 'x-axis'
|
|
||||||
// ? X_AXIS_UUID
|
|
||||||
// : Y_AXIS_UUID,
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// cmd_id: uuidv4(),
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (!isShiftDown) {
|
kclManager.executeAstMock(astWithUpdatedSource, true)
|
||||||
// engineCommandManager
|
|
||||||
// .sendSceneCommand({
|
|
||||||
// type: 'modeling_cmd_req',
|
|
||||||
// cmd: {
|
|
||||||
// type: 'select_clear',
|
|
||||||
// },
|
|
||||||
// cmd_id: uuidv4(),
|
|
||||||
// })
|
|
||||||
// .then(() => {
|
|
||||||
// engineCommandManager.sendSceneCommand(axisAddCmd)
|
|
||||||
// })
|
|
||||||
// } else {
|
|
||||||
// engineCommandManager.sendSceneCommand(axisAddCmd)
|
|
||||||
// }
|
|
||||||
|
|
||||||
const {
|
return {
|
||||||
codeMirrorSelection,
|
sketchPathToNode: _pathToNode,
|
||||||
selectionRangeTypeMap,
|
}
|
||||||
otherSelections,
|
|
||||||
} = handleSelectionWithShift({
|
|
||||||
otherSelection: setSelections.selection,
|
|
||||||
currentSelections: selectionRanges,
|
|
||||||
isShiftDown,
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
|
||||||
editorView.dispatch({
|
|
||||||
selection: codeMirrorSelection,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
selectionRangeTypeMap,
|
|
||||||
selectionRanges: {
|
|
||||||
codeBasedSelections: selectionRanges.codeBasedSelections,
|
|
||||||
otherSelections,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
} else if (setSelections.selectionType === 'singleCodeCursor') {
|
),
|
||||||
// This DOES NOT set the `selectionRanges` in xstate context
|
'AST add line segment': async (
|
||||||
// instead it updates/dispatches to the editor, which in turn updates the xstate context
|
{ sketchPathToNode, sketchEnginePathId },
|
||||||
// I've found this the best way to deal with the editor without causing an infinite loop
|
{ data: { coords, segmentId } }
|
||||||
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
|
) => {
|
||||||
// because we want to respect the user manually placing the cursor too.
|
if (!sketchPathToNode) return
|
||||||
|
const lastCoord = coords[coords.length - 1]
|
||||||
|
|
||||||
// for more details on how selections see `src/lib/selections.ts`.
|
const pathInfo = await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
const {
|
cmd_id: uuidv4(),
|
||||||
codeMirrorSelection,
|
cmd: {
|
||||||
selectionRangeTypeMap,
|
type: 'path_get_info',
|
||||||
otherSelections,
|
path_id: sketchEnginePathId,
|
||||||
} = handleSelectionWithShift({
|
},
|
||||||
codeSelection: setSelections.selection,
|
|
||||||
currentSelections: selectionRanges,
|
|
||||||
isShiftDown,
|
|
||||||
})
|
})
|
||||||
if (codeMirrorSelection) {
|
const firstSegment = pathInfo?.data?.data?.segments.find(
|
||||||
|
(seg: any) => seg.command === 'line_to'
|
||||||
|
)
|
||||||
|
const firstSegCoords = await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'curve_get_control_points',
|
||||||
|
curve_id: firstSegment.command_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const startPathCoord = firstSegCoords?.data?.data?.control_points[0]
|
||||||
|
|
||||||
|
const isClose = compareVec2Epsilon(
|
||||||
|
[startPathCoord.x, startPathCoord.y],
|
||||||
|
[lastCoord.x, lastCoord.y]
|
||||||
|
)
|
||||||
|
|
||||||
|
let _modifiedAst: Program
|
||||||
|
if (!isClose) {
|
||||||
|
const newSketchLn = addNewSketchLn({
|
||||||
|
node: kclManager.ast,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
to: [lastCoord.x, lastCoord.y],
|
||||||
|
from: [coords[0].x, coords[0].y],
|
||||||
|
fnName: 'line',
|
||||||
|
pathToNode: sketchPathToNode,
|
||||||
|
})
|
||||||
|
const _modifiedAst = newSketchLn.modifiedAst
|
||||||
|
kclManager.executeAstMock(_modifiedAst, true).then(() => {
|
||||||
|
const lineCallExp = getNodeFromPath<CallExpression>(
|
||||||
|
kclManager.ast,
|
||||||
|
newSketchLn.pathToNode
|
||||||
|
).node
|
||||||
|
if (segmentId)
|
||||||
|
engineCommandManager.artifactMap[segmentId] = {
|
||||||
|
type: 'result',
|
||||||
|
range: [lineCallExp.start, lineCallExp.end],
|
||||||
|
commandType: 'extend_path',
|
||||||
|
parentId: sketchEnginePathId,
|
||||||
|
data: null,
|
||||||
|
raw: {} as any,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
_modifiedAst = addCloseToPipe({
|
||||||
|
node: kclManager.ast,
|
||||||
|
programMemory: kclManager.programMemory,
|
||||||
|
pathToNode: sketchPathToNode,
|
||||||
|
})
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'edit_mode_exit' },
|
||||||
|
})
|
||||||
|
engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: { type: 'default_camera_disable_sketch_mode' },
|
||||||
|
})
|
||||||
|
kclManager.executeAstMock(_modifiedAst, true)
|
||||||
|
// updateAst(_modifiedAst, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'sketch exit execute': () => {
|
||||||
|
kclManager.executeAst()
|
||||||
|
},
|
||||||
|
'set tool': () => {}, // TODO
|
||||||
|
'Set selection': assign(({ selectionRanges }, event) => {
|
||||||
|
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
|
||||||
|
const setSelections = event.data
|
||||||
|
if (!editorView) return {}
|
||||||
|
if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
|
||||||
|
return { selectionRanges: setSelections.selection }
|
||||||
|
else if (setSelections.selectionType === 'otherSelection') {
|
||||||
|
// TODO KittyCAD/engine/issues/1620: send axis highlight when it's working (if that's what we settle on)
|
||||||
|
// const axisAddCmd: EngineCommand = {
|
||||||
|
// type: 'modeling_cmd_req',
|
||||||
|
// cmd: {
|
||||||
|
// type: 'highlight_set_entities',
|
||||||
|
// entities: [
|
||||||
|
// setSelections.selection === 'x-axis'
|
||||||
|
// ? X_AXIS_UUID
|
||||||
|
// : Y_AXIS_UUID,
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// cmd_id: uuidv4(),
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!isShiftDown) {
|
||||||
|
// engineCommandManager
|
||||||
|
// .sendSceneCommand({
|
||||||
|
// type: 'modeling_cmd_req',
|
||||||
|
// cmd: {
|
||||||
|
// type: 'select_clear',
|
||||||
|
// },
|
||||||
|
// cmd_id: uuidv4(),
|
||||||
|
// })
|
||||||
|
// .then(() => {
|
||||||
|
// engineCommandManager.sendSceneCommand(axisAddCmd)
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// engineCommandManager.sendSceneCommand(axisAddCmd)
|
||||||
|
// }
|
||||||
|
|
||||||
|
const {
|
||||||
|
codeMirrorSelection,
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
otherSelections,
|
||||||
|
} = handleSelectionWithShift({
|
||||||
|
otherSelection: setSelections.selection,
|
||||||
|
currentSelections: selectionRanges,
|
||||||
|
isShiftDown,
|
||||||
|
})
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
editorView.dispatch({
|
editorView.dispatch({
|
||||||
selection: codeMirrorSelection,
|
selection: codeMirrorSelection,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
return {
|
||||||
if (!setSelections.selection) {
|
selectionRangeTypeMap,
|
||||||
|
selectionRanges: {
|
||||||
|
codeBasedSelections: selectionRanges.codeBasedSelections,
|
||||||
|
otherSelections,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if (setSelections.selectionType === 'singleCodeCursor') {
|
||||||
|
// This DOES NOT set the `selectionRanges` in xstate context
|
||||||
|
// instead it updates/dispatches to the editor, which in turn updates the xstate context
|
||||||
|
// I've found this the best way to deal with the editor without causing an infinite loop
|
||||||
|
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
|
||||||
|
// because we want to respect the user manually placing the cursor too.
|
||||||
|
|
||||||
|
// for more details on how selections see `src/lib/selections.ts`.
|
||||||
|
|
||||||
|
const {
|
||||||
|
codeMirrorSelection,
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
otherSelections,
|
||||||
|
} = handleSelectionWithShift({
|
||||||
|
codeSelection: setSelections.selection,
|
||||||
|
currentSelections: selectionRanges,
|
||||||
|
isShiftDown,
|
||||||
|
})
|
||||||
|
if (codeMirrorSelection) {
|
||||||
|
setTimeout(() => {
|
||||||
|
editorView.dispatch({
|
||||||
|
selection: codeMirrorSelection,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!setSelections.selection) {
|
||||||
|
return {
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
selectionRanges: {
|
||||||
|
codeBasedSelections: selectionRanges.codeBasedSelections,
|
||||||
|
otherSelections,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
selectionRangeTypeMap,
|
selectionRangeTypeMap,
|
||||||
selectionRanges: {
|
selectionRanges: {
|
||||||
@ -355,171 +367,180 @@ export const ModelingMachineProvider = ({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// This DOES NOT set the `selectionRanges` in xstate context
|
||||||
|
// same as comment above
|
||||||
|
const { codeMirrorSelection, selectionRangeTypeMap } =
|
||||||
|
handleSelectionBatch({
|
||||||
|
selections: setSelections.selection,
|
||||||
|
})
|
||||||
|
if (codeMirrorSelection) {
|
||||||
|
setTimeout(() => {
|
||||||
|
editorView.dispatch({
|
||||||
|
selection: codeMirrorSelection,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return { selectionRangeTypeMap }
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
'Selection contains axis': () => true,
|
||||||
|
'Selection contains edge': () => true,
|
||||||
|
'Selection contains face': () => true,
|
||||||
|
'Selection contains line': () => true,
|
||||||
|
'Selection contains point': () => true,
|
||||||
|
'Selection is not empty': () => true,
|
||||||
|
'has valid extrude selection': ({ selectionRanges }) => {
|
||||||
|
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
||||||
|
// TODO: I believe this guard only allows for extruding a single face at a time
|
||||||
|
if (selectionRanges.codeBasedSelections.length < 1) return false
|
||||||
|
const isPipe = isSketchPipe(selectionRanges)
|
||||||
|
|
||||||
|
if (isSelectionLastLine(selectionRanges, code)) return true
|
||||||
|
if (!isPipe) return false
|
||||||
|
|
||||||
|
return canExtrudeSelection(selectionRanges)
|
||||||
|
},
|
||||||
|
'Selection is one face': ({ selectionRanges }) => {
|
||||||
|
return !!isCursorInSketchCommandRange(
|
||||||
|
engineCommandManager.artifactMap,
|
||||||
|
selectionRanges
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
'Get horizontal info': async ({
|
||||||
|
selectionRanges,
|
||||||
|
}): Promise<SetSelections> => {
|
||||||
|
const { modifiedAst, pathToNodeMap } =
|
||||||
|
await applyConstraintHorzVertDistance({
|
||||||
|
constraint: 'setHorzDistance',
|
||||||
|
selectionRanges,
|
||||||
|
})
|
||||||
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
return {
|
return {
|
||||||
selectionRangeTypeMap,
|
selectionType: 'completeSelection',
|
||||||
selectionRanges: {
|
selection: pathMapToSelections(
|
||||||
codeBasedSelections: selectionRanges.codeBasedSelections,
|
kclManager.ast,
|
||||||
otherSelections,
|
selectionRanges,
|
||||||
},
|
pathToNodeMap
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
// This DOES NOT set the `selectionRanges` in xstate context
|
'Get vertical info': async ({
|
||||||
// same as comment above
|
|
||||||
const { codeMirrorSelection, selectionRangeTypeMap } =
|
|
||||||
handleSelectionBatch({
|
|
||||||
selections: setSelections.selection,
|
|
||||||
})
|
|
||||||
if (codeMirrorSelection) {
|
|
||||||
setTimeout(() => {
|
|
||||||
editorView.dispatch({
|
|
||||||
selection: codeMirrorSelection,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return { selectionRangeTypeMap }
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
guards: {
|
|
||||||
'Selection contains axis': () => true,
|
|
||||||
'Selection contains edge': () => true,
|
|
||||||
'Selection contains face': () => true,
|
|
||||||
'Selection contains line': () => true,
|
|
||||||
'Selection contains point': () => true,
|
|
||||||
'Selection is not empty': () => true,
|
|
||||||
'Selection is one face': ({ selectionRanges }) => {
|
|
||||||
return !!isCursorInSketchCommandRange(
|
|
||||||
engineCommandManager.artifactMap,
|
|
||||||
selectionRanges
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
services: {
|
|
||||||
'Get horizontal info': async ({
|
|
||||||
selectionRanges,
|
|
||||||
}): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } =
|
|
||||||
await applyConstraintHorzVertDistance({
|
|
||||||
constraint: 'setHorzDistance',
|
|
||||||
selectionRanges,
|
|
||||||
})
|
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
|
||||||
return {
|
|
||||||
selectionType: 'completeSelection',
|
|
||||||
selection: pathMapToSelections(
|
|
||||||
kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
pathToNodeMap
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Get vertical info': async ({
|
|
||||||
selectionRanges,
|
|
||||||
}): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } =
|
|
||||||
await applyConstraintHorzVertDistance({
|
|
||||||
constraint: 'setVertDistance',
|
|
||||||
selectionRanges,
|
|
||||||
})
|
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
|
||||||
return {
|
|
||||||
selectionType: 'completeSelection',
|
|
||||||
selection: pathMapToSelections(
|
|
||||||
kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
pathToNodeMap
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Get angle info': async ({ selectionRanges }): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({
|
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
}).enabled
|
}): Promise<SetSelections> => {
|
||||||
? applyConstraintAngleBetween({
|
const { modifiedAst, pathToNodeMap } =
|
||||||
|
await applyConstraintHorzVertDistance({
|
||||||
|
constraint: 'setVertDistance',
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
})
|
})
|
||||||
: applyConstraintAngleLength({
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
return {
|
||||||
|
selectionType: 'completeSelection',
|
||||||
|
selection: pathMapToSelections(
|
||||||
|
kclManager.ast,
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
angleOrLength: 'setAngle',
|
pathToNodeMap
|
||||||
}))
|
),
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
}
|
||||||
return {
|
},
|
||||||
selectionType: 'completeSelection',
|
'Get angle info': async ({
|
||||||
selection: pathMapToSelections(
|
|
||||||
kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
pathToNodeMap
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Get length info': async ({
|
|
||||||
selectionRanges,
|
|
||||||
}): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } = await applyConstraintAngleLength(
|
|
||||||
{ selectionRanges }
|
|
||||||
)
|
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
|
||||||
return {
|
|
||||||
selectionType: 'completeSelection',
|
|
||||||
selection: pathMapToSelections(
|
|
||||||
kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
pathToNodeMap
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Get perpendicular distance info': async ({
|
|
||||||
selectionRanges,
|
|
||||||
}): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect({
|
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
})
|
}): Promise<SetSelections> => {
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({
|
||||||
return {
|
|
||||||
selectionType: 'completeSelection',
|
|
||||||
selection: pathMapToSelections(
|
|
||||||
kclManager.ast,
|
|
||||||
selectionRanges,
|
|
||||||
pathToNodeMap
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'Get ABS X info': async ({ selectionRanges }): Promise<SetSelections> => {
|
|
||||||
const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance(
|
|
||||||
{
|
|
||||||
constraint: 'xAbs',
|
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
|
}).enabled
|
||||||
|
? applyConstraintAngleBetween({
|
||||||
|
selectionRanges,
|
||||||
|
})
|
||||||
|
: applyConstraintAngleLength({
|
||||||
|
selectionRanges,
|
||||||
|
angleOrLength: 'setAngle',
|
||||||
|
}))
|
||||||
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
return {
|
||||||
|
selectionType: 'completeSelection',
|
||||||
|
selection: pathMapToSelections(
|
||||||
|
kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
pathToNodeMap
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
'Get length info': async ({
|
||||||
return {
|
selectionRanges,
|
||||||
selectionType: 'completeSelection',
|
}): Promise<SetSelections> => {
|
||||||
selection: pathMapToSelections(
|
const { modifiedAst, pathToNodeMap } =
|
||||||
kclManager.ast,
|
await applyConstraintAngleLength({ selectionRanges })
|
||||||
selectionRanges,
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
pathToNodeMap
|
return {
|
||||||
),
|
selectionType: 'completeSelection',
|
||||||
}
|
selection: pathMapToSelections(
|
||||||
},
|
kclManager.ast,
|
||||||
'Get ABS Y info': async ({ selectionRanges }): Promise<SetSelections> => {
|
selectionRanges,
|
||||||
const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance(
|
pathToNodeMap
|
||||||
{
|
),
|
||||||
constraint: 'yAbs',
|
|
||||||
selectionRanges,
|
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
await kclManager.updateAst(modifiedAst, true)
|
'Get perpendicular distance info': async ({
|
||||||
return {
|
selectionRanges,
|
||||||
selectionType: 'completeSelection',
|
}): Promise<SetSelections> => {
|
||||||
selection: pathMapToSelections(
|
const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect(
|
||||||
kclManager.ast,
|
{
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
pathToNodeMap
|
}
|
||||||
),
|
)
|
||||||
}
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
return {
|
||||||
|
selectionType: 'completeSelection',
|
||||||
|
selection: pathMapToSelections(
|
||||||
|
kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
pathToNodeMap
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Get ABS X info': async ({
|
||||||
|
selectionRanges,
|
||||||
|
}): Promise<SetSelections> => {
|
||||||
|
const { modifiedAst, pathToNodeMap } =
|
||||||
|
await applyConstraintAbsDistance({
|
||||||
|
constraint: 'xAbs',
|
||||||
|
selectionRanges,
|
||||||
|
})
|
||||||
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
return {
|
||||||
|
selectionType: 'completeSelection',
|
||||||
|
selection: pathMapToSelections(
|
||||||
|
kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
pathToNodeMap
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Get ABS Y info': async ({
|
||||||
|
selectionRanges,
|
||||||
|
}): Promise<SetSelections> => {
|
||||||
|
const { modifiedAst, pathToNodeMap } =
|
||||||
|
await applyConstraintAbsDistance({
|
||||||
|
constraint: 'yAbs',
|
||||||
|
selectionRanges,
|
||||||
|
})
|
||||||
|
await kclManager.updateAst(modifiedAst, true)
|
||||||
|
return {
|
||||||
|
selectionType: 'completeSelection',
|
||||||
|
selection: pathMapToSelections(
|
||||||
|
kclManager.ast,
|
||||||
|
selectionRanges,
|
||||||
|
pathToNodeMap
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
devTools: true,
|
||||||
devTools: true,
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
engineCommandManager.onPlaneSelected((plane_id: string) => {
|
engineCommandManager.onPlaneSelected((plane_id: string) => {
|
||||||
@ -538,13 +559,17 @@ export const ModelingMachineProvider = ({
|
|||||||
})
|
})
|
||||||
}, [modelingSend])
|
}, [modelingSend])
|
||||||
|
|
||||||
// useStateMachineCommands({
|
useStateMachineCommands({
|
||||||
// state: settingsState,
|
machineId: 'modeling',
|
||||||
// send: settingsSend,
|
state: modelingState,
|
||||||
// commands,
|
send: modelingSend,
|
||||||
// owner: 'settings',
|
actor: modelingActor,
|
||||||
// commandBarMeta: settingsCommandBarMeta,
|
commandBarConfig: modelingMachineConfig,
|
||||||
// })
|
onCancel: () => {
|
||||||
|
console.log('firing onCancel!!')
|
||||||
|
modelingSend({ type: 'Cancel' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModelingMachineContext.Provider
|
<ModelingMachineContext.Provider
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||||
import CommandBarProvider from './CommandBar'
|
import CommandBarProvider from './CommandBar/CommandBar'
|
||||||
import {
|
import {
|
||||||
NETWORK_CONTENT,
|
NETWORK_CONTENT,
|
||||||
NetworkHealthIndicator,
|
NetworkHealthIndicator,
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
import {
|
import { faExclamation, faWifi } from '@fortawesome/free-solid-svg-icons'
|
||||||
faCheck,
|
|
||||||
faExclamation,
|
|
||||||
faWifi,
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { Popover } from '@headlessui/react'
|
import { Popover } from '@headlessui/react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { ActionIcon } from './ActionIcon'
|
import { ActionIcon } from './ActionIcon'
|
||||||
@ -77,8 +73,8 @@ export const NetworkHealthIndicator = () => {
|
|||||||
data-testid="network-good"
|
data-testid="network-good"
|
||||||
>
|
>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon={faCheck}
|
icon="checkmark"
|
||||||
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded'}
|
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded-sm'}
|
||||||
iconClassName={'text-succeed-80 dark:text-succeed-30'}
|
iconClassName={'text-succeed-80 dark:text-succeed-30'}
|
||||||
/>
|
/>
|
||||||
{NETWORK_CONTENT.good}
|
{NETWORK_CONTENT.good}
|
||||||
|
@ -143,7 +143,7 @@ function ProjectCard({
|
|||||||
className: 'p-1',
|
className: 'p-1',
|
||||||
size: 'xs',
|
size: 'xs',
|
||||||
bgClassName: 'bg-destroy-80',
|
bgClassName: 'bg-destroy-80',
|
||||||
iconClassName: 'text-destroy-20 dark:text-destroy-40',
|
iconClassName: '!text-destroy-20 dark:!text-destroy-40',
|
||||||
}}
|
}}
|
||||||
className="!p-0 hover:border-destroy-40 dark:hover:border-destroy-40"
|
className="!p-0 hover:border-destroy-40 dark:hover:border-destroy-40"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -185,8 +185,7 @@ function ProjectCard({
|
|||||||
bgClassName: 'bg-destroy-80',
|
bgClassName: 'bg-destroy-80',
|
||||||
className: 'p-1',
|
className: 'p-1',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
iconClassName:
|
iconClassName: '!text-destroy-70 dark:!text-destroy-40',
|
||||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10 dark:text-destroy-20 dark:group-hover:text-destroy-10 dark:hover:text-destroy-10',
|
|
||||||
}}
|
}}
|
||||||
className="hover:border-destroy-40 dark:hover:border-destroy-40"
|
className="hover:border-destroy-40 dark:hover:border-destroy-40"
|
||||||
>
|
>
|
||||||
|
@ -3,7 +3,7 @@ import { BrowserRouter } from 'react-router-dom'
|
|||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||||
import CommandBarProvider from './CommandBar'
|
import CommandBarProvider from './CommandBar/CommandBar'
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const projectWellFormed = {
|
const projectWellFormed = {
|
||||||
|
@ -243,15 +243,14 @@ export const Stream = ({ className = '' }) => {
|
|||||||
})
|
})
|
||||||
} else if (
|
} else if (
|
||||||
!didDragInStream &&
|
!didDragInStream &&
|
||||||
(state.matches('Sketch.SketchIdle') ||
|
(state.matches('Sketch.SketchIdle') || state.matches('idle'))
|
||||||
state.matches('idle') ||
|
|
||||||
state.matches('awaiting selection'))
|
|
||||||
) {
|
) {
|
||||||
command.cmd = {
|
command.cmd = {
|
||||||
type: 'select_with_point',
|
type: 'select_with_point',
|
||||||
selected_at_window: { x, y },
|
selected_at_window: { x, y },
|
||||||
selection_type: 'add',
|
selection_type: 'add',
|
||||||
}
|
}
|
||||||
|
|
||||||
engineCommandManager.sendSceneCommand(command)
|
engineCommandManager.sendSceneCommand(command)
|
||||||
} else if (!didDragInStream && state.matches('Sketch.Move Tool')) {
|
} else if (!didDragInStream && state.matches('Sketch.Move Tool')) {
|
||||||
command.cmd = {
|
command.cmd = {
|
||||||
|
@ -64,7 +64,7 @@ export const TextEditor = ({
|
|||||||
|
|
||||||
const { settings: { context: { textWrapping } = {} } = {} } =
|
const { settings: { context: { textWrapping } = {} } = {} } =
|
||||||
useGlobalStateContext()
|
useGlobalStateContext()
|
||||||
const { setCommandBarOpen } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { enable: convertEnabled, handleClick: convertCallback } =
|
const { enable: convertEnabled, handleClick: convertCallback } =
|
||||||
useConvertToVariable()
|
useConvertToVariable()
|
||||||
|
|
||||||
@ -136,7 +136,7 @@ export const TextEditor = ({
|
|||||||
{
|
{
|
||||||
key: 'Meta-k',
|
key: 'Meta-k',
|
||||||
run: () => {
|
run: () => {
|
||||||
setCommandBarOpen(true)
|
commandBarSend({ type: 'Open' })
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||||
import CommandBarProvider from './CommandBar'
|
import CommandBarProvider from './CommandBar/CommandBar'
|
||||||
|
|
||||||
type User = Models['User_type']
|
type User = Models['User_type']
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CommandsContext } from 'components/CommandBar'
|
import { CommandsContext } from 'components/CommandBar/CommandBar'
|
||||||
import { useContext } from 'react'
|
import { useContext } from 'react'
|
||||||
|
|
||||||
export const useCommandsContext = () => {
|
export const useCommandsContext = () => {
|
||||||
|
27
src/hooks/usePlatform.ts
Normal file
27
src/hooks/usePlatform.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Platform, platform } from '@tauri-apps/api/os'
|
||||||
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export default function usePlatform() {
|
||||||
|
const [platformName, setPlatformName] = useState<Platform | ''>('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getPlatform() {
|
||||||
|
setPlatformName(await platform())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTauri()) {
|
||||||
|
void getPlatform()
|
||||||
|
} else {
|
||||||
|
if (navigator.userAgent.indexOf('Mac') !== -1) {
|
||||||
|
setPlatformName('darwin')
|
||||||
|
} else if (navigator.userAgent.indexOf('Win') !== -1) {
|
||||||
|
setPlatformName('win32')
|
||||||
|
} else if (navigator.userAgent.indexOf('Linux') !== -1) {
|
||||||
|
setPlatformName('linux')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [setPlatformName])
|
||||||
|
|
||||||
|
return platformName
|
||||||
|
}
|
@ -1,46 +1,68 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { AnyStateMachine, StateFrom } from 'xstate'
|
import { AnyStateMachine, InterpreterFrom, StateFrom } from 'xstate'
|
||||||
import {
|
import { createMachineCommand } from '../lib/createMachineCommand'
|
||||||
Command,
|
|
||||||
CommandBarConfig,
|
|
||||||
createMachineCommand,
|
|
||||||
} from '../lib/commands'
|
|
||||||
import { useCommandsContext } from './useCommandsContext'
|
import { useCommandsContext } from './useCommandsContext'
|
||||||
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
|
import { authMachine } from 'machines/authMachine'
|
||||||
|
import { settingsMachine } from 'machines/settingsMachine'
|
||||||
|
import { homeMachine } from 'machines/homeMachine'
|
||||||
|
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
|
||||||
|
|
||||||
interface UseStateMachineCommandsArgs<T extends AnyStateMachine> {
|
// This might not be necessary, AnyStateMachine from xstate is working
|
||||||
|
export type AllMachines =
|
||||||
|
| typeof modelingMachine
|
||||||
|
| typeof settingsMachine
|
||||||
|
| typeof authMachine
|
||||||
|
| typeof homeMachine
|
||||||
|
|
||||||
|
interface UseStateMachineCommandsArgs<
|
||||||
|
T extends AllMachines,
|
||||||
|
S extends CommandSetSchema<T>
|
||||||
|
> {
|
||||||
|
machineId: T['id']
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
send: Function
|
send: Function
|
||||||
commandBarConfig?: CommandBarConfig<T>
|
actor?: InterpreterFrom<T>
|
||||||
commands: Command[]
|
commandBarConfig?: CommandSetConfig<T, S>
|
||||||
owner: string
|
onCancel?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useStateMachineCommands<T extends AnyStateMachine>({
|
export default function useStateMachineCommands<
|
||||||
|
T extends AnyStateMachine,
|
||||||
|
S extends CommandSetSchema<T>
|
||||||
|
>({
|
||||||
|
machineId,
|
||||||
state,
|
state,
|
||||||
send,
|
send,
|
||||||
|
actor,
|
||||||
commandBarConfig,
|
commandBarConfig,
|
||||||
owner,
|
onCancel,
|
||||||
}: UseStateMachineCommandsArgs<T>) {
|
}: UseStateMachineCommandsArgs<T, S>) {
|
||||||
const { addCommands, removeCommands } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newCommands = state.nextEvents
|
const newCommands = state.nextEvents
|
||||||
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
||||||
.map((type) =>
|
.map((type) =>
|
||||||
createMachineCommand<T>({
|
createMachineCommand<T, S>({
|
||||||
|
ownerMachine: machineId,
|
||||||
type,
|
type,
|
||||||
state,
|
state,
|
||||||
send,
|
send,
|
||||||
|
actor,
|
||||||
commandBarConfig,
|
commandBarConfig,
|
||||||
owner,
|
onCancel,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.filter((c) => c !== null) as Command[] // TS isn't smart enough to know this filter removes nulls
|
.filter((c) => c !== null) as Command[] // TS isn't smart enough to know this filter removes nulls
|
||||||
|
|
||||||
addCommands(newCommands)
|
commandBarSend({ type: 'Add commands', data: { commands: newCommands } })
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
removeCommands(newCommands)
|
commandBarSend({
|
||||||
|
type: 'Remove commands',
|
||||||
|
data: { commands: newCommands },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [state])
|
}, [state])
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@apply border border-chalkboard-30 m-0.5 px-3 rounded text-xs;
|
@apply border border-chalkboard-30 m-0.5 px-3 rounded text-xs focus-visible:ring-energy-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
@ -65,7 +65,7 @@ button:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark button {
|
.dark button {
|
||||||
@apply border-chalkboard-70;
|
@apply border-chalkboard-70 focus-visible:ring-energy-10/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark button:hover {
|
.dark button:hover {
|
||||||
@ -88,6 +88,14 @@ a:not(.action-button) {
|
|||||||
@apply text-chalkboard-20 hover:text-energy-10;
|
@apply text-chalkboard-20 hover:text-energy-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
@apply selection:bg-energy-10/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark input {
|
||||||
|
@apply selection:bg-energy-10/40;
|
||||||
|
}
|
||||||
|
|
||||||
.mono {
|
.mono {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
monospace;
|
monospace;
|
||||||
|
@ -248,7 +248,8 @@ export function mutateObjExpProp(
|
|||||||
export function extrudeSketch(
|
export function extrudeSketch(
|
||||||
node: Program,
|
node: Program,
|
||||||
pathToNode: PathToNode,
|
pathToNode: PathToNode,
|
||||||
shouldPipe = true
|
shouldPipe = true,
|
||||||
|
distance = 4
|
||||||
): {
|
): {
|
||||||
modifiedAst: Program
|
modifiedAst: Program
|
||||||
pathToNode: PathToNode
|
pathToNode: PathToNode
|
||||||
@ -274,7 +275,7 @@ export function extrudeSketch(
|
|||||||
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
||||||
|
|
||||||
const extrudeCall = createCallExpressionStdLib('extrude', [
|
const extrudeCall = createCallExpressionStdLib('extrude', [
|
||||||
createLiteral(4),
|
createLiteral(distance),
|
||||||
shouldPipe
|
shouldPipe
|
||||||
? createPipeSubstitution()
|
? createPipeSubstitution()
|
||||||
: {
|
: {
|
||||||
|
17
src/lib/commandBarConfigs/authCommandConfig.ts
Normal file
17
src/lib/commandBarConfigs/authCommandConfig.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { CommandSetConfig } from 'lib/commandTypes'
|
||||||
|
import { authMachine } from 'machines/authMachine'
|
||||||
|
|
||||||
|
type AuthCommandSchema = {}
|
||||||
|
|
||||||
|
export const authCommandBarConfig: CommandSetConfig<
|
||||||
|
typeof authMachine,
|
||||||
|
AuthCommandSchema
|
||||||
|
> = {
|
||||||
|
'Log in': {
|
||||||
|
hide: 'both',
|
||||||
|
},
|
||||||
|
'Log out': {
|
||||||
|
args: [],
|
||||||
|
icon: 'arrowLeft',
|
||||||
|
},
|
||||||
|
}
|
87
src/lib/commandBarConfigs/homeCommandConfig.ts
Normal file
87
src/lib/commandBarConfigs/homeCommandConfig.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { CommandSetConfig } from 'lib/commandTypes'
|
||||||
|
import { homeMachine } from 'machines/homeMachine'
|
||||||
|
|
||||||
|
export type HomeCommandSchema = {
|
||||||
|
'Create project': {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
'Open project': {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
'Delete project': {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
'Rename project': {
|
||||||
|
oldName: string
|
||||||
|
newName: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const homeCommandBarConfig: CommandSetConfig<
|
||||||
|
typeof homeMachine,
|
||||||
|
HomeCommandSchema
|
||||||
|
> = {
|
||||||
|
'Open project': {
|
||||||
|
icon: 'arrowRight',
|
||||||
|
description: 'Open a project',
|
||||||
|
args: {
|
||||||
|
name: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
options: (context) =>
|
||||||
|
context.projects.map((p) => ({
|
||||||
|
name: p.name!,
|
||||||
|
value: p.name!,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Create project': {
|
||||||
|
icon: 'folderPlus',
|
||||||
|
description: 'Create a project',
|
||||||
|
args: {
|
||||||
|
name: {
|
||||||
|
inputType: 'string',
|
||||||
|
required: true,
|
||||||
|
defaultValue: (context) => context.defaultProjectName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Delete project': {
|
||||||
|
icon: 'close',
|
||||||
|
description: 'Delete a project',
|
||||||
|
needsReview: true,
|
||||||
|
args: {
|
||||||
|
name: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
options: (context) =>
|
||||||
|
context.projects.map((p) => ({
|
||||||
|
name: p.name!,
|
||||||
|
value: p.name!,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Rename project': {
|
||||||
|
icon: 'folder',
|
||||||
|
description: 'Rename a project',
|
||||||
|
needsReview: true,
|
||||||
|
args: {
|
||||||
|
oldName: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
options: (context) =>
|
||||||
|
context.projects.map((p) => ({
|
||||||
|
name: p.name!,
|
||||||
|
value: p.name!,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
newName: {
|
||||||
|
inputType: 'string',
|
||||||
|
required: true,
|
||||||
|
defaultValue: (context) => context.defaultProjectName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
57
src/lib/commandBarConfigs/modelingCommandConfig.ts
Normal file
57
src/lib/commandBarConfigs/modelingCommandConfig.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { CommandSetConfig } from 'lib/commandTypes'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
|
|
||||||
|
export const EXTRUSION_RESULTS = [
|
||||||
|
'new',
|
||||||
|
'add',
|
||||||
|
'subtract',
|
||||||
|
'intersect',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type ModelingCommandSchema = {
|
||||||
|
'Enter sketch': {}
|
||||||
|
Extrude: {
|
||||||
|
selection: Selections // & { type: 'face' } would be cool to lock that down
|
||||||
|
// result: (typeof EXTRUSION_RESULTS)[number]
|
||||||
|
distance: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modelingMachineConfig: CommandSetConfig<
|
||||||
|
typeof modelingMachine,
|
||||||
|
ModelingCommandSchema
|
||||||
|
> = {
|
||||||
|
'Enter sketch': {
|
||||||
|
description: 'Enter sketch mode.',
|
||||||
|
icon: 'sketch',
|
||||||
|
},
|
||||||
|
Extrude: {
|
||||||
|
description: 'Pull a sketch into 3D along its normal or perpendicular.',
|
||||||
|
icon: 'extrude',
|
||||||
|
needsReview: true,
|
||||||
|
args: {
|
||||||
|
selection: {
|
||||||
|
inputType: 'selection',
|
||||||
|
selectionTypes: ['face'],
|
||||||
|
multiple: false, // TODO: multiple selection
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// result: {
|
||||||
|
// inputType: 'options',
|
||||||
|
// payload: 'add',
|
||||||
|
// required: true,
|
||||||
|
// options: EXTRUSION_RESULTS.map((r) => ({
|
||||||
|
// name: r,
|
||||||
|
// isCurrent: r === 'add',
|
||||||
|
// value: r,
|
||||||
|
// })),
|
||||||
|
// },
|
||||||
|
distance: {
|
||||||
|
inputType: 'number',
|
||||||
|
defaultValue: 5,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
141
src/lib/commandBarConfigs/settingsCommandConfig.ts
Normal file
141
src/lib/commandBarConfigs/settingsCommandConfig.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { CommandSetConfig } from '../commandTypes'
|
||||||
|
import {
|
||||||
|
BaseUnit,
|
||||||
|
Toggle,
|
||||||
|
UnitSystem,
|
||||||
|
baseUnitsUnion,
|
||||||
|
settingsMachine,
|
||||||
|
} from 'machines/settingsMachine'
|
||||||
|
import { CameraSystem, cameraSystems } from '../cameraControls'
|
||||||
|
import { Themes } from '../theme'
|
||||||
|
|
||||||
|
// SETTINGS MACHINE
|
||||||
|
export type SettingsCommandSchema = {
|
||||||
|
'Set Base Unit': {
|
||||||
|
baseUnit: BaseUnit
|
||||||
|
}
|
||||||
|
'Set Camera Controls': {
|
||||||
|
cameraControls: CameraSystem
|
||||||
|
}
|
||||||
|
'Set Default Project Name': {
|
||||||
|
defaultProjectName: string
|
||||||
|
}
|
||||||
|
'Set Text Wrapping': {
|
||||||
|
textWrapping: Toggle
|
||||||
|
}
|
||||||
|
'Set Theme': {
|
||||||
|
theme: Themes
|
||||||
|
}
|
||||||
|
'Set Unit System': {
|
||||||
|
unitSystem: UnitSystem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingsCommandBarConfig: CommandSetConfig<
|
||||||
|
typeof settingsMachine,
|
||||||
|
SettingsCommandSchema
|
||||||
|
> = {
|
||||||
|
'Set Base Unit': {
|
||||||
|
icon: 'gear',
|
||||||
|
args: {
|
||||||
|
baseUnit: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
defaultValue: (context) => context.baseUnit,
|
||||||
|
options: (context) =>
|
||||||
|
Object.values(baseUnitsUnion).map((v) => ({
|
||||||
|
name: v,
|
||||||
|
value: v,
|
||||||
|
isCurrent: v === context.baseUnit,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Set Camera Controls': {
|
||||||
|
icon: 'gear',
|
||||||
|
args: {
|
||||||
|
cameraControls: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
defaultValue: (context) => context.cameraControls,
|
||||||
|
options: (context) =>
|
||||||
|
Object.values(cameraSystems).map((v) => ({
|
||||||
|
name: v,
|
||||||
|
value: v,
|
||||||
|
isCurrent: v === context.cameraControls,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Set Default Project Name': {
|
||||||
|
icon: 'gear',
|
||||||
|
hide: 'web',
|
||||||
|
args: {
|
||||||
|
defaultProjectName: {
|
||||||
|
inputType: 'string',
|
||||||
|
required: true,
|
||||||
|
defaultValue: (context) => context.defaultProjectName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Set Text Wrapping': {
|
||||||
|
icon: 'gear',
|
||||||
|
args: {
|
||||||
|
textWrapping: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
defaultValue: (context) => context.textWrapping,
|
||||||
|
options: (context) => [
|
||||||
|
{
|
||||||
|
name: 'On',
|
||||||
|
value: 'On' as Toggle,
|
||||||
|
isCurrent: context.textWrapping === 'On',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Off',
|
||||||
|
value: 'Off' as Toggle,
|
||||||
|
isCurrent: context.textWrapping === 'Off',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Set Theme': {
|
||||||
|
icon: 'gear',
|
||||||
|
args: {
|
||||||
|
theme: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
defaultValue: (context) => context.theme,
|
||||||
|
options: (context) =>
|
||||||
|
Object.values(Themes).map((v) => ({
|
||||||
|
name: v,
|
||||||
|
value: v,
|
||||||
|
isCurrent: v === context.theme,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Set Unit System': {
|
||||||
|
icon: 'gear',
|
||||||
|
args: {
|
||||||
|
unitSystem: {
|
||||||
|
inputType: 'options',
|
||||||
|
required: true,
|
||||||
|
defaultValue: (context) => context.unitSystem,
|
||||||
|
options: (context) => [
|
||||||
|
{
|
||||||
|
name: 'Imperial',
|
||||||
|
value: 'imperial' as UnitSystem,
|
||||||
|
isCurrent: context.unitSystem === 'imperial',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Metric',
|
||||||
|
value: 'metric' as UnitSystem,
|
||||||
|
isCurrent: context.unitSystem === 'metric',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
136
src/lib/commandTypes.ts
Normal file
136
src/lib/commandTypes.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { CustomIconName } from 'components/CustomIcon'
|
||||||
|
import { AllMachines } from 'hooks/useStateMachineCommands'
|
||||||
|
import {
|
||||||
|
AnyStateMachine,
|
||||||
|
ContextFrom,
|
||||||
|
EventFrom,
|
||||||
|
InterpreterFrom,
|
||||||
|
} from 'xstate'
|
||||||
|
import { Selection } from './selections'
|
||||||
|
|
||||||
|
type Icon = CustomIconName
|
||||||
|
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
||||||
|
const INPUT_TYPES = ['options', 'string', 'number', 'selection'] as const
|
||||||
|
export type CommandInputType = (typeof INPUT_TYPES)[number]
|
||||||
|
|
||||||
|
export type CommandSetSchema<T extends AnyStateMachine> = Partial<{
|
||||||
|
[EventType in EventFrom<T>['type']]: Record<string, any>
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type CommandSet<
|
||||||
|
T extends AllMachines,
|
||||||
|
Schema extends CommandSetSchema<T>
|
||||||
|
> = Partial<{
|
||||||
|
[EventType in EventFrom<T>['type']]: Command<
|
||||||
|
T,
|
||||||
|
EventFrom<T>['type'],
|
||||||
|
Schema[EventType]
|
||||||
|
>
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type CommandSetConfig<
|
||||||
|
T extends AllMachines,
|
||||||
|
Schema extends CommandSetSchema<T>
|
||||||
|
> = Partial<{
|
||||||
|
[EventType in EventFrom<T>['type']]: CommandConfig<
|
||||||
|
T,
|
||||||
|
EventFrom<T>['type'],
|
||||||
|
Schema[EventType]
|
||||||
|
>
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type Command<
|
||||||
|
T extends AnyStateMachine = AnyStateMachine,
|
||||||
|
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'],
|
||||||
|
CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName]
|
||||||
|
> = {
|
||||||
|
name: CommandName
|
||||||
|
ownerMachine: T['id']
|
||||||
|
needsReview: boolean
|
||||||
|
onSubmit: (data?: CommandSchema) => void
|
||||||
|
onCancel?: () => void
|
||||||
|
args?: {
|
||||||
|
[ArgName in keyof CommandSchema]: CommandArgument<CommandSchema[ArgName], T>
|
||||||
|
}
|
||||||
|
description?: string
|
||||||
|
icon?: Icon
|
||||||
|
hide?: (typeof PLATFORMS)[number]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommandConfig<
|
||||||
|
T extends AnyStateMachine = AnyStateMachine,
|
||||||
|
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'],
|
||||||
|
CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName]
|
||||||
|
> = Omit<
|
||||||
|
Command<T, CommandName, CommandSchema>,
|
||||||
|
'name' | 'ownerMachine' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview'
|
||||||
|
> & {
|
||||||
|
needsReview?: true
|
||||||
|
args?: {
|
||||||
|
[ArgName in keyof CommandSchema]: CommandArgumentConfig<
|
||||||
|
CommandSchema[ArgName],
|
||||||
|
T
|
||||||
|
>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommandArgumentConfig<
|
||||||
|
OutputType,
|
||||||
|
T extends AnyStateMachine = AnyStateMachine
|
||||||
|
> =
|
||||||
|
| {
|
||||||
|
description?: string
|
||||||
|
required: boolean
|
||||||
|
skip?: true
|
||||||
|
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
||||||
|
payload?: OutputType
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
inputType: Extract<CommandInputType, 'options'>
|
||||||
|
options:
|
||||||
|
| CommandArgumentOption<OutputType>[]
|
||||||
|
| ((context: ContextFrom<T>) => CommandArgumentOption<OutputType>[])
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
inputType: Extract<CommandInputType, 'selection'>
|
||||||
|
selectionTypes: Selection['type'][]
|
||||||
|
multiple: boolean
|
||||||
|
}
|
||||||
|
| { inputType: Exclude<CommandInputType, 'options' | 'selection'> }
|
||||||
|
)
|
||||||
|
|
||||||
|
export type CommandArgument<
|
||||||
|
OutputType,
|
||||||
|
T extends AnyStateMachine = AnyStateMachine
|
||||||
|
> =
|
||||||
|
| {
|
||||||
|
description?: string
|
||||||
|
required: boolean
|
||||||
|
payload?: OutputType // Payload sets the initialized value and more importantly its type
|
||||||
|
defaultValue?: OutputType // Default value is used as the starting value for the input on this argument
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
inputType: Extract<CommandInputType, 'options'>
|
||||||
|
options: CommandArgumentOption<OutputType>[]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
inputType: Extract<CommandInputType, 'selection'>
|
||||||
|
selectionTypes: Selection['type'][]
|
||||||
|
actor: InterpreterFrom<T>
|
||||||
|
multiple: boolean
|
||||||
|
}
|
||||||
|
| { inputType: Exclude<CommandInputType, 'options' | 'selection'> }
|
||||||
|
)
|
||||||
|
|
||||||
|
export type CommandArgumentWithName<
|
||||||
|
OutputType,
|
||||||
|
T extends AnyStateMachine = AnyStateMachine
|
||||||
|
> = CommandArgument<OutputType, T> & {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommandArgumentOption<A> = {
|
||||||
|
name: string
|
||||||
|
isCurrent?: boolean
|
||||||
|
value: A
|
||||||
|
}
|
@ -1,177 +0,0 @@
|
|||||||
import { AnyStateMachine, ContextFrom, EventFrom, StateFrom } from 'xstate'
|
|
||||||
import { isTauri } from './isTauri'
|
|
||||||
import { CustomIconName } from 'components/CustomIcon'
|
|
||||||
|
|
||||||
type Icon = CustomIconName
|
|
||||||
type Platform = 'both' | 'web' | 'desktop'
|
|
||||||
type InputType = 'select' | 'string' | 'interaction'
|
|
||||||
export type CommandArgumentOption = { name: string; isCurrent?: boolean }
|
|
||||||
|
|
||||||
// Command arguments can either be defined manually
|
|
||||||
// or flagged as needing to be looked up from the context.
|
|
||||||
// This is useful for things like settings, where
|
|
||||||
// we want to show the current setting value as the default.
|
|
||||||
// The lookup is done in createMachineCommand.
|
|
||||||
type CommandArgumentConfig<T extends AnyStateMachine> = {
|
|
||||||
name: string // TODO: I would love for this to be strongly-typed so we could guarantee it's a valid data payload key on the event type.
|
|
||||||
type: InputType
|
|
||||||
description?: string
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
type: 'select'
|
|
||||||
options?: CommandArgumentOption[]
|
|
||||||
getOptionsFromContext?: keyof ContextFrom<T>
|
|
||||||
defaultValue?: string
|
|
||||||
getDefaultValueFromContext?: keyof ContextFrom<T>
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'string'
|
|
||||||
defaultValue?: string
|
|
||||||
getDefaultValueFromContext?: keyof ContextFrom<T>
|
|
||||||
}
|
|
||||||
| { type: 'interaction' }
|
|
||||||
)
|
|
||||||
|
|
||||||
export type CommandBarConfig<T extends AnyStateMachine> = Partial<{
|
|
||||||
[EventType in EventFrom<T>['type']]:
|
|
||||||
| {
|
|
||||||
args: CommandArgumentConfig<T>[]
|
|
||||||
formatFunction?: (args: string[]) => string
|
|
||||||
icon?: Icon
|
|
||||||
hide?: Platform
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
hide?: Platform
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
|
|
||||||
export type Command = {
|
|
||||||
owner: string
|
|
||||||
name: string
|
|
||||||
callback: Function
|
|
||||||
icon?: Icon
|
|
||||||
args?: CommandArgument[]
|
|
||||||
formatFunction?: (args: string[]) => string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CommandArgument = {
|
|
||||||
name: string
|
|
||||||
defaultValue?: string
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
type: Extract<InputType, 'select'>
|
|
||||||
options: CommandArgumentOption[]
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: Exclude<InputType, 'select'>
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
interface CreateMachineCommandProps<T extends AnyStateMachine> {
|
|
||||||
type: EventFrom<T>['type']
|
|
||||||
state: StateFrom<T>
|
|
||||||
commandBarConfig?: CommandBarConfig<T>
|
|
||||||
send: Function
|
|
||||||
owner: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a command with subcommands, ready for use in the CommandBar component,
|
|
||||||
// from a more terse Command Bar Meta definition.
|
|
||||||
export function createMachineCommand<T extends AnyStateMachine>({
|
|
||||||
type,
|
|
||||||
state,
|
|
||||||
commandBarConfig,
|
|
||||||
send,
|
|
||||||
owner,
|
|
||||||
}: CreateMachineCommandProps<T>): Command | null {
|
|
||||||
const lookedUpMeta = commandBarConfig && commandBarConfig[type]
|
|
||||||
if (!lookedUpMeta) return null
|
|
||||||
|
|
||||||
// Hide commands based on platform by returning `null`
|
|
||||||
// so the consumer can filter them out
|
|
||||||
if ('hide' in lookedUpMeta) {
|
|
||||||
const { hide } = lookedUpMeta
|
|
||||||
if (hide === 'both') return null
|
|
||||||
else if (hide === 'desktop' && isTauri()) return null
|
|
||||||
else if (hide === 'web' && !isTauri()) return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const icon = ('icon' in lookedUpMeta && lookedUpMeta.icon) || undefined
|
|
||||||
const formatFunction =
|
|
||||||
('formatFunction' in lookedUpMeta && lookedUpMeta.formatFunction) ||
|
|
||||||
undefined
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: type,
|
|
||||||
owner,
|
|
||||||
icon,
|
|
||||||
callback: (data: EventFrom<T, typeof type>) => {
|
|
||||||
if (data !== undefined && data !== null) {
|
|
||||||
send(type, { data })
|
|
||||||
} else {
|
|
||||||
send(type)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
...('args' in lookedUpMeta
|
|
||||||
? {
|
|
||||||
args: getCommandArgumentValuesFromContext(state, lookedUpMeta.args),
|
|
||||||
formatFunction,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCommandArgumentValuesFromContext<T extends AnyStateMachine>(
|
|
||||||
state: StateFrom<T>,
|
|
||||||
args: CommandArgumentConfig<T>[]
|
|
||||||
): CommandArgument[] {
|
|
||||||
function getDefaultValue(
|
|
||||||
arg: CommandArgumentConfig<T> & { type: 'string' | 'select' }
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
arg.type === 'select' ||
|
|
||||||
('getDefaultValueFromContext' in arg && arg.getDefaultValueFromContext)
|
|
||||||
) {
|
|
||||||
return state.context[arg.getDefaultValueFromContext]
|
|
||||||
} else {
|
|
||||||
return arg.defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return args.map((arg) => {
|
|
||||||
switch (arg.type) {
|
|
||||||
case 'interaction':
|
|
||||||
return {
|
|
||||||
name: arg.name,
|
|
||||||
type: 'interaction',
|
|
||||||
}
|
|
||||||
case 'string':
|
|
||||||
return {
|
|
||||||
name: arg.name,
|
|
||||||
type: arg.type,
|
|
||||||
defaultValue: arg.getDefaultValueFromContext
|
|
||||||
? state.context[arg.getDefaultValueFromContext]
|
|
||||||
: arg.defaultValue,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
name: arg.name,
|
|
||||||
type: arg.type,
|
|
||||||
defaultValue: getDefaultValue(arg),
|
|
||||||
options: arg.getOptionsFromContext
|
|
||||||
? state.context[arg.getOptionsFromContext].map(
|
|
||||||
(v: string | { name: string }) => ({
|
|
||||||
name: typeof v === 'string' ? v : v.name,
|
|
||||||
isCurrent: v === getDefaultValue(arg),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
: arg.getDefaultValueFromContext
|
|
||||||
? arg.options?.map((v) => ({
|
|
||||||
...v,
|
|
||||||
isCurrent: v.name === getDefaultValue(arg),
|
|
||||||
}))
|
|
||||||
: arg.options,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
158
src/lib/createMachineCommand.ts
Normal file
158
src/lib/createMachineCommand.ts
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { AnyStateMachine, EventFrom, InterpreterFrom, StateFrom } from 'xstate'
|
||||||
|
import { isTauri } from './isTauri'
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandArgument,
|
||||||
|
CommandArgumentConfig,
|
||||||
|
CommandConfig,
|
||||||
|
CommandSetConfig,
|
||||||
|
CommandSetSchema,
|
||||||
|
} from './commandTypes'
|
||||||
|
|
||||||
|
interface CreateMachineCommandProps<
|
||||||
|
T extends AnyStateMachine,
|
||||||
|
S extends CommandSetSchema<T>
|
||||||
|
> {
|
||||||
|
type: EventFrom<T>['type']
|
||||||
|
ownerMachine: T['id']
|
||||||
|
state: StateFrom<T>
|
||||||
|
send: Function
|
||||||
|
actor?: InterpreterFrom<T>
|
||||||
|
commandBarConfig?: CommandSetConfig<T, S>
|
||||||
|
onCancel?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a command with subcommands, ready for use in the CommandBar component,
|
||||||
|
// from a more terse Command Bar Meta definition.
|
||||||
|
export function createMachineCommand<
|
||||||
|
T extends AnyStateMachine,
|
||||||
|
S extends CommandSetSchema<T>
|
||||||
|
>({
|
||||||
|
ownerMachine,
|
||||||
|
type,
|
||||||
|
state,
|
||||||
|
send,
|
||||||
|
actor,
|
||||||
|
commandBarConfig,
|
||||||
|
onCancel,
|
||||||
|
}: CreateMachineCommandProps<T, S>): Command<
|
||||||
|
T,
|
||||||
|
typeof type,
|
||||||
|
S[typeof type]
|
||||||
|
> | null {
|
||||||
|
const commandConfig = commandBarConfig && commandBarConfig[type]
|
||||||
|
if (!commandConfig) return null
|
||||||
|
|
||||||
|
// Hide commands based on platform by returning `null`
|
||||||
|
// so the consumer can filter them out
|
||||||
|
if ('hide' in commandConfig) {
|
||||||
|
const { hide } = commandConfig
|
||||||
|
if (hide === 'both') return null
|
||||||
|
else if (hide === 'desktop' && isTauri()) return null
|
||||||
|
else if (hide === 'web' && !isTauri()) return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon = ('icon' in commandConfig && commandConfig.icon) || undefined
|
||||||
|
|
||||||
|
const command: Command<T, typeof type, S[typeof type]> = {
|
||||||
|
name: type,
|
||||||
|
ownerMachine: ownerMachine,
|
||||||
|
icon,
|
||||||
|
needsReview: commandConfig.needsReview || false,
|
||||||
|
onSubmit: (data?: S[typeof type]) => {
|
||||||
|
if (data !== undefined && data !== null) {
|
||||||
|
send(type, { data })
|
||||||
|
} else {
|
||||||
|
send(type)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandConfig.args) {
|
||||||
|
const newArgs = buildCommandArguments(state, commandConfig.args, actor)
|
||||||
|
|
||||||
|
command.args = newArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onCancel) {
|
||||||
|
command.onCancel = onCancel
|
||||||
|
}
|
||||||
|
|
||||||
|
return command
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes the args from a CommandConfig and creates
|
||||||
|
// a finalized CommandArgument object for each one,
|
||||||
|
// bundled together into the args for a Command.
|
||||||
|
function buildCommandArguments<
|
||||||
|
T extends AnyStateMachine,
|
||||||
|
S extends CommandSetSchema<T>,
|
||||||
|
CommandName extends EventFrom<T>['type'] = EventFrom<T>['type']
|
||||||
|
>(
|
||||||
|
state: StateFrom<T>,
|
||||||
|
args: CommandConfig<T, CommandName, S>['args'],
|
||||||
|
actor?: InterpreterFrom<T>
|
||||||
|
): NonNullable<Command<T, CommandName, S>['args']> {
|
||||||
|
const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
|
||||||
|
|
||||||
|
for (const arg in args) {
|
||||||
|
const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T>
|
||||||
|
const newArg = buildCommandArgument(argConfig, state, actor)
|
||||||
|
newArgs[arg] = newArg
|
||||||
|
}
|
||||||
|
|
||||||
|
return newArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCommandArgument<
|
||||||
|
O extends CommandSetSchema<T>,
|
||||||
|
T extends AnyStateMachine
|
||||||
|
>(
|
||||||
|
arg: CommandArgumentConfig<O, T>,
|
||||||
|
state: StateFrom<T>,
|
||||||
|
actor?: InterpreterFrom<T>
|
||||||
|
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
||||||
|
const baseCommandArgument = {
|
||||||
|
description: arg.description,
|
||||||
|
required: arg.required,
|
||||||
|
payload: arg.payload,
|
||||||
|
defaultValue:
|
||||||
|
arg.defaultValue instanceof Function
|
||||||
|
? arg.defaultValue(state.context)
|
||||||
|
: arg.defaultValue,
|
||||||
|
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
||||||
|
|
||||||
|
if (arg.inputType === 'options') {
|
||||||
|
const options = arg.options
|
||||||
|
? arg.options instanceof Function
|
||||||
|
? arg.options(state.context)
|
||||||
|
: arg.options
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (!options) {
|
||||||
|
throw new Error('Options must be provided for options input type')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputType: arg.inputType,
|
||||||
|
...baseCommandArgument,
|
||||||
|
options,
|
||||||
|
} satisfies CommandArgument<O, T> & { inputType: 'options' }
|
||||||
|
} else if (arg.inputType === 'selection') {
|
||||||
|
if (!actor)
|
||||||
|
throw new Error('Actor must be provided for selection input type')
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputType: arg.inputType,
|
||||||
|
...baseCommandArgument,
|
||||||
|
multiple: arg.multiple,
|
||||||
|
selectionTypes: arg.selectionTypes,
|
||||||
|
actor,
|
||||||
|
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
inputType: arg.inputType,
|
||||||
|
...baseCommandArgument,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,10 @@ import { EditorSelection } from '@codemirror/state'
|
|||||||
import { kclManager } from 'lang/KclSinglton'
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
import { SelectionRange } from '@uiw/react-codemirror'
|
import { SelectionRange } from '@uiw/react-codemirror'
|
||||||
import { isOverlap } from 'lib/utils'
|
import { isOverlap } from 'lib/utils'
|
||||||
|
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||||
|
import { Program } from 'lang/wasm'
|
||||||
|
import { doesPipeHaveCallExp } from 'lang/queryAst'
|
||||||
|
import { CommandArgument } from './commandTypes'
|
||||||
|
|
||||||
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
|
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
|
||||||
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
|
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
|
||||||
@ -371,3 +375,128 @@ function resetAndSetEngineEntitySelectionCmds(
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSketchPipe(selectionRanges: Selections) {
|
||||||
|
return isCursorInSketchCommandRange(
|
||||||
|
engineCommandManager.artifactMap,
|
||||||
|
selectionRanges
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSelectionLastLine(
|
||||||
|
selectionRanges: Selections,
|
||||||
|
code: string,
|
||||||
|
i = 0
|
||||||
|
) {
|
||||||
|
return selectionRanges.codeBasedSelections[i].range[1] === code.length
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommonASTNode = {
|
||||||
|
selection: Selection
|
||||||
|
ast: Program
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCommonNodeFromSelection(
|
||||||
|
selectionRanges: Selections,
|
||||||
|
i: number
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
selection: selectionRanges.codeBasedSelections[i],
|
||||||
|
ast: kclManager.ast,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nodeHasExtrude(node: CommonASTNode) {
|
||||||
|
return doesPipeHaveCallExp({
|
||||||
|
calleeName: 'extrude',
|
||||||
|
...node,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nodeHasClose(node: CommonASTNode) {
|
||||||
|
return doesPipeHaveCallExp({
|
||||||
|
calleeName: 'close',
|
||||||
|
...node,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canExtrudeSelection(selection: Selections) {
|
||||||
|
const commonNodes = selection.codeBasedSelections.map((_, i) =>
|
||||||
|
buildCommonNodeFromSelection(selection, i)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
!!isSketchPipe(selection) &&
|
||||||
|
commonNodes.every((n) => nodeHasClose(n)) &&
|
||||||
|
commonNodes.every((n) => !nodeHasExtrude(n))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canExtrudeSelectionItem(selection: Selections, i: number) {
|
||||||
|
const commonNode = buildCommonNodeFromSelection(selection, i)
|
||||||
|
|
||||||
|
return (
|
||||||
|
!!isSketchPipe(selection) &&
|
||||||
|
nodeHasClose(commonNode) &&
|
||||||
|
!nodeHasExtrude(commonNode)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This accounts for non-geometry selections under "other"
|
||||||
|
export type ResolvedSelectionType = [Selection['type'] | 'other', number]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In the future, I'd like this function to properly return the type of each selected entity based on
|
||||||
|
* its code source range, so that we can show something like "0 objects" or "1 face" or "1 line, 2 edges",
|
||||||
|
* and then validate the selection in CommandBarSelectionInput.tsx and show the proper label.
|
||||||
|
* @param selection
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getSelectionType(
|
||||||
|
selection: Selections
|
||||||
|
): ResolvedSelectionType[] {
|
||||||
|
return selection.codeBasedSelections
|
||||||
|
.map((s, i) => {
|
||||||
|
if (canExtrudeSelectionItem(selection, i)) {
|
||||||
|
return ['face', 1] as ResolvedSelectionType // This is implicitly determining what a face is, which is bad
|
||||||
|
} else {
|
||||||
|
return ['other', 1] as ResolvedSelectionType
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.reduce((acc, [type, count]) => {
|
||||||
|
const foundIndex = acc.findIndex((item) => item && item[0] === type)
|
||||||
|
|
||||||
|
if (foundIndex === -1) {
|
||||||
|
return [...acc, [type, count]]
|
||||||
|
} else {
|
||||||
|
const temp = [...acc]
|
||||||
|
temp[foundIndex][1] += count
|
||||||
|
return temp
|
||||||
|
}
|
||||||
|
}, [] as ResolvedSelectionType[])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSelectionTypeDisplayText(
|
||||||
|
selection: Selections
|
||||||
|
): string | null {
|
||||||
|
const selectionsByType = getSelectionType(selection)
|
||||||
|
|
||||||
|
return (selectionsByType as Exclude<typeof selectionsByType, 'none'>)
|
||||||
|
.map(([type, count]) => `${count} ${type}${count > 1 ? 's' : ''}`)
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canSubmitSelectionArg(
|
||||||
|
selectionsByType: 'none' | ResolvedSelectionType[],
|
||||||
|
argument: CommandArgument<unknown> & { inputType: 'selection' }
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
selectionsByType !== 'none' &&
|
||||||
|
selectionsByType.every(([type, count]) => {
|
||||||
|
const foundIndex = argument.selectionTypes.findIndex((s) => s === type)
|
||||||
|
return (
|
||||||
|
foundIndex !== -1 &&
|
||||||
|
(!argument.multiple ? count < 2 && count > 0 : count > 0)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { createMachine, assign } from 'xstate'
|
import { createMachine, assign } from 'xstate'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import withBaseURL from '../lib/withBaseURL'
|
import withBaseURL from '../lib/withBaseURL'
|
||||||
import { CommandBarConfig } from '../lib/commands'
|
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import { invoke } from '@tauri-apps/api'
|
import { invoke } from '@tauri-apps/api'
|
||||||
import { VITE_KC_API_BASE_URL } from 'env'
|
import { VITE_KC_API_BASE_URL } from 'env'
|
||||||
@ -40,16 +39,6 @@ export type Events =
|
|||||||
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
|
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
|
||||||
const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
|
const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
|
||||||
|
|
||||||
export const authCommandBarConfig: CommandBarConfig<typeof authMachine> = {
|
|
||||||
'Log in': {
|
|
||||||
hide: 'both',
|
|
||||||
},
|
|
||||||
'Log out': {
|
|
||||||
args: [],
|
|
||||||
icon: 'arrowLeft',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const authMachine = createMachine<UserContext, Events>(
|
export const authMachine = createMachine<UserContext, Events>(
|
||||||
{
|
{
|
||||||
id: 'Auth',
|
id: 'Auth',
|
||||||
|
425
src/machines/commandBarMachine.ts
Normal file
425
src/machines/commandBarMachine.ts
Normal file
@ -0,0 +1,425 @@
|
|||||||
|
import { assign, createMachine } from 'xstate'
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandArgument,
|
||||||
|
CommandArgumentWithName,
|
||||||
|
} from 'lib/commandTypes'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
|
|
||||||
|
export const commandBarMachine = createMachine(
|
||||||
|
{
|
||||||
|
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswdJNWcNbPFLNMr5AsRFWUtJcVSdRR2VVMUmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUG1klTsSi0awQDgYWhmqkWjnUslBWhOZyegU61GuZAAghACN8-HhYH92FxAXFQAlRA5FpJ5FjFPYtNDpIpLOkEW4GNs4eJxJoGvJxOVcT82gSrj0AEpgdCoABuYC+8ogNOYI3psQmYlkGUkU2slhc6mkmUUCIyKLc4i0lkU8iUyjUHLlVIuJEkAGUwK8Pg8oDr-WQQ2HPpTzrTIkagUzEDkLHZkQK1CUtKCHAiNlsdjkVAdFEc-ed2oGAOJYbgACzAJAj+FIUAAruhBtxYGQACJwUPveO6pMA43AhA5UqWXOzBjS-nOR3LySqXNODTyZwONTV-GXXXPUcfHqTlOM4TrSublb5-lLewlIVySRaOrmGw-jLSI8FRPf0zzjS8HHCOlogZE1ERUEUSgPa05yQ1ZjEQeZP2-VwDzUN1zEabxTlPAkG2bVt207Hs+wHZAm1wGAvi7EgSD7DsSG7XscG4K9oOnNNEVUeQZGSOxc0qaQ7HhdDBJFeRc35extGkNI+UAgNJDIls2xwSMqK4-tJBJAB3LAYl0-AHjYLtuBjLsACN0B4djOL7XixhvBIDxUT8qj5VIkTwtQHRk8oUQcD15LMXQclcdTa00xttMojjqO42BJAANSwShOAgRsIzICB+DASQHg1VAAGtSo1HK8sbMASVSgz3JgmdMnKGYzRlbIdGXA8EX8zd3RsTY9yyY4iLxID6ySiiLP0misrq-LeF0shWxIVBAzYShGwAM229BJFq3LVsa5q3INf5r1gg9JIfXqqh0TQsiFTYrCyJZRpsXJ4oJVUNU4MBjLsxznITX5rqgjzYMsaZtGXaVNhcZFKgRd0RXUdJ6mUXQXX+jpAeB0GyQIRbuNa-jbwQcVSmhAUeTkWQ3HyGSBVKDQMyRAKAsJwNiZBshVXVLUXLSnjoeTPjUxpnmpFtcVs1cPNBq5T8fR5DrKwaHEppIomwCBoWAFEIGcinJcg6XYZnTRhJsbQJU9VlQTVtQNbqcVZhsSFxH5zoWzeSr2ya1z0qKkqypwCrqpOlaGrDiX9WtqdZYSeSEeXEplCceo1xkvQLGCmUIp0Mwck8fWQMVIOQ4spODIHYqcFK8qqpqhPuAu8P+zoCDDRlzzTCkCVWWlSxrBceTygRFxZHZNJbE2VQ5AFAO6PeevI0bmiNpY7bJF2g6jvjs7E8u9KqfTxAkK2fZYuRvdYUGjQZnqYVxRKdx-ZOHBUAgHAQQ00AyD1tgJUQCwLQMEyHUG0Gh7SOmmDuGB6gOTyVBMkDeRJIBgLahA4oFo8zWlSPUT00o0KFA9NMYKrhKwbH2GaQ81cawEljGOdskM8B4OpgkWY9MV5ZjqLUN6MlqGojoRFEo6QWYb1PC8McuCbpD1gosLYrhkTZCxNIawhYxEfW0HhCKKgzT8lBAHLS809KX37Dwm+iIWZSEinjTRy51BFnvNofM7hGZfgXBYuaOlrG9wyiZMya1IxWRsnY4eDi2TONsK45IaghQbEkO4VIKlsaVE2AE8iQTxZN2WufCJMS7o6JSBKNQLp7CT35EKZw2wnDVDyCvBczDmg10NsbYyZS7aZHBNU58q8sTQgxnud+kVsjyQzHrTprDLh11DjY+AyjwFy00DQ2EaQBSLAPLIDGWRNw2BUiXHkwVCJeCAA */
|
||||||
|
context: {
|
||||||
|
commands: [] as Command[],
|
||||||
|
selectedCommand: undefined as Command | undefined,
|
||||||
|
currentArgument: undefined as
|
||||||
|
| (CommandArgument<unknown> & { name: string })
|
||||||
|
| undefined,
|
||||||
|
selectionRanges: {
|
||||||
|
otherSelections: [],
|
||||||
|
codeBasedSelections: [],
|
||||||
|
} as Selections,
|
||||||
|
argumentsToSubmit: {} as { [x: string]: unknown },
|
||||||
|
},
|
||||||
|
id: 'Command Bar',
|
||||||
|
initial: 'Closed',
|
||||||
|
states: {
|
||||||
|
Closed: {
|
||||||
|
on: {
|
||||||
|
Open: {
|
||||||
|
target: 'Selecting command',
|
||||||
|
},
|
||||||
|
|
||||||
|
'Find and select command': {
|
||||||
|
target: 'Command selected',
|
||||||
|
actions: [
|
||||||
|
'Find and select command',
|
||||||
|
'Initialize arguments to submit',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Add commands': {
|
||||||
|
target: 'Closed',
|
||||||
|
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
commands: (context, event) =>
|
||||||
|
[...context.commands, ...event.data.commands].sort(
|
||||||
|
sortCommands
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
internal: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
'Remove commands': {
|
||||||
|
target: 'Closed',
|
||||||
|
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
commands: (context, event) =>
|
||||||
|
context.commands.filter(
|
||||||
|
(c) =>
|
||||||
|
!event.data.commands.some(
|
||||||
|
(c2) =>
|
||||||
|
c2.name === c.name &&
|
||||||
|
c2.ownerMachine === c.ownerMachine
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
internal: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Selecting command': {
|
||||||
|
on: {
|
||||||
|
'Select command': {
|
||||||
|
target: 'Command selected',
|
||||||
|
actions: ['Set selected command', 'Initialize arguments to submit'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Command selected': {
|
||||||
|
always: [
|
||||||
|
{
|
||||||
|
target: 'Closed',
|
||||||
|
cond: 'Command has no arguments',
|
||||||
|
actions: ['Execute command'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'Gathering arguments',
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
currentArgument: (context, event) => {
|
||||||
|
const { selectedCommand } = context
|
||||||
|
if (!(selectedCommand && selectedCommand.args))
|
||||||
|
return undefined
|
||||||
|
const argName = Object.keys(selectedCommand.args)[0]
|
||||||
|
return {
|
||||||
|
...selectedCommand.args[argName],
|
||||||
|
name: argName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Gathering arguments': {
|
||||||
|
states: {
|
||||||
|
'Awaiting input': {
|
||||||
|
on: {
|
||||||
|
'Submit argument': {
|
||||||
|
target: 'Validating',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Validating: {
|
||||||
|
invoke: {
|
||||||
|
src: 'Validate argument',
|
||||||
|
id: 'validateArgument',
|
||||||
|
onDone: {
|
||||||
|
target: '#Command Bar.Checking Arguments',
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
argumentsToSubmit: (context, event) => {
|
||||||
|
const [argName, argData] = Object.entries(event.data)[0]
|
||||||
|
const { currentArgument } = context
|
||||||
|
if (!currentArgument) return {}
|
||||||
|
return {
|
||||||
|
...context.argumentsToSubmit,
|
||||||
|
[argName]: argData,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: 'Awaiting input',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
initial: 'Awaiting input',
|
||||||
|
|
||||||
|
on: {
|
||||||
|
'Change current argument': {
|
||||||
|
target: 'Gathering arguments',
|
||||||
|
internal: true,
|
||||||
|
actions: ['Set current argument'],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Deselect command': {
|
||||||
|
target: 'Selecting command',
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
selectedCommand: (_c, _e) => undefined,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Review: {
|
||||||
|
entry: ['Clear current argument'],
|
||||||
|
on: {
|
||||||
|
'Submit command': {
|
||||||
|
target: 'Closed',
|
||||||
|
actions: ['Execute command'],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Add argument': {
|
||||||
|
target: 'Gathering arguments',
|
||||||
|
actions: ['Set current argument'],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Remove argument': {
|
||||||
|
target: 'Review',
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
argumentsToSubmit: (context, event) => {
|
||||||
|
const argName = Object.keys(event.data)[0]
|
||||||
|
const { argumentsToSubmit } = context
|
||||||
|
const newArgumentsToSubmit = { ...argumentsToSubmit }
|
||||||
|
newArgumentsToSubmit[argName] = undefined
|
||||||
|
return newArgumentsToSubmit
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'Edit argument': {
|
||||||
|
target: 'Gathering arguments',
|
||||||
|
actions: ['Set current argument'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Checking Arguments': {
|
||||||
|
invoke: {
|
||||||
|
src: 'Validate all arguments',
|
||||||
|
id: 'validateArguments',
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
target: 'Review',
|
||||||
|
cond: 'Command needs review',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'Closed',
|
||||||
|
actions: 'Execute command',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: 'Gathering arguments',
|
||||||
|
actions: ['Set current argument'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
Close: {
|
||||||
|
target: '.Closed',
|
||||||
|
},
|
||||||
|
|
||||||
|
Clear: {
|
||||||
|
target: '#Command Bar',
|
||||||
|
internal: true,
|
||||||
|
actions: ['Clear argument data'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
events: {} as
|
||||||
|
| { type: 'Open' }
|
||||||
|
| { type: 'Close' }
|
||||||
|
| { type: 'Clear' }
|
||||||
|
| {
|
||||||
|
type: 'Select command'
|
||||||
|
data: { command: Command }
|
||||||
|
}
|
||||||
|
| { type: 'Deselect command' }
|
||||||
|
| { type: 'Submit command'; data: { [x: string]: unknown } }
|
||||||
|
| {
|
||||||
|
type: 'Add argument'
|
||||||
|
data: { argument: CommandArgumentWithName<unknown> }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'Remove argument'
|
||||||
|
data: { [x: string]: CommandArgumentWithName<unknown> }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'Edit argument'
|
||||||
|
data: { arg: CommandArgumentWithName<unknown> }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'Add commands'
|
||||||
|
data: { commands: Command[] }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'Remove commands'
|
||||||
|
data: { commands: Command[] }
|
||||||
|
}
|
||||||
|
| { type: 'Submit argument'; data: { [x: string]: unknown } }
|
||||||
|
| {
|
||||||
|
type: 'done.invoke.validateArguments'
|
||||||
|
data: { [x: string]: unknown }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'error.platform.validateArguments'
|
||||||
|
data: { message: string; arg: CommandArgumentWithName<unknown> }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'Find and select command'
|
||||||
|
data: { name: string; ownerMachine: string }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'Change current argument'
|
||||||
|
data: { arg: CommandArgumentWithName<unknown> }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
predictableActionArguments: true,
|
||||||
|
preserveActionOrder: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: {
|
||||||
|
'Execute command': (context, event) => {
|
||||||
|
const { selectedCommand } = context
|
||||||
|
if (!selectedCommand) return
|
||||||
|
if (selectedCommand?.args) {
|
||||||
|
selectedCommand?.onSubmit(
|
||||||
|
event.type === 'Submit command' ||
|
||||||
|
event.type === 'done.invoke.validateArguments'
|
||||||
|
? event.data
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
selectedCommand?.onSubmit()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Clear current argument': assign({
|
||||||
|
currentArgument: undefined,
|
||||||
|
}),
|
||||||
|
'Set current argument': assign({
|
||||||
|
currentArgument: (context, event) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'error.platform.validateArguments':
|
||||||
|
return event.data.arg
|
||||||
|
case 'Edit argument':
|
||||||
|
return event.data.arg
|
||||||
|
case 'Change current argument':
|
||||||
|
return event.data.arg
|
||||||
|
default:
|
||||||
|
return context.currentArgument
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'Clear argument data': assign({
|
||||||
|
selectedCommand: undefined,
|
||||||
|
currentArgument: undefined,
|
||||||
|
argumentsToSubmit: {},
|
||||||
|
}),
|
||||||
|
'Set selected command': assign({
|
||||||
|
selectedCommand: (c, e) =>
|
||||||
|
e.type === 'Select command' ? e.data.command : c.selectedCommand,
|
||||||
|
}),
|
||||||
|
'Find and select command': assign({
|
||||||
|
selectedCommand: (c, e) => {
|
||||||
|
if (e.type !== 'Find and select command') return c.selectedCommand
|
||||||
|
const found = c.commands.find(
|
||||||
|
(cmd) =>
|
||||||
|
cmd.name === e.data.name &&
|
||||||
|
cmd.ownerMachine === e.data.ownerMachine
|
||||||
|
)
|
||||||
|
|
||||||
|
return !!found ? found : c.selectedCommand
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'Initialize arguments to submit': assign({
|
||||||
|
argumentsToSubmit: (c, e) => {
|
||||||
|
if (
|
||||||
|
e.type !== 'Select command' &&
|
||||||
|
e.type !== 'Find and select command'
|
||||||
|
)
|
||||||
|
return c.argumentsToSubmit
|
||||||
|
const command =
|
||||||
|
'command' in e.data ? e.data.command : c.selectedCommand!
|
||||||
|
if (!command.args) return {}
|
||||||
|
const args: { [x: string]: unknown } = {}
|
||||||
|
for (const [argName, arg] of Object.entries(command.args)) {
|
||||||
|
args[argName] = arg.payload
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
guards: {
|
||||||
|
'Command needs review': (context, _) =>
|
||||||
|
context.selectedCommand?.needsReview || false,
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
'Validate argument': (context, event) => {
|
||||||
|
if (event.type !== 'Submit argument') return Promise.reject()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// TODO: figure out if we should validate argument data here or in the form itself,
|
||||||
|
// and if we should support people configuring a argument's validation function
|
||||||
|
|
||||||
|
resolve(event.data)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
'Validate all arguments': (context, _) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
for (const [argName, arg] of Object.entries(
|
||||||
|
context.argumentsToSubmit
|
||||||
|
)) {
|
||||||
|
let argConfig = context.selectedCommand!.args![argName]
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof arg !== typeof argConfig.payload &&
|
||||||
|
typeof arg !== typeof argConfig.defaultValue &&
|
||||||
|
'options' in argConfig &&
|
||||||
|
typeof arg !== typeof argConfig.options[0].value
|
||||||
|
) {
|
||||||
|
return reject({
|
||||||
|
message: 'Argument payload is of the wrong type',
|
||||||
|
arg: {
|
||||||
|
...argConfig,
|
||||||
|
name: argName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!arg && argConfig.required) {
|
||||||
|
return reject({
|
||||||
|
message: 'Argument payload is falsy but is required',
|
||||||
|
arg: {
|
||||||
|
...argConfig,
|
||||||
|
name: argName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(context.argumentsToSubmit)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delays: {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function sortCommands(a: Command, b: Command) {
|
||||||
|
if (b.ownerMachine === 'auth') return -1
|
||||||
|
if (a.ownerMachine === 'auth') return 1
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
}
|
74
src/machines/commandBarMachine.typegen.ts
Normal file
74
src/machines/commandBarMachine.typegen.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// This file was automatically generated. Edits will be overwritten
|
||||||
|
|
||||||
|
export interface Typegen0 {
|
||||||
|
'@@xstate/typegen': true
|
||||||
|
internalEvents: {
|
||||||
|
'': { type: '' }
|
||||||
|
'done.invoke.validateArgument': {
|
||||||
|
type: 'done.invoke.validateArgument'
|
||||||
|
data: unknown
|
||||||
|
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||||
|
}
|
||||||
|
'done.invoke.validateArguments': {
|
||||||
|
type: 'done.invoke.validateArguments'
|
||||||
|
data: unknown
|
||||||
|
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||||
|
}
|
||||||
|
'error.platform.validateArgument': {
|
||||||
|
type: 'error.platform.validateArgument'
|
||||||
|
data: unknown
|
||||||
|
}
|
||||||
|
'error.platform.validateArguments': {
|
||||||
|
type: 'error.platform.validateArguments'
|
||||||
|
data: unknown
|
||||||
|
}
|
||||||
|
'xstate.init': { type: 'xstate.init' }
|
||||||
|
}
|
||||||
|
invokeSrcNameMap: {
|
||||||
|
'Validate all arguments': 'done.invoke.validateArguments'
|
||||||
|
'Validate argument': 'done.invoke.validateArgument'
|
||||||
|
}
|
||||||
|
missingImplementations: {
|
||||||
|
actions:
|
||||||
|
| 'Add arguments'
|
||||||
|
| 'Close dialog'
|
||||||
|
| 'Execute command'
|
||||||
|
| 'Open dialog'
|
||||||
|
delays: never
|
||||||
|
guards: never
|
||||||
|
services: never
|
||||||
|
}
|
||||||
|
eventsCausingActions: {
|
||||||
|
'Add arguments': 'done.invoke.validateArguments'
|
||||||
|
'Add commands': 'Add commands'
|
||||||
|
'Close dialog': 'Close'
|
||||||
|
'Execute command': '' | 'Submit'
|
||||||
|
'Open dialog': 'Open'
|
||||||
|
'Remove argument': 'Remove argument'
|
||||||
|
'Remove commands': 'Remove commands'
|
||||||
|
'Set current argument':
|
||||||
|
| 'Add argument'
|
||||||
|
| 'Edit argument'
|
||||||
|
| 'error.platform.validateArguments'
|
||||||
|
}
|
||||||
|
eventsCausingDelays: {}
|
||||||
|
eventsCausingGuards: {
|
||||||
|
'Arguments are ready': 'done.invoke.validateArguments'
|
||||||
|
'Command has no arguments': ''
|
||||||
|
}
|
||||||
|
eventsCausingServices: {
|
||||||
|
'Validate all arguments': 'done.invoke.validateArgument'
|
||||||
|
'Validate argument': 'Submit'
|
||||||
|
}
|
||||||
|
matchesStates:
|
||||||
|
| 'Checking Arguments'
|
||||||
|
| 'Closed'
|
||||||
|
| 'Command selected'
|
||||||
|
| 'Gathering arguments'
|
||||||
|
| 'Gathering arguments.Awaiting input'
|
||||||
|
| 'Gathering arguments.Validating'
|
||||||
|
| 'Review'
|
||||||
|
| 'Selecting command'
|
||||||
|
| { 'Gathering arguments'?: 'Awaiting input' | 'Validating' }
|
||||||
|
tags: never
|
||||||
|
}
|
@ -1,56 +1,6 @@
|
|||||||
import { assign, createMachine } from 'xstate'
|
import { assign, createMachine } from 'xstate'
|
||||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||||
import { CommandBarConfig } from '../lib/commands'
|
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
|
||||||
|
|
||||||
export const homeCommandConfig: CommandBarConfig<typeof homeMachine> = {
|
|
||||||
'Create project': {
|
|
||||||
icon: 'folderPlus',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
type: 'string',
|
|
||||||
getDefaultValueFromContext: 'defaultProjectName',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Open project': {
|
|
||||||
icon: 'arrowRight',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
type: 'select',
|
|
||||||
getOptionsFromContext: 'projects',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Delete project': {
|
|
||||||
icon: 'close',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
type: 'select',
|
|
||||||
getOptionsFromContext: 'projects',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Rename project': {
|
|
||||||
icon: 'folder',
|
|
||||||
formatFunction: (args: string[]) =>
|
|
||||||
`Rename project "${args[0]}" to "${args[1]}"`,
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'oldName',
|
|
||||||
type: 'select',
|
|
||||||
getOptionsFromContext: 'projects',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'newName',
|
|
||||||
type: 'string',
|
|
||||||
getDefaultValueFromContext: 'defaultProjectName',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const homeMachine = createMachine(
|
export const homeMachine = createMachine(
|
||||||
{
|
{
|
||||||
@ -188,10 +138,10 @@ export const homeMachine = createMachine(
|
|||||||
|
|
||||||
schema: {
|
schema: {
|
||||||
events: {} as
|
events: {} as
|
||||||
| { type: 'Open project'; data: { name: string } }
|
| { type: 'Open project'; data: HomeCommandSchema['Open project'] }
|
||||||
| { type: 'Rename project'; data: { oldName: string; newName: string } }
|
| { type: 'Rename project'; data: HomeCommandSchema['Rename project'] }
|
||||||
| { type: 'Create project'; data: { name: string } }
|
| { type: 'Create project'; data: HomeCommandSchema['Create project'] }
|
||||||
| { type: 'Delete project'; data: { name: string } }
|
| { type: 'Delete project'; data: HomeCommandSchema['Delete project'] }
|
||||||
| { type: 'navigate'; data: { name: string } }
|
| { type: 'navigate'; data: { name: string } }
|
||||||
| {
|
| {
|
||||||
type: 'done.invoke.read-projects'
|
type: 'done.invoke.read-projects'
|
||||||
|
File diff suppressed because one or more lines are too long
@ -32,14 +32,14 @@
|
|||||||
"Get vertical info": "done.invoke.get-vertical-info";
|
"Get vertical info": "done.invoke.get-vertical-info";
|
||||||
};
|
};
|
||||||
missingImplementations: {
|
missingImplementations: {
|
||||||
actions: "AST add line segment" | "AST start new sketch" | "Modify AST" | "Set selection" | "Update code selection cursors" | "create path" | "set tool" | "show default planes" | "sketch exit execute" | "toast extrude failed";
|
actions: "AST add line segment" | "AST start new sketch" | "Modify AST" | "Set selection" | "Update code selection cursors" | "create path" | "set tool" | "show default planes" | "sketch exit execute";
|
||||||
delays: never;
|
delays: never;
|
||||||
guards: "Selection contains axis" | "Selection contains edge" | "Selection contains face" | "Selection contains line" | "Selection contains point" | "Selection is not empty" | "Selection is one face";
|
guards: "Selection contains axis" | "Selection contains edge" | "Selection contains face" | "Selection contains line" | "Selection contains point" | "Selection is not empty" | "Selection is one face" | "has valid extrude selection";
|
||||||
services: "Get ABS X info" | "Get ABS Y info" | "Get angle info" | "Get horizontal info" | "Get length info" | "Get perpendicular distance info" | "Get vertical info";
|
services: "Get ABS X info" | "Get ABS Y info" | "Get angle info" | "Get horizontal info" | "Get length info" | "Get perpendicular distance info" | "Get vertical info";
|
||||||
};
|
};
|
||||||
eventsCausingActions: {
|
eventsCausingActions: {
|
||||||
"AST add line segment": "Add point";
|
"AST add line segment": "Add point";
|
||||||
"AST extrude": "" | "extrude intent";
|
"AST extrude": "Extrude";
|
||||||
"AST start new sketch": "Add point";
|
"AST start new sketch": "Add point";
|
||||||
"Add to code-based selection": "Deselect point" | "Deselect segment" | "Select all" | "Select edge" | "Select face" | "Select point" | "Select segment";
|
"Add to code-based selection": "Deselect point" | "Deselect segment" | "Select all" | "Select edge" | "Select face" | "Select point" | "Select segment";
|
||||||
"Add to other selection": "Select axis";
|
"Add to other selection": "Select axis";
|
||||||
@ -63,7 +63,7 @@
|
|||||||
"edit mode enter": "Enter sketch" | "Re-execute";
|
"edit mode enter": "Enter sketch" | "Re-execute";
|
||||||
"edit_mode_exit": "Cancel";
|
"edit_mode_exit": "Cancel";
|
||||||
"equip select": "CancelSketch" | "Constrain equal length" | "Constrain horizontally align" | "Constrain parallel" | "Constrain remove constraints" | "Constrain snap to X" | "Constrain snap to Y" | "Constrain vertically align" | "Deselect point" | "Deselect segment" | "Enter sketch" | "Make segment horizontal" | "Make segment vertical" | "Re-execute" | "Select default plane" | "Select point" | "Select segment" | "Set selection" | "done.invoke.get-abs-x-info" | "done.invoke.get-abs-y-info" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-perpendicular-distance-info" | "done.invoke.get-vertical-info" | "error.platform.get-abs-x-info" | "error.platform.get-abs-y-info" | "error.platform.get-angle-info" | "error.platform.get-horizontal-info" | "error.platform.get-length-info" | "error.platform.get-perpendicular-distance-info" | "error.platform.get-vertical-info";
|
"equip select": "CancelSketch" | "Constrain equal length" | "Constrain horizontally align" | "Constrain parallel" | "Constrain remove constraints" | "Constrain snap to X" | "Constrain snap to Y" | "Constrain vertically align" | "Deselect point" | "Deselect segment" | "Enter sketch" | "Make segment horizontal" | "Make segment vertical" | "Re-execute" | "Select default plane" | "Select point" | "Select segment" | "Set selection" | "done.invoke.get-abs-x-info" | "done.invoke.get-abs-y-info" | "done.invoke.get-angle-info" | "done.invoke.get-horizontal-info" | "done.invoke.get-length-info" | "done.invoke.get-perpendicular-distance-info" | "done.invoke.get-vertical-info" | "error.platform.get-abs-x-info" | "error.platform.get-abs-y-info" | "error.platform.get-angle-info" | "error.platform.get-horizontal-info" | "error.platform.get-length-info" | "error.platform.get-perpendicular-distance-info" | "error.platform.get-vertical-info";
|
||||||
"hide default planes": "Cancel" | "Select default plane" | "xstate.stop";
|
"hide default planes": "Cancel" | "Select default plane" | "Set selection" | "xstate.stop";
|
||||||
"reset sketch metadata": "Cancel" | "Select default plane";
|
"reset sketch metadata": "Cancel" | "Select default plane";
|
||||||
"set default plane id": "Select default plane";
|
"set default plane id": "Select default plane";
|
||||||
"set sketch metadata": "Enter sketch";
|
"set sketch metadata": "Enter sketch";
|
||||||
@ -72,9 +72,8 @@
|
|||||||
"set tool line": "Equip tool";
|
"set tool line": "Equip tool";
|
||||||
"set tool move": "Equip move tool" | "Re-execute" | "Set selection";
|
"set tool move": "Equip move tool" | "Re-execute" | "Set selection";
|
||||||
"show default planes": "Enter sketch";
|
"show default planes": "Enter sketch";
|
||||||
"sketch exit execute": "Cancel" | "Complete line" | "xstate.stop";
|
"sketch exit execute": "Cancel" | "Complete line" | "Set selection" | "xstate.stop";
|
||||||
"sketch mode enabled": "Enter sketch" | "Re-execute" | "Select default plane";
|
"sketch mode enabled": "Enter sketch" | "Re-execute" | "Select default plane";
|
||||||
"toast extrude failed": "";
|
|
||||||
};
|
};
|
||||||
eventsCausingDelays: {
|
eventsCausingDelays: {
|
||||||
|
|
||||||
@ -105,8 +104,7 @@
|
|||||||
"Selection is one face": "Enter sketch";
|
"Selection is one face": "Enter sketch";
|
||||||
"can move": "";
|
"can move": "";
|
||||||
"can move with execute": "";
|
"can move with execute": "";
|
||||||
"has no selection": "extrude intent";
|
"has valid extrude selection": "Extrude";
|
||||||
"has valid extrude selection": "" | "extrude intent";
|
|
||||||
"is editing existing sketch": "";
|
"is editing existing sketch": "";
|
||||||
};
|
};
|
||||||
eventsCausingServices: {
|
eventsCausingServices: {
|
||||||
@ -118,7 +116,7 @@
|
|||||||
"Get perpendicular distance info": "Constrain perpendicular distance";
|
"Get perpendicular distance info": "Constrain perpendicular distance";
|
||||||
"Get vertical info": "Constrain vertical distance";
|
"Get vertical info": "Constrain vertical distance";
|
||||||
};
|
};
|
||||||
matchesStates: "Sketch" | "Sketch no face" | "Sketch.Await ABS X info" | "Sketch.Await ABS Y info" | "Sketch.Await angle info" | "Sketch.Await horizontal distance info" | "Sketch.Await length info" | "Sketch.Await perpendicular distance info" | "Sketch.Await vertical distance info" | "Sketch.Line Tool" | "Sketch.Line Tool.Done" | "Sketch.Line Tool.Init" | "Sketch.Line Tool.No Points" | "Sketch.Line Tool.Point Added" | "Sketch.Line Tool.Segment Added" | "Sketch.Move Tool" | "Sketch.Move Tool.Move init" | "Sketch.Move Tool.Move with execute" | "Sketch.Move Tool.Move without re-execute" | "Sketch.Move Tool.No move" | "Sketch.SketchIdle" | "awaiting selection" | "checking selection" | "idle" | { "Sketch"?: "Await ABS X info" | "Await ABS Y info" | "Await angle info" | "Await horizontal distance info" | "Await length info" | "Await perpendicular distance info" | "Await vertical distance info" | "Line Tool" | "Move Tool" | "SketchIdle" | { "Line Tool"?: "Done" | "Init" | "No Points" | "Point Added" | "Segment Added";
|
matchesStates: "Sketch" | "Sketch no face" | "Sketch.Await ABS X info" | "Sketch.Await ABS Y info" | "Sketch.Await angle info" | "Sketch.Await horizontal distance info" | "Sketch.Await length info" | "Sketch.Await perpendicular distance info" | "Sketch.Await vertical distance info" | "Sketch.Line Tool" | "Sketch.Line Tool.Done" | "Sketch.Line Tool.Init" | "Sketch.Line Tool.No Points" | "Sketch.Line Tool.Point Added" | "Sketch.Line Tool.Segment Added" | "Sketch.Move Tool" | "Sketch.Move Tool.Move init" | "Sketch.Move Tool.Move with execute" | "Sketch.Move Tool.Move without re-execute" | "Sketch.Move Tool.No move" | "Sketch.SketchIdle" | "idle" | { "Sketch"?: "Await ABS X info" | "Await ABS Y info" | "Await angle info" | "Await horizontal distance info" | "Await length info" | "Await perpendicular distance info" | "Await vertical distance info" | "Line Tool" | "Move Tool" | "SketchIdle" | { "Line Tool"?: "Done" | "Init" | "No Points" | "Point Added" | "Segment Added";
|
||||||
"Move Tool"?: "Move init" | "Move with execute" | "Move without re-execute" | "No move"; }; };
|
"Move Tool"?: "Move init" | "Move with execute" | "Move without re-execute" | "No move"; }; };
|
||||||
tags: never;
|
tags: never;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { assign, createMachine } from 'xstate'
|
import { assign, createMachine } from 'xstate'
|
||||||
import { CommandBarConfig } from '../lib/commands'
|
|
||||||
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
|
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
|
||||||
import { CameraSystem, cameraSystems } from 'lib/cameraControls'
|
import { CameraSystem } from 'lib/cameraControls'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
|
|
||||||
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
|
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
|
||||||
@ -24,85 +23,6 @@ export type Toggle = 'On' | 'Off'
|
|||||||
|
|
||||||
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
|
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
|
||||||
|
|
||||||
export const settingsCommandBarConfig: CommandBarConfig<
|
|
||||||
typeof settingsMachine
|
|
||||||
> = {
|
|
||||||
'Set Base Unit': {
|
|
||||||
icon: 'gear',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'baseUnit',
|
|
||||||
type: 'select',
|
|
||||||
getDefaultValueFromContext: 'baseUnit',
|
|
||||||
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Set Camera Controls': {
|
|
||||||
icon: 'gear',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'cameraControls',
|
|
||||||
type: 'select',
|
|
||||||
getDefaultValueFromContext: 'cameraControls',
|
|
||||||
options: Object.values(cameraSystems).map((v) => ({ name: v })),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Set Default Directory': {
|
|
||||||
hide: 'both',
|
|
||||||
},
|
|
||||||
'Set Default Project Name': {
|
|
||||||
icon: 'gear',
|
|
||||||
hide: 'web',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'defaultProjectName',
|
|
||||||
type: 'string',
|
|
||||||
getDefaultValueFromContext: 'defaultProjectName',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Set Onboarding Status': {
|
|
||||||
hide: 'both',
|
|
||||||
},
|
|
||||||
'Set Text Wrapping': {
|
|
||||||
icon: 'gear',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'textWrapping',
|
|
||||||
type: 'select',
|
|
||||||
getDefaultValueFromContext: 'textWrapping',
|
|
||||||
options: [{ name: 'On' }, { name: 'Off' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Set Theme': {
|
|
||||||
icon: 'gear',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'theme',
|
|
||||||
type: 'select',
|
|
||||||
getDefaultValueFromContext: 'theme',
|
|
||||||
options: Object.values(Themes).map((v): { name: string } => ({
|
|
||||||
name: v,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Set Unit System': {
|
|
||||||
icon: 'gear',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
name: 'unitSystem',
|
|
||||||
type: 'select',
|
|
||||||
getDefaultValueFromContext: 'unitSystem',
|
|
||||||
options: [{ name: UnitSystem.Imperial }, { name: UnitSystem.Metric }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const settingsMachine = createMachine(
|
export const settingsMachine = createMachine(
|
||||||
{
|
{
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIAVACzAFswBtABgF1FQAHAe1iYsfbNxAAPRAA42+AEwB2KQFYAzGznKAnADZli1QBoQAT2kBGKfm37lOned3nzqgL6vjlLLgJFSFdCoAETAAMwBDAFdiagAFACc+ACswAGNqADlw5nYuJBB+QWFRfMkEABY5fDYa2rra83LjMwQdLWV8BXLyuxlVLU1Ld090bzxCEnJKYLComODMeLS0PniTXLFCoUwRMTK7fC1zNql7NgUjtnKjU0RlBSqpLVUVPVUda60tYZAvHHG-FNAgBVbBCKjIEywNBMDb5LbFPaILqdfRSORsS4qcxXZqIHqyK6qY4XOxsGTKco-P4+Cb+aYAIXCsDAVFBQjhvAE212pWkskUKnUml0+gUNxaqkU+EccnKF1UCnucnMcjcHl+o3+vkmZBofCgUFIMwARpEoFRYuFsGBiJyCtzEXzWrJlGxlKdVFKvfY1XiEBjyvhVOVzBdzu13pYFNStbTAQFqAB5bAmvjheIQf4QtDhNCRWD2hE7EqgfayHTEh7lHQNSxSf1Scz4cpHHFyFVujTKczuDXYPgQOBiGl4TaOktIhAAWg6X3nC4Xp39050sYw2rpYHHRUnztVhPJqmUlIGbEriv9WhrLZ6uibHcqUr7riAA */
|
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIAVACzAFswBtABgF1FQAHAe1iYsfbNxAAPRAA42+AEwB2KQFYAzGznKAnADZli1QBoQAT2kBGKfm37lOned3nzqgL6vjlLLgJFSFdCoAETAAMwBDAFdiagAFACc+ACswAGNqADlw5nYuJBB+QWFRfMkEABY5fDYa2rra83LjMwQdLWV8BXLyuxlVLU1Ld090bzxCEnJKYLComODMeLS0PniTXLFCoUwRMTK7fC1zNql7NgUjtnKjU0RlBSqpLVUVPVUda60tYZAvHHG-FNAgBVbBCKjIEywNBMDb5LbFPaILqdfRSORsS4qcxXZqIHqyK6qY4XOxsGTKco-P4+Cb+aYAIXCsDAVFBQjhvAE212pWkskUKnUml0+gUNxaqkU+EccnKF1UCnucnMcjcHl+o3+vkmZBofCgUFIMwARpEoFRYuFsGBiJyCtzEXzWrJlGxlKdVFKvfY1XiEBjyvhVOVzBdzu13pYFNStbTAQFqAB5bAmvjheIQf4QtDhNCRWD2hE7EqgfayHTEh7lHQNSxSf1Scz4cpHHFyFVujTKczuDXYPgQOBiGl4TaOktIhAAWg6X3nC4Xp39050sYw2rpYHHRUnztVhPJqmUlIGbEriv9WhrLZ6uibHcqUr7riAA */
|
||||||
|
@ -17,7 +17,7 @@ import { Link } from 'react-router-dom'
|
|||||||
import { ProjectWithEntryPointMetadata, HomeLoaderData } from '../Router'
|
import { ProjectWithEntryPointMetadata, HomeLoaderData } from '../Router'
|
||||||
import Loading from '../components/Loading'
|
import Loading from '../components/Loading'
|
||||||
import { useMachine } from '@xstate/react'
|
import { useMachine } from '@xstate/react'
|
||||||
import { homeCommandConfig, homeMachine } from '../machines/homeMachine'
|
import { homeMachine } from '../machines/homeMachine'
|
||||||
import { ContextFrom, EventFrom } from 'xstate'
|
import { ContextFrom, EventFrom } from 'xstate'
|
||||||
import { paths } from '../Router'
|
import { paths } from '../Router'
|
||||||
import {
|
import {
|
||||||
@ -30,11 +30,12 @@ import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { DEFAULT_PROJECT_NAME } from 'machines/settingsMachine'
|
import { DEFAULT_PROJECT_NAME } from 'machines/settingsMachine'
|
||||||
import { sep } from '@tauri-apps/api/path'
|
import { sep } from '@tauri-apps/api/path'
|
||||||
|
import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig'
|
||||||
|
|
||||||
// This route only opens in the Tauri desktop context for now,
|
// This route only opens in the Tauri desktop context for now,
|
||||||
// as defined in Router.tsx, so we can use the Tauri APIs and types.
|
// as defined in Router.tsx, so we can use the Tauri APIs and types.
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const { commands, setCommandBarOpen } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { projects: loadedProjects } = useLoaderData() as HomeLoaderData
|
const { projects: loadedProjects } = useLoaderData() as HomeLoaderData
|
||||||
const {
|
const {
|
||||||
@ -56,7 +57,7 @@ const Home = () => {
|
|||||||
event: EventFrom<typeof homeMachine>
|
event: EventFrom<typeof homeMachine>
|
||||||
) => {
|
) => {
|
||||||
if (event.data && 'name' in event.data) {
|
if (event.data && 'name' in event.data) {
|
||||||
setCommandBarOpen(false)
|
commandBarSend({ type: 'Close' })
|
||||||
navigate(
|
navigate(
|
||||||
`${paths.FILE}/${encodeURIComponent(
|
`${paths.FILE}/${encodeURIComponent(
|
||||||
context.defaultDirectory + sep + event.data.name
|
context.defaultDirectory + sep + event.data.name
|
||||||
@ -143,12 +144,11 @@ const Home = () => {
|
|||||||
|
|
||||||
const isSortByModified = sort?.includes('modified') || !sort || sort === null
|
const isSortByModified = sort?.includes('modified') || !sort || sort === null
|
||||||
|
|
||||||
useStateMachineCommands<typeof homeMachine>({
|
useStateMachineCommands({
|
||||||
commands,
|
machineId: 'home',
|
||||||
send,
|
send,
|
||||||
state,
|
state,
|
||||||
commandBarConfig: homeCommandConfig,
|
commandBarConfig: homeCommandBarConfig,
|
||||||
owner: 'home',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
|
import usePlatform from 'hooks/usePlatform'
|
||||||
import { OnboardingButtons, onboardingPaths, useDismiss, useNextClick } from '.'
|
import { OnboardingButtons, onboardingPaths, useDismiss, useNextClick } from '.'
|
||||||
import { useStore } from '../../useStore'
|
import { useStore } from '../../useStore'
|
||||||
import { Platform, platform } from '@tauri-apps/api/os'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
export default function CmdK() {
|
export default function CmdK() {
|
||||||
const { buttonDownInStream } = useStore((s) => ({
|
const { buttonDownInStream } = useStore((s) => ({
|
||||||
@ -9,14 +8,7 @@ export default function CmdK() {
|
|||||||
}))
|
}))
|
||||||
const dismiss = useDismiss()
|
const dismiss = useDismiss()
|
||||||
const next = useNextClick(onboardingPaths.USER_MENU)
|
const next = useNextClick(onboardingPaths.USER_MENU)
|
||||||
const [platformName, setPlatformName] = useState<Platform | ''>('')
|
const platformName = usePlatform()
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getPlatform() {
|
|
||||||
setPlatformName(await platform())
|
|
||||||
}
|
|
||||||
void getPlatform()
|
|
||||||
}, [setPlatformName])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 grid items-end justify-center pointer-events-none">
|
<div className="fixed inset-0 z-50 grid items-end justify-center pointer-events-none">
|
||||||
@ -29,13 +21,13 @@ export default function CmdK() {
|
|||||||
<h2 className="text-2xl">Command Bar</h2>
|
<h2 className="text-2xl">Command Bar</h2>
|
||||||
<p className="my-4">
|
<p className="my-4">
|
||||||
Press{' '}
|
Press{' '}
|
||||||
{platformName === 'win32' ? (
|
{platformName === 'darwin' ? (
|
||||||
<>
|
<>
|
||||||
<kbd>Win</kbd> + <kbd>/</kbd>
|
<kbd>⌘</kbd> + <kbd>K</kbd>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<kbd>OS</kbd> + <kbd>K</kbd>
|
<kbd>Ctrl</kbd> + <kbd>/</kbd>
|
||||||
</>
|
</>
|
||||||
)}{' '}
|
)}{' '}
|
||||||
to open the command bar. Try changing your theme with it.
|
to open the command bar. Try changing your theme with it.
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { ActionButton } from '../components/ActionButton'
|
import { ActionButton } from '../components/ActionButton'
|
||||||
import { isTauri } from '../lib/isTauri'
|
import { isTauri } from '../lib/isTauri'
|
||||||
import { invoke } from '@tauri-apps/api/tauri'
|
import { invoke } from '@tauri-apps/api/tauri'
|
||||||
@ -65,7 +64,7 @@ const SignIn = () => {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={signInTauri}
|
onClick={signInTauri}
|
||||||
icon={{ icon: faSignInAlt }}
|
icon={{ icon: 'arrowRight' }}
|
||||||
className="w-fit mt-4"
|
className="w-fit mt-4"
|
||||||
data-testid="sign-in-button"
|
data-testid="sign-in-button"
|
||||||
>
|
>
|
||||||
@ -80,7 +79,7 @@ const SignIn = () => {
|
|||||||
typeof window !== 'undefined' &&
|
typeof window !== 'undefined' &&
|
||||||
window.location.href.replace('signin', '')
|
window.location.href.replace('signin', '')
|
||||||
)}`}
|
)}`}
|
||||||
icon={{ icon: faSignInAlt }}
|
icon={{ icon: 'arrowRight' }}
|
||||||
className="w-fit mt-4"
|
className="w-fit mt-4"
|
||||||
>
|
>
|
||||||
Sign in
|
Sign in
|
||||||
|
@ -8497,9 +8497,9 @@ ws@^8.8.0:
|
|||||||
integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==
|
integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==
|
||||||
|
|
||||||
xstate@^4.38.2:
|
xstate@^4.38.2:
|
||||||
version "4.38.2"
|
version "4.38.3"
|
||||||
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.2.tgz#1b74544fc9c8c6c713ba77f81c6017e65aa89804"
|
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.3.tgz#4e15e7ad3aa0ca1eea2010548a5379966d8f1075"
|
||||||
integrity sha512-Fba/DwEPDLneHT3tbJ9F3zafbQXszOlyCJyQqqdzmtlY/cwE2th462KK48yaANf98jHlP6lJvxfNtN0LFKXPQg==
|
integrity sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==
|
||||||
|
|
||||||
y18n@^5.0.5:
|
y18n@^5.0.5:
|
||||||
version "5.0.8"
|
version "5.0.8"
|
||||||
|
Reference in New Issue
Block a user