diff --git a/e2e/playwright/export-snapshots/gltf-binary.gltf b/e2e/playwright/export-snapshots/gltf-binary.gltf index 11a4fe0c5..c20f6547b 100644 Binary files a/e2e/playwright/export-snapshots/gltf-binary.gltf and b/e2e/playwright/export-snapshots/gltf-binary.gltf differ diff --git a/e2e/playwright/export-snapshots/gltf-embedded.gltf b/e2e/playwright/export-snapshots/gltf-embedded.gltf index 9f36fe412..a14d6b758 100644 --- a/e2e/playwright/export-snapshots/gltf-embedded.gltf +++ b/e2e/playwright/export-snapshots/gltf-embedded.gltf @@ -626,497 +626,1056 @@ "KITTYCAD_boundary_representation": { "solids": [ { - "outerShell": 0, + "shells": [ + [ + 0, + 1 + ] + ], "mesh": 0 } ], "shells": [ { "faces": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15 + [ + 0, + 1 + ], + [ + 1, + 1 + ], + [ + 2, + 1 + ], + [ + 3, + 1 + ], + [ + 4, + 1 + ], + [ + 5, + 1 + ], + [ + 6, + 1 + ], + [ + 7, + 1 + ], + [ + 8, + 1 + ], + [ + 9, + 1 + ], + [ + 10, + 1 + ], + [ + 11, + 1 + ], + [ + 12, + 1 + ], + [ + 13, + 1 + ], + [ + 14, + 1 + ], + [ + 15, + 1 + ] ] } ], "faces": [ { - "surface": 0, - "outerLoop": 0 + "surface": [ + 0, + 1 + ], + "loops": [ + [ + 0, + 1 + ] + ] }, { - "surface": 1, - "outerLoop": 1 + "surface": [ + 1, + 1 + ], + "loops": [ + [ + 1, + 1 + ] + ] }, { - "surface": 2, - "outerLoop": 2 + "surface": [ + 2, + 1 + ], + "loops": [ + [ + 2, + 1 + ] + ] }, { - "surface": 3, - "outerLoop": 3 + "surface": [ + 3, + 1 + ], + "loops": [ + [ + 3, + 1 + ] + ] }, { - "surface": 4, - "outerLoop": 4 + "surface": [ + 4, + 1 + ], + "loops": [ + [ + 4, + 1 + ] + ] }, { - "surface": 5, - "outerLoop": 5 + "surface": [ + 5, + 1 + ], + "loops": [ + [ + 5, + 1 + ] + ] }, { - "surface": 6, - "outerLoop": 6 + "surface": [ + 6, + 1 + ], + "loops": [ + [ + 6, + 1 + ] + ] }, { - "surface": 7, - "outerLoop": 7 + "surface": [ + 7, + 1 + ], + "loops": [ + [ + 7, + 1 + ] + ] }, { - "surface": 8, - "outerLoop": 8 + "surface": [ + 8, + 1 + ], + "loops": [ + [ + 8, + 1 + ] + ] }, { - "surface": 9, - "outerLoop": 9 + "surface": [ + 9, + 1 + ], + "loops": [ + [ + 9, + 1 + ] + ] }, { - "surface": 10, - "outerLoop": 10 + "surface": [ + 10, + 1 + ], + "loops": [ + [ + 10, + 1 + ] + ] }, { - "surface": 11, - "outerLoop": 11 + "surface": [ + 11, + 1 + ], + "loops": [ + [ + 11, + 1 + ] + ] }, { - "surface": 12, - "outerLoop": 12 + "surface": [ + 12, + 1 + ], + "loops": [ + [ + 12, + 1 + ] + ] }, { - "surface": 13, - "outerLoop": 13 + "surface": [ + 13, + 1 + ], + "loops": [ + [ + 13, + 1 + ] + ] }, { - "surface": -14, - "outerLoop": -14 + "surface": [ + 14, + -1 + ], + "loops": [ + [ + 14, + -1 + ] + ] }, { - "surface": 15, - "outerLoop": 15 + "surface": [ + 15, + 1 + ], + "loops": [ + [ + 15, + 1 + ] + ] } ], "loops": [ { "edges": [ - 0, - 1, - -2, - -3 + [ + 0, + 1 + ], + [ + 1, + 1 + ], + [ + 2, + -1 + ], + [ + 3, + -1 + ] ] }, { "edges": [ - 4, - 5, - -6, - -1 + [ + 4, + 1 + ], + [ + 5, + 1 + ], + [ + 6, + -1 + ], + [ + 1, + -1 + ] ] }, { "edges": [ - 7, - 8, - -9, - -5 + [ + 7, + 1 + ], + [ + 8, + 1 + ], + [ + 9, + -1 + ], + [ + 5, + -1 + ] ] }, { "edges": [ - 10, - 11, - -12, - -8 + [ + 10, + 1 + ], + [ + 11, + 1 + ], + [ + 12, + -1 + ], + [ + 8, + -1 + ] ] }, { "edges": [ - 13, - 14, - -15, - -11 + [ + 13, + 1 + ], + [ + 14, + 1 + ], + [ + 15, + -1 + ], + [ + 11, + -1 + ] ] }, { "edges": [ - 16, - 17, - -18, - -14 + [ + 16, + 1 + ], + [ + 17, + 1 + ], + [ + 18, + -1 + ], + [ + 14, + -1 + ] ] }, { "edges": [ - 19, - 20, - -21, - -17 + [ + 19, + 1 + ], + [ + 20, + 1 + ], + [ + 21, + -1 + ], + [ + 17, + -1 + ] ] }, { "edges": [ - 22, - 23, - -24, - -20 + [ + 22, + 1 + ], + [ + 23, + 1 + ], + [ + 24, + -1 + ], + [ + 20, + -1 + ] ] }, { "edges": [ - 25, - 26, - -27, - -23 + [ + 25, + 1 + ], + [ + 26, + 1 + ], + [ + 27, + -1 + ], + [ + 23, + -1 + ] ] }, { "edges": [ - 28, - 29, - -30, - -26 + [ + 28, + 1 + ], + [ + 29, + 1 + ], + [ + 30, + -1 + ], + [ + 26, + -1 + ] ] }, { "edges": [ - 31, - 32, - -33, - -29 + [ + 31, + 1 + ], + [ + 32, + 1 + ], + [ + 33, + -1 + ], + [ + 29, + -1 + ] ] }, { "edges": [ - 34, - 35, - -36, - -32 + [ + 34, + 1 + ], + [ + 35, + 1 + ], + [ + 36, + -1 + ], + [ + 32, + -1 + ] ] }, { "edges": [ - 37, - 38, - -39, - -35 + [ + 37, + 1 + ], + [ + 38, + 1 + ], + [ + 39, + -1 + ], + [ + 35, + -1 + ] ] }, { "edges": [ - 40, - 3, - -41, - -38 + [ + 40, + 1 + ], + [ + 3, + 1 + ], + [ + 41, + -1 + ], + [ + 38, + -1 + ] ] }, { "edges": [ - 0, - 4, - 7, - 10, - 13, - 16, - 19, - 22, - 25, - 28, - 31, - 34, - 37, - 40 + [ + 0, + 1 + ], + [ + 4, + 1 + ], + [ + 7, + 1 + ], + [ + 10, + 1 + ], + [ + 13, + 1 + ], + [ + 16, + 1 + ], + [ + 19, + 1 + ], + [ + 22, + 1 + ], + [ + 25, + 1 + ], + [ + 28, + 1 + ], + [ + 31, + 1 + ], + [ + 34, + 1 + ], + [ + 37, + 1 + ], + [ + 40, + 1 + ] ] }, { "edges": [ - 2, - 6, - 9, - 12, - 15, - 18, - 21, - 24, - 27, - 30, - 33, - 36, - 39, - 41 + [ + 2, + 1 + ], + [ + 6, + 1 + ], + [ + 9, + 1 + ], + [ + 12, + 1 + ], + [ + 15, + 1 + ], + [ + 18, + 1 + ], + [ + 21, + 1 + ], + [ + 24, + 1 + ], + [ + 27, + 1 + ], + [ + 30, + 1 + ], + [ + 33, + 1 + ], + [ + 36, + 1 + ], + [ + 39, + 1 + ], + [ + 41, + 1 + ] ] } ], "edges": [ { - "curve": 0, + "curve": [ + 0, + 1 + ], "start": 0, "end": 1, "closed": false }, { - "curve": 1, + "curve": [ + 1, + 1 + ], "start": 1, "end": 2, "closed": false }, { - "curve": 2, + "curve": [ + 2, + 1 + ], "start": 3, "end": 2, "closed": false }, { - "curve": 3, + "curve": [ + 3, + 1 + ], "start": 0, "end": 3, "closed": false }, { - "curve": 4, + "curve": [ + 4, + 1 + ], "start": 1, "end": 4, "closed": false }, { - "curve": 5, + "curve": [ + 5, + 1 + ], "start": 4, "end": 5, "closed": false }, { - "curve": 6, + "curve": [ + 6, + 1 + ], "start": 2, "end": 5, "closed": false }, { - "curve": 7, + "curve": [ + 7, + 1 + ], "start": 4, "end": 6, "closed": false }, { - "curve": 8, + "curve": [ + 8, + 1 + ], "start": 6, "end": 7, "closed": false }, { - "curve": 9, + "curve": [ + 9, + 1 + ], "start": 5, "end": 7, "closed": false }, { - "curve": 10, + "curve": [ + 10, + 1 + ], "start": 6, "end": 8, "closed": false }, { - "curve": 11, + "curve": [ + 11, + 1 + ], "start": 8, "end": 9, "closed": false }, { - "curve": 12, + "curve": [ + 12, + 1 + ], "start": 7, "end": 9, "closed": false }, { - "curve": 13, + "curve": [ + 13, + 1 + ], "start": 8, "end": 10, "closed": false }, { - "curve": 14, + "curve": [ + 14, + 1 + ], "start": 10, "end": 11, "closed": false }, { - "curve": 15, + "curve": [ + 15, + 1 + ], "start": 9, "end": 11, "closed": false }, { - "curve": 16, + "curve": [ + 16, + 1 + ], "start": 10, "end": 12, "closed": false }, { - "curve": 17, + "curve": [ + 17, + 1 + ], "start": 12, "end": 13, "closed": false }, { - "curve": 18, + "curve": [ + 18, + 1 + ], "start": 11, "end": 13, "closed": false }, { - "curve": 19, + "curve": [ + 19, + 1 + ], "start": 12, "end": 14, "closed": false }, { - "curve": 20, + "curve": [ + 20, + 1 + ], "start": 14, "end": 15, "closed": false }, { - "curve": 21, + "curve": [ + 21, + 1 + ], "start": 13, "end": 15, "closed": false }, { - "curve": 22, + "curve": [ + 22, + 1 + ], "start": 14, "end": 16, "closed": false }, { - "curve": 23, + "curve": [ + 23, + 1 + ], "start": 16, "end": 17, "closed": false }, { - "curve": 24, + "curve": [ + 24, + 1 + ], "start": 15, "end": 17, "closed": false }, { - "curve": 25, + "curve": [ + 25, + 1 + ], "start": 16, "end": 18, "closed": false }, { - "curve": 26, + "curve": [ + 26, + 1 + ], "start": 18, "end": 19, "closed": false }, { - "curve": 27, + "curve": [ + 27, + 1 + ], "start": 17, "end": 19, "closed": false }, { - "curve": 28, + "curve": [ + 28, + 1 + ], "start": 18, "end": 20, "closed": false }, { - "curve": 29, + "curve": [ + 29, + 1 + ], "start": 20, "end": 21, "closed": false }, { - "curve": 30, + "curve": [ + 30, + 1 + ], "start": 19, "end": 21, "closed": false }, { - "curve": 31, + "curve": [ + 31, + 1 + ], "start": 20, "end": 22, "closed": false }, { - "curve": 32, + "curve": [ + 32, + 1 + ], "start": 22, "end": 23, "closed": false }, { - "curve": 33, + "curve": [ + 33, + 1 + ], "start": 21, "end": 23, "closed": false }, { - "curve": 34, + "curve": [ + 34, + 1 + ], "start": 22, "end": 24, "closed": false }, { - "curve": 35, + "curve": [ + 35, + 1 + ], "start": 24, "end": 25, "closed": false }, { - "curve": 36, + "curve": [ + 36, + 1 + ], "start": 23, "end": 25, "closed": false }, { - "curve": 37, + "curve": [ + 37, + 1 + ], "start": 24, "end": 26, "closed": false }, { - "curve": 38, + "curve": [ + 38, + 1 + ], "start": 26, "end": 27, "closed": false }, { - "curve": 39, + "curve": [ + 39, + 1 + ], "start": 25, "end": 27, "closed": false }, { - "curve": 40, + "curve": [ + 40, + 1 + ], "start": 26, "end": 0, "closed": false }, { - "curve": 41, + "curve": [ + 41, + 1 + ], "start": 27, "end": 3, "closed": false diff --git a/e2e/playwright/export-snapshots/gltf-standard-2.gltf b/e2e/playwright/export-snapshots/gltf-standard-2.gltf index f775571f3..08b855e94 100644 --- a/e2e/playwright/export-snapshots/gltf-standard-2.gltf +++ b/e2e/playwright/export-snapshots/gltf-standard-2.gltf @@ -626,497 +626,1056 @@ "KITTYCAD_boundary_representation": { "solids": [ { - "outerShell": 0, + "shells": [ + [ + 0, + 1 + ] + ], "mesh": 0 } ], "shells": [ { "faces": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15 + [ + 0, + 1 + ], + [ + 1, + 1 + ], + [ + 2, + 1 + ], + [ + 3, + 1 + ], + [ + 4, + 1 + ], + [ + 5, + 1 + ], + [ + 6, + 1 + ], + [ + 7, + 1 + ], + [ + 8, + 1 + ], + [ + 9, + 1 + ], + [ + 10, + 1 + ], + [ + 11, + 1 + ], + [ + 12, + 1 + ], + [ + 13, + 1 + ], + [ + 14, + 1 + ], + [ + 15, + 1 + ] ] } ], "faces": [ { - "surface": 0, - "outerLoop": 0 + "surface": [ + 0, + 1 + ], + "loops": [ + [ + 0, + 1 + ] + ] }, { - "surface": 1, - "outerLoop": 1 + "surface": [ + 1, + 1 + ], + "loops": [ + [ + 1, + 1 + ] + ] }, { - "surface": 2, - "outerLoop": 2 + "surface": [ + 2, + 1 + ], + "loops": [ + [ + 2, + 1 + ] + ] }, { - "surface": 3, - "outerLoop": 3 + "surface": [ + 3, + 1 + ], + "loops": [ + [ + 3, + 1 + ] + ] }, { - "surface": 4, - "outerLoop": 4 + "surface": [ + 4, + 1 + ], + "loops": [ + [ + 4, + 1 + ] + ] }, { - "surface": 5, - "outerLoop": 5 + "surface": [ + 5, + 1 + ], + "loops": [ + [ + 5, + 1 + ] + ] }, { - "surface": 6, - "outerLoop": 6 + "surface": [ + 6, + 1 + ], + "loops": [ + [ + 6, + 1 + ] + ] }, { - "surface": 7, - "outerLoop": 7 + "surface": [ + 7, + 1 + ], + "loops": [ + [ + 7, + 1 + ] + ] }, { - "surface": 8, - "outerLoop": 8 + "surface": [ + 8, + 1 + ], + "loops": [ + [ + 8, + 1 + ] + ] }, { - "surface": 9, - "outerLoop": 9 + "surface": [ + 9, + 1 + ], + "loops": [ + [ + 9, + 1 + ] + ] }, { - "surface": 10, - "outerLoop": 10 + "surface": [ + 10, + 1 + ], + "loops": [ + [ + 10, + 1 + ] + ] }, { - "surface": 11, - "outerLoop": 11 + "surface": [ + 11, + 1 + ], + "loops": [ + [ + 11, + 1 + ] + ] }, { - "surface": 12, - "outerLoop": 12 + "surface": [ + 12, + 1 + ], + "loops": [ + [ + 12, + 1 + ] + ] }, { - "surface": 13, - "outerLoop": 13 + "surface": [ + 13, + 1 + ], + "loops": [ + [ + 13, + 1 + ] + ] }, { - "surface": -14, - "outerLoop": -14 + "surface": [ + 14, + -1 + ], + "loops": [ + [ + 14, + -1 + ] + ] }, { - "surface": 15, - "outerLoop": 15 + "surface": [ + 15, + 1 + ], + "loops": [ + [ + 15, + 1 + ] + ] } ], "loops": [ { "edges": [ - 0, - 1, - -2, - -3 + [ + 0, + 1 + ], + [ + 1, + 1 + ], + [ + 2, + -1 + ], + [ + 3, + -1 + ] ] }, { "edges": [ - 4, - 5, - -6, - -1 + [ + 4, + 1 + ], + [ + 5, + 1 + ], + [ + 6, + -1 + ], + [ + 1, + -1 + ] ] }, { "edges": [ - 7, - 8, - -9, - -5 + [ + 7, + 1 + ], + [ + 8, + 1 + ], + [ + 9, + -1 + ], + [ + 5, + -1 + ] ] }, { "edges": [ - 10, - 11, - -12, - -8 + [ + 10, + 1 + ], + [ + 11, + 1 + ], + [ + 12, + -1 + ], + [ + 8, + -1 + ] ] }, { "edges": [ - 13, - 14, - -15, - -11 + [ + 13, + 1 + ], + [ + 14, + 1 + ], + [ + 15, + -1 + ], + [ + 11, + -1 + ] ] }, { "edges": [ - 16, - 17, - -18, - -14 + [ + 16, + 1 + ], + [ + 17, + 1 + ], + [ + 18, + -1 + ], + [ + 14, + -1 + ] ] }, { "edges": [ - 19, - 20, - -21, - -17 + [ + 19, + 1 + ], + [ + 20, + 1 + ], + [ + 21, + -1 + ], + [ + 17, + -1 + ] ] }, { "edges": [ - 22, - 23, - -24, - -20 + [ + 22, + 1 + ], + [ + 23, + 1 + ], + [ + 24, + -1 + ], + [ + 20, + -1 + ] ] }, { "edges": [ - 25, - 26, - -27, - -23 + [ + 25, + 1 + ], + [ + 26, + 1 + ], + [ + 27, + -1 + ], + [ + 23, + -1 + ] ] }, { "edges": [ - 28, - 29, - -30, - -26 + [ + 28, + 1 + ], + [ + 29, + 1 + ], + [ + 30, + -1 + ], + [ + 26, + -1 + ] ] }, { "edges": [ - 31, - 32, - -33, - -29 + [ + 31, + 1 + ], + [ + 32, + 1 + ], + [ + 33, + -1 + ], + [ + 29, + -1 + ] ] }, { "edges": [ - 34, - 35, - -36, - -32 + [ + 34, + 1 + ], + [ + 35, + 1 + ], + [ + 36, + -1 + ], + [ + 32, + -1 + ] ] }, { "edges": [ - 37, - 38, - -39, - -35 + [ + 37, + 1 + ], + [ + 38, + 1 + ], + [ + 39, + -1 + ], + [ + 35, + -1 + ] ] }, { "edges": [ - 40, - 3, - -41, - -38 + [ + 40, + 1 + ], + [ + 3, + 1 + ], + [ + 41, + -1 + ], + [ + 38, + -1 + ] ] }, { "edges": [ - 0, - 4, - 7, - 10, - 13, - 16, - 19, - 22, - 25, - 28, - 31, - 34, - 37, - 40 + [ + 0, + 1 + ], + [ + 4, + 1 + ], + [ + 7, + 1 + ], + [ + 10, + 1 + ], + [ + 13, + 1 + ], + [ + 16, + 1 + ], + [ + 19, + 1 + ], + [ + 22, + 1 + ], + [ + 25, + 1 + ], + [ + 28, + 1 + ], + [ + 31, + 1 + ], + [ + 34, + 1 + ], + [ + 37, + 1 + ], + [ + 40, + 1 + ] ] }, { "edges": [ - 2, - 6, - 9, - 12, - 15, - 18, - 21, - 24, - 27, - 30, - 33, - 36, - 39, - 41 + [ + 2, + 1 + ], + [ + 6, + 1 + ], + [ + 9, + 1 + ], + [ + 12, + 1 + ], + [ + 15, + 1 + ], + [ + 18, + 1 + ], + [ + 21, + 1 + ], + [ + 24, + 1 + ], + [ + 27, + 1 + ], + [ + 30, + 1 + ], + [ + 33, + 1 + ], + [ + 36, + 1 + ], + [ + 39, + 1 + ], + [ + 41, + 1 + ] ] } ], "edges": [ { - "curve": 0, + "curve": [ + 0, + 1 + ], "start": 0, "end": 1, "closed": false }, { - "curve": 1, + "curve": [ + 1, + 1 + ], "start": 1, "end": 2, "closed": false }, { - "curve": 2, + "curve": [ + 2, + 1 + ], "start": 3, "end": 2, "closed": false }, { - "curve": 3, + "curve": [ + 3, + 1 + ], "start": 0, "end": 3, "closed": false }, { - "curve": 4, + "curve": [ + 4, + 1 + ], "start": 1, "end": 4, "closed": false }, { - "curve": 5, + "curve": [ + 5, + 1 + ], "start": 4, "end": 5, "closed": false }, { - "curve": 6, + "curve": [ + 6, + 1 + ], "start": 2, "end": 5, "closed": false }, { - "curve": 7, + "curve": [ + 7, + 1 + ], "start": 4, "end": 6, "closed": false }, { - "curve": 8, + "curve": [ + 8, + 1 + ], "start": 6, "end": 7, "closed": false }, { - "curve": 9, + "curve": [ + 9, + 1 + ], "start": 5, "end": 7, "closed": false }, { - "curve": 10, + "curve": [ + 10, + 1 + ], "start": 6, "end": 8, "closed": false }, { - "curve": 11, + "curve": [ + 11, + 1 + ], "start": 8, "end": 9, "closed": false }, { - "curve": 12, + "curve": [ + 12, + 1 + ], "start": 7, "end": 9, "closed": false }, { - "curve": 13, + "curve": [ + 13, + 1 + ], "start": 8, "end": 10, "closed": false }, { - "curve": 14, + "curve": [ + 14, + 1 + ], "start": 10, "end": 11, "closed": false }, { - "curve": 15, + "curve": [ + 15, + 1 + ], "start": 9, "end": 11, "closed": false }, { - "curve": 16, + "curve": [ + 16, + 1 + ], "start": 10, "end": 12, "closed": false }, { - "curve": 17, + "curve": [ + 17, + 1 + ], "start": 12, "end": 13, "closed": false }, { - "curve": 18, + "curve": [ + 18, + 1 + ], "start": 11, "end": 13, "closed": false }, { - "curve": 19, + "curve": [ + 19, + 1 + ], "start": 12, "end": 14, "closed": false }, { - "curve": 20, + "curve": [ + 20, + 1 + ], "start": 14, "end": 15, "closed": false }, { - "curve": 21, + "curve": [ + 21, + 1 + ], "start": 13, "end": 15, "closed": false }, { - "curve": 22, + "curve": [ + 22, + 1 + ], "start": 14, "end": 16, "closed": false }, { - "curve": 23, + "curve": [ + 23, + 1 + ], "start": 16, "end": 17, "closed": false }, { - "curve": 24, + "curve": [ + 24, + 1 + ], "start": 15, "end": 17, "closed": false }, { - "curve": 25, + "curve": [ + 25, + 1 + ], "start": 16, "end": 18, "closed": false }, { - "curve": 26, + "curve": [ + 26, + 1 + ], "start": 18, "end": 19, "closed": false }, { - "curve": 27, + "curve": [ + 27, + 1 + ], "start": 17, "end": 19, "closed": false }, { - "curve": 28, + "curve": [ + 28, + 1 + ], "start": 18, "end": 20, "closed": false }, { - "curve": 29, + "curve": [ + 29, + 1 + ], "start": 20, "end": 21, "closed": false }, { - "curve": 30, + "curve": [ + 30, + 1 + ], "start": 19, "end": 21, "closed": false }, { - "curve": 31, + "curve": [ + 31, + 1 + ], "start": 20, "end": 22, "closed": false }, { - "curve": 32, + "curve": [ + 32, + 1 + ], "start": 22, "end": 23, "closed": false }, { - "curve": 33, + "curve": [ + 33, + 1 + ], "start": 21, "end": 23, "closed": false }, { - "curve": 34, + "curve": [ + 34, + 1 + ], "start": 22, "end": 24, "closed": false }, { - "curve": 35, + "curve": [ + 35, + 1 + ], "start": 24, "end": 25, "closed": false }, { - "curve": 36, + "curve": [ + 36, + 1 + ], "start": 23, "end": 25, "closed": false }, { - "curve": 37, + "curve": [ + 37, + 1 + ], "start": 24, "end": 26, "closed": false }, { - "curve": 38, + "curve": [ + 38, + 1 + ], "start": 26, "end": 27, "closed": false }, { - "curve": 39, + "curve": [ + 39, + 1 + ], "start": 25, "end": 27, "closed": false }, { - "curve": 40, + "curve": [ + 40, + 1 + ], "start": 26, "end": 0, "closed": false }, { - "curve": 41, + "curve": [ + 41, + 1 + ], "start": 27, "end": 3, "closed": false diff --git a/e2e/playwright/export-snapshots/ply-ascii.ply b/e2e/playwright/export-snapshots/ply-ascii.ply index 7eda8ff5f..5c2a8ccaa 100644 --- a/e2e/playwright/export-snapshots/ply-ascii.ply +++ b/e2e/playwright/export-snapshots/ply-ascii.ply @@ -8,275 +8,275 @@ property float z element face 68 property list uchar uint vertex_indices end_header -0 0 4 -0 0 0 -0 -1 4 -0 -1 4 -0 0 0 -0 -1 0 -0 -1 4 -0 -1 0 -3.0950184 -1 4 -3.0950184 -1 4 -0 -1 0 -3.0950184 -1 0 -3.0950184 -1 4 -3.0950184 -1 0 -5.9513144 -3 4 -5.9513144 -3 4 -3.0950184 -1 0 -5.9513144 -3 0 -5.9513144 -3 4 -5.9513144 -3 0 -9.5 -3 4 -9.5 -3 4 -5.9513144 -3 0 -9.5 -3 0 -9.5 -3 4 -9.5 -3 0 -9.5 -2.5 4 -9.5 -2.5 4 -9.5 -3 0 -9.5 -2.5 0 -9.5 -2.5 4 -9.5 -2.5 0 -6.108964 -2.5 4 -6.108964 -2.5 4 -9.5 -2.5 0 -6.108964 -2.5 0 -3.4311862 -0.625 4 -4.323779 -1.25 4 -4.323779 -1.25 0 -4.323779 -1.25 4 -6.108964 -2.5 4 -6.108964 -2.5 0 -3.4311862 -0.625 0 -2.5385938 0 0 -2.5385938 0 4 -3.4311862 -0.625 4 -3.4311862 -0.625 0 -2.5385938 0 4 -4.323779 -1.25 4 -6.108964 -2.5 0 -4.323779 -1.25 0 -3.4311862 -0.625 0 -3.4311862 -0.625 4 -4.323779 -1.25 0 -3.342784 0.375 4 -2.5385938 0 4 -2.5385938 0 0 -4.146974 0.75 4 -3.342784 0.375 4 -3.342784 0.375 0 -3.342784 0.375 0 -4.146974 0.75 0 -4.146974 0.75 4 -4.146974 0.75 0 -5.755354 1.5 0 -5.755354 1.5 4 -3.342784 0.375 4 -2.5385938 0 0 -3.342784 0.375 0 -5.755354 1.5 4 -4.146974 0.75 4 -4.146974 0.75 0 -5.755354 1.5 4 -5.755354 1.5 0 -9.5 1.5 4 -9.5 1.5 4 -5.755354 1.5 0 -9.5 1.5 0 -9.5 1.5 4 -9.5 1.5 0 -9.5 2 4 -9.5 2 4 -9.5 1.5 0 -9.5 2 0 -9.5 2 4 -9.5 2 0 -5.644507 2 4 -5.644507 2 4 -9.5 2 0 -5.644507 2 0 -5.644507 2 4 -5.644507 2 0 -3.5 1 4 -3.5 1 4 -5.644507 2 0 -3.5 1 0 -3.5 1 4 -3.5 1 0 -0 1 4 -0 1 4 -3.5 1 0 -0 1 0 -0 1 4 -0 1 0 -0 0 4 -0 0 4 -0 1 0 -0 0 0 -3.342784 0.375 0 -2.5385938 0 0 -3.5 1 0 -3.4311862 -0.625 0 -4.323779 -1.25 0 -3.0950184 -1 0 -3.342784 0.375 0 -3.5 1 0 -4.146974 0.75 0 -4.323779 -1.25 0 -5.9513144 -3 0 -3.0950184 -1 0 -0 -1 0 -2.5385938 0 0 -3.0950184 -1 0 -0 -1 0 -0 0 0 -2.5385938 0 0 -9.5 -3 0 -6.108964 -2.5 0 -9.5 -2.5 0 -9.5 -3 0 -5.9513144 -3 0 -6.108964 -2.5 0 -5.9513144 -3 0 -4.323779 -1.25 0 -6.108964 -2.5 0 -5.644507 2 0 -5.755354 1.5 0 -4.146974 0.75 0 -3.0950184 -1 0 -2.5385938 0 0 -3.4311862 -0.625 0 -4.146974 0.75 0 -3.5 1 0 -5.644507 2 0 -9.5 1.5 0 -5.755354 1.5 0 -9.5 2 0 -5.755354 1.5 0 -5.644507 2 0 -9.5 2 0 -2.5385938 0 0 -0 0 0 -0 1 0 -3.5 1 0 -2.5385938 0 0 -0 1 0 -3.342784 0.375 4 -3.5 1 4 -2.5385938 0 4 -4.146974 0.75 4 -3.5 1 4 -3.342784 0.375 4 -3.4311862 -0.625 4 -3.0950184 -1 4 -4.323779 -1.25 4 -4.146974 0.75 4 -5.755354 1.5 4 -5.644507 2 4 -0 1 4 -2.5385938 0 4 -3.5 1 4 -0 1 4 -0 0 4 -2.5385938 0 4 -5.644507 2 4 -5.755354 1.5 4 -9.5 2 4 -9.5 2 4 -5.755354 1.5 4 -9.5 1.5 4 -4.146974 0.75 4 -5.644507 2 4 -3.5 1 4 -2.5385938 0 4 -3.0950184 -1 4 -3.4311862 -0.625 4 -4.323779 -1.25 4 -3.0950184 -1 4 -5.9513144 -3 4 -6.108964 -2.5 4 -4.323779 -1.25 4 -5.9513144 -3 4 -9.5 -2.5 4 -6.108964 -2.5 4 -9.5 -3 4 -6.108964 -2.5 4 -5.9513144 -3 4 -9.5 -3 4 -2.5385938 0 4 -0 -1 4 -3.0950184 -1 4 -0 -1 4 -2.5385938 0 4 -0 0 4 -3 0 1 2 -3 3 4 5 -3 6 7 8 -3 9 10 11 -3 12 13 14 -3 15 16 17 -3 18 19 20 -3 21 22 23 -3 24 25 26 -3 27 28 29 -3 30 31 32 -3 33 34 35 -3 36 37 38 -3 39 40 41 -3 42 43 44 -3 45 46 47 -3 48 49 50 -3 51 52 53 -3 54 55 56 -3 57 58 59 -3 60 61 62 -3 63 64 65 -3 66 67 68 -3 69 70 71 -3 72 73 74 -3 75 76 77 -3 78 79 80 -3 81 82 83 -3 84 85 86 -3 87 88 89 -3 90 91 92 -3 93 94 95 -3 96 97 98 -3 99 100 101 -3 102 103 104 -3 105 106 107 -3 108 109 110 -3 111 112 113 -3 114 115 116 -3 117 118 119 -3 120 121 122 -3 123 124 125 -3 126 127 128 -3 129 130 131 -3 132 133 134 -3 135 136 137 -3 138 139 140 -3 141 142 143 -3 144 145 146 -3 147 148 149 -3 150 151 152 -3 153 154 155 -3 156 157 158 -3 159 160 161 -3 162 163 164 -3 165 166 167 -3 168 169 170 -3 171 172 173 -3 174 175 176 -3 177 178 179 -3 180 181 182 -3 183 184 185 -3 186 187 188 -3 189 190 191 -3 192 193 194 -3 195 196 197 -3 198 199 200 -3 201 202 203 +0 0 4 +0 0 0 +0 -1 4 +0 -1 4 +0 0 0 +0 -1 0 +0 -1 4 +0 -1 0 +3.0950184 -1 4 +3.0950184 -1 4 +0 -1 0 +3.0950184 -1 0 +3.0950184 -1 4 +3.0950184 -1 0 +5.9513144 -3 4 +5.9513144 -3 4 +3.0950184 -1 0 +5.9513144 -3 0 +5.9513144 -3 4 +5.9513144 -3 0 +9.5 -3 4 +9.5 -3 4 +5.9513144 -3 0 +9.5 -3 0 +9.5 -3 4 +9.5 -3 0 +9.5 -2.5 4 +9.5 -2.5 4 +9.5 -3 0 +9.5 -2.5 0 +9.5 -2.5 4 +9.5 -2.5 0 +6.108964 -2.5 4 +6.108964 -2.5 4 +9.5 -2.5 0 +6.108964 -2.5 0 +3.4311862 -0.625 4 +4.323779 -1.25 4 +4.323779 -1.25 0 +4.323779 -1.25 4 +6.108964 -2.5 4 +6.108964 -2.5 0 +3.4311862 -0.625 0 +2.5385938 0 0 +2.5385938 0 4 +3.4311862 -0.625 4 +3.4311862 -0.625 0 +2.5385938 0 4 +4.323779 -1.25 4 +6.108964 -2.5 0 +4.323779 -1.25 0 +3.4311862 -0.625 0 +3.4311862 -0.625 4 +4.323779 -1.25 0 +3.342784 0.375 4 +2.5385938 0 4 +2.5385938 0 0 +4.146974 0.75 4 +3.342784 0.375 4 +3.342784 0.375 0 +3.342784 0.375 0 +4.146974 0.75 0 +4.146974 0.75 4 +4.146974 0.75 0 +5.755354 1.5 0 +5.755354 1.5 4 +3.342784 0.375 4 +2.5385938 0 0 +3.342784 0.375 0 +5.755354 1.5 4 +4.146974 0.75 4 +4.146974 0.75 0 +5.755354 1.5 4 +5.755354 1.5 0 +9.5 1.5 4 +9.5 1.5 4 +5.755354 1.5 0 +9.5 1.5 0 +9.5 1.5 4 +9.5 1.5 0 +9.5 2 4 +9.5 2 4 +9.5 1.5 0 +9.5 2 0 +9.5 2 4 +9.5 2 0 +5.644507 2 4 +5.644507 2 4 +9.5 2 0 +5.644507 2 0 +5.644507 2 4 +5.644507 2 0 +3.5 1 4 +3.5 1 4 +5.644507 2 0 +3.5 1 0 +3.5 1 4 +3.5 1 0 +0 1 4 +0 1 4 +3.5 1 0 +0 1 0 +0 1 4 +0 1 0 +0 0 4 +0 0 4 +0 1 0 +0 0 0 +3.342784 0.375 0 +2.5385938 0 0 +3.5 1 0 +3.4311862 -0.625 0 +4.323779 -1.25 0 +3.0950184 -1 0 +3.342784 0.375 0 +3.5 1 0 +4.146974 0.75 0 +4.323779 -1.25 0 +5.9513144 -3 0 +3.0950184 -1 0 +0 -1 0 +2.5385938 0 0 +3.0950184 -1 0 +0 -1 0 +0 0 0 +2.5385938 0 0 +9.5 -3 0 +6.108964 -2.5 0 +9.5 -2.5 0 +9.5 -3 0 +5.9513144 -3 0 +6.108964 -2.5 0 +5.9513144 -3 0 +4.323779 -1.25 0 +6.108964 -2.5 0 +5.644507 2 0 +5.755354 1.5 0 +4.146974 0.75 0 +3.0950184 -1 0 +2.5385938 0 0 +3.4311862 -0.625 0 +4.146974 0.75 0 +3.5 1 0 +5.644507 2 0 +9.5 1.5 0 +5.755354 1.5 0 +9.5 2 0 +5.755354 1.5 0 +5.644507 2 0 +9.5 2 0 +2.5385938 0 0 +0 0 0 +0 1 0 +3.5 1 0 +2.5385938 0 0 +0 1 0 +3.342784 0.375 4 +3.5 1 4 +2.5385938 0 4 +4.146974 0.75 4 +3.5 1 4 +3.342784 0.375 4 +3.4311862 -0.625 4 +3.0950184 -1 4 +4.323779 -1.25 4 +4.146974 0.75 4 +5.755354 1.5 4 +5.644507 2 4 +0 1 4 +2.5385938 0 4 +3.5 1 4 +0 1 4 +0 0 4 +2.5385938 0 4 +5.644507 2 4 +5.755354 1.5 4 +9.5 2 4 +9.5 2 4 +5.755354 1.5 4 +9.5 1.5 4 +4.146974 0.75 4 +5.644507 2 4 +3.5 1 4 +2.5385938 0 4 +3.0950184 -1 4 +3.4311862 -0.625 4 +4.323779 -1.25 4 +3.0950184 -1 4 +5.9513144 -3 4 +6.108964 -2.5 4 +4.323779 -1.25 4 +5.9513144 -3 4 +9.5 -2.5 4 +6.108964 -2.5 4 +9.5 -3 4 +6.108964 -2.5 4 +5.9513144 -3 4 +9.5 -3 4 +2.5385938 0 4 +0 -1 4 +3.0950184 -1 4 +0 -1 4 +2.5385938 0 4 +0 0 4 +3 0 1 2 +3 3 4 5 +3 6 7 8 +3 9 10 11 +3 12 13 14 +3 15 16 17 +3 18 19 20 +3 21 22 23 +3 24 25 26 +3 27 28 29 +3 30 31 32 +3 33 34 35 +3 36 37 38 +3 39 40 41 +3 42 43 44 +3 45 46 47 +3 48 49 50 +3 51 52 53 +3 54 55 56 +3 57 58 59 +3 60 61 62 +3 63 64 65 +3 66 67 68 +3 69 70 71 +3 72 73 74 +3 75 76 77 +3 78 79 80 +3 81 82 83 +3 84 85 86 +3 87 88 89 +3 90 91 92 +3 93 94 95 +3 96 97 98 +3 99 100 101 +3 102 103 104 +3 105 106 107 +3 108 109 110 +3 111 112 113 +3 114 115 116 +3 117 118 119 +3 120 121 122 +3 123 124 125 +3 126 127 128 +3 129 130 131 +3 132 133 134 +3 135 136 137 +3 138 139 140 +3 141 142 143 +3 144 145 146 +3 147 148 149 +3 150 151 152 +3 153 154 155 +3 156 157 158 +3 159 160 161 +3 162 163 164 +3 165 166 167 +3 168 169 170 +3 171 172 173 +3 174 175 176 +3 177 178 179 +3 180 181 182 +3 183 184 185 +3 186 187 188 +3 189 190 191 +3 192 193 194 +3 195 196 197 +3 198 199 200 +3 201 202 203 diff --git a/e2e/playwright/export-snapshots/ply-binary_big_endian.ply b/e2e/playwright/export-snapshots/ply-binary_big_endian.ply index ad4b6e11a..4053a8ba6 100644 Binary files a/e2e/playwright/export-snapshots/ply-binary_big_endian.ply and b/e2e/playwright/export-snapshots/ply-binary_big_endian.ply differ diff --git a/e2e/playwright/export-snapshots/ply-binary_little_endian.ply b/e2e/playwright/export-snapshots/ply-binary_little_endian.ply index ce09aae85..1bb2fa5b8 100644 Binary files a/e2e/playwright/export-snapshots/ply-binary_little_endian.ply and b/e2e/playwright/export-snapshots/ply-binary_little_endian.ply differ diff --git a/e2e/playwright/flow-tests.spec.ts b/e2e/playwright/flow-tests.spec.ts index 192feb4ab..6875096cc 100644 --- a/e2e/playwright/flow-tests.spec.ts +++ b/e2e/playwright/flow-tests.spec.ts @@ -5,6 +5,7 @@ import { v4 as uuidv4 } from 'uuid' import { getUtils } from './test-utils' import waitOn from 'wait-on' 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 @@ -643,7 +644,11 @@ test('Command bar works and can change a setting', async ({ page }) => { let cmdSearchBar = page.getByPlaceholder('Search commands') // 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 page.keyboard.press('Escape') 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' }) await expect(themeOption).toBeVisible() await themeOption.click() - const themeInput = page.getByPlaceholder(Themes.System) + const themeInput = page.getByPlaceholder('Select an option') await expect(themeInput).toBeVisible() await expect(themeInput).toBeFocused() // Select dark theme await page.keyboard.press('ArrowDown') - await page.keyboard.press('ArrowUp') + await page.keyboard.press('ArrowDown') await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute( 'data-headlessui-state', 'active' @@ -675,3 +680,59 @@ test('Command bar works and can change a setting', async ({ page }) => { // Check that the theme changed 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, %)` + ) +}) diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/change-camera-show-planes-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/change-camera-show-planes-1-Google-Chrome-linux.png index b05191d7d..705493fae 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/change-camera-show-planes-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/change-camera-show-planes-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/change-camera-show-planes-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/change-camera-show-planes-2-Google-Chrome-linux.png index caa4f8793..d4bc9a09d 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/change-camera-show-planes-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/change-camera-show-planes-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/change-camera-show-planes-3-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/change-camera-show-planes-3-Google-Chrome-linux.png index f63be3c6d..5652a902b 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/change-camera-show-planes-3-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/change-camera-show-planes-3-Google-Chrome-linux.png differ diff --git a/src/App.test.tsx b/src/App.test.tsx index 1b057fd89..75aaac5d4 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -8,7 +8,7 @@ import { createRoutesFromElements, } from 'react-router-dom' import { GlobalStateProvider } from './components/GlobalStateProvider' -import CommandBarProvider from 'components/CommandBar' +import CommandBarProvider from 'components/CommandBar/CommandBar' import ModelingMachineProvider from 'components/ModelingMachineProvider' import { BROWSER_FILE_NAME } from 'Router' diff --git a/src/Router.tsx b/src/Router.tsx index ea499a39d..c111eb76c 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -38,7 +38,7 @@ import { settingsMachine, } from './machines/settingsMachine' import { ContextFrom } from 'xstate' -import CommandBarProvider from 'components/CommandBar' +import CommandBarProvider from 'components/CommandBar/CommandBar' import { TEST, VITE_KC_SENTRY_DSN } from './env' import * as Sentry from '@sentry/react' import ModelingMachineProvider from 'components/ModelingMachineProvider' diff --git a/src/Toolbar.tsx b/src/Toolbar.tsx index 4c6d9712b..6f4e6220b 100644 --- a/src/Toolbar.tsx +++ b/src/Toolbar.tsx @@ -4,9 +4,11 @@ import { engineCommandManager } from './lang/std/engineConnection' import { useModelingContext } from 'hooks/useModelingContext' import { useCommandsContext } from 'hooks/useCommandsContext' import { ActionButton } from 'components/ActionButton' +import usePlatform from 'hooks/usePlatform' export const Toolbar = () => { - const { setCommandBarOpen } = useCommandsContext() + const platform = usePlatform() + const { commandBarSend } = useCommandsContext() const { state, send, context } = useModelingContext() const toolbarButtonsRef = useRef(null) const bgClassName = @@ -177,10 +179,15 @@ export const Toolbar = () => { send('extrude intent')} - disabled={!state.can('extrude intent')} + onClick={() => + commandBarSend({ + type: 'Find and select command', + data: { name: 'Extrude', ownerMachine: 'modeling' }, + }) + } + disabled={!state.can('Extrude')} title={ - state.can('extrude intent') + state.can('Extrude') ? 'extrude' : 'sketches need to be closed, or not already extruded' } @@ -204,10 +211,10 @@ export const Toolbar = () => { 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" > - ⌘K + {platform === 'darwin' ? '⌘K' : 'Ctrl+/'} ) diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx index 5085ed0e4..c0f652995 100644 --- a/src/components/AppHeader.tsx +++ b/src/components/AppHeader.tsx @@ -7,6 +7,7 @@ import styles from './AppHeader.module.css' import { NetworkHealthIndicator } from './NetworkHealthIndicator' import { useCommandsContext } from 'hooks/useCommandsContext' import { ActionButton } from './ActionButton' +import usePlatform from 'hooks/usePlatform' interface AppHeaderProps extends React.PropsWithChildren { showToolbar?: boolean @@ -22,7 +23,8 @@ export const AppHeader = ({ className = '', enableMenu = false, }: AppHeaderProps) => { - const { setCommandBarOpen } = useCommandsContext() + const platform = usePlatform() + const { commandBarSend } = useCommandsContext() const { auth } = useGlobalStateContext() const user = auth?.context?.user @@ -47,12 +49,12 @@ export const AppHeader = ({ ) : ( setCommandBarOpen(true)} + onClick={() => commandBarSend({ type: 'Open' })} className="text-sm self-center flex items-center w-fit gap-3" > Command Palette{' '} - ⌘K + {platform === 'darwin' ? '⌘K' : 'Ctrl+/'} )} diff --git a/src/components/CollapsiblePanel.tsx b/src/components/CollapsiblePanel.tsx index ba265f345..b89ec83f6 100644 --- a/src/components/CollapsiblePanel.tsx +++ b/src/components/CollapsiblePanel.tsx @@ -24,13 +24,13 @@ export const PanelHeader = ({ }: CollapsiblePanelProps) => { return ( -
+
void - removeCommands: (commands: Command[]) => void - commandBarOpen: boolean - setCommandBarOpen: Dispatch> - } -) - -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 ( - - {children} - - - ) -} - -const CommandBar = () => { - const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext() - useHotkeys(['meta+k', 'meta+/'], () => { - if (commands?.length === 0) return - setCommandBarOpen(!commandBarOpen) - }) - - const [selectedCommand, setSelectedCommand] = useState() - const [commandArguments, setCommandArguments] = useState( - [] - ) - const [commandArgumentData, setCommandArgumentData] = useState< - CommandArgumentData[] - >([]) - const [commandArgumentIndex, setCommandArgumentIndex] = useState(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 ( - clearState()} - as={Fragment} - > - { - setCommandBarOpen(false) - }} - className="fixed inset-0 z-40 overflow-y-auto pb-4 pt-1" - > - - - {!( - commandArguments && - commandArguments.length && - selectedCommand - ) ? ( - - ) : ( - <> -
-

- {selectedCommand && - 'icon' in selectedCommand && - selectedCommand.icon && ( - - )} - {getDisplayValue(selectedCommand)} -

- {commandArguments.map((arg, i) => ( -

- {commandArgumentIndex >= i && commandArgumentData[i] ? ( - commandArgumentData[i][1] - ) : arg.defaultValue ? ( - arg.defaultValue - ) : ( - {arg.name} - )} -

- ))} -
-
- - - )} - - -
-
- ) -} - -function Argument({ - arg, - appendCommandArgumentData, - stepBack, -}: { - arg: CommandArgument - appendCommandArgumentData: Dispatch> - stepBack: () => void -}) { - const { setCommandBarOpen } = useCommandsContext() - const inputRef = useRef(null) - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus() - inputRef.current.select() - } - }, [arg, inputRef]) - - return arg.type === 'select' ? ( - - ) : ( -
{ - event.preventDefault() - - appendCommandArgumentData({ name: inputRef.current?.value }) - }} - > - -
- ) -} - -export default CommandBarProvider - -function CommandComboBox({ - options, - handleSelection, - stepBack, - placeholder, -}: { - options: ComboboxOption[] - handleSelection: Dispatch> - stepBack: () => void - placeholder?: string -}) { - const { setCommandBarOpen } = useCommandsContext() - const [query, setQuery] = useState('') - const [filteredOptions, setFilteredOptions] = useState() - - 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 ( - -
- - 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 - /> -
- - {filteredOptions?.map((option) => ( - - {'icon' in option && option.icon && ( - - )} -

{option.name}

- {'isCurrent' in option && option.isCurrent && ( - - current - - )} -
- ))} -
-
- ) -} diff --git a/src/components/CommandBar/CommandArgOptionInput.tsx b/src/components/CommandBar/CommandArgOptionInput.tsx new file mode 100644 index 000000000..3f0cfe4ae --- /dev/null +++ b/src/components/CommandBar/CommandArgOptionInput.tsx @@ -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[] + argName: string + stepBack: () => void + onSubmit: (data: unknown) => void + placeholder?: string +}) { + const { commandBarSend, commandBarState } = useCommandsContext() + const inputRef = useRef(null) + const formRef = useRef(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() + + 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) { + setArgValue(option) + onSubmit(option.value) + } + + function handleSubmit(e: React.FormEvent) { + e.preventDefault() + onSubmit(argValue) + } + + return ( +
+ +
+ + 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)?.name || + placeholder || + 'Select an option for ' + argName + } + autoCapitalize="off" + autoComplete="off" + autoCorrect="off" + spellCheck="false" + autoFocus + /> +
+ + {filteredOptions?.map((option) => ( + +

{option.name}

+ {'isCurrent' in option && option.isCurrent && ( + + current + + )} +
+ ))} +
+
+
+ ) +} + +export default CommandArgOptionInput diff --git a/src/components/CommandBar/CommandBar.tsx b/src/components/CommandBar/CommandBar.tsx new file mode 100644 index 000000000..06c956534 --- /dev/null +++ b/src/components/CommandBar/CommandBar.tsx @@ -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 + commandBarSend: (event: EventFrom) => void +} + +export const CommandsContext = createContext({ + 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 ( + + {children} + + + ) +} + +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 ( + { + if (selectedCommand?.onCancel) selectedCommand.onCancel() + commandBarSend({ type: 'Clear' }) + }} + as={Fragment} + > + { + commandBarSend({ type: 'Close' }) + }} + className={ + 'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' + + (isSelectionArgument ? 'pointer-events-none' : '') + } + > + + + {commandBarState.matches('Selecting command') ? ( + + ) : commandBarState.matches('Gathering arguments') ? ( + + ) : ( + commandBarState.matches('Review') && ( + + ) + )} + + + + + ) +} + +export default CommandBarProvider diff --git a/src/components/CommandBar/CommandBarArgument.tsx b/src/components/CommandBar/CommandBarArgument.tsx new file mode 100644 index 000000000..d78449edb --- /dev/null +++ b/src/components/CommandBar/CommandBarArgument.tsx @@ -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 && ( + + + + ) + ) +} + +export default CommandBarArgument + +function ArgumentInput({ + arg, + stepBack, + onSubmit, +}: { + arg: CommandArgument & { name: string } + stepBack: () => void + onSubmit: (event: any) => void +}) { + switch (arg.inputType) { + case 'options': + return ( + + ) + case 'selection': + return ( + + ) + default: + return ( + + ) + } +} diff --git a/src/components/CommandBar/CommandBarBasicInput.tsx b/src/components/CommandBar/CommandBarBasicInput.tsx new file mode 100644 index 000000000..7ce3a3c1b --- /dev/null +++ b/src/components/CommandBar/CommandBarBasicInput.tsx @@ -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 & { + 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(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) { + e.preventDefault() + onSubmit(inputRef.current?.value) + } + + return ( +
+ +
+ ) +} + +export default CommandBarBasicInput diff --git a/src/components/CommandBar/CommandBarHeader.tsx b/src/components/CommandBar/CommandBarHeader.tsx new file mode 100644 index 000000000..0c7c86ead --- /dev/null +++ b/src/components/CommandBar/CommandBarHeader.tsx @@ -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 && ( + <> +
+
+

+ {selectedCommand && + 'icon' in selectedCommand && + selectedCommand.icon && ( + + )} + {selectedCommand?.name} +

+ {Object.entries(selectedCommand?.args || {}).map( + ([argName, arg], i) => ( + + ) + )} +
+ {isReviewing ? : } +
+
+ {children} + + ) + ) +} + +function ReviewingButton() { + return ( + + Submit command + + ) +} + +function GatheringArgsButton() { + return ( + + Continue + + ) +} + +export default CommandBarHeader diff --git a/src/components/CommandBar/CommandBarReview.tsx b/src/components/CommandBar/CommandBarReview.tsx new file mode 100644 index 000000000..4764955f6 --- /dev/null +++ b/src/components/CommandBar/CommandBarReview.tsx @@ -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 ( + +

Confirm {selectedCommand?.name}

+
+ {Object.entries(argumentsToSubmit).map(([key, value], i) => { + const arg = selectedCommand?.args + ? selectedCommand?.args[key] + : undefined + if (!arg) return null + + return ( + + ) + })} +
+
+ ) +} + +export default CommandBarReview diff --git a/src/components/CommandBar/CommandBarSelectionInput.tsx b/src/components/CommandBar/CommandBarSelectionInput.tsx new file mode 100644 index 000000000..b630c49dc --- /dev/null +++ b/src/components/CommandBar/CommandBarSelectionInput.tsx @@ -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) => + snapshot.context.selectionRanges + +function CommandBarSelectionInput({ + arg, + stepBack, + onSubmit, +}: { + arg: CommandArgument & { inputType: 'selection'; name: string } + stepBack: () => void + onSubmit: (data: unknown) => void +}) { + const { code } = useKclContext() + const inputRef = useRef(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( + 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) { + e.preventDefault() + + if (!canSubmitSelection) { + setHasSubmitted(true) + return + } + + onSubmit(selection) + } + + return ( +
+ +
+ ) +} + +export default CommandBarSelectionInput diff --git a/src/components/CommandComboBox.tsx b/src/components/CommandComboBox.tsx new file mode 100644 index 000000000..da4ef9396 --- /dev/null +++ b/src/components/CommandComboBox.tsx @@ -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() + + 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 ( + +
+ + 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 + /> +
+ + {filteredOptions?.map((option) => ( + + {'icon' in option && option.icon && ( + + )} +

{option.name}

+
+ ))} +
+
+ ) +} + +export default CommandComboBox diff --git a/src/components/CustomIcon.tsx b/src/components/CustomIcon.tsx index 55266eb1a..36c9cab29 100644 --- a/src/components/CustomIcon.tsx +++ b/src/components/CustomIcon.tsx @@ -3,6 +3,7 @@ export type CustomIconName = | 'arrowLeft' | 'arrowRight' | 'arrowUp' + | 'checkmark' | 'close' | 'equal' | 'extrude' @@ -90,6 +91,22 @@ export const CustomIcon = ({ /> ) + case 'checkmark': + return ( + + + + ) case 'close': return ( { const navigate = useNavigate() - const { setCommandBarOpen } = useCommandsContext() + const { commandBarSend } = useCommandsContext() const { project } = useRouteLoaderData(paths.FILE) as IndexLoaderData const [state, send] = useMachine(fileMachine, { @@ -54,7 +54,7 @@ export const FileMachineProvider = ({ event: EventFrom ) => { if (event.data && 'name' in event.data) { - setCommandBarOpen(false) + commandBarSend({ type: 'Close' }) navigate( `${paths.FILE}/${encodeURIComponent( context.selectedDirectory + sep + event.data.name diff --git a/src/components/GlobalStateProvider.tsx b/src/components/GlobalStateProvider.tsx index 9ab8c35ed..0b6b0ab3d 100644 --- a/src/components/GlobalStateProvider.tsx +++ b/src/components/GlobalStateProvider.tsx @@ -1,19 +1,11 @@ import { useMachine } from '@xstate/react' import { useNavigate } from 'react-router-dom' import { paths } from '../Router' -import { - authCommandBarConfig, - authMachine, - TOKEN_PERSIST_KEY, -} from '../machines/authMachine' +import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine' import withBaseUrl from '../lib/withBaseURL' import React, { createContext, useEffect, useRef } from 'react' import useStateMachineCommands from '../hooks/useStateMachineCommands' -import { - SETTINGS_PERSIST_KEY, - settingsCommandBarConfig, - settingsMachine, -} from 'machines/settingsMachine' +import { SETTINGS_PERSIST_KEY, settingsMachine } from 'machines/settingsMachine' import { toast } from 'react-hot-toast' import { setThemeClass, Themes } from 'lib/theme' import { @@ -23,8 +15,9 @@ import { Prop, StateFrom, } from 'xstate' -import { useCommandsContext } from 'hooks/useCommandsContext' import { isTauri } from 'lib/isTauri' +import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig' +import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig' type MachineContext = { state: StateFrom @@ -45,7 +38,6 @@ export const GlobalStateProvider = ({ children: React.ReactNode }) => { const navigate = useNavigate() - const { commands } = useCommandsContext() // Settings machine setup const retrievedSettings = useRef( @@ -81,10 +73,9 @@ export const GlobalStateProvider = ({ }) useStateMachineCommands({ + machineId: 'settings', state: settingsState, send: settingsSend, - commands, - owner: 'settings', commandBarConfig: settingsCommandBarConfig, }) @@ -121,11 +112,10 @@ export const GlobalStateProvider = ({ }) useStateMachineCommands({ + machineId: 'auth', state: authState, send: authSend, - commands, commandBarConfig: authCommandBarConfig, - owner: 'auth', }) return ( diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx index 53801bc22..38866ed76 100644 --- a/src/components/Loading.tsx +++ b/src/components/Loading.tsx @@ -15,23 +15,23 @@ const Loading = ({ children }: React.PropsWithChildren) => { data-testid="loading" > - + -

+

{children || 'Loading'}

diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index 831244c91..336321038 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -29,19 +29,26 @@ import { addNewSketchLn, compareVec2Epsilon, } from 'lang/std/sketch' -import { kclManager } from 'lang/KclSinglton' +import { kclManager, useKclContext } from 'lang/KclSinglton' import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance' import { angleBetweenInfo, applyConstraintAngleBetween, } from './Toolbar/SetAngleBetween' import { applyConstraintAngleLength } from './Toolbar/setAngleLength' -import { toast } from 'react-hot-toast' import { pathMapToSelections } from 'lang/util' 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 { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance' +import useStateMachineCommands from 'hooks/useStateMachineCommands' +import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig' type MachineContext = { state: StateFrom @@ -59,6 +66,7 @@ export const ModelingMachineProvider = ({ children: React.ReactNode }) => { const { auth } = useGlobalStateContext() + const { code } = useKclContext() const token = auth?.context?.token const streamRef = useRef(null) useSetupEngineManager(streamRef, token) @@ -68,8 +76,6 @@ export const ModelingMachineProvider = ({ editorView: s.editorView, })) - // const { commands } = useCommandsContext() - // Settings machine setup // const retrievedSettings = useRef( // localStorage?.getItem(MODELING_PERSIST_KEY) || '{}' @@ -83,148 +89,85 @@ export const ModelingMachineProvider = ({ // > // ) - const [modelingState, modelingSend] = useMachine(modelingMachine, { - // context: persistedSettings, - actions: { - 'Modify AST': () => {}, - 'Update code selection cursors': () => {}, - 'show default planes': () => { - kclManager.showPlanes() - }, - 'create path': assign({ - 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 + const [modelingState, modelingSend, modelingActor] = useMachine( + modelingMachine, + { + // context: persistedSettings, + actions: { + 'Modify AST': () => {}, + 'Update code selection cursors': () => {}, + 'show default planes': () => { + kclManager.showPlanes() }, - }), - 'AST start new sketch': assign( - ({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => { - if (!axis) { - // Something really weird must have happened for this to happen. - console.error('axis is undefined for starting a new sketch') - return {} - } - if (!segmentId) { - // Something really weird must have happened for this to happen. - console.error('segmentId is undefined for starting a new sketch') - return {} - } - - const _addStartSketch = addStartSketch( - kclManager.ast, - 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( - 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, + 'create path': assign({ + 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 + }, + }), + 'AST start new sketch': assign( + ({ sketchEnginePathId }, { data: { coords, axis, segmentId } }) => { + if (!axis) { + // Something really weird must have happened for this to happen. + console.error('axis is undefined for starting a new sketch') + return {} } - const lineCallExp = updatedPipeNode.body.find( - (exp) => exp.type === 'CallExpression' && exp.callee.name === 'line' - ) - if (lineCallExp) - engineCommandManager.artifactMap[segmentId] = { - type: 'result', - range: [lineCallExp.start, lineCallExp.end], - commandType: 'extend_path', - parentId: sketchEnginePathId, - data: null, - raw: {} as any, + if (!segmentId) { + // Something really weird must have happened for this to happen. + console.error('segmentId is undefined for starting a new sketch') + return {} } - kclManager.executeAstMock(astWithUpdatedSource, true) - - 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( + const _addStartSketch = addStartSketch( 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( + astWithUpdatedSource, + _pathToNode ).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] = { type: 'result', range: [lineCallExp.start, lineCallExp.end], @@ -233,120 +176,189 @@ export const ModelingMachineProvider = ({ 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 - '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) { - // engineCommandManager - // .sendSceneCommand({ - // type: 'modeling_cmd_req', - // cmd: { - // type: 'select_clear', - // }, - // cmd_id: uuidv4(), - // }) - // .then(() => { - // engineCommandManager.sendSceneCommand(axisAddCmd) - // }) - // } else { - // engineCommandManager.sendSceneCommand(axisAddCmd) - // } + kclManager.executeAstMock(astWithUpdatedSource, true) - const { - codeMirrorSelection, - selectionRangeTypeMap, - otherSelections, - } = handleSelectionWithShift({ - otherSelection: setSelections.selection, - currentSelections: selectionRanges, - isShiftDown, - }) - setTimeout(() => { - editorView.dispatch({ - selection: codeMirrorSelection, - }) - }) - return { - selectionRangeTypeMap, - selectionRanges: { - codeBasedSelections: selectionRanges.codeBasedSelections, - otherSelections, - }, + return { + sketchPathToNode: _pathToNode, + } } - } 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. + ), + 'AST add line segment': async ( + { sketchPathToNode, sketchEnginePathId }, + { data: { coords, segmentId } } + ) => { + if (!sketchPathToNode) return + const lastCoord = coords[coords.length - 1] - // for more details on how selections see `src/lib/selections.ts`. - - const { - codeMirrorSelection, - selectionRangeTypeMap, - otherSelections, - } = handleSelectionWithShift({ - codeSelection: setSelections.selection, - currentSelections: selectionRanges, - isShiftDown, + const pathInfo = await engineCommandManager.sendSceneCommand({ + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { + type: 'path_get_info', + path_id: sketchEnginePathId, + }, }) - 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( + 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(() => { editorView.dispatch({ selection: codeMirrorSelection, }) }) - } - if (!setSelections.selection) { + return { + 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 { selectionRangeTypeMap, 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 => { + const { modifiedAst, pathToNodeMap } = + await applyConstraintHorzVertDistance({ + constraint: 'setHorzDistance', + selectionRanges, + }) + await kclManager.updateAst(modifiedAst, true) return { - selectionRangeTypeMap, - selectionRanges: { - codeBasedSelections: selectionRanges.codeBasedSelections, - otherSelections, - }, + selectionType: 'completeSelection', + selection: pathMapToSelections( + kclManager.ast, + selectionRanges, + pathToNodeMap + ), } - } - // 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, - 'Selection is one face': ({ selectionRanges }) => { - return !!isCursorInSketchCommandRange( - engineCommandManager.artifactMap, - selectionRanges - ) - }, - }, - services: { - 'Get horizontal info': async ({ - selectionRanges, - }): Promise => { - 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 => { - 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 => { - const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({ + }, + 'Get vertical info': async ({ selectionRanges, - }).enabled - ? applyConstraintAngleBetween({ + }): Promise => { + const { modifiedAst, pathToNodeMap } = + await applyConstraintHorzVertDistance({ + constraint: 'setVertDistance', selectionRanges, }) - : applyConstraintAngleLength({ + await kclManager.updateAst(modifiedAst, true) + return { + selectionType: 'completeSelection', + selection: pathMapToSelections( + kclManager.ast, selectionRanges, - angleOrLength: 'setAngle', - })) - await kclManager.updateAst(modifiedAst, true) - return { - selectionType: 'completeSelection', - selection: pathMapToSelections( - kclManager.ast, - selectionRanges, - pathToNodeMap - ), - } - }, - 'Get length info': async ({ - selectionRanges, - }): Promise => { - 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 => { - const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect({ + pathToNodeMap + ), + } + }, + 'Get angle info': async ({ selectionRanges, - }) - await kclManager.updateAst(modifiedAst, true) - return { - selectionType: 'completeSelection', - selection: pathMapToSelections( - kclManager.ast, - selectionRanges, - pathToNodeMap - ), - } - }, - 'Get ABS X info': async ({ selectionRanges }): Promise => { - const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance( - { - constraint: 'xAbs', + }): Promise => { + const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({ 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) - return { - selectionType: 'completeSelection', - selection: pathMapToSelections( - kclManager.ast, - selectionRanges, - pathToNodeMap - ), - } - }, - 'Get ABS Y info': async ({ selectionRanges }): Promise => { - const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance( - { - constraint: 'yAbs', - selectionRanges, + }, + 'Get length info': async ({ + selectionRanges, + }): Promise => { + const { modifiedAst, pathToNodeMap } = + await applyConstraintAngleLength({ selectionRanges }) + await kclManager.updateAst(modifiedAst, true) + return { + selectionType: 'completeSelection', + selection: pathMapToSelections( + kclManager.ast, + selectionRanges, + pathToNodeMap + ), } - ) - await kclManager.updateAst(modifiedAst, true) - return { - selectionType: 'completeSelection', - selection: pathMapToSelections( - kclManager.ast, - selectionRanges, - pathToNodeMap - ), - } + }, + 'Get perpendicular distance info': async ({ + selectionRanges, + }): Promise => { + const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect( + { + selectionRanges, + } + ) + await kclManager.updateAst(modifiedAst, true) + return { + selectionType: 'completeSelection', + selection: pathMapToSelections( + kclManager.ast, + selectionRanges, + pathToNodeMap + ), + } + }, + 'Get ABS X info': async ({ + selectionRanges, + }): Promise => { + 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 => { + 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(() => { engineCommandManager.onPlaneSelected((plane_id: string) => { @@ -538,13 +559,17 @@ export const ModelingMachineProvider = ({ }) }, [modelingSend]) - // useStateMachineCommands({ - // state: settingsState, - // send: settingsSend, - // commands, - // owner: 'settings', - // commandBarMeta: settingsCommandBarMeta, - // }) + useStateMachineCommands({ + machineId: 'modeling', + state: modelingState, + send: modelingSend, + actor: modelingActor, + commandBarConfig: modelingMachineConfig, + onCancel: () => { + console.log('firing onCancel!!') + modelingSend({ type: 'Cancel' }) + }, + }) return ( { data-testid="network-good" > {NETWORK_CONTENT.good} diff --git a/src/components/ProjectCard.tsx b/src/components/ProjectCard.tsx index 9168c8467..35bd89ba7 100644 --- a/src/components/ProjectCard.tsx +++ b/src/components/ProjectCard.tsx @@ -143,7 +143,7 @@ function ProjectCard({ className: 'p-1', size: 'xs', 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" onClick={(e) => { @@ -185,8 +185,7 @@ function ProjectCard({ bgClassName: 'bg-destroy-80', className: 'p-1', size: 'sm', - iconClassName: - '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', + iconClassName: '!text-destroy-70 dark:!text-destroy-40', }} className="hover:border-destroy-40 dark:hover:border-destroy-40" > diff --git a/src/components/ProjectSidebarMenu.test.tsx b/src/components/ProjectSidebarMenu.test.tsx index c8b130b92..77da01c1f 100644 --- a/src/components/ProjectSidebarMenu.test.tsx +++ b/src/components/ProjectSidebarMenu.test.tsx @@ -3,7 +3,7 @@ import { BrowserRouter } from 'react-router-dom' import ProjectSidebarMenu from './ProjectSidebarMenu' import { ProjectWithEntryPointMetadata } from '../Router' import { GlobalStateProvider } from './GlobalStateProvider' -import CommandBarProvider from './CommandBar' +import CommandBarProvider from './CommandBar/CommandBar' const now = new Date() const projectWellFormed = { diff --git a/src/components/Stream.tsx b/src/components/Stream.tsx index 74aeba1a9..7464529f8 100644 --- a/src/components/Stream.tsx +++ b/src/components/Stream.tsx @@ -243,15 +243,14 @@ export const Stream = ({ className = '' }) => { }) } else if ( !didDragInStream && - (state.matches('Sketch.SketchIdle') || - state.matches('idle') || - state.matches('awaiting selection')) + (state.matches('Sketch.SketchIdle') || state.matches('idle')) ) { command.cmd = { type: 'select_with_point', selected_at_window: { x, y }, selection_type: 'add', } + engineCommandManager.sendSceneCommand(command) } else if (!didDragInStream && state.matches('Sketch.Move Tool')) { command.cmd = { diff --git a/src/components/TextEditor.tsx b/src/components/TextEditor.tsx index 29186e87c..7a703e88b 100644 --- a/src/components/TextEditor.tsx +++ b/src/components/TextEditor.tsx @@ -64,7 +64,7 @@ export const TextEditor = ({ const { settings: { context: { textWrapping } = {} } = {} } = useGlobalStateContext() - const { setCommandBarOpen } = useCommandsContext() + const { commandBarSend } = useCommandsContext() const { enable: convertEnabled, handleClick: convertCallback } = useConvertToVariable() @@ -136,7 +136,7 @@ export const TextEditor = ({ { key: 'Meta-k', run: () => { - setCommandBarOpen(true) + commandBarSend({ type: 'Open' }) return false }, }, diff --git a/src/components/UserSidebarMenu.test.tsx b/src/components/UserSidebarMenu.test.tsx index 0ce3e52db..5c14046fb 100644 --- a/src/components/UserSidebarMenu.test.tsx +++ b/src/components/UserSidebarMenu.test.tsx @@ -8,7 +8,7 @@ import { } from 'react-router-dom' import { Models } from '@kittycad/lib' import { GlobalStateProvider } from './GlobalStateProvider' -import CommandBarProvider from './CommandBar' +import CommandBarProvider from './CommandBar/CommandBar' type User = Models['User_type'] diff --git a/src/hooks/useCommandsContext.ts b/src/hooks/useCommandsContext.ts index 6eea8670d..393397450 100644 --- a/src/hooks/useCommandsContext.ts +++ b/src/hooks/useCommandsContext.ts @@ -1,4 +1,4 @@ -import { CommandsContext } from 'components/CommandBar' +import { CommandsContext } from 'components/CommandBar/CommandBar' import { useContext } from 'react' export const useCommandsContext = () => { diff --git a/src/hooks/usePlatform.ts b/src/hooks/usePlatform.ts new file mode 100644 index 000000000..c3601d816 --- /dev/null +++ b/src/hooks/usePlatform.ts @@ -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('') + + 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 +} diff --git a/src/hooks/useStateMachineCommands.ts b/src/hooks/useStateMachineCommands.ts index 5af90f091..395abdd01 100644 --- a/src/hooks/useStateMachineCommands.ts +++ b/src/hooks/useStateMachineCommands.ts @@ -1,46 +1,68 @@ import { useEffect } from 'react' -import { AnyStateMachine, StateFrom } from 'xstate' -import { - Command, - CommandBarConfig, - createMachineCommand, -} from '../lib/commands' +import { AnyStateMachine, InterpreterFrom, StateFrom } from 'xstate' +import { createMachineCommand } from '../lib/createMachineCommand' 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 { +// 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 +> { + machineId: T['id'] state: StateFrom send: Function - commandBarConfig?: CommandBarConfig - commands: Command[] - owner: string + actor?: InterpreterFrom + commandBarConfig?: CommandSetConfig + onCancel?: () => void } -export default function useStateMachineCommands({ +export default function useStateMachineCommands< + T extends AnyStateMachine, + S extends CommandSetSchema +>({ + machineId, state, send, + actor, commandBarConfig, - owner, -}: UseStateMachineCommandsArgs) { - const { addCommands, removeCommands } = useCommandsContext() + onCancel, +}: UseStateMachineCommandsArgs) { + const { commandBarSend } = useCommandsContext() useEffect(() => { const newCommands = state.nextEvents .filter((e) => !['done.', 'error.'].some((n) => e.includes(n))) .map((type) => - createMachineCommand({ + createMachineCommand({ + ownerMachine: machineId, type, state, send, + actor, commandBarConfig, - owner, + onCancel, }) ) .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 () => { - removeCommands(newCommands) + commandBarSend({ + type: 'Remove commands', + data: { commands: newCommands }, + }) } }, [state]) } diff --git a/src/index.css b/src/index.css index 0f1cccc38..370c87f92 100644 --- a/src/index.css +++ b/src/index.css @@ -57,7 +57,7 @@ select { } 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 { @@ -65,7 +65,7 @@ button:hover { } .dark button { - @apply border-chalkboard-70; + @apply border-chalkboard-70 focus-visible:ring-energy-10/50; } .dark button:hover { @@ -88,6 +88,14 @@ a:not(.action-button) { @apply text-chalkboard-20 hover:text-energy-10; } +input { + @apply selection:bg-energy-10/50; +} + +.dark input { + @apply selection:bg-energy-10/40; +} + .mono { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index fa4e72b70..c9f4ba670 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -248,7 +248,8 @@ export function mutateObjExpProp( export function extrudeSketch( node: Program, pathToNode: PathToNode, - shouldPipe = true + shouldPipe = true, + distance = 4 ): { modifiedAst: Program pathToNode: PathToNode @@ -274,7 +275,7 @@ export function extrudeSketch( getNodeFromPath(_node, pathToNode, 'VariableDeclarator') const extrudeCall = createCallExpressionStdLib('extrude', [ - createLiteral(4), + createLiteral(distance), shouldPipe ? createPipeSubstitution() : { diff --git a/src/lib/commandBarConfigs/authCommandConfig.ts b/src/lib/commandBarConfigs/authCommandConfig.ts new file mode 100644 index 000000000..17d87a14a --- /dev/null +++ b/src/lib/commandBarConfigs/authCommandConfig.ts @@ -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', + }, +} diff --git a/src/lib/commandBarConfigs/homeCommandConfig.ts b/src/lib/commandBarConfigs/homeCommandConfig.ts new file mode 100644 index 000000000..5210530d3 --- /dev/null +++ b/src/lib/commandBarConfigs/homeCommandConfig.ts @@ -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, + }, + }, + }, +} diff --git a/src/lib/commandBarConfigs/modelingCommandConfig.ts b/src/lib/commandBarConfigs/modelingCommandConfig.ts new file mode 100644 index 000000000..682c45442 --- /dev/null +++ b/src/lib/commandBarConfigs/modelingCommandConfig.ts @@ -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, + }, + }, + }, +} diff --git a/src/lib/commandBarConfigs/settingsCommandConfig.ts b/src/lib/commandBarConfigs/settingsCommandConfig.ts new file mode 100644 index 000000000..15b092f03 --- /dev/null +++ b/src/lib/commandBarConfigs/settingsCommandConfig.ts @@ -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', + }, + ], + }, + }, + }, +} diff --git a/src/lib/commandTypes.ts b/src/lib/commandTypes.ts new file mode 100644 index 000000000..1b6ffef9f --- /dev/null +++ b/src/lib/commandTypes.ts @@ -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 = Partial<{ + [EventType in EventFrom['type']]: Record +}> + +export type CommandSet< + T extends AllMachines, + Schema extends CommandSetSchema +> = Partial<{ + [EventType in EventFrom['type']]: Command< + T, + EventFrom['type'], + Schema[EventType] + > +}> + +export type CommandSetConfig< + T extends AllMachines, + Schema extends CommandSetSchema +> = Partial<{ + [EventType in EventFrom['type']]: CommandConfig< + T, + EventFrom['type'], + Schema[EventType] + > +}> + +export type Command< + T extends AnyStateMachine = AnyStateMachine, + CommandName extends EventFrom['type'] = EventFrom['type'], + CommandSchema extends CommandSetSchema[CommandName] = CommandSetSchema[CommandName] +> = { + name: CommandName + ownerMachine: T['id'] + needsReview: boolean + onSubmit: (data?: CommandSchema) => void + onCancel?: () => void + args?: { + [ArgName in keyof CommandSchema]: CommandArgument + } + description?: string + icon?: Icon + hide?: (typeof PLATFORMS)[number] +} + +export type CommandConfig< + T extends AnyStateMachine = AnyStateMachine, + CommandName extends EventFrom['type'] = EventFrom['type'], + CommandSchema extends CommandSetSchema[CommandName] = CommandSetSchema[CommandName] +> = Omit< + Command, + '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) => OutputType) + payload?: OutputType + } & ( + | { + inputType: Extract + options: + | CommandArgumentOption[] + | ((context: ContextFrom) => CommandArgumentOption[]) + } + | { + inputType: Extract + selectionTypes: Selection['type'][] + multiple: boolean + } + | { inputType: Exclude } + ) + +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 + options: CommandArgumentOption[] + } + | { + inputType: Extract + selectionTypes: Selection['type'][] + actor: InterpreterFrom + multiple: boolean + } + | { inputType: Exclude } + ) + +export type CommandArgumentWithName< + OutputType, + T extends AnyStateMachine = AnyStateMachine +> = CommandArgument & { + name: string +} + +export type CommandArgumentOption = { + name: string + isCurrent?: boolean + value: A +} diff --git a/src/lib/commands.ts b/src/lib/commands.ts deleted file mode 100644 index 2df8ce6f1..000000000 --- a/src/lib/commands.ts +++ /dev/null @@ -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 = { - 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 - defaultValue?: string - getDefaultValueFromContext?: keyof ContextFrom - } - | { - type: 'string' - defaultValue?: string - getDefaultValueFromContext?: keyof ContextFrom - } - | { type: 'interaction' } -) - -export type CommandBarConfig = Partial<{ - [EventType in EventFrom['type']]: - | { - args: CommandArgumentConfig[] - 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 - options: CommandArgumentOption[] - } - | { - type: Exclude - } -) - -interface CreateMachineCommandProps { - type: EventFrom['type'] - state: StateFrom - commandBarConfig?: CommandBarConfig - 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({ - type, - state, - commandBarConfig, - send, - owner, -}: CreateMachineCommandProps): 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) => { - if (data !== undefined && data !== null) { - send(type, { data }) - } else { - send(type) - } - }, - ...('args' in lookedUpMeta - ? { - args: getCommandArgumentValuesFromContext(state, lookedUpMeta.args), - formatFunction, - } - : {}), - } -} - -function getCommandArgumentValuesFromContext( - state: StateFrom, - args: CommandArgumentConfig[] -): CommandArgument[] { - function getDefaultValue( - arg: CommandArgumentConfig & { 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, - } - } - }) -} diff --git a/src/lib/createMachineCommand.ts b/src/lib/createMachineCommand.ts new file mode 100644 index 000000000..41a049fe3 --- /dev/null +++ b/src/lib/createMachineCommand.ts @@ -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 +> { + type: EventFrom['type'] + ownerMachine: T['id'] + state: StateFrom + send: Function + actor?: InterpreterFrom + commandBarConfig?: CommandSetConfig + 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 +>({ + ownerMachine, + type, + state, + send, + actor, + commandBarConfig, + onCancel, +}: CreateMachineCommandProps): 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 = { + 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, + CommandName extends EventFrom['type'] = EventFrom['type'] +>( + state: StateFrom, + args: CommandConfig['args'], + actor?: InterpreterFrom +): NonNullable['args']> { + const newArgs = {} as NonNullable['args']> + + for (const arg in args) { + const argConfig = args[arg] as CommandArgumentConfig + const newArg = buildCommandArgument(argConfig, state, actor) + newArgs[arg] = newArg + } + + return newArgs +} + +function buildCommandArgument< + O extends CommandSetSchema, + T extends AnyStateMachine +>( + arg: CommandArgumentConfig, + state: StateFrom, + actor?: InterpreterFrom +): CommandArgument & { 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, '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 & { 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 & { inputType: 'selection' } + } else { + return { + inputType: arg.inputType, + ...baseCommandArgument, + } + } +} diff --git a/src/lib/selections.ts b/src/lib/selections.ts index b35250547..e2674cd1d 100644 --- a/src/lib/selections.ts +++ b/src/lib/selections.ts @@ -7,6 +7,10 @@ import { EditorSelection } from '@codemirror/state' import { kclManager } from 'lang/KclSinglton' import { SelectionRange } from '@uiw/react-codemirror' 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 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) + .map(([type, count]) => `${count} ${type}${count > 1 ? 's' : ''}`) + .join(', ') +} + +export function canSubmitSelectionArg( + selectionsByType: 'none' | ResolvedSelectionType[], + argument: CommandArgument & { 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) + ) + }) + ) +} diff --git a/src/machines/authMachine.ts b/src/machines/authMachine.ts index cfcffac4b..97b3a8627 100644 --- a/src/machines/authMachine.ts +++ b/src/machines/authMachine.ts @@ -1,7 +1,6 @@ import { createMachine, assign } from 'xstate' import { Models } from '@kittycad/lib' import withBaseURL from '../lib/withBaseURL' -import { CommandBarConfig } from '../lib/commands' import { isTauri } from 'lib/isTauri' import { invoke } from '@tauri-apps/api' import { VITE_KC_API_BASE_URL } from 'env' @@ -40,16 +39,6 @@ export type Events = export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY' const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || '' -export const authCommandBarConfig: CommandBarConfig = { - 'Log in': { - hide: 'both', - }, - 'Log out': { - args: [], - icon: 'arrowLeft', - }, -} - export const authMachine = createMachine( { id: 'Auth', diff --git a/src/machines/commandBarMachine.ts b/src/machines/commandBarMachine.ts new file mode 100644 index 000000000..8d8b29baf --- /dev/null +++ b/src/machines/commandBarMachine.ts @@ -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 & { 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 } + } + | { + type: 'Remove argument' + data: { [x: string]: CommandArgumentWithName } + } + | { + type: 'Edit argument' + data: { arg: CommandArgumentWithName } + } + | { + 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 } + } + | { + type: 'Find and select command' + data: { name: string; ownerMachine: string } + } + | { + type: 'Change current argument' + data: { arg: CommandArgumentWithName } + }, + }, + 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) +} diff --git a/src/machines/commandBarMachine.typegen.ts b/src/machines/commandBarMachine.typegen.ts new file mode 100644 index 000000000..214568bd0 --- /dev/null +++ b/src/machines/commandBarMachine.typegen.ts @@ -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 +} diff --git a/src/machines/homeMachine.ts b/src/machines/homeMachine.ts index 23477245a..386b0b0b0 100644 --- a/src/machines/homeMachine.ts +++ b/src/machines/homeMachine.ts @@ -1,56 +1,6 @@ import { assign, createMachine } from 'xstate' import { ProjectWithEntryPointMetadata } from '../Router' -import { CommandBarConfig } from '../lib/commands' - -export const homeCommandConfig: CommandBarConfig = { - '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', - }, - ], - }, -} +import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig' export const homeMachine = createMachine( { @@ -188,10 +138,10 @@ export const homeMachine = createMachine( schema: { events: {} as - | { type: 'Open project'; data: { name: string } } - | { type: 'Rename project'; data: { oldName: string; newName: string } } - | { type: 'Create project'; data: { name: string } } - | { type: 'Delete project'; data: { name: string } } + | { type: 'Open project'; data: HomeCommandSchema['Open project'] } + | { type: 'Rename project'; data: HomeCommandSchema['Rename project'] } + | { type: 'Create project'; data: HomeCommandSchema['Create project'] } + | { type: 'Delete project'; data: HomeCommandSchema['Delete project'] } | { type: 'navigate'; data: { name: string } } | { type: 'done.invoke.read-projects' diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index e47969654..309b7214e 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -47,6 +47,7 @@ import { absDistanceInfo, applyConstraintAxisAlign, } from 'components/Toolbar/SetAbsDistance' +import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig' export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY' @@ -128,12 +129,12 @@ export type ModelingMachineEvent = | { type: 'Constrain equal length' } | { type: 'Constrain parallel' } | { type: 'Constrain remove constraints' } - | { type: 'extrude intent' } | { type: 'Re-execute' } + | { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] } export const modelingMachine = createMachine( { - /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogDMAVgDsAOmEiAjLMGCAHIIAsAJlHKANCACeiaQDZBEsctGrVATmmiaNQQF8H2tBhz5x2CJjAEAymDsAASwWGDknNy0DEggLGyRPLECCMrCCuL20qrCRpYagvnaegjZBtLiBqLploIGCtZVyk4u6Fh4UJ7evgAicGERQSx47NG88RxcSaApdcKSNKIKi8ppwsrLwsX60oriqgaWBgbKImpWqi0gru0eXj4EfaE+g5AwY7ETibyzx5lLu1sygM61ERV0+ho0mU4gURlOwihlis2SuN3cnXuvX6L2CJD42FgH2YrEm3B+QnM4mqdhoyPUILqBm2CAaFTS5houQUCMczmubQxXQeAVxQ1QI2JcVJ32SiGUXPEhjBtWklgaokMzIhqVUNHEG0WgjVqkEUN2aMFHWFvlF4WCbzAUq+UwpqRoGQ2pwacPW4JKJxhgYU0kWu3Wymklrc1qx-gGeIJRPo4xlrrlqUEqkqdMKOSRNEOLPWGTVhkswlU0O5fNaMbu3XjYoAZiRKM60+SMwqMgY7Go6mbalCWaIjLCjtJ0n36tUFNHbpjGwBRXDsMAAJxCAGtAuQABYdhLpmY7SPiSzAyOXwGFUQs01BtXVEEV9VSZr89Gxldrzc7vdD2kGISWPLtTwQepBGpEN8lDGhVBDLYdTUfVZzvYFQ3MS4vytBsHieBMglbdsU0+Ttpn4Sk0KzBR1EKdING1EozQqYxajyNQ6MOBchTjO1BhITBMCPMlKJSSNoIsEEllQrlhGQko9RhZEDFUL1vWEUMcLrRcbUeHF7SCISRLI0CxLdRR2ROJQwQQ2RmMQENJD1OxjR5aE+1rAV6yXB4wD4dgNwAVwwIIRjANdRNlCDlGRJU6kfapK0vdYWSnBQJEsfJMtODYrM-XS+MbAKgtCsBwr-KLgNTMDxPlawJ0DKtXNkLl0o2NjNULPMPQ2HSfL0vxd3YA9iDIShMGGwDopPKjSg0fUaDURFFoVbJ7x1S9oOSmQcl2CtRF461ptG-dxFOg8AElGwE4JhiiszpTqt1pAaSwLxscwpFDIxFPlZZqXWpR1TMUNCsGoVLvO6GbpFIjQigABbSLRiel1wPmt7ss+0RvuNHq0p1FQDEqOKDgaBS9SOY6PGhi6RuuxtCLFB60ZA56LIzN7cgvSMbDMBULAfN7qTsaSCcLJZ51w3yGcA+Wzrh7FniMxGUcejmMfq0oeQyei1SqKxrH+1lScQk5rDpNqpGEWnOnp2GVwAR2C7AmCCdhUFQUytYoyyuTY6dtMLTzBFHPslUQuL4KWZao1lobGZh5PlYIZdXfdoIkdQAA3CqvZ92bMdmD02Ok9RlsjKQWQrGFVnKdZ7BsnJ7cVg92-3NOAmCVWIimYudbVMwczx4wtXqRyEDHfVsgURFDizMRZDbx3U8bZASF3EIwGR1GgnzjdOHIITB9epClXWP7HwUW-NpKWRzzUQxcg9H67cTqHk87tPN+39X977lQBubAAAvbg7BT7o39tzMEGR0j2FUkYNSFZ0qIMqFUeQVYlDTiOp-E638nYPCINwWAQUSB4CCEAkB4C1xCSCBAQkkCKBOmgS9WB1QcwHGNLtEcOoqxiAvLOdSV4cEyyKgQhWRDfAkNwGQjcFDcAH03MfehjCyETVYX7dhEEbCWH1FpHhIhyjAinnqEQBpjA9iWFYW+4jIaSLOj-Rssj5GKKCAAQQAEJ+CCAADTPtzZYpNspLDqPPZUigHyalJplWkCpjSHCqKvQh69iGkPIZQ7xviACagTdHBNhAhDSdR7B0QfPPGimUXzQmBHgiRdNUmATTq4zJSiyBQB8PkrGcIVKrCnMtd8fZ76IGkh9Gwb0oRjhyKaFJUi0kyIyQoyhPh8DsEPGwrmBT0gXhyKqLkBx8jh34YoNC3FryIlyHMpx0jiBLPcUwTcjzcCMPIMFTAJAtzqOYaRbRWysY0kyMII4KgswWAHA+MQpMjDGirKIVimprkd1ua05ZSjqFgIgSZHQxkcBQFwN0lINg4SwjNOoRCRwKam2yKsA0AsbHLDpHFJFKdmkuPuZQw+qjhI4qEtgfFhL9DLGggqUxtgjZiHSiIHaj8LgiC0nCFlzj0lyLaSEXAJAPZe38YK3Wxh9jyqOCleCCh0p431CocwcF0gJPsd+Rp8y2UqrcZQ2AGqtWoCCHkzZMUen6uWiGA5mUjWWHSpleYVR7C5GBNlGm+CHU3IWXc1VaKghgFdvQ1ZUB1m6rejYA0voJXlCnFoHU1Z4FaWHH2K29glUoo5UopgnyTLUB9XNIllSLyXmvLU9Qoay3HHmCWsEORTimjjQ0h2TSlbspTe4jcYAc75yCOQBt7Bkx-N9USvG2YbXqUUCIJEpqy0KXGYoBSdJ57lDxnWpNAAlMAABaAK4RgrrlzdUpU9hNR4yvTYVQLJyj6kwj6KsHEwRKoADJ4AqgAFW9pgdOmcPZLoLgh3V8hb6SBsQpNU6RPKjiUF+hexpT14whvaqdCtoO4DgwhggD7n18Ffe+ttJchDLTQhczK89xXFg+nSD0IJKZJMjFBmDQR4M+3EAABQlGuTxEAMAQAIB4pT4pJRsZ1maYE4hFAVnnolNQqxixQiVCcKE4a4rzykOJ2jkmEMXV3hrYIanlOqfU2zDDiEKiHEyl5M0hZ6gsgHAaA46kQTlBDNJOzdHpMBD3gptzkA7lIyYD4dcQR3BaNqv82YdTYSrXhWkBoOQQtYbehoSMQIrDMvjVRpxNG4uYCc4l1zSmUsZzdh7WjAB3T26GtOWUrvsN6k8EJHCOCydySpsrVzkJTCjeEGsdyaw56TV1cAcAILq9Sap-jys1NGqcgH7CZHqGITUSE7AGFi+tlrm3ttUBquRHR81x0CeRGaQoSgQSlpKJWUmhQuTzysIsRYt36ud3EGtqTLWABynq5MjFgB5iAGnNa5a3aMscH1rDqDBClOkAGdSVhhNCLk+j9GkcykqtAy64fxl7gmAeQ2MxMiyrUMl8JgWLFHGkSxfZCyqkyioOnedmsMafS+t5rHN3tqEDyaCNh5D6LhMKqeV2C0WzsJWCwBxxcM8c-TiqeAnts4giIfR1IwXlApVCf7kI-hJMLAk6EhQlty3pib+74gfdm-YDtl75lscIBUPPBKw9zD1GsMidK2QPq5EKIUaEdcrCG+a37iXlUnuqHl+xsP6QMgNDUsYdSzd7Dx+t5Pa7IZdgWih-TDxvWKHBAxbQyBmAGFMM0ZVZsqACAQG4GATwuBc6oF3OIGA7BH3t6xZgR9eB++7e4pkKWiSvLThFs5McfYwYaHyBOhxCaO7N9b1Q4BmK6Fd++b3pfA-NwbmAeIdLJB2D943EjKfgRZ+X470JRfXAZfC3d7VfJoeFLMSLTUR3BAKsU4C8KQOiBEPWU4JVM-DgZRI+bAE+G-HvFhPvAfIfWjUfcfSfafR9LlbAgA+-FfeQSodUXYCwcNDQB8ZaeYaWMwcoRQSmNAlvDAygnA7vDRfA+-AgR-Z-V-d-YBL-cggQ6goA1AWgmEJkMuOkNSARVg5af4W+bhS8c0D+SdaHdA1zHxfxAgwfYfEgifEfcgkgAAI1gEfT4EAOAPzx1kQj2DBSzGq0KHKGiXUiBTqFFQPQMOPxW3OmMM8VML8XMPEI3Bfw+SkM-2-xn3sMcOcJoJAJSA8IyDmz1Fw2J37SUiqH1iuzxjajMAGkoyML4JMNyXMKIJHzwFIJsJ-zSMfR0BcMUKyNGVsk+mGVknyAUmPSUjog+kjDhRK2qXqTCJqPP2yS9ViI3Cf3iMkI-xkLaIcI6K6NoPgWDWBTG3MHkAfGRHmHVEQjhByFsArF4PPw6R8AaMsOaOsJSMfXuKfUyLcLdByNhBMWXmFVogfBmyV2+kMCnGsFuIwPeKWJWISLf3WNePeJ2J6NgJ5H1AOFWERHjnohgIsDBEyAODBBWjUlCOqKb1qKy0imzX3EeOIOeLIJ-yzXWWRK+IzA8JCWRBiSZRtQfAbgvDHHWF2CaDOEhOCCZJpNELiLhKSI2Jn3FJZKxwV1RP1WOELEZTsVtl5K0LEGXnD1T0sFFKGCeUileXeU+SEJ+VNwUIsLpLHxePIMeQ3GeVNI+Q3EfVvxYQVNezyxxx5EkDG2BHWDNBOEhUyn2B5EOELAOQ2BmLJO-kiMdOdOwLNK+TwMoBhIkMSIRIdONJeWTNdPdLTI+IUN21kn9N6T+2DNxKkA+mjkjL7DJ1p0b2TiCFwE9RIltCIgwFbHeXug+Vo1zXUkTwrDemvB5GBGORKGrk+ihHCS0kQiOLbhIFqI6B3lxCmCZzXPtFZ1ZN0VDmpH0THAjJOFkGpWjWpD3x9H80vDtWW3EAPHCG3FXL7kSB2xROhHzRyEDDEAOAQhGVKFyGzA0nNFKTEyhwfPICfPwC3P7m4CD13KxkjCDFhVsAPwjKKP0DSGgi4LSGylNA9AQicH5DbIwHgFiEo0VILyHEkBkDkAPTUBYJ1EfUMGzCpwT0RGMzVDbixEop1mBF3WByqEOBiVBSlXHATzsGsFvnd28jjMAl4tekOGzGWnzDWihHUBZEfU4QVGsD2l-UUE9yTkdRnR8AUu5i+wNVVN2HkF8P-ORHRIUhsqsiOMMq-mowkzhzMogmkhMC+2BWqDBXUiBLQjyKNAKHhVJLvPplh0cx6GHy8vmmMDNEkEwR80swZGLFND030SA2BVsVkqiu-hiuk2RySw6wgASuyOp2yvhVyhspOH-LJzJkp1vhBHWAsDuzh1axc0U2U0qqEF-IPOBQjGOCHEyqDAVGBVaoUnOE6sc0e3YH6tgNI0kD8qGMCpgIVX2HHVBVWCHEh0MOio8sc0RyCFKvXSWoxNJlOHKC+znjhHGuat52pwyljMKoVh908u9ND3kDxl2WjSYnhT8K2isD5hnEjAOQOEiq92-k+uN2zwDyWpUByH+oarqTqGkDQROANGBGtlkBpSPzkqcThukx916w4CATfSCAXSYxYzACRuqBUnLzt0q0rEnKd3rlxpBDnnFVvJho+uzy6rJoprTWY1l3pu+qVOMF5lWlvi+1OBsHj3UD0wOiFmBEDQKv5uJsFpOs9VQwZsETpCmqrWhBJwfjek9HCTHAOjm0NLn2vwtLvwUMuuBzX12Bu3kCnBO34RLX2HSDJyaFtubIVkiLkNwOEPTPv0uoSWpHMTOFyCNn-L1FkAvCsS5EqPUDAsOvjIpIWJiKjsloLz2wmpUHnKQPKX4UXj01sChHFmJUMENIWJyQIOjuhEyDvikHMBK0Vv4TogkF2B40MDYK0jeq1tPwpOhILpDyVL237up0ZGxl+gfBG3hVQsnHkDMENPFJbsLvcP5iEW4LHnyA2CBPVDFkrAcgiTBAOtmPJPP0TJNPzPNI9Mjudt3u+OMAyGMSUHsByBUGBEhXgJhQ9ESS9oNODrOlbPbLbAlunoLyOAyAiss1jwXjNvlEWGyrRMnG0gTkMOXNb2fJZ0xm1jdFyiEVsHnmRCQiqHSkYi7SWAKHyhDFcutAgqgqgBgt9RIe5n1xSusw2h5FqAwoAroiVC9ppAszoiIocCAA */ + /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogDMAVgDsAOmEiAjLMGCAHIIAsAJlHKANCACeiaQDZBEsctGrVATmmiaNQQF8H2tBhz4CAZTDsABLCwwck5uWgYkEBY2EJ4IgQRpZWFpcQVLe1V7SwVbYUttPQRBRMtxZUtlGkt0wVVpBTMnF3QsPChxbAhMMC8ff0Dgrlww3iiOId54pIVxe2lVYSNLDUFlgv1VAxSDUWE0wQM0ww0mkFdW-A6unoAROAG-Fjx2EYixmMmhA2FJGlEFP7KJLCZQA4TrBLSRTiTaWAwGZQiNRWVSnc7udqdboEO4BbrBXyQGCvZiscbcT5FeGzf5Q2zKb5mNa6fQ0RKpIyI4RsyxWeZoloYq7Y3EPXwkPjYWAkyJkj5xITmcS7OxVCyib4HAwQo5lMSZRYNESOZxnQVtYU9bz4x6oZ4y94TBUISo-Y7VYrZZaGbUsl2ZMoA0Se1SCNlQgVuC1Yq1iolgB1yp2gKY0GagxFpBSM5mFBHKcT5hTSP5QkHKaSRi6Y669G3iyXS+ijJMU52I1SFqqrBY8mgGfJ+kEzaTWAfCOpJQ4m5pRy4xutBPwAMxIlET0WT-EQlRmBjsagOYcsYekEI1glScOke33h12CirQoXAFFcOwwAAnfwAax85AACw3clYhTfREhSCoEVHCtjFWUQIVDAstmWRY8jyBQpGUJ9o1rN8P2-WA-3YQCqGkcJSU3NswIQadlWLb07FUYtwT9NQaELHJ4IZEtzFRU10VwkV7nrVd12bN5W1A7cilsQtagUdRVj2DRfUKU9xGME8ljURSBxw+da2tJdxUwTBgPlGjYJhTYxAaTIaGEJzEJoAteQMVQM0zZIaH42dq0tHERJMkgzIsrd4kUFIGVBLTfNkNTEGLSQHLmBpEn3GczTnGtsRfPh2E-ABXDBwuomT0s0nJ0kyWxjCkCFEiSGzbGLURDHmLYDPaTxiMA4gyEoTBev-ICJMokDKRsSpZjUbkNFctl1AhCpL12KRkgWKFLDEbrxBGkiAP2vqAIASSMsUnnfMrpPiepqnEaxRHMKQSyMVjClBDiFvqVYGna1y9oOwDjtG87sWMgkAigABbMBrvG2UqNu-Q0lKJ6XuKfskQhFQDELcpNjSJzMjhIGTtBw7wduYKCSul5Ecdcq7sw-GKlkDQFosRD6mVJjNix-t-kfATzUuYGjol6mgrxEzobhhGKKRybnXqUEYWDUcdisawPqS-HmIRawqlkRyRHJ0bKcA6WXwARyK7AmF8dhUFQczGakykw2STTbxLTItgZQRz33cR5gaY2-gBCsLcOq2zrw+3Hd8GHUAANzAZ3XfdpWmZRoo0xSWpNnUVyKwav0doLIEthBewEUU4RY5BqWLr8WXBlCD3kamioJH3Z7jB9Q5EoQDUOPD7kB1qMRZGbyWTul5ASD-fpYfhvwM8-ThyFCm6ppYsOQXepCFDPhC-VkCsyjqb590w4om9FnL4-jpeV8z+WN98ADUE-bAABe3B2B727irGiNhsiSABD2eEBwrB6wSFkQsOx5B1CULeUQ88361iINwWAhUSB4B-n-QBwDQq+AgFKEBFAExgMsjJGwuwuwCxsE5U8PMxCPXvJ5GKGCRb+SFBLHB2I8G4AIZ+IhuBfBbx3hQqhBDBp0Nzp7VWoh0iSChDYEQgd4QuREGUYwu5-hWDPgI7KAVhGt1EfgwhxCACCAAhTwvgAAa+9VYAjZssJQhxkioIUIhdq+MciqkqMUAcOxsHWJ6GIiRUjfBOJcQATQ8RArxqRfJeQOPYRSiFMIcSMDkXYgcGRYOfpYimMTiC2MkcQsgUBuhpMYdmNyQIbyuWyIsP4iFYRh3agCGwHkJxZUEuLKpi9cG1ISd0fA7AxoqJ7p4vYj0FgnhLBOAcwZEKKEKXpCsO1HIGGiZMmx4i7HSKYF+K5uAqHkCKpgEg34FE0PEos8BjCVSzDyJyYunlQyITEPjIwxQ6jBjZO1E5YMpnnLqdI3+-8gHvlCpgHQplsBQGGPQiK+h-ghLDOoZicIiaIPmECMo00TEAiqOUKFVMYXxOIbI7Au8zJotChirF7yGEs3sGUVy8JbDazEI1Y0yor4ohEMkbMdLrYMouf4XAJAnYuzcc0lmxgYRSrhBOSOgTL7PQ4iocw3o9jhPMWMnqEzoVnMZdI2ASqVWoF8Kk7FzNUaatcsWRyzF1HfEHIUeozCdj2EWAyaoZMKlCOtfS21CqwD2wobMqA8z1WoxsIGO+L0tg3i0JfaYkhkgnn7CbOwoyxZWsttUuJCqmBPJRdQN1+d6jckehUA5TV1ABv0PCN0IJ1ELA7KsY5UaLRWNObE6ZxDPxgFThnXw5Ap3vibNynFCRnqdjNZ5RQIgeT6sDU5UoUJMLci6VsZ6sqE7YgAEpgAALRgD4EEIqH400JGKWHew7VnqsxsKoCEWwOI8SzHUbS6jsEABk8CZwACrZwIHbB2Ts52Zxdm7N98gz6SBMU5UcewMrniUJ+qej88jPWwqO8ZlsoO4Fg-B29D6n33NfU2r2i0CZ4ZyJhIVEI8izHSNmPYO1Ikx0o5WuONG6Nu3EAABTtO+RJEAMAQAIPYpTvh6YYYFVVHaD8PJqCBLxtkYcERshyCCLpUhIPQd8HB6T3h14KbU8p1T6nNOsedApbY2Rx7yDsPCfdO5Qw3303feo8xNjWdo7Z7O+0wCOb8M5yANSYZMG6B+Xw7hlEtiWTRPGEgCmD2mLyRBoIRw5HafSKwtKxOv0kzF+z8WFaJaU8lxDydaMAHcs7oY83l0uMJ6gj18nCOEuNeajnZskY9VcotScwOIU6uAOAEDfZ5UcNIpXtTDTeADfL+zcYGR0-sc2GsLaWytsiq73UIFDA9NUYY-oHBBLxzYml0h7F1X8P4I7BFjopvVuzC2AByzq5PPFgK5iAGn5MM2u-ndQA5HrzGejtKwapXsFkSI5dINQbw5GwWgedQPejtweEMDDgq21+YFkkdIF9CgaB+Iifc-Z1lcQo39qjccifzYIAxx9z6WPw69kafp8gBOHHrueXmoJDZ2AnBYSLtXhG87O+INXeBLt9ZkiIDRwYLBbCJWyPNgb+zArhP2cJiRVic4sdGy2augca-TpnLX7BVvkRyx8yKoI3SejMETawvJGrzFKIsVYqwSggisIT136vNfLY91QVQIvPN7BmGkDyxhPL13sKHjRI8WKFy0eWl+wj7GdaIX4BFZDkWYEodQpRvg8DLlQAQCA3AwAdFwGnVAf5xAwHYHe2vSKQGYDva31Aa29KzCFhEzKt4ebJQ1PuMw01liRq5+JkGlfq8kMReQhvLzm9T4IF+T8f9xBpZIOwNvn4YaD58CP0hY-QqT9wG3mfb2dhmDQVmxIHmRER6KQRSLkBoI0bBPfDgGRL8ORY-JvWhFvT-dvTvWjHvPvAfIfO9ZlVlD-L-HXeIZieQQsbIKECwczDQFyZqYWMwLYRQYmKAqvGA3A+RRAygZAtvc-T8S-T8a-R5O-P+R-bA1gifKfGfFQTSeEQuKoDyOoRBByAsKOPSW3cMJ+bfV+aAxLZxNxTg1ArvDA-vbvbAkgAAI1gDvT4HwOn0IMQGIJmGLlqArGDGvCCU8m+QOEqEzHNhVwpi0MSR0NcT0O4N4P4Nv3v2EOfzMIsKsPENsNuyUBmGqHYlwzVG7Vux2BmHUCDXahLEDyYP3ySRdWCLQO7zwEwOMKiPMLvR0GsIkIkDHD+H+HKHWkCwSKsApTBWmGKXKQ0Ir2YO0JSWCIvyvxv0EIfyf2H2iJqLqPiPsOgXUTyCG3MHkEQhK0ekUjPmGVsB2gKJgIaW6BKIMPKKMMmLvQOPvTiLTxonmOzESFnhgQUh2V5iNBeg6iWL2L8AuOGJ4NGIEIiLOIuNmOuJkmYhmk2CBG5FchRxUF6XUVmE2HUTmmGU+My3hhTQAiOPQJOKwOf2TXmWBO9x5TsN8TbWyP3AqDNUQhrkeg1BBChF-yRFRPxMxLPxGL4LGIBOwJZMJMkly1BKUH7jvGpTMQ2mpNcmw1nhUGLHKEsFRKuU-BuTuQeSeUb0USQLP1KMMNxOHwVKVJZRVM-DvRP1oV5ImmJNu2aM0VaUZDDAREBRyBhAaAHBLQnFBF6Pt3+0tn8L1PhmVMeWeXYLdxQJCL+PCKELON9NuQNIDONKDLNOVgtPUAaGtIRFtKt0BVWCdPDVdM8gJ18NGl8FwGdTEljHrAwFXAeUeEeVozfTJXDx2jVlHAaCDghHLmRzZAOEwnDhWKcFNGLIwHgAiEtSJLXWPEkBkDkB3TUCoL9DvUME7EUCBHKB9WDHHD2hjFHJuwZE3VWCFi2UMBUGDkviPDDnRxNjPhtzL0qVGi3ObQHE7Fcl7AWkqBRwhDvWYX7BvFvnsAzH0gLLjhiTvN7mC1DGkKhHkGHQZ0QF5A4gWCkEgraiQlOyB2AudENxMF5D112D+VNzsLDFSFX06mzz2B8L6IBxs2dxuC7zQry3BUkFQWYhLHqER1e0vD3MAzyFMWvIdwk0oti3Byc1awgFotBJqHe2DAq0goRGgoQDdIJhxy2KcmRBQtiwc2a0U2U1Esik2G+h2irgZCMCqExwUowkExjz8k9O5xBkB1iwu3YG0vwpvEkCwrIykCVzwrkt5gsEj0RCBGPF+ysp3yOlsuk1B18EEvYCHPNLXQhPxhZ1HFDHDmzBMtMzyCqDmBUjj2J2zkcqKGMAaIWBkrKQODPErgIqsEjyQhbUrAApBid1i0Tw4DypUAWFWTDVUjXLKrNwRDKAZBNlkHrMCstVfgaukzV06w4F-hfV8BnUYyFzABat2DclzyNyDRGUaitz6rvE2lCUOGyvmxd3nUmvmUJCYxfUWr5J90VEWF+GFiwsRBsFD3UE0m2kqE8kOBLB4q9J53j2d3CpQyWq4SqAwmLUDn-UvnqHTC7I1G2mSNRNHyPzVNeWDLbzyruw4jsChH83kBvF2z9HkILAWEwg+o0HhrqqOn8NEORtPxQPRvCWVEyHkBBEWG1lksyFkEeiMUckD3UFE3Iu9IGICJcSCKn3pvZARBUF2rzIhsKFhHYtsDZCYhsGLGGorU0KFqKOST0PFqUPPikHMGmCeoJsUgkGPVpDX1ww9JGv6P32+LFquqTOcLDhqE1HujejhO+mMD+GvHkEaApvEH8JZJ1sdtiorEgiKWKW4jaLuxmFyDqEMG7L9XlOuT9JjNVJNI4Idpipu1DH+F9mnAyCwgZEBWAJBTTAiTxrlIpqLJLLXEupzvzjhDjt3XdsOQ8jbL+HewaFkNNjqD7IcCAA */ id: 'Modeling', tsTypes: {} as import('./modelingMachine.typegen').Typegen0, @@ -279,18 +280,12 @@ export const modelingMachine = createMachine( cond: 'Selection is not empty', }, - 'extrude intent': [ - { - target: 'awaiting selection', - cond: 'has no selection', - }, - { - target: 'idle', - cond: 'has valid extrude selection', - internal: true, - actions: 'AST extrude', - }, - ], + Extrude: { + target: 'idle', + cond: 'has valid extrude selection', + actions: ['AST extrude'], + internal: true, + }, }, }, @@ -690,29 +685,6 @@ export const modelingMachine = createMachine( }, }, }, - - 'awaiting selection': { - on: { - 'Set selection': { - target: 'checking selection', - actions: 'Set selection', - }, - }, - }, - - 'checking selection': { - always: [ - { - target: 'idle', - cond: 'has valid extrude selection', - actions: 'AST extrude', - }, - { - target: 'idle', - actions: 'toast extrude failed', - }, - ], - }, }, initial: 'idle', @@ -728,6 +700,12 @@ export const modelingMachine = createMachine( 'reset sketch metadata', ], }, + + 'Set selection': { + target: '#Modeling', + internal: true, + actions: 'Set selection', + }, }, }, { @@ -771,36 +749,19 @@ export const modelingMachine = createMachine( equalAngleInfo({ selectionRanges }).enabled, 'Can constrain remove constraints': ({ selectionRanges }) => removeConstrainingValuesInfo({ selectionRanges }).enabled, - 'has no selection': ({ selectionRanges }) => { - if (selectionRanges?.codeBasedSelections?.length < 1) return true - const selection = selectionRanges?.codeBasedSelections?.[0] || {} + // 'has no selection': ({ selectionRanges }) => { + // if (selectionRanges?.codeBasedSelections?.length < 1) return true + // const selection = selectionRanges?.codeBasedSelections?.[0] || {} - return ( - selectionRanges.codeBasedSelections.length === 1 && - !hasExtrudeSketchGroup({ - ast: kclManager.ast, - programMemory: kclManager.programMemory, - selection, - }) - ) - }, - 'has valid extrude selection': ({ selectionRanges }) => { - if (selectionRanges.codeBasedSelections.length !== 1) return false - const isSketchPipe = isCursorInSketchCommandRange( - engineCommandManager.artifactMap, - selectionRanges - ) - const common = { - selection: selectionRanges.codeBasedSelections[0], - ast: kclManager.ast, - } - const hasClose = doesPipeHaveCallExp({ calleeName: 'close', ...common }) - const hasExtrude = doesPipeHaveCallExp({ - calleeName: 'extrude', - ...common, - }) - return !!isSketchPipe && hasClose && !hasExtrude - }, + // return ( + // selectionRanges.codeBasedSelections.length === 1 && + // !hasExtrudeSketchGroup({ + // ast: kclManager.ast, + // programMemory: kclManager.programMemory, + // selection, + // }) + // ) + // }, 'can move': ({ selectionRanges }) => // todo check all cursors are also in the right sketch selectionRanges.codeBasedSelections.every( @@ -1010,14 +971,18 @@ export const modelingMachine = createMachine( }) kclManager.updateAst(modifiedAst, true) }, - 'AST extrude': ({ selectionRanges }) => { + 'AST extrude': (_, event) => { + if (!event.data) return + const { selection, distance } = event.data const pathToNode = getNodePathFromSourceRange( kclManager.ast, - selectionRanges.codeBasedSelections[0].range + selection.codeBasedSelections[0].range ) const { modifiedAst, pathToExtrudeArg } = extrudeSketch( kclManager.ast, - pathToNode + pathToNode, + true, + distance ) // TODO not handling focusPath correctly I think kclManager.updateAst(modifiedAst, true, { diff --git a/src/machines/modelingMachine.typegen.ts b/src/machines/modelingMachine.typegen.ts index cebb3b8b3..9fe1c28e2 100644 --- a/src/machines/modelingMachine.typegen.ts +++ b/src/machines/modelingMachine.typegen.ts @@ -32,14 +32,14 @@ "Get vertical info": "done.invoke.get-vertical-info"; }; 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; - 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"; }; eventsCausingActions: { "AST add line segment": "Add point"; -"AST extrude": "" | "extrude intent"; +"AST extrude": "Extrude"; "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 other selection": "Select axis"; @@ -63,7 +63,7 @@ "edit mode enter": "Enter sketch" | "Re-execute"; "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"; -"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"; "set default plane id": "Select default plane"; "set sketch metadata": "Enter sketch"; @@ -72,9 +72,8 @@ "set tool line": "Equip tool"; "set tool move": "Equip move tool" | "Re-execute" | "Set selection"; "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"; -"toast extrude failed": ""; }; eventsCausingDelays: { @@ -105,8 +104,7 @@ "Selection is one face": "Enter sketch"; "can move": ""; "can move with execute": ""; -"has no selection": "extrude intent"; -"has valid extrude selection": "" | "extrude intent"; +"has valid extrude selection": "Extrude"; "is editing existing sketch": ""; }; eventsCausingServices: { @@ -118,7 +116,7 @@ "Get perpendicular distance info": "Constrain perpendicular 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"; }; }; tags: never; } diff --git a/src/machines/settingsMachine.ts b/src/machines/settingsMachine.ts index cb688efb6..08649aadc 100644 --- a/src/machines/settingsMachine.ts +++ b/src/machines/settingsMachine.ts @@ -1,7 +1,6 @@ import { assign, createMachine } from 'xstate' -import { CommandBarConfig } from '../lib/commands' import { Themes, getSystemTheme, setThemeClass } from '../lib/theme' -import { CameraSystem, cameraSystems } from 'lib/cameraControls' +import { CameraSystem } from 'lib/cameraControls' import { Models } from '@kittycad/lib' 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 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( { /** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIAVACzAFswBtABgF1FQAHAe1iYsfbNxAAPRAA42+AEwB2KQFYAzGznKAnADZli1QBoQAT2kBGKfm37lOned3nzqgL6vjlLLgJFSFdCoAETAAMwBDAFdiagAFACc+ACswAGNqADlw5nYuJBB+QWFRfMkEABY5fDYa2rra83LjMwQdLWV8BXLyuxlVLU1Ld090bzxCEnJKYLComODMeLS0PniTXLFCoUwRMTK7fC1zNql7NgUjtnKjU0RlBSqpLVUVPVUda60tYZAvHHG-FNAgBVbBCKjIEywNBMDb5LbFPaILqdfRSORsS4qcxXZqIHqyK6qY4XOxsGTKco-P4+Cb+aYAIXCsDAVFBQjhvAE212pWkskUKnUml0+gUNxaqkU+EccnKF1UCnucnMcjcHl+o3+vkmZBofCgUFIMwARpEoFRYuFsGBiJyCtzEXzWrJlGxlKdVFKvfY1XiEBjyvhVOVzBdzu13pYFNStbTAQFqAB5bAmvjheIQf4QtDhNCRWD2hE7EqgfayHTEh7lHQNSxSf1Scz4cpHHFyFVujTKczuDXYPgQOBiGl4TaOktIhAAWg6X3nC4Xp39050sYw2rpYHHRUnztVhPJqmUlIGbEriv9WhrLZ6uibHcqUr7riAA */ diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index 03b232572..3985956bb 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -17,7 +17,7 @@ import { Link } from 'react-router-dom' import { ProjectWithEntryPointMetadata, HomeLoaderData } from '../Router' import Loading from '../components/Loading' import { useMachine } from '@xstate/react' -import { homeCommandConfig, homeMachine } from '../machines/homeMachine' +import { homeMachine } from '../machines/homeMachine' import { ContextFrom, EventFrom } from 'xstate' import { paths } from '../Router' import { @@ -30,11 +30,12 @@ import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useCommandsContext } from 'hooks/useCommandsContext' import { DEFAULT_PROJECT_NAME } from 'machines/settingsMachine' import { sep } from '@tauri-apps/api/path' +import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig' // 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. const Home = () => { - const { commands, setCommandBarOpen } = useCommandsContext() + const { commandBarSend } = useCommandsContext() const navigate = useNavigate() const { projects: loadedProjects } = useLoaderData() as HomeLoaderData const { @@ -56,7 +57,7 @@ const Home = () => { event: EventFrom ) => { if (event.data && 'name' in event.data) { - setCommandBarOpen(false) + commandBarSend({ type: 'Close' }) navigate( `${paths.FILE}/${encodeURIComponent( context.defaultDirectory + sep + event.data.name @@ -143,12 +144,11 @@ const Home = () => { const isSortByModified = sort?.includes('modified') || !sort || sort === null - useStateMachineCommands({ - commands, + useStateMachineCommands({ + machineId: 'home', send, state, - commandBarConfig: homeCommandConfig, - owner: 'home', + commandBarConfig: homeCommandBarConfig, }) useEffect(() => { diff --git a/src/routes/Onboarding/CmdK.tsx b/src/routes/Onboarding/CmdK.tsx index 40a218895..945dfec5b 100644 --- a/src/routes/Onboarding/CmdK.tsx +++ b/src/routes/Onboarding/CmdK.tsx @@ -1,7 +1,6 @@ +import usePlatform from 'hooks/usePlatform' import { OnboardingButtons, onboardingPaths, useDismiss, useNextClick } from '.' import { useStore } from '../../useStore' -import { Platform, platform } from '@tauri-apps/api/os' -import { useEffect, useState } from 'react' export default function CmdK() { const { buttonDownInStream } = useStore((s) => ({ @@ -9,14 +8,7 @@ export default function CmdK() { })) const dismiss = useDismiss() const next = useNextClick(onboardingPaths.USER_MENU) - const [platformName, setPlatformName] = useState('') - - useEffect(() => { - async function getPlatform() { - setPlatformName(await platform()) - } - void getPlatform() - }, [setPlatformName]) + const platformName = usePlatform() return (

@@ -29,13 +21,13 @@ export default function CmdK() {

Command Bar

Press{' '} - {platformName === 'win32' ? ( + {platformName === 'darwin' ? ( <> - Win + / + + K ) : ( <> - OS + K + Ctrl + / )}{' '} to open the command bar. Try changing your theme with it. diff --git a/src/routes/SignIn.tsx b/src/routes/SignIn.tsx index 1a0ad50c3..53b98d2cb 100644 --- a/src/routes/SignIn.tsx +++ b/src/routes/SignIn.tsx @@ -1,4 +1,3 @@ -import { faSignInAlt } from '@fortawesome/free-solid-svg-icons' import { ActionButton } from '../components/ActionButton' import { isTauri } from '../lib/isTauri' import { invoke } from '@tauri-apps/api/tauri' @@ -65,7 +64,7 @@ const SignIn = () => { @@ -80,7 +79,7 @@ const SignIn = () => { typeof window !== 'undefined' && window.location.href.replace('signin', '') )}`} - icon={{ icon: faSignInAlt }} + icon={{ icon: 'arrowRight' }} className="w-fit mt-4" > Sign in diff --git a/yarn.lock b/yarn.lock index 724ca4457..d6ddf16a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8497,9 +8497,9 @@ ws@^8.8.0: integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== xstate@^4.38.2: - version "4.38.2" - resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.2.tgz#1b74544fc9c8c6c713ba77f81c6017e65aa89804" - integrity sha512-Fba/DwEPDLneHT3tbJ9F3zafbQXszOlyCJyQqqdzmtlY/cwE2th462KK48yaANf98jHlP6lJvxfNtN0LFKXPQg== + version "4.38.3" + resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.3.tgz#4e15e7ad3aa0ca1eea2010548a5379966d8f1075" + integrity sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw== y18n@^5.0.5: version "5.0.8"