Merge branch 'main' into pierremtb/issue1217
							
								
								
									
										16
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -107,7 +107,7 @@ jobs:
 | 
			
		||||
          echo "$(jq --arg url 'https://dl.kittycad.io/releases/modeling-app/nightly/last_update.json' \
 | 
			
		||||
            '.tauri.updater.endpoints[]=$url' src-tauri/tauri.release.conf.json --indent 2)" > src-tauri/tauri.release.conf.json
 | 
			
		||||
 | 
			
		||||
      - uses: actions/upload-artifact@v3
 | 
			
		||||
      - uses: actions/upload-artifact@v4
 | 
			
		||||
        if: github.event_name == 'schedule'
 | 
			
		||||
        with:
 | 
			
		||||
          path: |
 | 
			
		||||
@ -128,7 +128,7 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      - uses: actions/download-artifact@v3
 | 
			
		||||
      - uses: actions/download-artifact@v4
 | 
			
		||||
        if: github.event_name == 'schedule'
 | 
			
		||||
 | 
			
		||||
      - name: Copy updated .json files
 | 
			
		||||
@ -243,7 +243,7 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          args: "${{ matrix.os == 'macos-latest' && '--target universal-apple-darwin' || '' }} ${{ env.TAURI_CONF_ARGS }}"
 | 
			
		||||
 | 
			
		||||
      - uses: actions/upload-artifact@v3
 | 
			
		||||
      - uses: actions/upload-artifact@v4
 | 
			
		||||
        env:
 | 
			
		||||
          PREFIX: ${{ matrix.os == 'macos-latest' && 'src-tauri/target/universal-apple-darwin' || 'src-tauri/target' }}
 | 
			
		||||
          MODE: ${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}
 | 
			
		||||
@ -273,7 +273,7 @@ jobs:
 | 
			
		||||
      NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Nightly build, commit {0}', github.sha) }}
 | 
			
		||||
      BUCKET_DIR: ${{ github.event_name == 'release' && 'dl.kittycad.io/releases/modeling-app' || 'dl.kittycad.io/releases/modeling-app/nightly' }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/download-artifact@v3
 | 
			
		||||
      - uses: actions/download-artifact@v4
 | 
			
		||||
 | 
			
		||||
      - name: Generate the update static endpoint
 | 
			
		||||
        run: |
 | 
			
		||||
@ -351,12 +351,12 @@ jobs:
 | 
			
		||||
          credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
 | 
			
		||||
 | 
			
		||||
      - name: Set up Google Cloud SDK
 | 
			
		||||
        uses: google-github-actions/setup-gcloud@v1.1.1
 | 
			
		||||
        uses: google-github-actions/setup-gcloud@v2.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          project_id: kittycadapi
 | 
			
		||||
 | 
			
		||||
      - name: Upload release files to public bucket
 | 
			
		||||
        uses: google-github-actions/upload-cloud-storage@v1.0.3
 | 
			
		||||
        uses: google-github-actions/upload-cloud-storage@v2.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: artifact
 | 
			
		||||
          glob: '*/*itty*'
 | 
			
		||||
@ -364,13 +364,13 @@ jobs:
 | 
			
		||||
          destination: ${{ env.BUCKET_DIR }}/${{ env.VERSION }}
 | 
			
		||||
 | 
			
		||||
      - name: Upload update endpoint to public bucket
 | 
			
		||||
        uses: google-github-actions/upload-cloud-storage@v1.0.3
 | 
			
		||||
        uses: google-github-actions/upload-cloud-storage@v2.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: last_update.json
 | 
			
		||||
          destination: ${{ env.BUCKET_DIR }}
 | 
			
		||||
 | 
			
		||||
      - name: Upload download endpoint to public bucket
 | 
			
		||||
        uses: google-github-actions/upload-cloud-storage@v1.0.3
 | 
			
		||||
        uses: google-github-actions/upload-cloud-storage@v2.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: last_download.json
 | 
			
		||||
          destination: ${{ env.BUCKET_DIR }}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										6
									
								
								.github/workflows/playwright.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -33,7 +33,7 @@ jobs:
 | 
			
		||||
      env:
 | 
			
		||||
        CI: true
 | 
			
		||||
        token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
 | 
			
		||||
    - uses: actions/upload-artifact@v3
 | 
			
		||||
    - uses: actions/upload-artifact@v4
 | 
			
		||||
      if: always()
 | 
			
		||||
      with:
 | 
			
		||||
        name: playwright-report
 | 
			
		||||
@ -68,7 +68,7 @@ jobs:
 | 
			
		||||
      env:
 | 
			
		||||
        CI: true
 | 
			
		||||
        token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
 | 
			
		||||
    - uses: actions/upload-artifact@v3
 | 
			
		||||
    - uses: actions/upload-artifact@v4
 | 
			
		||||
      if: always()
 | 
			
		||||
      with:
 | 
			
		||||
        name: playwright-report
 | 
			
		||||
@ -106,7 +106,7 @@ jobs:
 | 
			
		||||
      env:
 | 
			
		||||
        CI: true
 | 
			
		||||
        token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
 | 
			
		||||
    - uses: actions/upload-artifact@v3
 | 
			
		||||
    - uses: actions/upload-artifact@v4
 | 
			
		||||
      if: always()
 | 
			
		||||
      with:
 | 
			
		||||
        name: playwright-report
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## KittyCAD Modeling App
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -643,7 +643,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 +662,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 +679,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('|> startProfileAt([-6.95, 4.98], %)').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, %)`
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 113 KiB  | 
| 
		 Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB  | 
| 
		 Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 54 KiB  | 
@ -60,6 +60,7 @@
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "vite",
 | 
			
		||||
    "start:prod": "vite preview --port=3000",
 | 
			
		||||
    "serve": "vite serve --port=3000",
 | 
			
		||||
    "build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
 | 
			
		||||
    "build:local": "vite build",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								public/kcma-logomark-outlined.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 11 KiB  | 
@ -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'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						@ -172,11 +172,8 @@ export function App() {
 | 
			
		||||
      <ModalContainer />
 | 
			
		||||
      <Resizable
 | 
			
		||||
        className={
 | 
			
		||||
          'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
 | 
			
		||||
          (buttonDownInStream || onboardingStatus === 'camera'
 | 
			
		||||
            ? ' pointer-events-none '
 | 
			
		||||
            : ' ') +
 | 
			
		||||
          paneOpacity
 | 
			
		||||
          'pointer-events-none h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
 | 
			
		||||
          +paneOpacity
 | 
			
		||||
        }
 | 
			
		||||
        defaultSize={{
 | 
			
		||||
          width: '550px',
 | 
			
		||||
@ -188,10 +185,16 @@ export function App() {
 | 
			
		||||
        maxHeight={'auto'}
 | 
			
		||||
        handleClasses={{
 | 
			
		||||
          right:
 | 
			
		||||
            'hover:bg-liquid-30/40 dark:hover:bg-liquid-10/40 bg-transparent transition-colors duration-100 transition-ease-out delay-100',
 | 
			
		||||
            'hover:bg-chalkboard-10/50 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ' +
 | 
			
		||||
            (buttonDownInStream || onboardingStatus === 'camera'
 | 
			
		||||
              ? 'pointer-events-none '
 | 
			
		||||
              : 'pointer-events-auto'),
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <div id="code-pane" className="h-full flex flex-col justify-between">
 | 
			
		||||
        <div
 | 
			
		||||
          id="code-pane"
 | 
			
		||||
          className="h-full flex flex-col justify-between pointer-events-none"
 | 
			
		||||
        >
 | 
			
		||||
          <CollapsiblePanel
 | 
			
		||||
            title="Code"
 | 
			
		||||
            icon={faCode}
 | 
			
		||||
 | 
			
		||||
@ -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'
 | 
			
		||||
 | 
			
		||||
@ -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<HTMLUListElement>(null)
 | 
			
		||||
  const bgClassName =
 | 
			
		||||
@ -177,10 +179,15 @@ export const Toolbar = () => {
 | 
			
		||||
            <ActionButton
 | 
			
		||||
              Element="button"
 | 
			
		||||
              className="text-sm"
 | 
			
		||||
              onClick={() => 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 = () => {
 | 
			
		||||
      </menu>
 | 
			
		||||
      <ActionButton
 | 
			
		||||
        Element="button"
 | 
			
		||||
        onClick={() => setCommandBarOpen(true)}
 | 
			
		||||
        onClick={() => commandBarSend({ type: 'Open' })}
 | 
			
		||||
        className="rounded-r-full pr-4 self-stretch border-energy-10 hover:border-energy-10 dark:border-chalkboard-80 bg-energy-10/50 hover:bg-energy-10 dark:bg-chalkboard-80 dark:text-energy-10"
 | 
			
		||||
      >
 | 
			
		||||
        ⌘K
 | 
			
		||||
        {platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
 | 
			
		||||
      </ActionButton>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
@ -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 = ({
 | 
			
		||||
        ) : (
 | 
			
		||||
          <ActionButton
 | 
			
		||||
            Element="button"
 | 
			
		||||
            onClick={() => setCommandBarOpen(true)}
 | 
			
		||||
            onClick={() => commandBarSend({ type: 'Open' })}
 | 
			
		||||
            className="text-sm self-center flex items-center w-fit gap-3"
 | 
			
		||||
          >
 | 
			
		||||
            Command Palette{' '}
 | 
			
		||||
            <kbd className="bg-energy-10/50 dark:bg-chalkboard-100 dark:text-energy-10 inline-block px-1 py-0.5 border-energy-10 dark:border-chalkboard-90">
 | 
			
		||||
              ⌘K
 | 
			
		||||
              {platform === 'darwin' ? '⌘K' : 'Ctrl+/'}
 | 
			
		||||
            </kbd>
 | 
			
		||||
          </ActionButton>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
@ -24,13 +24,13 @@ export const PanelHeader = ({
 | 
			
		||||
}: CollapsiblePanelProps) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <summary className={styles.header}>
 | 
			
		||||
      <div className="flex gap-2 align-center items-center flex-1">
 | 
			
		||||
      <div className="flex gap-2 items-center flex-1">
 | 
			
		||||
        <ActionIcon
 | 
			
		||||
          icon={icon}
 | 
			
		||||
          className="p-1"
 | 
			
		||||
          size="sm"
 | 
			
		||||
          bgClassName={
 | 
			
		||||
            'dark:!bg-chalkboard-100 group-open:bg-chalkboard-80 dark:group-open:!bg-chalkboard-90 group-open:border dark:group-open:border-chalkboard-60 rounded-sm ' +
 | 
			
		||||
            'dark:!bg-chalkboard-100 group-open:bg-chalkboard-80 dark:group-open:!bg-chalkboard-90 border border-transparent dark:group-open:border-chalkboard-60 rounded-sm ' +
 | 
			
		||||
            (iconClassNames?.bg || '')
 | 
			
		||||
          }
 | 
			
		||||
          iconClassName={
 | 
			
		||||
@ -60,7 +60,9 @@ export const CollapsiblePanel = ({
 | 
			
		||||
    <details
 | 
			
		||||
      {...props}
 | 
			
		||||
      data-testid={detailsTestId}
 | 
			
		||||
      className={styles.panel + ' group ' + (className || '')}
 | 
			
		||||
      className={
 | 
			
		||||
        styles.panel + ' pointer-events-auto group ' + (className || '')
 | 
			
		||||
      }
 | 
			
		||||
    >
 | 
			
		||||
      <PanelHeader
 | 
			
		||||
        title={title}
 | 
			
		||||
 | 
			
		||||
@ -1,404 +0,0 @@
 | 
			
		||||
import { Combobox, Dialog, Transition } from '@headlessui/react'
 | 
			
		||||
import {
 | 
			
		||||
  Dispatch,
 | 
			
		||||
  Fragment,
 | 
			
		||||
  SetStateAction,
 | 
			
		||||
  createContext,
 | 
			
		||||
  useEffect,
 | 
			
		||||
  useRef,
 | 
			
		||||
  useState,
 | 
			
		||||
} from 'react'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
import Fuse from 'fuse.js'
 | 
			
		||||
import {
 | 
			
		||||
  Command,
 | 
			
		||||
  CommandArgument,
 | 
			
		||||
  CommandArgumentOption,
 | 
			
		||||
} from '../lib/commands'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { CustomIcon } from './CustomIcon'
 | 
			
		||||
 | 
			
		||||
type ComboboxOption = Command | CommandArgumentOption
 | 
			
		||||
type CommandArgumentData = [string, any]
 | 
			
		||||
 | 
			
		||||
export const CommandsContext = createContext(
 | 
			
		||||
  {} as {
 | 
			
		||||
    commands: Command[]
 | 
			
		||||
    addCommands: (commands: Command[]) => void
 | 
			
		||||
    removeCommands: (commands: Command[]) => void
 | 
			
		||||
    commandBarOpen: boolean
 | 
			
		||||
    setCommandBarOpen: Dispatch<SetStateAction<boolean>>
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export const CommandBarProvider = ({
 | 
			
		||||
  children,
 | 
			
		||||
}: {
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
}) => {
 | 
			
		||||
  const [commands, internalSetCommands] = useState([] as Command[])
 | 
			
		||||
  const [commandBarOpen, setCommandBarOpen] = useState(false)
 | 
			
		||||
 | 
			
		||||
  function sortCommands(a: Command, b: Command) {
 | 
			
		||||
    if (b.owner === 'auth') return -1
 | 
			
		||||
    if (a.owner === 'auth') return 1
 | 
			
		||||
    return a.name.localeCompare(b.name)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => console.log('commands updated', commands), [commands])
 | 
			
		||||
 | 
			
		||||
  const addCommands = (newCommands: Command[]) => {
 | 
			
		||||
    internalSetCommands((prevCommands) =>
 | 
			
		||||
      [...newCommands, ...prevCommands].sort(sortCommands)
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  const removeCommands = (newCommands: Command[]) => {
 | 
			
		||||
    internalSetCommands((prevCommands) =>
 | 
			
		||||
      prevCommands
 | 
			
		||||
        .filter((command) => !newCommands.includes(command))
 | 
			
		||||
        .sort(sortCommands)
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <CommandsContext.Provider
 | 
			
		||||
      value={{
 | 
			
		||||
        commands,
 | 
			
		||||
        addCommands,
 | 
			
		||||
        removeCommands,
 | 
			
		||||
        commandBarOpen,
 | 
			
		||||
        setCommandBarOpen,
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
      <CommandBar />
 | 
			
		||||
    </CommandsContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CommandBar = () => {
 | 
			
		||||
  const { commands, commandBarOpen, setCommandBarOpen } = useCommandsContext()
 | 
			
		||||
  useHotkeys(['meta+k', 'meta+/'], () => {
 | 
			
		||||
    if (commands?.length === 0) return
 | 
			
		||||
    setCommandBarOpen(!commandBarOpen)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const [selectedCommand, setSelectedCommand] = useState<Command>()
 | 
			
		||||
  const [commandArguments, setCommandArguments] = useState<CommandArgument[]>(
 | 
			
		||||
    []
 | 
			
		||||
  )
 | 
			
		||||
  const [commandArgumentData, setCommandArgumentData] = useState<
 | 
			
		||||
    CommandArgumentData[]
 | 
			
		||||
  >([])
 | 
			
		||||
  const [commandArgumentIndex, setCommandArgumentIndex] = useState<number>(0)
 | 
			
		||||
 | 
			
		||||
  function clearState() {
 | 
			
		||||
    setCommandBarOpen(false)
 | 
			
		||||
    setSelectedCommand(undefined)
 | 
			
		||||
    setCommandArguments([])
 | 
			
		||||
    setCommandArgumentData([])
 | 
			
		||||
    setCommandArgumentIndex(0)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function selectCommand(command: Command) {
 | 
			
		||||
    console.log('selecting command', command)
 | 
			
		||||
    if (!('args' in command && command.args?.length)) {
 | 
			
		||||
      submitCommand({ command })
 | 
			
		||||
    } else {
 | 
			
		||||
      setCommandArguments(command.args)
 | 
			
		||||
      setSelectedCommand(command)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function stepBack() {
 | 
			
		||||
    if (!selectedCommand) {
 | 
			
		||||
      clearState()
 | 
			
		||||
    } else {
 | 
			
		||||
      if (commandArgumentIndex === 0) {
 | 
			
		||||
        setSelectedCommand(undefined)
 | 
			
		||||
      } else {
 | 
			
		||||
        setCommandArgumentIndex((prevIndex) => Math.max(0, prevIndex - 1))
 | 
			
		||||
      }
 | 
			
		||||
      if (commandArgumentData.length > 0) {
 | 
			
		||||
        setCommandArgumentData((prevData) => prevData.slice(0, -1))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function appendCommandArgumentData(data: { name: any }) {
 | 
			
		||||
    const transformedData = [
 | 
			
		||||
      commandArguments[commandArgumentIndex].name,
 | 
			
		||||
      data.name,
 | 
			
		||||
    ]
 | 
			
		||||
    if (commandArgumentIndex + 1 === commandArguments.length) {
 | 
			
		||||
      submitCommand({
 | 
			
		||||
        dataArr: [
 | 
			
		||||
          ...commandArgumentData,
 | 
			
		||||
          transformedData,
 | 
			
		||||
        ] as CommandArgumentData[],
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      setCommandArgumentData(
 | 
			
		||||
        (prevData) => [...prevData, transformedData] as CommandArgumentData[]
 | 
			
		||||
      )
 | 
			
		||||
      setCommandArgumentIndex((prevIndex) => prevIndex + 1)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function submitCommand({
 | 
			
		||||
    command = selectedCommand,
 | 
			
		||||
    dataArr = commandArgumentData,
 | 
			
		||||
  }) {
 | 
			
		||||
    console.log('submitting command', command, dataArr)
 | 
			
		||||
    if (dataArr.length === 0) {
 | 
			
		||||
      command?.callback()
 | 
			
		||||
    } else {
 | 
			
		||||
      const data = Object.fromEntries(dataArr)
 | 
			
		||||
      console.log('submitting data', data)
 | 
			
		||||
      command?.callback(data)
 | 
			
		||||
    }
 | 
			
		||||
    setCommandBarOpen(false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function getDisplayValue(command: Command) {
 | 
			
		||||
    if (
 | 
			
		||||
      'args' in command &&
 | 
			
		||||
      command.args &&
 | 
			
		||||
      command.args?.length > 0 &&
 | 
			
		||||
      'formatFunction' in command &&
 | 
			
		||||
      command.formatFunction
 | 
			
		||||
    ) {
 | 
			
		||||
      command.formatFunction(
 | 
			
		||||
        command.args.map((c, i) =>
 | 
			
		||||
          commandArgumentData[i] ? commandArgumentData[i][0] : `<${c.name}>`
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return command.name
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Transition.Root
 | 
			
		||||
      show={commandBarOpen || false}
 | 
			
		||||
      afterLeave={() => clearState()}
 | 
			
		||||
      as={Fragment}
 | 
			
		||||
    >
 | 
			
		||||
      <Dialog
 | 
			
		||||
        onClose={() => {
 | 
			
		||||
          setCommandBarOpen(false)
 | 
			
		||||
        }}
 | 
			
		||||
        className="fixed inset-0 z-40 overflow-y-auto pb-4 pt-1"
 | 
			
		||||
      >
 | 
			
		||||
        <Transition.Child
 | 
			
		||||
          enter="duration-100 ease-out"
 | 
			
		||||
          enterFrom="opacity-0 scale-95"
 | 
			
		||||
          enterTo="opacity-100 scale-100"
 | 
			
		||||
          leave="duration-75 ease-in"
 | 
			
		||||
          leaveFrom="opacity-100 scale-100"
 | 
			
		||||
          leaveTo="opacity-0 scale-95"
 | 
			
		||||
        >
 | 
			
		||||
          <Dialog.Panel
 | 
			
		||||
            className="relative w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
 | 
			
		||||
            as="div"
 | 
			
		||||
          >
 | 
			
		||||
            {!(
 | 
			
		||||
              commandArguments &&
 | 
			
		||||
              commandArguments.length &&
 | 
			
		||||
              selectedCommand
 | 
			
		||||
            ) ? (
 | 
			
		||||
              <CommandComboBox
 | 
			
		||||
                options={commands}
 | 
			
		||||
                handleSelection={selectCommand}
 | 
			
		||||
                stepBack={stepBack}
 | 
			
		||||
              />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <>
 | 
			
		||||
                <div className="px-4 text-sm flex flex-wrap gap-2">
 | 
			
		||||
                  <p className="pr-4 flex gap-2 items-center">
 | 
			
		||||
                    {selectedCommand &&
 | 
			
		||||
                      'icon' in selectedCommand &&
 | 
			
		||||
                      selectedCommand.icon && (
 | 
			
		||||
                        <CustomIcon
 | 
			
		||||
                          name={selectedCommand.icon}
 | 
			
		||||
                          className="w-5 h-5"
 | 
			
		||||
                        />
 | 
			
		||||
                      )}
 | 
			
		||||
                    {getDisplayValue(selectedCommand)}
 | 
			
		||||
                  </p>
 | 
			
		||||
                  {commandArguments.map((arg, i) => (
 | 
			
		||||
                    <p
 | 
			
		||||
                      key={arg.name}
 | 
			
		||||
                      className={`w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
 | 
			
		||||
                        i === commandArgumentIndex
 | 
			
		||||
                          ? 'bg-energy-10/50 dark:bg-energy-10/20 border-energy-10 dark:border-energy-10'
 | 
			
		||||
                          : 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
 | 
			
		||||
                      }`}
 | 
			
		||||
                    >
 | 
			
		||||
                      {commandArgumentIndex >= i && commandArgumentData[i] ? (
 | 
			
		||||
                        commandArgumentData[i][1]
 | 
			
		||||
                      ) : arg.defaultValue ? (
 | 
			
		||||
                        arg.defaultValue
 | 
			
		||||
                      ) : (
 | 
			
		||||
                        <em>{arg.name}</em>
 | 
			
		||||
                      )}
 | 
			
		||||
                    </p>
 | 
			
		||||
                  ))}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="block w-full my-2 h-[1px] bg-chalkboard-20 dark:bg-chalkboard-80" />
 | 
			
		||||
                <Argument
 | 
			
		||||
                  arg={commandArguments[commandArgumentIndex]}
 | 
			
		||||
                  appendCommandArgumentData={appendCommandArgumentData}
 | 
			
		||||
                  stepBack={stepBack}
 | 
			
		||||
                />
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
          </Dialog.Panel>
 | 
			
		||||
        </Transition.Child>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
    </Transition.Root>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Argument({
 | 
			
		||||
  arg,
 | 
			
		||||
  appendCommandArgumentData,
 | 
			
		||||
  stepBack,
 | 
			
		||||
}: {
 | 
			
		||||
  arg: CommandArgument
 | 
			
		||||
  appendCommandArgumentData: Dispatch<SetStateAction<any>>
 | 
			
		||||
  stepBack: () => void
 | 
			
		||||
}) {
 | 
			
		||||
  const { setCommandBarOpen } = useCommandsContext()
 | 
			
		||||
  const inputRef = useRef<HTMLInputElement>(null)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (inputRef.current) {
 | 
			
		||||
      inputRef.current.focus()
 | 
			
		||||
      inputRef.current.select()
 | 
			
		||||
    }
 | 
			
		||||
  }, [arg, inputRef])
 | 
			
		||||
 | 
			
		||||
  return arg.type === 'select' ? (
 | 
			
		||||
    <CommandComboBox
 | 
			
		||||
      options={arg.options}
 | 
			
		||||
      handleSelection={appendCommandArgumentData}
 | 
			
		||||
      stepBack={stepBack}
 | 
			
		||||
      placeholder="Select an option"
 | 
			
		||||
    />
 | 
			
		||||
  ) : (
 | 
			
		||||
    <form
 | 
			
		||||
      onSubmit={(event) => {
 | 
			
		||||
        event.preventDefault()
 | 
			
		||||
 | 
			
		||||
        appendCommandArgumentData({ name: inputRef.current?.value })
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <label className="flex items-center mx-4 my-4">
 | 
			
		||||
        <span className="px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80">
 | 
			
		||||
          {arg.name}
 | 
			
		||||
        </span>
 | 
			
		||||
        <input
 | 
			
		||||
          ref={inputRef}
 | 
			
		||||
          className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
 | 
			
		||||
          placeholder="Enter a value"
 | 
			
		||||
          defaultValue={arg.defaultValue}
 | 
			
		||||
          onKeyDown={(event) => {
 | 
			
		||||
            if (event.metaKey && event.key === 'k') setCommandBarOpen(false)
 | 
			
		||||
            if (event.key === 'Backspace' && !event.currentTarget.value) {
 | 
			
		||||
              stepBack()
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
          autoFocus
 | 
			
		||||
        />
 | 
			
		||||
      </label>
 | 
			
		||||
    </form>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandBarProvider
 | 
			
		||||
 | 
			
		||||
function CommandComboBox({
 | 
			
		||||
  options,
 | 
			
		||||
  handleSelection,
 | 
			
		||||
  stepBack,
 | 
			
		||||
  placeholder,
 | 
			
		||||
}: {
 | 
			
		||||
  options: ComboboxOption[]
 | 
			
		||||
  handleSelection: Dispatch<SetStateAction<any>>
 | 
			
		||||
  stepBack: () => void
 | 
			
		||||
  placeholder?: string
 | 
			
		||||
}) {
 | 
			
		||||
  const { setCommandBarOpen } = useCommandsContext()
 | 
			
		||||
  const [query, setQuery] = useState('')
 | 
			
		||||
  const [filteredOptions, setFilteredOptions] = useState<ComboboxOption[]>()
 | 
			
		||||
 | 
			
		||||
  const defaultOption =
 | 
			
		||||
    options.find((o) => 'isCurrent' in o && o.isCurrent) || null
 | 
			
		||||
 | 
			
		||||
  const fuse = new Fuse(options, {
 | 
			
		||||
    keys: ['name', 'description'],
 | 
			
		||||
    threshold: 0.3,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const results = fuse.search(query).map((result) => result.item)
 | 
			
		||||
    setFilteredOptions(query.length > 0 ? results : options)
 | 
			
		||||
  }, [query])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Combobox defaultValue={defaultOption} onChange={handleSelection}>
 | 
			
		||||
      <div className="flex items-center gap-2 px-4 pb-2 border-solid border-0 border-b border-b-chalkboard-20 dark:border-b-chalkboard-80">
 | 
			
		||||
        <CustomIcon
 | 
			
		||||
          name="search"
 | 
			
		||||
          className="w-5 h-5 bg-energy-10/50 dark:bg-chalkboard-90 dark:text-energy-10"
 | 
			
		||||
        />
 | 
			
		||||
        <Combobox.Input
 | 
			
		||||
          onChange={(event) => setQuery(event.target.value)}
 | 
			
		||||
          className="w-full bg-transparent focus:outline-none selection:bg-energy-10/50 dark:selection:bg-energy-10/20 dark:focus:outline-none"
 | 
			
		||||
          onKeyDown={(event) => {
 | 
			
		||||
            if (event.metaKey && event.key === 'k') setCommandBarOpen(false)
 | 
			
		||||
            if (event.key === 'Backspace' && !event.currentTarget.value) {
 | 
			
		||||
              stepBack()
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
          placeholder={
 | 
			
		||||
            (defaultOption && defaultOption.name) ||
 | 
			
		||||
            placeholder ||
 | 
			
		||||
            'Search commands'
 | 
			
		||||
          }
 | 
			
		||||
          autoCapitalize="off"
 | 
			
		||||
          autoComplete="off"
 | 
			
		||||
          autoCorrect="off"
 | 
			
		||||
          spellCheck="false"
 | 
			
		||||
          autoFocus
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
      <Combobox.Options
 | 
			
		||||
        static
 | 
			
		||||
        className="overflow-y-auto max-h-96 cursor-pointer"
 | 
			
		||||
      >
 | 
			
		||||
        {filteredOptions?.map((option) => (
 | 
			
		||||
          <Combobox.Option
 | 
			
		||||
            key={option.name}
 | 
			
		||||
            value={option}
 | 
			
		||||
            className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
 | 
			
		||||
          >
 | 
			
		||||
            {'icon' in option && option.icon && (
 | 
			
		||||
              <CustomIcon
 | 
			
		||||
                name={option.icon}
 | 
			
		||||
                className="w-5 h-5 dark:text-energy-10"
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            <p className="flex-grow">{option.name} </p>
 | 
			
		||||
            {'isCurrent' in option && option.isCurrent && (
 | 
			
		||||
              <small className="text-chalkboard-70 dark:text-chalkboard-50">
 | 
			
		||||
                current
 | 
			
		||||
              </small>
 | 
			
		||||
            )}
 | 
			
		||||
          </Combobox.Option>
 | 
			
		||||
        ))}
 | 
			
		||||
      </Combobox.Options>
 | 
			
		||||
    </Combobox>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										114
									
								
								src/components/CommandBar/CommandArgOptionInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,114 @@
 | 
			
		||||
import { Combobox } from '@headlessui/react'
 | 
			
		||||
import Fuse from 'fuse.js'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { CommandArgumentOption } from 'lib/commandTypes'
 | 
			
		||||
import { useEffect, useRef, useState } from 'react'
 | 
			
		||||
 | 
			
		||||
function CommandArgOptionInput({
 | 
			
		||||
  options,
 | 
			
		||||
  argName,
 | 
			
		||||
  stepBack,
 | 
			
		||||
  onSubmit,
 | 
			
		||||
  placeholder,
 | 
			
		||||
}: {
 | 
			
		||||
  options: CommandArgumentOption<unknown>[]
 | 
			
		||||
  argName: string
 | 
			
		||||
  stepBack: () => void
 | 
			
		||||
  onSubmit: (data: unknown) => void
 | 
			
		||||
  placeholder?: string
 | 
			
		||||
}) {
 | 
			
		||||
  const { commandBarSend, commandBarState } = useCommandsContext()
 | 
			
		||||
  const inputRef = useRef<HTMLInputElement>(null)
 | 
			
		||||
  const formRef = useRef<HTMLFormElement>(null)
 | 
			
		||||
  const [argValue, setArgValue] = useState<(typeof options)[number]['value']>(
 | 
			
		||||
    options.find((o) => 'isCurrent' in o && o.isCurrent)?.value ||
 | 
			
		||||
      commandBarState.context.argumentsToSubmit[argName] ||
 | 
			
		||||
      options[0].value
 | 
			
		||||
  )
 | 
			
		||||
  const [query, setQuery] = useState('')
 | 
			
		||||
  const [filteredOptions, setFilteredOptions] = useState<typeof options>()
 | 
			
		||||
 | 
			
		||||
  const fuse = new Fuse(options, {
 | 
			
		||||
    keys: ['name', 'description'],
 | 
			
		||||
    threshold: 0.3,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    inputRef.current?.focus()
 | 
			
		||||
    inputRef.current?.select()
 | 
			
		||||
  }, [inputRef])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const results = fuse.search(query).map((result) => result.item)
 | 
			
		||||
    setFilteredOptions(query.length > 0 ? results : options)
 | 
			
		||||
  }, [query])
 | 
			
		||||
 | 
			
		||||
  function handleSelectOption(option: CommandArgumentOption<unknown>) {
 | 
			
		||||
    setArgValue(option)
 | 
			
		||||
    onSubmit(option.value)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    onSubmit(argValue)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <form id="arg-form" onSubmit={handleSubmit} ref={formRef}>
 | 
			
		||||
      <Combobox value={argValue} onChange={handleSelectOption} name="options">
 | 
			
		||||
        <div className="flex items-center mx-4 mt-4 mb-2">
 | 
			
		||||
          <label
 | 
			
		||||
            htmlFor="option-input"
 | 
			
		||||
            className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
 | 
			
		||||
          >
 | 
			
		||||
            {argName}
 | 
			
		||||
          </label>
 | 
			
		||||
          <Combobox.Input
 | 
			
		||||
            id="option-input"
 | 
			
		||||
            ref={inputRef}
 | 
			
		||||
            onChange={(event) => setQuery(event.target.value)}
 | 
			
		||||
            className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
 | 
			
		||||
            onKeyDown={(event) => {
 | 
			
		||||
              if (event.metaKey && event.key === 'k')
 | 
			
		||||
                commandBarSend({ type: 'Close' })
 | 
			
		||||
              if (event.key === 'Backspace' && !event.currentTarget.value) {
 | 
			
		||||
                stepBack()
 | 
			
		||||
              }
 | 
			
		||||
            }}
 | 
			
		||||
            placeholder={
 | 
			
		||||
              (argValue as CommandArgumentOption<unknown>)?.name ||
 | 
			
		||||
              placeholder ||
 | 
			
		||||
              'Select an option for ' + argName
 | 
			
		||||
            }
 | 
			
		||||
            autoCapitalize="off"
 | 
			
		||||
            autoComplete="off"
 | 
			
		||||
            autoCorrect="off"
 | 
			
		||||
            spellCheck="false"
 | 
			
		||||
            autoFocus
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <Combobox.Options
 | 
			
		||||
          static
 | 
			
		||||
          className="overflow-y-auto max-h-96 cursor-pointer"
 | 
			
		||||
        >
 | 
			
		||||
          {filteredOptions?.map((option) => (
 | 
			
		||||
            <Combobox.Option
 | 
			
		||||
              key={option.name}
 | 
			
		||||
              value={option}
 | 
			
		||||
              className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
 | 
			
		||||
            >
 | 
			
		||||
              <p className="flex-grow">{option.name} </p>
 | 
			
		||||
              {'isCurrent' in option && option.isCurrent && (
 | 
			
		||||
                <small className="text-chalkboard-70 dark:text-chalkboard-50">
 | 
			
		||||
                  current
 | 
			
		||||
                </small>
 | 
			
		||||
              )}
 | 
			
		||||
            </Combobox.Option>
 | 
			
		||||
          ))}
 | 
			
		||||
        </Combobox.Options>
 | 
			
		||||
      </Combobox>
 | 
			
		||||
    </form>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandArgOptionInput
 | 
			
		||||
							
								
								
									
										166
									
								
								src/components/CommandBar/CommandBar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,166 @@
 | 
			
		||||
import { Dialog, Popover, Transition } from '@headlessui/react'
 | 
			
		||||
import { Fragment, createContext, useEffect } from 'react'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { useMachine } from '@xstate/react'
 | 
			
		||||
import { commandBarMachine } from 'machines/commandBarMachine'
 | 
			
		||||
import { EventFrom, StateFrom } from 'xstate'
 | 
			
		||||
import CommandBarArgument from './CommandBarArgument'
 | 
			
		||||
import CommandComboBox from '../CommandComboBox'
 | 
			
		||||
import { useLocation } from 'react-router-dom'
 | 
			
		||||
import CommandBarReview from './CommandBarReview'
 | 
			
		||||
 | 
			
		||||
type CommandsContextType = {
 | 
			
		||||
  commandBarState: StateFrom<typeof commandBarMachine>
 | 
			
		||||
  commandBarSend: (event: EventFrom<typeof commandBarMachine>) => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CommandsContext = createContext<CommandsContextType>({
 | 
			
		||||
  commandBarState: commandBarMachine.initialState,
 | 
			
		||||
  commandBarSend: () => {},
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const CommandBarProvider = ({
 | 
			
		||||
  children,
 | 
			
		||||
}: {
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
}) => {
 | 
			
		||||
  const { pathname } = useLocation()
 | 
			
		||||
  const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
 | 
			
		||||
    guards: {
 | 
			
		||||
      'Arguments are ready': (context, _) => {
 | 
			
		||||
        return context.selectedCommand?.args
 | 
			
		||||
          ? context.argumentsToSubmit.length ===
 | 
			
		||||
              Object.keys(context.selectedCommand.args)?.length
 | 
			
		||||
          : false
 | 
			
		||||
      },
 | 
			
		||||
      'Command has no arguments': (context, _event) => {
 | 
			
		||||
        return (
 | 
			
		||||
          !context.selectedCommand?.args ||
 | 
			
		||||
          Object.keys(context.selectedCommand?.args).length === 0
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // Close the command bar when navigating
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    commandBarSend({ type: 'Close' })
 | 
			
		||||
  }, [pathname])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <CommandsContext.Provider
 | 
			
		||||
      value={{
 | 
			
		||||
        commandBarState,
 | 
			
		||||
        commandBarSend,
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
      <CommandBar />
 | 
			
		||||
    </CommandsContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CommandBar = () => {
 | 
			
		||||
  const { commandBarState, commandBarSend } = useCommandsContext()
 | 
			
		||||
  const {
 | 
			
		||||
    context: { selectedCommand, currentArgument, commands },
 | 
			
		||||
  } = commandBarState
 | 
			
		||||
  const isSelectionArgument = currentArgument?.inputType === 'selection'
 | 
			
		||||
  const WrapperComponent = isSelectionArgument ? Popover : Dialog
 | 
			
		||||
 | 
			
		||||
  useHotkeys(['mod+k', 'mod+/'], () => {
 | 
			
		||||
    if (commandBarState.context.commands.length === 0) return
 | 
			
		||||
    if (commandBarState.matches('Closed')) {
 | 
			
		||||
      commandBarSend({ type: 'Open' })
 | 
			
		||||
    } else {
 | 
			
		||||
      commandBarSend({ type: 'Close' })
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  function stepBack() {
 | 
			
		||||
    if (!currentArgument) {
 | 
			
		||||
      if (commandBarState.matches('Review')) {
 | 
			
		||||
        const entries = Object.entries(selectedCommand?.args || {})
 | 
			
		||||
 | 
			
		||||
        commandBarSend({
 | 
			
		||||
          type: commandBarState.matches('Review')
 | 
			
		||||
            ? 'Edit argument'
 | 
			
		||||
            : 'Change current argument',
 | 
			
		||||
          data: {
 | 
			
		||||
            arg: {
 | 
			
		||||
              name: entries[entries.length - 1][0],
 | 
			
		||||
              ...entries[entries.length - 1][1],
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
      } else {
 | 
			
		||||
        commandBarSend({ type: 'Deselect command' })
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      const entries = Object.entries(selectedCommand?.args || {})
 | 
			
		||||
      const index = entries.findIndex(
 | 
			
		||||
        ([key, _]) => key === currentArgument.name
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      if (index === 0) {
 | 
			
		||||
        commandBarSend({ type: 'Deselect command' })
 | 
			
		||||
      } else {
 | 
			
		||||
        commandBarSend({
 | 
			
		||||
          type: 'Change current argument',
 | 
			
		||||
          data: {
 | 
			
		||||
            arg: { name: entries[index - 1][0], ...entries[index - 1][1] },
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Transition.Root
 | 
			
		||||
      show={!commandBarState.matches('Closed') || false}
 | 
			
		||||
      afterLeave={() => {
 | 
			
		||||
        if (selectedCommand?.onCancel) selectedCommand.onCancel()
 | 
			
		||||
        commandBarSend({ type: 'Clear' })
 | 
			
		||||
      }}
 | 
			
		||||
      as={Fragment}
 | 
			
		||||
    >
 | 
			
		||||
      <WrapperComponent
 | 
			
		||||
        open={!commandBarState.matches('Closed') || isSelectionArgument}
 | 
			
		||||
        onClose={() => {
 | 
			
		||||
          commandBarSend({ type: 'Close' })
 | 
			
		||||
        }}
 | 
			
		||||
        className={
 | 
			
		||||
          'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
 | 
			
		||||
          (isSelectionArgument ? 'pointer-events-none' : '')
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        <Transition.Child
 | 
			
		||||
          enter="duration-100 ease-out"
 | 
			
		||||
          enterFrom="opacity-0 scale-95"
 | 
			
		||||
          enterTo="opacity-100 scale-100"
 | 
			
		||||
          leave="duration-75 ease-in"
 | 
			
		||||
          leaveFrom="opacity-100 scale-100"
 | 
			
		||||
          leaveTo="opacity-0 scale-95"
 | 
			
		||||
        >
 | 
			
		||||
          <WrapperComponent.Panel
 | 
			
		||||
            className="relative z-50 pointer-events-auto w-full max-w-xl py-2 mx-auto border rounded shadow-lg bg-chalkboard-10 dark:bg-chalkboard-100 dark:border-chalkboard-70"
 | 
			
		||||
            as="div"
 | 
			
		||||
          >
 | 
			
		||||
            {commandBarState.matches('Selecting command') ? (
 | 
			
		||||
              <CommandComboBox options={commands} />
 | 
			
		||||
            ) : commandBarState.matches('Gathering arguments') ? (
 | 
			
		||||
              <CommandBarArgument stepBack={stepBack} />
 | 
			
		||||
            ) : (
 | 
			
		||||
              commandBarState.matches('Review') && (
 | 
			
		||||
                <CommandBarReview stepBack={stepBack} />
 | 
			
		||||
              )
 | 
			
		||||
            )}
 | 
			
		||||
          </WrapperComponent.Panel>
 | 
			
		||||
        </Transition.Child>
 | 
			
		||||
      </WrapperComponent>
 | 
			
		||||
    </Transition.Root>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandBarProvider
 | 
			
		||||
							
								
								
									
										80
									
								
								src/components/CommandBar/CommandBarArgument.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,80 @@
 | 
			
		||||
import CommandArgOptionInput from './CommandArgOptionInput'
 | 
			
		||||
import CommandBarBasicInput from './CommandBarBasicInput'
 | 
			
		||||
import CommandBarSelectionInput from './CommandBarSelectionInput'
 | 
			
		||||
import { CommandArgument } from 'lib/commandTypes'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import CommandBarHeader from './CommandBarHeader'
 | 
			
		||||
 | 
			
		||||
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
 | 
			
		||||
  const { commandBarState, commandBarSend } = useCommandsContext()
 | 
			
		||||
  const {
 | 
			
		||||
    context: { currentArgument },
 | 
			
		||||
  } = commandBarState
 | 
			
		||||
 | 
			
		||||
  function onSubmit(data: unknown) {
 | 
			
		||||
    if (!currentArgument) return
 | 
			
		||||
 | 
			
		||||
    commandBarSend({
 | 
			
		||||
      type: 'Submit argument',
 | 
			
		||||
      data: {
 | 
			
		||||
        [currentArgument.name]:
 | 
			
		||||
          currentArgument.inputType === 'number'
 | 
			
		||||
            ? parseFloat((data as string) || '0')
 | 
			
		||||
            : data,
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    currentArgument && (
 | 
			
		||||
      <CommandBarHeader>
 | 
			
		||||
        <ArgumentInput
 | 
			
		||||
          arg={currentArgument}
 | 
			
		||||
          stepBack={stepBack}
 | 
			
		||||
          onSubmit={onSubmit}
 | 
			
		||||
        />
 | 
			
		||||
      </CommandBarHeader>
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandBarArgument
 | 
			
		||||
 | 
			
		||||
function ArgumentInput({
 | 
			
		||||
  arg,
 | 
			
		||||
  stepBack,
 | 
			
		||||
  onSubmit,
 | 
			
		||||
}: {
 | 
			
		||||
  arg: CommandArgument<unknown> & { name: string }
 | 
			
		||||
  stepBack: () => void
 | 
			
		||||
  onSubmit: (event: any) => void
 | 
			
		||||
}) {
 | 
			
		||||
  switch (arg.inputType) {
 | 
			
		||||
    case 'options':
 | 
			
		||||
      return (
 | 
			
		||||
        <CommandArgOptionInput
 | 
			
		||||
          options={arg.options}
 | 
			
		||||
          argName={arg.name}
 | 
			
		||||
          stepBack={stepBack}
 | 
			
		||||
          onSubmit={onSubmit}
 | 
			
		||||
          placeholder="Select an option"
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
    case 'selection':
 | 
			
		||||
      return (
 | 
			
		||||
        <CommandBarSelectionInput
 | 
			
		||||
          arg={arg}
 | 
			
		||||
          stepBack={stepBack}
 | 
			
		||||
          onSubmit={onSubmit}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
    default:
 | 
			
		||||
      return (
 | 
			
		||||
        <CommandBarBasicInput
 | 
			
		||||
          arg={arg}
 | 
			
		||||
          stepBack={stepBack}
 | 
			
		||||
          onSubmit={onSubmit}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								src/components/CommandBar/CommandBarBasicInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,66 @@
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { CommandArgument } from 'lib/commandTypes'
 | 
			
		||||
import { useEffect, useRef } from 'react'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
 | 
			
		||||
function CommandBarBasicInput({
 | 
			
		||||
  arg,
 | 
			
		||||
  stepBack,
 | 
			
		||||
  onSubmit,
 | 
			
		||||
}: {
 | 
			
		||||
  arg: CommandArgument<unknown> & {
 | 
			
		||||
    inputType: 'number' | 'string'
 | 
			
		||||
    name: string
 | 
			
		||||
  }
 | 
			
		||||
  stepBack: () => void
 | 
			
		||||
  onSubmit: (event: unknown) => void
 | 
			
		||||
}) {
 | 
			
		||||
  const { commandBarSend, commandBarState } = useCommandsContext()
 | 
			
		||||
  useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
 | 
			
		||||
  const inputRef = useRef<HTMLInputElement>(null)
 | 
			
		||||
  const inputType = arg.inputType === 'number' ? 'number' : 'text'
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (inputRef.current) {
 | 
			
		||||
      inputRef.current.focus()
 | 
			
		||||
      inputRef.current.select()
 | 
			
		||||
    }
 | 
			
		||||
  }, [arg, inputRef])
 | 
			
		||||
 | 
			
		||||
  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    onSubmit(inputRef.current?.value)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <form id="arg-form" onSubmit={handleSubmit}>
 | 
			
		||||
      <label className="flex items-center mx-4 my-4">
 | 
			
		||||
        <span className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80">
 | 
			
		||||
          {arg.name}
 | 
			
		||||
        </span>
 | 
			
		||||
        <input
 | 
			
		||||
          id="arg-form"
 | 
			
		||||
          name={inputType}
 | 
			
		||||
          ref={inputRef}
 | 
			
		||||
          type={inputType}
 | 
			
		||||
          required
 | 
			
		||||
          className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
 | 
			
		||||
          placeholder="Enter a value"
 | 
			
		||||
          defaultValue={
 | 
			
		||||
            (commandBarState.context.argumentsToSubmit[arg.name] as
 | 
			
		||||
              | string
 | 
			
		||||
              | undefined) || (arg.defaultValue as string)
 | 
			
		||||
          }
 | 
			
		||||
          onKeyDown={(event) => {
 | 
			
		||||
            if (event.key === 'Backspace' && !event.currentTarget.value) {
 | 
			
		||||
              stepBack()
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
          autoFocus
 | 
			
		||||
        />
 | 
			
		||||
      </label>
 | 
			
		||||
    </form>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandBarBasicInput
 | 
			
		||||
							
								
								
									
										171
									
								
								src/components/CommandBar/CommandBarHeader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,171 @@
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { CustomIcon } from '../CustomIcon'
 | 
			
		||||
import React, { useState } from 'react'
 | 
			
		||||
import { ActionButton } from '../ActionButton'
 | 
			
		||||
import { Selections, getSelectionTypeDisplayText } from 'lib/selections'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
 | 
			
		||||
function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
 | 
			
		||||
  const { commandBarState, commandBarSend } = useCommandsContext()
 | 
			
		||||
  const {
 | 
			
		||||
    context: { selectedCommand, currentArgument, argumentsToSubmit },
 | 
			
		||||
  } = commandBarState
 | 
			
		||||
  const isReviewing = commandBarState.matches('Review')
 | 
			
		||||
  const [showShortcuts, setShowShortcuts] = useState(false)
 | 
			
		||||
 | 
			
		||||
  useHotkeys(
 | 
			
		||||
    'alt',
 | 
			
		||||
    () => setShowShortcuts(true),
 | 
			
		||||
    { enableOnFormTags: true, enableOnContentEditable: true },
 | 
			
		||||
    [showShortcuts]
 | 
			
		||||
  )
 | 
			
		||||
  useHotkeys(
 | 
			
		||||
    'alt',
 | 
			
		||||
    () => setShowShortcuts(false),
 | 
			
		||||
    { keyup: true, enableOnFormTags: true, enableOnContentEditable: true },
 | 
			
		||||
    [showShortcuts]
 | 
			
		||||
  )
 | 
			
		||||
  useHotkeys(
 | 
			
		||||
    [
 | 
			
		||||
      'alt+1',
 | 
			
		||||
      'alt+2',
 | 
			
		||||
      'alt+3',
 | 
			
		||||
      'alt+4',
 | 
			
		||||
      'alt+5',
 | 
			
		||||
      'alt+6',
 | 
			
		||||
      'alt+7',
 | 
			
		||||
      'alt+8',
 | 
			
		||||
      'alt+9',
 | 
			
		||||
      'alt+0',
 | 
			
		||||
    ],
 | 
			
		||||
    (_, b) => {
 | 
			
		||||
      if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
 | 
			
		||||
        if (!selectedCommand?.args) return
 | 
			
		||||
        const argName = Object.keys(selectedCommand.args)[
 | 
			
		||||
          parseInt(b.keys[0], 10) - 1
 | 
			
		||||
        ]
 | 
			
		||||
        const arg = selectedCommand?.args[argName]
 | 
			
		||||
        commandBarSend({
 | 
			
		||||
          type: 'Change current argument',
 | 
			
		||||
          data: { arg: { ...arg, name: argName } },
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    { keyup: true, enableOnFormTags: true, enableOnContentEditable: true },
 | 
			
		||||
    [argumentsToSubmit, selectedCommand]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    selectedCommand &&
 | 
			
		||||
    argumentsToSubmit && (
 | 
			
		||||
      <>
 | 
			
		||||
        <div className="px-4 text-sm flex gap-4 items-start">
 | 
			
		||||
          <div className="flex flex-1 flex-wrap gap-2">
 | 
			
		||||
            <p
 | 
			
		||||
              data-command-name={selectedCommand?.name}
 | 
			
		||||
              className="pr-4 flex gap-2 items-center"
 | 
			
		||||
            >
 | 
			
		||||
              {selectedCommand &&
 | 
			
		||||
                'icon' in selectedCommand &&
 | 
			
		||||
                selectedCommand.icon && (
 | 
			
		||||
                  <CustomIcon name={selectedCommand.icon} className="w-5 h-5" />
 | 
			
		||||
                )}
 | 
			
		||||
              {selectedCommand?.name}
 | 
			
		||||
            </p>
 | 
			
		||||
            {Object.entries(selectedCommand?.args || {}).map(
 | 
			
		||||
              ([argName, arg], i) => (
 | 
			
		||||
                <button
 | 
			
		||||
                  disabled={!isReviewing && currentArgument?.name === argName}
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    commandBarSend({
 | 
			
		||||
                      type: isReviewing
 | 
			
		||||
                        ? 'Edit argument'
 | 
			
		||||
                        : 'Change current argument',
 | 
			
		||||
                      data: { arg: { ...arg, name: argName } },
 | 
			
		||||
                    })
 | 
			
		||||
                  }}
 | 
			
		||||
                  key={argName}
 | 
			
		||||
                  className={`relative w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
 | 
			
		||||
                    argName === currentArgument?.name
 | 
			
		||||
                      ? 'disabled:bg-energy-10/50 dark:disabled:bg-energy-10/20 disabled:border-energy-10 dark:disabled:border-energy-10 disabled:text-chalkboard-100 dark:disabled:text-chalkboard-10'
 | 
			
		||||
                      : 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
 | 
			
		||||
                  }`}
 | 
			
		||||
                >
 | 
			
		||||
                  {argumentsToSubmit[argName] ? (
 | 
			
		||||
                    arg.inputType === 'selection' ? (
 | 
			
		||||
                      getSelectionTypeDisplayText(
 | 
			
		||||
                        argumentsToSubmit[argName] as Selections
 | 
			
		||||
                      )
 | 
			
		||||
                    ) : typeof argumentsToSubmit[argName] === 'object' ? (
 | 
			
		||||
                      JSON.stringify(argumentsToSubmit[argName])
 | 
			
		||||
                    ) : (
 | 
			
		||||
                      argumentsToSubmit[argName]
 | 
			
		||||
                    )
 | 
			
		||||
                  ) : arg.payload ? (
 | 
			
		||||
                    arg.inputType === 'selection' ? (
 | 
			
		||||
                      getSelectionTypeDisplayText(arg.payload as Selections)
 | 
			
		||||
                    ) : typeof arg.payload === 'object' ? (
 | 
			
		||||
                      JSON.stringify(arg.payload)
 | 
			
		||||
                    ) : (
 | 
			
		||||
                      arg.payload
 | 
			
		||||
                    )
 | 
			
		||||
                  ) : (
 | 
			
		||||
                    <em>{argName}</em>
 | 
			
		||||
                  )}
 | 
			
		||||
                  {showShortcuts && (
 | 
			
		||||
                    <small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100">
 | 
			
		||||
                      <span className="sr-only">Hotkey: </span>
 | 
			
		||||
                      {i + 1}
 | 
			
		||||
                    </small>
 | 
			
		||||
                  )}
 | 
			
		||||
                </button>
 | 
			
		||||
              )
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
          {isReviewing ? <ReviewingButton /> : <GatheringArgsButton />}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="block w-full my-2 h-[1px] bg-chalkboard-20 dark:bg-chalkboard-80" />
 | 
			
		||||
        {children}
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ReviewingButton() {
 | 
			
		||||
  return (
 | 
			
		||||
    <ActionButton
 | 
			
		||||
      Element="button"
 | 
			
		||||
      autoFocus
 | 
			
		||||
      type="submit"
 | 
			
		||||
      form="review-form"
 | 
			
		||||
      className="w-fit !p-0 rounded-sm border !border-chalkboard-100 dark:!border-energy-10 hover:shadow"
 | 
			
		||||
      icon={{
 | 
			
		||||
        icon: 'checkmark',
 | 
			
		||||
        bgClassName:
 | 
			
		||||
          'p-1 rounded-sm !bg-chalkboard-100 hover:!bg-chalkboard-110 dark:!bg-energy-20 dark:hover:!bg-energy-10',
 | 
			
		||||
        iconClassName: '!text-energy-10 dark:!text-chalkboard-100',
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <span className="sr-only">Submit command</span>
 | 
			
		||||
    </ActionButton>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function GatheringArgsButton() {
 | 
			
		||||
  return (
 | 
			
		||||
    <ActionButton
 | 
			
		||||
      Element="button"
 | 
			
		||||
      type="submit"
 | 
			
		||||
      form="arg-form"
 | 
			
		||||
      className="w-fit !p-0 rounded-sm"
 | 
			
		||||
      icon={{
 | 
			
		||||
        icon: 'arrowRight',
 | 
			
		||||
        bgClassName: 'p-1 rounded-sm',
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <span className="sr-only">Continue</span>
 | 
			
		||||
    </ActionButton>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandBarHeader
 | 
			
		||||
							
								
								
									
										81
									
								
								src/components/CommandBar/CommandBarReview.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,81 @@
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import CommandBarHeader from './CommandBarHeader'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
 | 
			
		||||
function CommandBarReview({ stepBack }: { stepBack: () => void }) {
 | 
			
		||||
  const { commandBarState, commandBarSend } = useCommandsContext()
 | 
			
		||||
  const {
 | 
			
		||||
    context: { argumentsToSubmit, selectedCommand },
 | 
			
		||||
  } = commandBarState
 | 
			
		||||
 | 
			
		||||
  useHotkeys('backspace', stepBack, {
 | 
			
		||||
    enableOnFormTags: true,
 | 
			
		||||
    enableOnContentEditable: true,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useHotkeys(
 | 
			
		||||
    '1, 2, 3, 4, 5, 6, 7, 8, 9, 0',
 | 
			
		||||
    (_, b) => {
 | 
			
		||||
      if (b.keys && !Number.isNaN(parseInt(b.keys[0], 10))) {
 | 
			
		||||
        if (!selectedCommand?.args) return
 | 
			
		||||
        const argName = Object.keys(selectedCommand.args)[
 | 
			
		||||
          parseInt(b.keys[0], 10) - 1
 | 
			
		||||
        ]
 | 
			
		||||
        const arg = selectedCommand?.args[argName]
 | 
			
		||||
        commandBarSend({
 | 
			
		||||
          type: 'Edit argument',
 | 
			
		||||
          data: { arg: { ...arg, name: argName } },
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    { keyup: true, enableOnFormTags: true, enableOnContentEditable: true },
 | 
			
		||||
    [argumentsToSubmit, selectedCommand]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  Object.keys(argumentsToSubmit).forEach((key, i) => {
 | 
			
		||||
    const arg = selectedCommand?.args ? selectedCommand?.args[key] : undefined
 | 
			
		||||
    if (!arg) return
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  function submitCommand() {
 | 
			
		||||
    commandBarSend({
 | 
			
		||||
      type: 'Submit command',
 | 
			
		||||
      data: argumentsToSubmit,
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <CommandBarHeader>
 | 
			
		||||
      <p className="px-4">Confirm {selectedCommand?.name}</p>
 | 
			
		||||
      <form
 | 
			
		||||
        id="review-form"
 | 
			
		||||
        className="absolute opacity-0 inset-0 pointer-events-none"
 | 
			
		||||
        onSubmit={submitCommand}
 | 
			
		||||
      >
 | 
			
		||||
        {Object.entries(argumentsToSubmit).map(([key, value], i) => {
 | 
			
		||||
          const arg = selectedCommand?.args
 | 
			
		||||
            ? selectedCommand?.args[key]
 | 
			
		||||
            : undefined
 | 
			
		||||
          if (!arg) return null
 | 
			
		||||
 | 
			
		||||
          return (
 | 
			
		||||
            <input
 | 
			
		||||
              id={key}
 | 
			
		||||
              name={key}
 | 
			
		||||
              key={key}
 | 
			
		||||
              type="text"
 | 
			
		||||
              defaultValue={
 | 
			
		||||
                typeof value === 'object'
 | 
			
		||||
                  ? JSON.stringify(value)
 | 
			
		||||
                  : (value as string)
 | 
			
		||||
              }
 | 
			
		||||
              hidden
 | 
			
		||||
            />
 | 
			
		||||
          )
 | 
			
		||||
        })}
 | 
			
		||||
      </form>
 | 
			
		||||
    </CommandBarHeader>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandBarReview
 | 
			
		||||
							
								
								
									
										114
									
								
								src/components/CommandBar/CommandBarSelectionInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,114 @@
 | 
			
		||||
import { useSelector } from '@xstate/react'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { useKclContext } from 'lang/KclSinglton'
 | 
			
		||||
import { CommandArgument } from 'lib/commandTypes'
 | 
			
		||||
import {
 | 
			
		||||
  ResolvedSelectionType,
 | 
			
		||||
  canSubmitSelectionArg,
 | 
			
		||||
  getSelectionType,
 | 
			
		||||
  getSelectionTypeDisplayText,
 | 
			
		||||
} from 'lib/selections'
 | 
			
		||||
import { modelingMachine } from 'machines/modelingMachine'
 | 
			
		||||
import { useEffect, useRef, useState } from 'react'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
import { StateFrom } from 'xstate'
 | 
			
		||||
 | 
			
		||||
const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) =>
 | 
			
		||||
  snapshot.context.selectionRanges
 | 
			
		||||
 | 
			
		||||
function CommandBarSelectionInput({
 | 
			
		||||
  arg,
 | 
			
		||||
  stepBack,
 | 
			
		||||
  onSubmit,
 | 
			
		||||
}: {
 | 
			
		||||
  arg: CommandArgument<unknown> & { inputType: 'selection'; name: string }
 | 
			
		||||
  stepBack: () => void
 | 
			
		||||
  onSubmit: (data: unknown) => void
 | 
			
		||||
}) {
 | 
			
		||||
  const { code } = useKclContext()
 | 
			
		||||
  const inputRef = useRef<HTMLInputElement>(null)
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
  const [hasSubmitted, setHasSubmitted] = useState(false)
 | 
			
		||||
  const selection = useSelector(arg.actor, selectionSelector)
 | 
			
		||||
  const [selectionsByType, setSelectionsByType] = useState<
 | 
			
		||||
    'none' | ResolvedSelectionType[]
 | 
			
		||||
  >(
 | 
			
		||||
    selection.codeBasedSelections[0]?.range[1] === code.length
 | 
			
		||||
      ? 'none'
 | 
			
		||||
      : getSelectionType(selection)
 | 
			
		||||
  )
 | 
			
		||||
  const [canSubmitSelection, setCanSubmitSelection] = useState<boolean>(
 | 
			
		||||
    canSubmitSelectionArg(selectionsByType, arg)
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useHotkeys('tab', () => onSubmit(selection), {
 | 
			
		||||
    enableOnFormTags: true,
 | 
			
		||||
    enableOnContentEditable: true,
 | 
			
		||||
    keyup: true,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    inputRef.current?.focus()
 | 
			
		||||
  }, [selection, inputRef])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setSelectionsByType(
 | 
			
		||||
      selection.codeBasedSelections[0]?.range[1] === code.length
 | 
			
		||||
        ? 'none'
 | 
			
		||||
        : getSelectionType(selection)
 | 
			
		||||
    )
 | 
			
		||||
  }, [selection])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setCanSubmitSelection(canSubmitSelectionArg(selectionsByType, arg))
 | 
			
		||||
  }, [selectionsByType, arg])
 | 
			
		||||
 | 
			
		||||
  function handleChange() {
 | 
			
		||||
    inputRef.current?.focus()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
 | 
			
		||||
    if (!canSubmitSelection) {
 | 
			
		||||
      setHasSubmitted(true)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onSubmit(selection)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <form id="arg-form" onSubmit={handleSubmit}>
 | 
			
		||||
      <label
 | 
			
		||||
        className={
 | 
			
		||||
          'relative flex items-center mx-4 my-4 ' +
 | 
			
		||||
          (!hasSubmitted || canSubmitSelection || 'text-destroy-50')
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        {canSubmitSelection
 | 
			
		||||
          ? getSelectionTypeDisplayText(selection) + ' selected'
 | 
			
		||||
          : `Please select ${arg.multiple ? 'one or more faces' : 'one face'}`}
 | 
			
		||||
        <input
 | 
			
		||||
          id="selection"
 | 
			
		||||
          name="selection"
 | 
			
		||||
          ref={inputRef}
 | 
			
		||||
          required
 | 
			
		||||
          placeholder="Select an entity with your mouse"
 | 
			
		||||
          className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
 | 
			
		||||
          onKeyDown={(event) => {
 | 
			
		||||
            if (event.key === 'Backspace') {
 | 
			
		||||
              stepBack()
 | 
			
		||||
            } else if (event.key === 'Escape') {
 | 
			
		||||
              commandBarSend({ type: 'Close' })
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
          onChange={handleChange}
 | 
			
		||||
          value={JSON.stringify(selection || {})}
 | 
			
		||||
        />
 | 
			
		||||
      </label>
 | 
			
		||||
    </form>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandBarSelectionInput
 | 
			
		||||
							
								
								
									
										90
									
								
								src/components/CommandComboBox.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,90 @@
 | 
			
		||||
import { Combobox } from '@headlessui/react'
 | 
			
		||||
import Fuse from 'fuse.js'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { Command } from 'lib/commandTypes'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { CustomIcon } from './CustomIcon'
 | 
			
		||||
 | 
			
		||||
function CommandComboBox({
 | 
			
		||||
  options,
 | 
			
		||||
  placeholder,
 | 
			
		||||
}: {
 | 
			
		||||
  options: Command[]
 | 
			
		||||
  placeholder?: string
 | 
			
		||||
}) {
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
  const [query, setQuery] = useState('')
 | 
			
		||||
  const [filteredOptions, setFilteredOptions] = useState<typeof options>()
 | 
			
		||||
 | 
			
		||||
  const defaultOption =
 | 
			
		||||
    options.find((o) => 'isCurrent' in o && o.isCurrent) || null
 | 
			
		||||
 | 
			
		||||
  const fuse = new Fuse(options, {
 | 
			
		||||
    keys: ['name', 'description'],
 | 
			
		||||
    threshold: 0.3,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const results = fuse.search(query).map((result) => result.item)
 | 
			
		||||
    setFilteredOptions(query.length > 0 ? results : options)
 | 
			
		||||
  }, [query])
 | 
			
		||||
 | 
			
		||||
  function handleSelection(command: Command) {
 | 
			
		||||
    commandBarSend({ type: 'Select command', data: { command } })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Combobox defaultValue={defaultOption} onChange={handleSelection}>
 | 
			
		||||
      <div className="flex items-center gap-2 px-4 pb-2 border-solid border-0 border-b border-b-chalkboard-20 dark:border-b-chalkboard-80">
 | 
			
		||||
        <CustomIcon
 | 
			
		||||
          name="search"
 | 
			
		||||
          className="w-5 h-5 bg-energy-10/50 dark:bg-chalkboard-90 dark:text-energy-10"
 | 
			
		||||
        />
 | 
			
		||||
        <Combobox.Input
 | 
			
		||||
          onChange={(event) => setQuery(event.target.value)}
 | 
			
		||||
          className="w-full bg-transparent focus:outline-none selection:bg-energy-10/50 dark:selection:bg-energy-10/20 dark:focus:outline-none"
 | 
			
		||||
          onKeyDown={(event) => {
 | 
			
		||||
            if (
 | 
			
		||||
              (event.metaKey && event.key === 'k') ||
 | 
			
		||||
              (event.key === 'Backspace' && !event.currentTarget.value)
 | 
			
		||||
            ) {
 | 
			
		||||
              commandBarSend({ type: 'Close' })
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
          placeholder={
 | 
			
		||||
            (defaultOption && defaultOption.name) ||
 | 
			
		||||
            placeholder ||
 | 
			
		||||
            'Search commands'
 | 
			
		||||
          }
 | 
			
		||||
          autoCapitalize="off"
 | 
			
		||||
          autoComplete="off"
 | 
			
		||||
          autoCorrect="off"
 | 
			
		||||
          spellCheck="false"
 | 
			
		||||
          autoFocus
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
      <Combobox.Options
 | 
			
		||||
        static
 | 
			
		||||
        className="overflow-y-auto max-h-96 cursor-pointer"
 | 
			
		||||
      >
 | 
			
		||||
        {filteredOptions?.map((option) => (
 | 
			
		||||
          <Combobox.Option
 | 
			
		||||
            key={option.name}
 | 
			
		||||
            value={option}
 | 
			
		||||
            className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
 | 
			
		||||
          >
 | 
			
		||||
            {'icon' in option && option.icon && (
 | 
			
		||||
              <CustomIcon
 | 
			
		||||
                name={option.icon}
 | 
			
		||||
                className="w-5 h-5 dark:text-energy-10"
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            <p className="flex-grow">{option.name} </p>
 | 
			
		||||
          </Combobox.Option>
 | 
			
		||||
        ))}
 | 
			
		||||
      </Combobox.Options>
 | 
			
		||||
    </Combobox>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandComboBox
 | 
			
		||||
@ -3,6 +3,7 @@ export type CustomIconName =
 | 
			
		||||
  | 'arrowLeft'
 | 
			
		||||
  | 'arrowRight'
 | 
			
		||||
  | 'arrowUp'
 | 
			
		||||
  | 'checkmark'
 | 
			
		||||
  | 'close'
 | 
			
		||||
  | 'equal'
 | 
			
		||||
  | 'extrude'
 | 
			
		||||
@ -90,6 +91,22 @@ export const CustomIcon = ({
 | 
			
		||||
          />
 | 
			
		||||
        </svg>
 | 
			
		||||
      )
 | 
			
		||||
    case 'checkmark':
 | 
			
		||||
      return (
 | 
			
		||||
        <svg
 | 
			
		||||
          {...props}
 | 
			
		||||
          viewBox="0 0 20 20"
 | 
			
		||||
          fill="none"
 | 
			
		||||
          xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
        >
 | 
			
		||||
          <path
 | 
			
		||||
            fillRule="evenodd"
 | 
			
		||||
            clipRule="evenodd"
 | 
			
		||||
            d="M8.29956 13.5388L13.9537 6L14.7537 6.6L8.75367 14.6L8.00012 14.6536L5 11.6536L5.70709 10.9465L8.29956 13.5388Z"
 | 
			
		||||
            fill="currentColor"
 | 
			
		||||
          />
 | 
			
		||||
        </svg>
 | 
			
		||||
      )
 | 
			
		||||
    case 'close':
 | 
			
		||||
      return (
 | 
			
		||||
        <svg
 | 
			
		||||
 | 
			
		||||
@ -10,45 +10,48 @@ const DownloadAppBanner = () => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog
 | 
			
		||||
      className="fixed inset-0 top-auto z-50 bg-warn-20 text-warn-80 px-8 py-4"
 | 
			
		||||
      className="fixed inset-0 z-50"
 | 
			
		||||
      open={!isBannerDismissed}
 | 
			
		||||
      onClose={() => ({})}
 | 
			
		||||
    >
 | 
			
		||||
      <Dialog.Panel className="max-w-3xl mx-auto">
 | 
			
		||||
        <div className="flex gap-2 justify-between items-start">
 | 
			
		||||
          <h2 className="text-xl font-bold mb-4">
 | 
			
		||||
            KittyCAD Modeling App is better as a desktop app!
 | 
			
		||||
          </h2>
 | 
			
		||||
          <ActionButton
 | 
			
		||||
            Element="button"
 | 
			
		||||
            onClick={() => setBannerDismissed(true)}
 | 
			
		||||
            icon={{
 | 
			
		||||
              icon: 'close',
 | 
			
		||||
              className: 'p-1',
 | 
			
		||||
              bgClassName:
 | 
			
		||||
                'bg-warn-70 hover:bg-warn-80 dark:bg-warn-70 dark:hover:bg-warn-80',
 | 
			
		||||
              iconClassName:
 | 
			
		||||
                'text-warn-10 group-hover:text-warn-10 dark:text-warn-10 dark:group-hover:text-warn-10',
 | 
			
		||||
            }}
 | 
			
		||||
            className="!p-0 !bg-transparent !border-transparent"
 | 
			
		||||
          />
 | 
			
		||||
      <Dialog.Overlay className="fixed inset-0 bg-chalkboard-100/50" />
 | 
			
		||||
      <Dialog.Panel className="absolute inset-0 top-auto bg-warn-20 text-warn-80 px-8 py-4">
 | 
			
		||||
        <div className="max-w-3xl mx-auto">
 | 
			
		||||
          <div className="flex gap-2 justify-between items-start">
 | 
			
		||||
            <h2 className="text-xl font-bold mb-4">
 | 
			
		||||
              KittyCAD Modeling App is better as a desktop app!
 | 
			
		||||
            </h2>
 | 
			
		||||
            <ActionButton
 | 
			
		||||
              Element="button"
 | 
			
		||||
              onClick={() => setBannerDismissed(true)}
 | 
			
		||||
              icon={{
 | 
			
		||||
                icon: 'close',
 | 
			
		||||
                className: 'p-1',
 | 
			
		||||
                bgClassName:
 | 
			
		||||
                  'bg-warn-70 hover:bg-warn-80 dark:bg-warn-70 dark:hover:bg-warn-80',
 | 
			
		||||
                iconClassName:
 | 
			
		||||
                  'text-warn-10 group-hover:text-warn-10 dark:text-warn-10 dark:group-hover:text-warn-10',
 | 
			
		||||
              }}
 | 
			
		||||
              className="!p-0 !bg-transparent !border-transparent"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <p>
 | 
			
		||||
            The browser version of the app only saves your data temporarily in{' '}
 | 
			
		||||
            <code className="text-base inline-block px-0.5 bg-warn-30/50 rounded">
 | 
			
		||||
              localStorage
 | 
			
		||||
            </code>
 | 
			
		||||
            , and isn't backed up anywhere! Visit{' '}
 | 
			
		||||
            <a
 | 
			
		||||
              href="https://kittycad.io/modeling-app/download"
 | 
			
		||||
              rel="noopener noreferrer"
 | 
			
		||||
              target="_blank"
 | 
			
		||||
              className="!text-warn-80 dark:!text-warn-80 dark:hover:!text-warn-70 underline"
 | 
			
		||||
            >
 | 
			
		||||
              our website
 | 
			
		||||
            </a>{' '}
 | 
			
		||||
            to download the app for the best experience.
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
        <p>
 | 
			
		||||
          The browser version of the app only saves your data temporarily in{' '}
 | 
			
		||||
          <code className="text-base inline-block px-0.5 bg-warn-30/50 rounded">
 | 
			
		||||
            localStorage
 | 
			
		||||
          </code>
 | 
			
		||||
          , and isn't backed up anywhere! Visit{' '}
 | 
			
		||||
          <a
 | 
			
		||||
            href="https://kittycad.io/modeling-app/download"
 | 
			
		||||
            rel="noopener noreferrer"
 | 
			
		||||
            target="_blank"
 | 
			
		||||
            className="text-warn-80 dark:text-warn-80 dark:hover:text-warn-70 underline"
 | 
			
		||||
          >
 | 
			
		||||
            our website
 | 
			
		||||
          </a>{' '}
 | 
			
		||||
          to download the app for the best experience.
 | 
			
		||||
        </p>
 | 
			
		||||
      </Dialog.Panel>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,7 @@ export const FileMachineProvider = ({
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
}) => {
 | 
			
		||||
  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<typeof fileMachine>
 | 
			
		||||
      ) => {
 | 
			
		||||
        if (event.data && 'name' in event.data) {
 | 
			
		||||
          setCommandBarOpen(false)
 | 
			
		||||
          commandBarSend({ type: 'Close' })
 | 
			
		||||
          navigate(
 | 
			
		||||
            `${paths.FILE}/${encodeURIComponent(
 | 
			
		||||
              context.selectedDirectory + sep + event.data.name
 | 
			
		||||
 | 
			
		||||
@ -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<T extends AnyStateMachine> = {
 | 
			
		||||
  state: StateFrom<T>
 | 
			
		||||
@ -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 (
 | 
			
		||||
 | 
			
		||||
@ -15,23 +15,23 @@ const Loading = ({ children }: React.PropsWithChildren) => {
 | 
			
		||||
      data-testid="loading"
 | 
			
		||||
    >
 | 
			
		||||
      <svg viewBox="0 0 10 10" className="w-8 h-8">
 | 
			
		||||
        <circle cx="5" cy="5" r="4" stroke="var(--liquid-20)" fill="none" />
 | 
			
		||||
        <circle cx="5" cy="5" r="4" stroke="var(--energy-50)" fill="none" />
 | 
			
		||||
        <circle
 | 
			
		||||
          cx="5"
 | 
			
		||||
          cy="5"
 | 
			
		||||
          r="4"
 | 
			
		||||
          stroke="var(--liquid-10)"
 | 
			
		||||
          stroke="var(--energy-10)"
 | 
			
		||||
          fill="none"
 | 
			
		||||
          strokeDasharray="4, 4"
 | 
			
		||||
          className="animate-spin origin-center"
 | 
			
		||||
        />
 | 
			
		||||
      </svg>
 | 
			
		||||
      <p className="text-base mt-4 text-liquid-80 dark:text-liquid-20">
 | 
			
		||||
      <p className="text-base mt-4 text-energy-80 dark:text-energy-30">
 | 
			
		||||
        {children || 'Loading'}
 | 
			
		||||
      </p>
 | 
			
		||||
      <p
 | 
			
		||||
        className={
 | 
			
		||||
          'text-sm mt-4 text-liquid-90 dark:text-liquid-10 transition-opacity duration-500' +
 | 
			
		||||
          'text-sm mt-4 text-energy-70 dark:text-energy-50 transition-opacity duration-500' +
 | 
			
		||||
          (hasLongLoadTime ? ' opacity-100' : ' opacity-0')
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
 | 
			
		||||
@ -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<T extends AnyStateMachine> = {
 | 
			
		||||
  state: StateFrom<T>
 | 
			
		||||
@ -59,6 +66,7 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
}) => {
 | 
			
		||||
  const { auth } = useGlobalStateContext()
 | 
			
		||||
  const { code } = useKclContext()
 | 
			
		||||
  const token = auth?.context?.token
 | 
			
		||||
  const streamRef = useRef<HTMLDivElement>(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<PipeExpression>(
 | 
			
		||||
            astWithUpdatedSource,
 | 
			
		||||
            _pathToNode
 | 
			
		||||
          ).node
 | 
			
		||||
          const startProfileAtCallExp = updatedPipeNode.body.find(
 | 
			
		||||
            (exp) =>
 | 
			
		||||
              exp.type === 'CallExpression' &&
 | 
			
		||||
              exp.callee.name === 'startProfileAt'
 | 
			
		||||
          )
 | 
			
		||||
          if (startProfileAtCallExp)
 | 
			
		||||
            engineCommandManager.artifactMap[sketchEnginePathId] = {
 | 
			
		||||
              type: 'result',
 | 
			
		||||
              range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
 | 
			
		||||
              commandType: 'start_path',
 | 
			
		||||
              data: null,
 | 
			
		||||
              raw: {} as any,
 | 
			
		||||
        '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<CallExpression>(
 | 
			
		||||
            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<PipeExpression>(
 | 
			
		||||
              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<CallExpression>(
 | 
			
		||||
                kclManager.ast,
 | 
			
		||||
                newSketchLn.pathToNode
 | 
			
		||||
              ).node
 | 
			
		||||
              if (segmentId)
 | 
			
		||||
                engineCommandManager.artifactMap[segmentId] = {
 | 
			
		||||
                  type: 'result',
 | 
			
		||||
                  range: [lineCallExp.start, lineCallExp.end],
 | 
			
		||||
                  commandType: 'extend_path',
 | 
			
		||||
                  parentId: sketchEnginePathId,
 | 
			
		||||
                  data: null,
 | 
			
		||||
                  raw: {} as any,
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
          } else {
 | 
			
		||||
            _modifiedAst = addCloseToPipe({
 | 
			
		||||
              node: kclManager.ast,
 | 
			
		||||
              programMemory: kclManager.programMemory,
 | 
			
		||||
              pathToNode: sketchPathToNode,
 | 
			
		||||
            })
 | 
			
		||||
            engineCommandManager.sendSceneCommand({
 | 
			
		||||
              type: 'modeling_cmd_req',
 | 
			
		||||
              cmd_id: uuidv4(),
 | 
			
		||||
              cmd: { type: 'edit_mode_exit' },
 | 
			
		||||
            })
 | 
			
		||||
            engineCommandManager.sendSceneCommand({
 | 
			
		||||
              type: 'modeling_cmd_req',
 | 
			
		||||
              cmd_id: uuidv4(),
 | 
			
		||||
              cmd: { type: 'default_camera_disable_sketch_mode' },
 | 
			
		||||
            })
 | 
			
		||||
            kclManager.executeAstMock(_modifiedAst, true)
 | 
			
		||||
            // updateAst(_modifiedAst, true)
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        'sketch exit execute': () => {
 | 
			
		||||
          kclManager.executeAst()
 | 
			
		||||
        },
 | 
			
		||||
        'set tool': () => {}, // TODO
 | 
			
		||||
        'Set selection': assign(({ selectionRanges }, event) => {
 | 
			
		||||
          if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
 | 
			
		||||
          const setSelections = event.data
 | 
			
		||||
          if (!editorView) return {}
 | 
			
		||||
          if (setSelections.selectionType === 'mirrorCodeMirrorSelections')
 | 
			
		||||
            return { selectionRanges: setSelections.selection }
 | 
			
		||||
          else if (setSelections.selectionType === 'otherSelection') {
 | 
			
		||||
            // TODO KittyCAD/engine/issues/1620: send axis highlight when it's working (if that's what we settle on)
 | 
			
		||||
            // const axisAddCmd: EngineCommand = {
 | 
			
		||||
            //   type: 'modeling_cmd_req',
 | 
			
		||||
            //   cmd: {
 | 
			
		||||
            //     type: 'highlight_set_entities',
 | 
			
		||||
            //     entities: [
 | 
			
		||||
            //       setSelections.selection === 'x-axis'
 | 
			
		||||
            //         ? X_AXIS_UUID
 | 
			
		||||
            //         : Y_AXIS_UUID,
 | 
			
		||||
            //     ],
 | 
			
		||||
            //   },
 | 
			
		||||
            //   cmd_id: uuidv4(),
 | 
			
		||||
            // }
 | 
			
		||||
 | 
			
		||||
            // if (!isShiftDown) {
 | 
			
		||||
            //   engineCommandManager
 | 
			
		||||
            //     .sendSceneCommand({
 | 
			
		||||
            //       type: 'modeling_cmd_req',
 | 
			
		||||
            //       cmd: {
 | 
			
		||||
            //         type: 'select_clear',
 | 
			
		||||
            //       },
 | 
			
		||||
            //       cmd_id: uuidv4(),
 | 
			
		||||
            //     })
 | 
			
		||||
            //     .then(() => {
 | 
			
		||||
            //       engineCommandManager.sendSceneCommand(axisAddCmd)
 | 
			
		||||
            //     })
 | 
			
		||||
            // } else {
 | 
			
		||||
            //   engineCommandManager.sendSceneCommand(axisAddCmd)
 | 
			
		||||
            // }
 | 
			
		||||
 | 
			
		||||
            const {
 | 
			
		||||
              codeMirrorSelection,
 | 
			
		||||
              selectionRangeTypeMap,
 | 
			
		||||
              otherSelections,
 | 
			
		||||
            } = handleSelectionWithShift({
 | 
			
		||||
              otherSelection: setSelections.selection,
 | 
			
		||||
              currentSelections: selectionRanges,
 | 
			
		||||
              isShiftDown,
 | 
			
		||||
            })
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              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<SetSelections> => {
 | 
			
		||||
          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<SetSelections> => {
 | 
			
		||||
        const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
          await applyConstraintHorzVertDistance({
 | 
			
		||||
            constraint: 'setHorzDistance',
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
          })
 | 
			
		||||
        await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
        return {
 | 
			
		||||
          selectionType: 'completeSelection',
 | 
			
		||||
          selection: pathMapToSelections(
 | 
			
		||||
            kclManager.ast,
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
            pathToNodeMap
 | 
			
		||||
          ),
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      'Get vertical info': async ({
 | 
			
		||||
        selectionRanges,
 | 
			
		||||
      }): Promise<SetSelections> => {
 | 
			
		||||
        const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
          await applyConstraintHorzVertDistance({
 | 
			
		||||
            constraint: 'setVertDistance',
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
          })
 | 
			
		||||
        await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
        return {
 | 
			
		||||
          selectionType: 'completeSelection',
 | 
			
		||||
          selection: pathMapToSelections(
 | 
			
		||||
            kclManager.ast,
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
            pathToNodeMap
 | 
			
		||||
          ),
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      'Get angle info': async ({ selectionRanges }): Promise<SetSelections> => {
 | 
			
		||||
        const { modifiedAst, pathToNodeMap } = await (angleBetweenInfo({
 | 
			
		||||
        },
 | 
			
		||||
        'Get vertical info': async ({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
        }).enabled
 | 
			
		||||
          ? applyConstraintAngleBetween({
 | 
			
		||||
        }): Promise<SetSelections> => {
 | 
			
		||||
          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<SetSelections> => {
 | 
			
		||||
        const { modifiedAst, pathToNodeMap } = await applyConstraintAngleLength(
 | 
			
		||||
          { selectionRanges }
 | 
			
		||||
        )
 | 
			
		||||
        await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
        return {
 | 
			
		||||
          selectionType: 'completeSelection',
 | 
			
		||||
          selection: pathMapToSelections(
 | 
			
		||||
            kclManager.ast,
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
            pathToNodeMap
 | 
			
		||||
          ),
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      'Get perpendicular distance info': async ({
 | 
			
		||||
        selectionRanges,
 | 
			
		||||
      }): Promise<SetSelections> => {
 | 
			
		||||
        const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect({
 | 
			
		||||
              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<SetSelections> => {
 | 
			
		||||
        const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance(
 | 
			
		||||
          {
 | 
			
		||||
            constraint: 'xAbs',
 | 
			
		||||
        }): Promise<SetSelections> => {
 | 
			
		||||
          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<SetSelections> => {
 | 
			
		||||
        const { modifiedAst, pathToNodeMap } = await applyConstraintAbsDistance(
 | 
			
		||||
          {
 | 
			
		||||
            constraint: 'yAbs',
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
        },
 | 
			
		||||
        'Get length info': async ({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
        }): Promise<SetSelections> => {
 | 
			
		||||
          const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
            await applyConstraintAngleLength({ selectionRanges })
 | 
			
		||||
          await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
          return {
 | 
			
		||||
            selectionType: 'completeSelection',
 | 
			
		||||
            selection: pathMapToSelections(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            ),
 | 
			
		||||
          }
 | 
			
		||||
        )
 | 
			
		||||
        await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
        return {
 | 
			
		||||
          selectionType: 'completeSelection',
 | 
			
		||||
          selection: pathMapToSelections(
 | 
			
		||||
            kclManager.ast,
 | 
			
		||||
            selectionRanges,
 | 
			
		||||
            pathToNodeMap
 | 
			
		||||
          ),
 | 
			
		||||
        }
 | 
			
		||||
        },
 | 
			
		||||
        'Get perpendicular distance info': async ({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
        }): Promise<SetSelections> => {
 | 
			
		||||
          const { modifiedAst, pathToNodeMap } = await applyConstraintIntersect(
 | 
			
		||||
            {
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
            }
 | 
			
		||||
          )
 | 
			
		||||
          await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
          return {
 | 
			
		||||
            selectionType: 'completeSelection',
 | 
			
		||||
            selection: pathMapToSelections(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            ),
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        'Get ABS X info': async ({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
        }): Promise<SetSelections> => {
 | 
			
		||||
          const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
            await applyConstraintAbsDistance({
 | 
			
		||||
              constraint: 'xAbs',
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
            })
 | 
			
		||||
          await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
          return {
 | 
			
		||||
            selectionType: 'completeSelection',
 | 
			
		||||
            selection: pathMapToSelections(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            ),
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        'Get ABS Y info': async ({
 | 
			
		||||
          selectionRanges,
 | 
			
		||||
        }): Promise<SetSelections> => {
 | 
			
		||||
          const { modifiedAst, pathToNodeMap } =
 | 
			
		||||
            await applyConstraintAbsDistance({
 | 
			
		||||
              constraint: 'yAbs',
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
            })
 | 
			
		||||
          await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
          return {
 | 
			
		||||
            selectionType: 'completeSelection',
 | 
			
		||||
            selection: pathMapToSelections(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              selectionRanges,
 | 
			
		||||
              pathToNodeMap
 | 
			
		||||
            ),
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    devTools: true,
 | 
			
		||||
  })
 | 
			
		||||
      devTools: true,
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    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 (
 | 
			
		||||
    <ModelingMachineContext.Provider
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { fireEvent, render, screen } from '@testing-library/react'
 | 
			
		||||
import { BrowserRouter } from 'react-router-dom'
 | 
			
		||||
import { GlobalStateProvider } from './GlobalStateProvider'
 | 
			
		||||
import CommandBarProvider from './CommandBar'
 | 
			
		||||
import CommandBarProvider from './CommandBar/CommandBar'
 | 
			
		||||
import {
 | 
			
		||||
  NETWORK_CONTENT,
 | 
			
		||||
  NetworkHealthIndicator,
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,4 @@
 | 
			
		||||
import {
 | 
			
		||||
  faCheck,
 | 
			
		||||
  faExclamation,
 | 
			
		||||
  faWifi,
 | 
			
		||||
} from '@fortawesome/free-solid-svg-icons'
 | 
			
		||||
import { faExclamation, faWifi } from '@fortawesome/free-solid-svg-icons'
 | 
			
		||||
import { Popover } from '@headlessui/react'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { ActionIcon } from './ActionIcon'
 | 
			
		||||
@ -77,8 +73,8 @@ export const NetworkHealthIndicator = () => {
 | 
			
		||||
            data-testid="network-good"
 | 
			
		||||
          >
 | 
			
		||||
            <ActionIcon
 | 
			
		||||
              icon={faCheck}
 | 
			
		||||
              bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded'}
 | 
			
		||||
              icon="checkmark"
 | 
			
		||||
              bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded-sm'}
 | 
			
		||||
              iconClassName={'text-succeed-80 dark:text-succeed-30'}
 | 
			
		||||
            />
 | 
			
		||||
            {NETWORK_CONTENT.good}
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
                  >
 | 
			
		||||
 | 
			
		||||
@ -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 = {
 | 
			
		||||
 | 
			
		||||
@ -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 = {
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@ -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']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { CommandsContext } from 'components/CommandBar'
 | 
			
		||||
import { CommandsContext } from 'components/CommandBar/CommandBar'
 | 
			
		||||
import { useContext } from 'react'
 | 
			
		||||
 | 
			
		||||
export const useCommandsContext = () => {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								src/hooks/usePlatform.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,27 @@
 | 
			
		||||
import { Platform, platform } from '@tauri-apps/api/os'
 | 
			
		||||
import { isTauri } from 'lib/isTauri'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
 | 
			
		||||
export default function usePlatform() {
 | 
			
		||||
  const [platformName, setPlatformName] = useState<Platform | ''>('')
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    async function getPlatform() {
 | 
			
		||||
      setPlatformName(await platform())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isTauri()) {
 | 
			
		||||
      void getPlatform()
 | 
			
		||||
    } else {
 | 
			
		||||
      if (navigator.userAgent.indexOf('Mac') !== -1) {
 | 
			
		||||
        setPlatformName('darwin')
 | 
			
		||||
      } else if (navigator.userAgent.indexOf('Win') !== -1) {
 | 
			
		||||
        setPlatformName('win32')
 | 
			
		||||
      } else if (navigator.userAgent.indexOf('Linux') !== -1) {
 | 
			
		||||
        setPlatformName('linux')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [setPlatformName])
 | 
			
		||||
 | 
			
		||||
  return platformName
 | 
			
		||||
}
 | 
			
		||||
@ -1,46 +1,68 @@
 | 
			
		||||
import { useEffect } from 'react'
 | 
			
		||||
import { 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<T extends AnyStateMachine> {
 | 
			
		||||
// This might not be necessary, AnyStateMachine from xstate is working
 | 
			
		||||
export type AllMachines =
 | 
			
		||||
  | typeof modelingMachine
 | 
			
		||||
  | typeof settingsMachine
 | 
			
		||||
  | typeof authMachine
 | 
			
		||||
  | typeof homeMachine
 | 
			
		||||
 | 
			
		||||
interface UseStateMachineCommandsArgs<
 | 
			
		||||
  T extends AllMachines,
 | 
			
		||||
  S extends CommandSetSchema<T>
 | 
			
		||||
> {
 | 
			
		||||
  machineId: T['id']
 | 
			
		||||
  state: StateFrom<T>
 | 
			
		||||
  send: Function
 | 
			
		||||
  commandBarConfig?: CommandBarConfig<T>
 | 
			
		||||
  commands: Command[]
 | 
			
		||||
  owner: string
 | 
			
		||||
  actor?: InterpreterFrom<T>
 | 
			
		||||
  commandBarConfig?: CommandSetConfig<T, S>
 | 
			
		||||
  onCancel?: () => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function useStateMachineCommands<T extends AnyStateMachine>({
 | 
			
		||||
export default function useStateMachineCommands<
 | 
			
		||||
  T extends AnyStateMachine,
 | 
			
		||||
  S extends CommandSetSchema<T>
 | 
			
		||||
>({
 | 
			
		||||
  machineId,
 | 
			
		||||
  state,
 | 
			
		||||
  send,
 | 
			
		||||
  actor,
 | 
			
		||||
  commandBarConfig,
 | 
			
		||||
  owner,
 | 
			
		||||
}: UseStateMachineCommandsArgs<T>) {
 | 
			
		||||
  const { addCommands, removeCommands } = useCommandsContext()
 | 
			
		||||
  onCancel,
 | 
			
		||||
}: UseStateMachineCommandsArgs<T, S>) {
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const newCommands = state.nextEvents
 | 
			
		||||
      .filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
 | 
			
		||||
      .map((type) =>
 | 
			
		||||
        createMachineCommand<T>({
 | 
			
		||||
        createMachineCommand<T, S>({
 | 
			
		||||
          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])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
 | 
			
		||||
 | 
			
		||||
  const extrudeCall = createCallExpressionStdLib('extrude', [
 | 
			
		||||
    createLiteral(4),
 | 
			
		||||
    createLiteral(distance),
 | 
			
		||||
    shouldPipe
 | 
			
		||||
      ? createPipeSubstitution()
 | 
			
		||||
      : {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								src/lib/commandBarConfigs/authCommandConfig.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,17 @@
 | 
			
		||||
import { CommandSetConfig } from 'lib/commandTypes'
 | 
			
		||||
import { authMachine } from 'machines/authMachine'
 | 
			
		||||
 | 
			
		||||
type AuthCommandSchema = {}
 | 
			
		||||
 | 
			
		||||
export const authCommandBarConfig: CommandSetConfig<
 | 
			
		||||
  typeof authMachine,
 | 
			
		||||
  AuthCommandSchema
 | 
			
		||||
> = {
 | 
			
		||||
  'Log in': {
 | 
			
		||||
    hide: 'both',
 | 
			
		||||
  },
 | 
			
		||||
  'Log out': {
 | 
			
		||||
    args: [],
 | 
			
		||||
    icon: 'arrowLeft',
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										87
									
								
								src/lib/commandBarConfigs/homeCommandConfig.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,87 @@
 | 
			
		||||
import { CommandSetConfig } from 'lib/commandTypes'
 | 
			
		||||
import { homeMachine } from 'machines/homeMachine'
 | 
			
		||||
 | 
			
		||||
export type HomeCommandSchema = {
 | 
			
		||||
  'Create project': {
 | 
			
		||||
    name: string
 | 
			
		||||
  }
 | 
			
		||||
  'Open project': {
 | 
			
		||||
    name: string
 | 
			
		||||
  }
 | 
			
		||||
  'Delete project': {
 | 
			
		||||
    name: string
 | 
			
		||||
  }
 | 
			
		||||
  'Rename project': {
 | 
			
		||||
    oldName: string
 | 
			
		||||
    newName: string
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const homeCommandBarConfig: CommandSetConfig<
 | 
			
		||||
  typeof homeMachine,
 | 
			
		||||
  HomeCommandSchema
 | 
			
		||||
> = {
 | 
			
		||||
  'Open project': {
 | 
			
		||||
    icon: 'arrowRight',
 | 
			
		||||
    description: 'Open a project',
 | 
			
		||||
    args: {
 | 
			
		||||
      name: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        options: (context) =>
 | 
			
		||||
          context.projects.map((p) => ({
 | 
			
		||||
            name: p.name!,
 | 
			
		||||
            value: p.name!,
 | 
			
		||||
          })),
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Create project': {
 | 
			
		||||
    icon: 'folderPlus',
 | 
			
		||||
    description: 'Create a project',
 | 
			
		||||
    args: {
 | 
			
		||||
      name: {
 | 
			
		||||
        inputType: 'string',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: (context) => context.defaultProjectName,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Delete project': {
 | 
			
		||||
    icon: 'close',
 | 
			
		||||
    description: 'Delete a project',
 | 
			
		||||
    needsReview: true,
 | 
			
		||||
    args: {
 | 
			
		||||
      name: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        options: (context) =>
 | 
			
		||||
          context.projects.map((p) => ({
 | 
			
		||||
            name: p.name!,
 | 
			
		||||
            value: p.name!,
 | 
			
		||||
          })),
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Rename project': {
 | 
			
		||||
    icon: 'folder',
 | 
			
		||||
    description: 'Rename a project',
 | 
			
		||||
    needsReview: true,
 | 
			
		||||
    args: {
 | 
			
		||||
      oldName: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        options: (context) =>
 | 
			
		||||
          context.projects.map((p) => ({
 | 
			
		||||
            name: p.name!,
 | 
			
		||||
            value: p.name!,
 | 
			
		||||
          })),
 | 
			
		||||
      },
 | 
			
		||||
      newName: {
 | 
			
		||||
        inputType: 'string',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: (context) => context.defaultProjectName,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								src/lib/commandBarConfigs/modelingCommandConfig.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,57 @@
 | 
			
		||||
import { CommandSetConfig } from 'lib/commandTypes'
 | 
			
		||||
import { Selections } from 'lib/selections'
 | 
			
		||||
import { modelingMachine } from 'machines/modelingMachine'
 | 
			
		||||
 | 
			
		||||
export const EXTRUSION_RESULTS = [
 | 
			
		||||
  'new',
 | 
			
		||||
  'add',
 | 
			
		||||
  'subtract',
 | 
			
		||||
  'intersect',
 | 
			
		||||
] as const
 | 
			
		||||
 | 
			
		||||
export type ModelingCommandSchema = {
 | 
			
		||||
  'Enter sketch': {}
 | 
			
		||||
  Extrude: {
 | 
			
		||||
    selection: Selections // & { type: 'face' } would be cool to lock that down
 | 
			
		||||
    // result: (typeof EXTRUSION_RESULTS)[number]
 | 
			
		||||
    distance: number
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const modelingMachineConfig: CommandSetConfig<
 | 
			
		||||
  typeof modelingMachine,
 | 
			
		||||
  ModelingCommandSchema
 | 
			
		||||
> = {
 | 
			
		||||
  'Enter sketch': {
 | 
			
		||||
    description: 'Enter sketch mode.',
 | 
			
		||||
    icon: 'sketch',
 | 
			
		||||
  },
 | 
			
		||||
  Extrude: {
 | 
			
		||||
    description: 'Pull a sketch into 3D along its normal or perpendicular.',
 | 
			
		||||
    icon: 'extrude',
 | 
			
		||||
    needsReview: true,
 | 
			
		||||
    args: {
 | 
			
		||||
      selection: {
 | 
			
		||||
        inputType: 'selection',
 | 
			
		||||
        selectionTypes: ['face'],
 | 
			
		||||
        multiple: false, // TODO: multiple selection
 | 
			
		||||
        required: true,
 | 
			
		||||
      },
 | 
			
		||||
      // result: {
 | 
			
		||||
      //   inputType: 'options',
 | 
			
		||||
      //   payload: 'add',
 | 
			
		||||
      //   required: true,
 | 
			
		||||
      //   options: EXTRUSION_RESULTS.map((r) => ({
 | 
			
		||||
      //     name: r,
 | 
			
		||||
      //     isCurrent: r === 'add',
 | 
			
		||||
      //     value: r,
 | 
			
		||||
      //   })),
 | 
			
		||||
      // },
 | 
			
		||||
      distance: {
 | 
			
		||||
        inputType: 'number',
 | 
			
		||||
        defaultValue: 5,
 | 
			
		||||
        required: true,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										141
									
								
								src/lib/commandBarConfigs/settingsCommandConfig.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,141 @@
 | 
			
		||||
import { CommandSetConfig } from '../commandTypes'
 | 
			
		||||
import {
 | 
			
		||||
  BaseUnit,
 | 
			
		||||
  Toggle,
 | 
			
		||||
  UnitSystem,
 | 
			
		||||
  baseUnitsUnion,
 | 
			
		||||
  settingsMachine,
 | 
			
		||||
} from 'machines/settingsMachine'
 | 
			
		||||
import { CameraSystem, cameraSystems } from '../cameraControls'
 | 
			
		||||
import { Themes } from '../theme'
 | 
			
		||||
 | 
			
		||||
// SETTINGS MACHINE
 | 
			
		||||
export type SettingsCommandSchema = {
 | 
			
		||||
  'Set Base Unit': {
 | 
			
		||||
    baseUnit: BaseUnit
 | 
			
		||||
  }
 | 
			
		||||
  'Set Camera Controls': {
 | 
			
		||||
    cameraControls: CameraSystem
 | 
			
		||||
  }
 | 
			
		||||
  'Set Default Project Name': {
 | 
			
		||||
    defaultProjectName: string
 | 
			
		||||
  }
 | 
			
		||||
  'Set Text Wrapping': {
 | 
			
		||||
    textWrapping: Toggle
 | 
			
		||||
  }
 | 
			
		||||
  'Set Theme': {
 | 
			
		||||
    theme: Themes
 | 
			
		||||
  }
 | 
			
		||||
  'Set Unit System': {
 | 
			
		||||
    unitSystem: UnitSystem
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const settingsCommandBarConfig: CommandSetConfig<
 | 
			
		||||
  typeof settingsMachine,
 | 
			
		||||
  SettingsCommandSchema
 | 
			
		||||
> = {
 | 
			
		||||
  'Set Base Unit': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    args: {
 | 
			
		||||
      baseUnit: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: (context) => context.baseUnit,
 | 
			
		||||
        options: (context) =>
 | 
			
		||||
          Object.values(baseUnitsUnion).map((v) => ({
 | 
			
		||||
            name: v,
 | 
			
		||||
            value: v,
 | 
			
		||||
            isCurrent: v === context.baseUnit,
 | 
			
		||||
          })),
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Set Camera Controls': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    args: {
 | 
			
		||||
      cameraControls: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: (context) => context.cameraControls,
 | 
			
		||||
        options: (context) =>
 | 
			
		||||
          Object.values(cameraSystems).map((v) => ({
 | 
			
		||||
            name: v,
 | 
			
		||||
            value: v,
 | 
			
		||||
            isCurrent: v === context.cameraControls,
 | 
			
		||||
          })),
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Set Default Project Name': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    hide: 'web',
 | 
			
		||||
    args: {
 | 
			
		||||
      defaultProjectName: {
 | 
			
		||||
        inputType: 'string',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: (context) => context.defaultProjectName,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Set Text Wrapping': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    args: {
 | 
			
		||||
      textWrapping: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: (context) => context.textWrapping,
 | 
			
		||||
        options: (context) => [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'On',
 | 
			
		||||
            value: 'On' as Toggle,
 | 
			
		||||
            isCurrent: context.textWrapping === 'On',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'Off',
 | 
			
		||||
            value: 'Off' as Toggle,
 | 
			
		||||
            isCurrent: context.textWrapping === 'Off',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Set Theme': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    args: {
 | 
			
		||||
      theme: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: (context) => context.theme,
 | 
			
		||||
        options: (context) =>
 | 
			
		||||
          Object.values(Themes).map((v) => ({
 | 
			
		||||
            name: v,
 | 
			
		||||
            value: v,
 | 
			
		||||
            isCurrent: v === context.theme,
 | 
			
		||||
          })),
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Set Unit System': {
 | 
			
		||||
    icon: 'gear',
 | 
			
		||||
    args: {
 | 
			
		||||
      unitSystem: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: (context) => context.unitSystem,
 | 
			
		||||
        options: (context) => [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'Imperial',
 | 
			
		||||
            value: 'imperial' as UnitSystem,
 | 
			
		||||
            isCurrent: context.unitSystem === 'imperial',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'Metric',
 | 
			
		||||
            value: 'metric' as UnitSystem,
 | 
			
		||||
            isCurrent: context.unitSystem === 'metric',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										136
									
								
								src/lib/commandTypes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,136 @@
 | 
			
		||||
import { CustomIconName } from 'components/CustomIcon'
 | 
			
		||||
import { AllMachines } from 'hooks/useStateMachineCommands'
 | 
			
		||||
import {
 | 
			
		||||
  AnyStateMachine,
 | 
			
		||||
  ContextFrom,
 | 
			
		||||
  EventFrom,
 | 
			
		||||
  InterpreterFrom,
 | 
			
		||||
} from 'xstate'
 | 
			
		||||
import { Selection } from './selections'
 | 
			
		||||
 | 
			
		||||
type Icon = CustomIconName
 | 
			
		||||
const PLATFORMS = ['both', 'web', 'desktop'] as const
 | 
			
		||||
const INPUT_TYPES = ['options', 'string', 'number', 'selection'] as const
 | 
			
		||||
export type CommandInputType = (typeof INPUT_TYPES)[number]
 | 
			
		||||
 | 
			
		||||
export type CommandSetSchema<T extends AnyStateMachine> = Partial<{
 | 
			
		||||
  [EventType in EventFrom<T>['type']]: Record<string, any>
 | 
			
		||||
}>
 | 
			
		||||
 | 
			
		||||
export type CommandSet<
 | 
			
		||||
  T extends AllMachines,
 | 
			
		||||
  Schema extends CommandSetSchema<T>
 | 
			
		||||
> = Partial<{
 | 
			
		||||
  [EventType in EventFrom<T>['type']]: Command<
 | 
			
		||||
    T,
 | 
			
		||||
    EventFrom<T>['type'],
 | 
			
		||||
    Schema[EventType]
 | 
			
		||||
  >
 | 
			
		||||
}>
 | 
			
		||||
 | 
			
		||||
export type CommandSetConfig<
 | 
			
		||||
  T extends AllMachines,
 | 
			
		||||
  Schema extends CommandSetSchema<T>
 | 
			
		||||
> = Partial<{
 | 
			
		||||
  [EventType in EventFrom<T>['type']]: CommandConfig<
 | 
			
		||||
    T,
 | 
			
		||||
    EventFrom<T>['type'],
 | 
			
		||||
    Schema[EventType]
 | 
			
		||||
  >
 | 
			
		||||
}>
 | 
			
		||||
 | 
			
		||||
export type Command<
 | 
			
		||||
  T extends AnyStateMachine = AnyStateMachine,
 | 
			
		||||
  CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'],
 | 
			
		||||
  CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName]
 | 
			
		||||
> = {
 | 
			
		||||
  name: CommandName
 | 
			
		||||
  ownerMachine: T['id']
 | 
			
		||||
  needsReview: boolean
 | 
			
		||||
  onSubmit: (data?: CommandSchema) => void
 | 
			
		||||
  onCancel?: () => void
 | 
			
		||||
  args?: {
 | 
			
		||||
    [ArgName in keyof CommandSchema]: CommandArgument<CommandSchema[ArgName], T>
 | 
			
		||||
  }
 | 
			
		||||
  description?: string
 | 
			
		||||
  icon?: Icon
 | 
			
		||||
  hide?: (typeof PLATFORMS)[number]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type CommandConfig<
 | 
			
		||||
  T extends AnyStateMachine = AnyStateMachine,
 | 
			
		||||
  CommandName extends EventFrom<T>['type'] = EventFrom<T>['type'],
 | 
			
		||||
  CommandSchema extends CommandSetSchema<T>[CommandName] = CommandSetSchema<T>[CommandName]
 | 
			
		||||
> = Omit<
 | 
			
		||||
  Command<T, CommandName, CommandSchema>,
 | 
			
		||||
  'name' | 'ownerMachine' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview'
 | 
			
		||||
> & {
 | 
			
		||||
  needsReview?: true
 | 
			
		||||
  args?: {
 | 
			
		||||
    [ArgName in keyof CommandSchema]: CommandArgumentConfig<
 | 
			
		||||
      CommandSchema[ArgName],
 | 
			
		||||
      T
 | 
			
		||||
    >
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type CommandArgumentConfig<
 | 
			
		||||
  OutputType,
 | 
			
		||||
  T extends AnyStateMachine = AnyStateMachine
 | 
			
		||||
> =
 | 
			
		||||
  | {
 | 
			
		||||
      description?: string
 | 
			
		||||
      required: boolean
 | 
			
		||||
      skip?: true
 | 
			
		||||
      defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
 | 
			
		||||
      payload?: OutputType
 | 
			
		||||
    } & (
 | 
			
		||||
      | {
 | 
			
		||||
          inputType: Extract<CommandInputType, 'options'>
 | 
			
		||||
          options:
 | 
			
		||||
            | CommandArgumentOption<OutputType>[]
 | 
			
		||||
            | ((context: ContextFrom<T>) => CommandArgumentOption<OutputType>[])
 | 
			
		||||
        }
 | 
			
		||||
      | {
 | 
			
		||||
          inputType: Extract<CommandInputType, 'selection'>
 | 
			
		||||
          selectionTypes: Selection['type'][]
 | 
			
		||||
          multiple: boolean
 | 
			
		||||
        }
 | 
			
		||||
      | { inputType: Exclude<CommandInputType, 'options' | 'selection'> }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
export type CommandArgument<
 | 
			
		||||
  OutputType,
 | 
			
		||||
  T extends AnyStateMachine = AnyStateMachine
 | 
			
		||||
> =
 | 
			
		||||
  | {
 | 
			
		||||
      description?: string
 | 
			
		||||
      required: boolean
 | 
			
		||||
      payload?: OutputType // Payload sets the initialized value and more importantly its type
 | 
			
		||||
      defaultValue?: OutputType // Default value is used as the starting value for the input on this argument
 | 
			
		||||
    } & (
 | 
			
		||||
      | {
 | 
			
		||||
          inputType: Extract<CommandInputType, 'options'>
 | 
			
		||||
          options: CommandArgumentOption<OutputType>[]
 | 
			
		||||
        }
 | 
			
		||||
      | {
 | 
			
		||||
          inputType: Extract<CommandInputType, 'selection'>
 | 
			
		||||
          selectionTypes: Selection['type'][]
 | 
			
		||||
          actor: InterpreterFrom<T>
 | 
			
		||||
          multiple: boolean
 | 
			
		||||
        }
 | 
			
		||||
      | { inputType: Exclude<CommandInputType, 'options' | 'selection'> }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
export type CommandArgumentWithName<
 | 
			
		||||
  OutputType,
 | 
			
		||||
  T extends AnyStateMachine = AnyStateMachine
 | 
			
		||||
> = CommandArgument<OutputType, T> & {
 | 
			
		||||
  name: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type CommandArgumentOption<A> = {
 | 
			
		||||
  name: string
 | 
			
		||||
  isCurrent?: boolean
 | 
			
		||||
  value: A
 | 
			
		||||
}
 | 
			
		||||
@ -1,177 +0,0 @@
 | 
			
		||||
import { AnyStateMachine, ContextFrom, EventFrom, StateFrom } from 'xstate'
 | 
			
		||||
import { isTauri } from './isTauri'
 | 
			
		||||
import { CustomIconName } from 'components/CustomIcon'
 | 
			
		||||
 | 
			
		||||
type Icon = CustomIconName
 | 
			
		||||
type Platform = 'both' | 'web' | 'desktop'
 | 
			
		||||
type InputType = 'select' | 'string' | 'interaction'
 | 
			
		||||
export type CommandArgumentOption = { name: string; isCurrent?: boolean }
 | 
			
		||||
 | 
			
		||||
// Command arguments can either be defined manually
 | 
			
		||||
// or flagged as needing to be looked up from the context.
 | 
			
		||||
// This is useful for things like settings, where
 | 
			
		||||
// we want to show the current setting value as the default.
 | 
			
		||||
// The lookup is done in createMachineCommand.
 | 
			
		||||
type CommandArgumentConfig<T extends AnyStateMachine> = {
 | 
			
		||||
  name: string // TODO: I would love for this to be strongly-typed so we could guarantee it's a valid data payload key on the event type.
 | 
			
		||||
  type: InputType
 | 
			
		||||
  description?: string
 | 
			
		||||
} & (
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'select'
 | 
			
		||||
      options?: CommandArgumentOption[]
 | 
			
		||||
      getOptionsFromContext?: keyof ContextFrom<T>
 | 
			
		||||
      defaultValue?: string
 | 
			
		||||
      getDefaultValueFromContext?: keyof ContextFrom<T>
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'string'
 | 
			
		||||
      defaultValue?: string
 | 
			
		||||
      getDefaultValueFromContext?: keyof ContextFrom<T>
 | 
			
		||||
    }
 | 
			
		||||
  | { type: 'interaction' }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export type CommandBarConfig<T extends AnyStateMachine> = Partial<{
 | 
			
		||||
  [EventType in EventFrom<T>['type']]:
 | 
			
		||||
    | {
 | 
			
		||||
        args: CommandArgumentConfig<T>[]
 | 
			
		||||
        formatFunction?: (args: string[]) => string
 | 
			
		||||
        icon?: Icon
 | 
			
		||||
        hide?: Platform
 | 
			
		||||
      }
 | 
			
		||||
    | {
 | 
			
		||||
        hide?: Platform
 | 
			
		||||
      }
 | 
			
		||||
}>
 | 
			
		||||
 | 
			
		||||
export type Command = {
 | 
			
		||||
  owner: string
 | 
			
		||||
  name: string
 | 
			
		||||
  callback: Function
 | 
			
		||||
  icon?: Icon
 | 
			
		||||
  args?: CommandArgument[]
 | 
			
		||||
  formatFunction?: (args: string[]) => string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type CommandArgument = {
 | 
			
		||||
  name: string
 | 
			
		||||
  defaultValue?: string
 | 
			
		||||
} & (
 | 
			
		||||
  | {
 | 
			
		||||
      type: Extract<InputType, 'select'>
 | 
			
		||||
      options: CommandArgumentOption[]
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: Exclude<InputType, 'select'>
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
interface CreateMachineCommandProps<T extends AnyStateMachine> {
 | 
			
		||||
  type: EventFrom<T>['type']
 | 
			
		||||
  state: StateFrom<T>
 | 
			
		||||
  commandBarConfig?: CommandBarConfig<T>
 | 
			
		||||
  send: Function
 | 
			
		||||
  owner: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Creates a command with subcommands, ready for use in the CommandBar component,
 | 
			
		||||
// from a more terse Command Bar Meta definition.
 | 
			
		||||
export function createMachineCommand<T extends AnyStateMachine>({
 | 
			
		||||
  type,
 | 
			
		||||
  state,
 | 
			
		||||
  commandBarConfig,
 | 
			
		||||
  send,
 | 
			
		||||
  owner,
 | 
			
		||||
}: CreateMachineCommandProps<T>): Command | null {
 | 
			
		||||
  const lookedUpMeta = commandBarConfig && commandBarConfig[type]
 | 
			
		||||
  if (!lookedUpMeta) return null
 | 
			
		||||
 | 
			
		||||
  // Hide commands based on platform by returning `null`
 | 
			
		||||
  // so the consumer can filter them out
 | 
			
		||||
  if ('hide' in lookedUpMeta) {
 | 
			
		||||
    const { hide } = lookedUpMeta
 | 
			
		||||
    if (hide === 'both') return null
 | 
			
		||||
    else if (hide === 'desktop' && isTauri()) return null
 | 
			
		||||
    else if (hide === 'web' && !isTauri()) return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const icon = ('icon' in lookedUpMeta && lookedUpMeta.icon) || undefined
 | 
			
		||||
  const formatFunction =
 | 
			
		||||
    ('formatFunction' in lookedUpMeta && lookedUpMeta.formatFunction) ||
 | 
			
		||||
    undefined
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    name: type,
 | 
			
		||||
    owner,
 | 
			
		||||
    icon,
 | 
			
		||||
    callback: (data: EventFrom<T, typeof type>) => {
 | 
			
		||||
      if (data !== undefined && data !== null) {
 | 
			
		||||
        send(type, { data })
 | 
			
		||||
      } else {
 | 
			
		||||
        send(type)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    ...('args' in lookedUpMeta
 | 
			
		||||
      ? {
 | 
			
		||||
          args: getCommandArgumentValuesFromContext(state, lookedUpMeta.args),
 | 
			
		||||
          formatFunction,
 | 
			
		||||
        }
 | 
			
		||||
      : {}),
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getCommandArgumentValuesFromContext<T extends AnyStateMachine>(
 | 
			
		||||
  state: StateFrom<T>,
 | 
			
		||||
  args: CommandArgumentConfig<T>[]
 | 
			
		||||
): CommandArgument[] {
 | 
			
		||||
  function getDefaultValue(
 | 
			
		||||
    arg: CommandArgumentConfig<T> & { type: 'string' | 'select' }
 | 
			
		||||
  ) {
 | 
			
		||||
    if (
 | 
			
		||||
      arg.type === 'select' ||
 | 
			
		||||
      ('getDefaultValueFromContext' in arg && arg.getDefaultValueFromContext)
 | 
			
		||||
    ) {
 | 
			
		||||
      return state.context[arg.getDefaultValueFromContext]
 | 
			
		||||
    } else {
 | 
			
		||||
      return arg.defaultValue
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return args.map((arg) => {
 | 
			
		||||
    switch (arg.type) {
 | 
			
		||||
      case 'interaction':
 | 
			
		||||
        return {
 | 
			
		||||
          name: arg.name,
 | 
			
		||||
          type: 'interaction',
 | 
			
		||||
        }
 | 
			
		||||
      case 'string':
 | 
			
		||||
        return {
 | 
			
		||||
          name: arg.name,
 | 
			
		||||
          type: arg.type,
 | 
			
		||||
          defaultValue: arg.getDefaultValueFromContext
 | 
			
		||||
            ? state.context[arg.getDefaultValueFromContext]
 | 
			
		||||
            : arg.defaultValue,
 | 
			
		||||
        }
 | 
			
		||||
      default:
 | 
			
		||||
        return {
 | 
			
		||||
          name: arg.name,
 | 
			
		||||
          type: arg.type,
 | 
			
		||||
          defaultValue: getDefaultValue(arg),
 | 
			
		||||
          options: arg.getOptionsFromContext
 | 
			
		||||
            ? state.context[arg.getOptionsFromContext].map(
 | 
			
		||||
                (v: string | { name: string }) => ({
 | 
			
		||||
                  name: typeof v === 'string' ? v : v.name,
 | 
			
		||||
                  isCurrent: v === getDefaultValue(arg),
 | 
			
		||||
                })
 | 
			
		||||
              )
 | 
			
		||||
            : arg.getDefaultValueFromContext
 | 
			
		||||
            ? arg.options?.map((v) => ({
 | 
			
		||||
                ...v,
 | 
			
		||||
                isCurrent: v.name === getDefaultValue(arg),
 | 
			
		||||
              }))
 | 
			
		||||
            : arg.options,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										158
									
								
								src/lib/createMachineCommand.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,158 @@
 | 
			
		||||
import { AnyStateMachine, EventFrom, InterpreterFrom, StateFrom } from 'xstate'
 | 
			
		||||
import { isTauri } from './isTauri'
 | 
			
		||||
import {
 | 
			
		||||
  Command,
 | 
			
		||||
  CommandArgument,
 | 
			
		||||
  CommandArgumentConfig,
 | 
			
		||||
  CommandConfig,
 | 
			
		||||
  CommandSetConfig,
 | 
			
		||||
  CommandSetSchema,
 | 
			
		||||
} from './commandTypes'
 | 
			
		||||
 | 
			
		||||
interface CreateMachineCommandProps<
 | 
			
		||||
  T extends AnyStateMachine,
 | 
			
		||||
  S extends CommandSetSchema<T>
 | 
			
		||||
> {
 | 
			
		||||
  type: EventFrom<T>['type']
 | 
			
		||||
  ownerMachine: T['id']
 | 
			
		||||
  state: StateFrom<T>
 | 
			
		||||
  send: Function
 | 
			
		||||
  actor?: InterpreterFrom<T>
 | 
			
		||||
  commandBarConfig?: CommandSetConfig<T, S>
 | 
			
		||||
  onCancel?: () => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Creates a command with subcommands, ready for use in the CommandBar component,
 | 
			
		||||
// from a more terse Command Bar Meta definition.
 | 
			
		||||
export function createMachineCommand<
 | 
			
		||||
  T extends AnyStateMachine,
 | 
			
		||||
  S extends CommandSetSchema<T>
 | 
			
		||||
>({
 | 
			
		||||
  ownerMachine,
 | 
			
		||||
  type,
 | 
			
		||||
  state,
 | 
			
		||||
  send,
 | 
			
		||||
  actor,
 | 
			
		||||
  commandBarConfig,
 | 
			
		||||
  onCancel,
 | 
			
		||||
}: CreateMachineCommandProps<T, S>): Command<
 | 
			
		||||
  T,
 | 
			
		||||
  typeof type,
 | 
			
		||||
  S[typeof type]
 | 
			
		||||
> | null {
 | 
			
		||||
  const commandConfig = commandBarConfig && commandBarConfig[type]
 | 
			
		||||
  if (!commandConfig) return null
 | 
			
		||||
 | 
			
		||||
  // Hide commands based on platform by returning `null`
 | 
			
		||||
  // so the consumer can filter them out
 | 
			
		||||
  if ('hide' in commandConfig) {
 | 
			
		||||
    const { hide } = commandConfig
 | 
			
		||||
    if (hide === 'both') return null
 | 
			
		||||
    else if (hide === 'desktop' && isTauri()) return null
 | 
			
		||||
    else if (hide === 'web' && !isTauri()) return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const icon = ('icon' in commandConfig && commandConfig.icon) || undefined
 | 
			
		||||
 | 
			
		||||
  const command: Command<T, typeof type, S[typeof type]> = {
 | 
			
		||||
    name: type,
 | 
			
		||||
    ownerMachine: ownerMachine,
 | 
			
		||||
    icon,
 | 
			
		||||
    needsReview: commandConfig.needsReview || false,
 | 
			
		||||
    onSubmit: (data?: S[typeof type]) => {
 | 
			
		||||
      if (data !== undefined && data !== null) {
 | 
			
		||||
        send(type, { data })
 | 
			
		||||
      } else {
 | 
			
		||||
        send(type)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (commandConfig.args) {
 | 
			
		||||
    const newArgs = buildCommandArguments(state, commandConfig.args, actor)
 | 
			
		||||
 | 
			
		||||
    command.args = newArgs
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (onCancel) {
 | 
			
		||||
    command.onCancel = onCancel
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return command
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Takes the args from a CommandConfig and creates
 | 
			
		||||
// a finalized CommandArgument object for each one,
 | 
			
		||||
// bundled together into the args for a Command.
 | 
			
		||||
function buildCommandArguments<
 | 
			
		||||
  T extends AnyStateMachine,
 | 
			
		||||
  S extends CommandSetSchema<T>,
 | 
			
		||||
  CommandName extends EventFrom<T>['type'] = EventFrom<T>['type']
 | 
			
		||||
>(
 | 
			
		||||
  state: StateFrom<T>,
 | 
			
		||||
  args: CommandConfig<T, CommandName, S>['args'],
 | 
			
		||||
  actor?: InterpreterFrom<T>
 | 
			
		||||
): NonNullable<Command<T, CommandName, S>['args']> {
 | 
			
		||||
  const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
 | 
			
		||||
 | 
			
		||||
  for (const arg in args) {
 | 
			
		||||
    const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T>
 | 
			
		||||
    const newArg = buildCommandArgument(argConfig, state, actor)
 | 
			
		||||
    newArgs[arg] = newArg
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return newArgs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildCommandArgument<
 | 
			
		||||
  O extends CommandSetSchema<T>,
 | 
			
		||||
  T extends AnyStateMachine
 | 
			
		||||
>(
 | 
			
		||||
  arg: CommandArgumentConfig<O, T>,
 | 
			
		||||
  state: StateFrom<T>,
 | 
			
		||||
  actor?: InterpreterFrom<T>
 | 
			
		||||
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
 | 
			
		||||
  const baseCommandArgument = {
 | 
			
		||||
    description: arg.description,
 | 
			
		||||
    required: arg.required,
 | 
			
		||||
    payload: arg.payload,
 | 
			
		||||
    defaultValue:
 | 
			
		||||
      arg.defaultValue instanceof Function
 | 
			
		||||
        ? arg.defaultValue(state.context)
 | 
			
		||||
        : arg.defaultValue,
 | 
			
		||||
  } satisfies Omit<CommandArgument<O, T>, 'inputType'>
 | 
			
		||||
 | 
			
		||||
  if (arg.inputType === 'options') {
 | 
			
		||||
    const options = arg.options
 | 
			
		||||
      ? arg.options instanceof Function
 | 
			
		||||
        ? arg.options(state.context)
 | 
			
		||||
        : arg.options
 | 
			
		||||
      : undefined
 | 
			
		||||
 | 
			
		||||
    if (!options) {
 | 
			
		||||
      throw new Error('Options must be provided for options input type')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      inputType: arg.inputType,
 | 
			
		||||
      ...baseCommandArgument,
 | 
			
		||||
      options,
 | 
			
		||||
    } satisfies CommandArgument<O, T> & { inputType: 'options' }
 | 
			
		||||
  } else if (arg.inputType === 'selection') {
 | 
			
		||||
    if (!actor)
 | 
			
		||||
      throw new Error('Actor must be provided for selection input type')
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      inputType: arg.inputType,
 | 
			
		||||
      ...baseCommandArgument,
 | 
			
		||||
      multiple: arg.multiple,
 | 
			
		||||
      selectionTypes: arg.selectionTypes,
 | 
			
		||||
      actor,
 | 
			
		||||
    } satisfies CommandArgument<O, T> & { inputType: 'selection' }
 | 
			
		||||
  } else {
 | 
			
		||||
    return {
 | 
			
		||||
      inputType: arg.inputType,
 | 
			
		||||
      ...baseCommandArgument,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -7,6 +7,10 @@ import { EditorSelection } from '@codemirror/state'
 | 
			
		||||
import { kclManager } from 'lang/KclSinglton'
 | 
			
		||||
import { 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<typeof selectionsByType, 'none'>)
 | 
			
		||||
    .map(([type, count]) => `${count} ${type}${count > 1 ? 's' : ''}`)
 | 
			
		||||
    .join(', ')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function canSubmitSelectionArg(
 | 
			
		||||
  selectionsByType: 'none' | ResolvedSelectionType[],
 | 
			
		||||
  argument: CommandArgument<unknown> & { inputType: 'selection' }
 | 
			
		||||
) {
 | 
			
		||||
  return (
 | 
			
		||||
    selectionsByType !== 'none' &&
 | 
			
		||||
    selectionsByType.every(([type, count]) => {
 | 
			
		||||
      const foundIndex = argument.selectionTypes.findIndex((s) => s === type)
 | 
			
		||||
      return (
 | 
			
		||||
        foundIndex !== -1 &&
 | 
			
		||||
        (!argument.multiple ? count < 2 && count > 0 : count > 0)
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import { createMachine, assign } from 'xstate'
 | 
			
		||||
import { 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<typeof authMachine> = {
 | 
			
		||||
  'Log in': {
 | 
			
		||||
    hide: 'both',
 | 
			
		||||
  },
 | 
			
		||||
  'Log out': {
 | 
			
		||||
    args: [],
 | 
			
		||||
    icon: 'arrowLeft',
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const authMachine = createMachine<UserContext, Events>(
 | 
			
		||||
  {
 | 
			
		||||
    id: 'Auth',
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										425
									
								
								src/machines/commandBarMachine.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,425 @@
 | 
			
		||||
import { assign, createMachine } from 'xstate'
 | 
			
		||||
import {
 | 
			
		||||
  Command,
 | 
			
		||||
  CommandArgument,
 | 
			
		||||
  CommandArgumentWithName,
 | 
			
		||||
} from 'lib/commandTypes'
 | 
			
		||||
import { Selections } from 'lib/selections'
 | 
			
		||||
 | 
			
		||||
export const commandBarMachine = createMachine(
 | 
			
		||||
  {
 | 
			
		||||
    /** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswdJNWcNbPFLNMr5AsRFWUtJcVSdRR2VVMUmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUG1klTsSi0awQDgYWhmqkWjnUslBWhOZyegU61GuZAAghACN8-HhYH92FxAXFQAlRA5FpJ5FjFPYtNDpIpLOkEW4GNs4eJxJoGvJxOVcT82gSrj0AEpgdCoABuYC+8ogNOYI3psQmYlkGUkU2slhc6mkmUUCIyKLc4i0lkU8iUyjUHLlVIuJEkAGUwK8Pg8oDr-WQQ2HPpTzrTIkagUzEDkLHZkQK1CUtKCHAiNlsdjkVAdFEc-ed2oGAOJYbgACzAJAj+FIUAAruhBtxYGQACJwUPveO6pMA43AhA5UqWXOzBjS-nOR3LySqXNODTyZwONTV-GXXXPUcfHqTlOM4TrSublb5-lLewlIVySRaOrmGw-jLSI8FRPf0zzjS8HHCOlogZE1ERUEUSgPa05yQ1ZjEQeZP2-VwDzUN1zEabxTlPAkG2bVt207Hs+wHZAm1wGAvi7EgSD7DsSG7XscG4K9oOnNNEVUeQZGSOxc0qaQ7HhdDBJFeRc35extGkNI+UAgNJDIls2xwSMqK4-tJBJAB3LAYl0-AHjYLtuBjLsACN0B4djOL7XixhvBIDxUT8qj5VIkTwtQHRk8oUQcD15LMXQclcdTa00xttMojjqO42BJAANSwShOAgRsIzICB+DASQHg1VAAGtSo1HK8sbMASVSgz3JgmdMnKGYzRlbIdGXA8EX8zd3RsTY9yyY4iLxID6ySiiLP0misrq-LeF0shWxIVBAzYShGwAM229BJFq3LVsa5q3INf5r1gg9JIfXqqh0TQsiFTYrCyJZRpsXJ4oJVUNU4MBjLsxznITX5rqgjzYMsaZtGXaVNhcZFKgRd0RXUdJ6mUXQXX+jpAeB0GyQIRbuNa-jbwQcVSmhAUeTkWQ3HyGSBVKDQMyRAKAsJwNiZBshVXVLUXLSnjoeTPjUxpnmpFtcVs1cPNBq5T8fR5DrKwaHEppIomwCBoWAFEIGcinJcg6XYZnTRhJsbQJU9VlQTVtQNbqcVZhsSFxH5zoWzeSr2ya1z0qKkqypwCrqpOlaGrDiX9WtqdZYSeSEeXEplCceo1xkvQLGCmUIp0Mwck8fWQMVIOQ4spODIHYqcFK8qqpqhPuAu8P+zoCDDRlzzTCkCVWWlSxrBceTygRFxZHZNJbE2VQ5AFAO6PeevI0bmiNpY7bJF2g6jvjs7E8u9KqfTxAkK2fZYuRvdYUGjQZnqYVxRKdx-ZOHBUAgHAQQ00AyD1tgJUQCwLQMEyHUG0Gh7SOmmDuGB6gOTyVBMkDeRJIBgLahA4oFo8zWlSPUT00o0KFA9NMYKrhKwbH2GaQ81cawEljGOdskM8B4OpgkWY9MV5ZjqLUN6MlqGojoRFEo6QWYb1PC8McuCbpD1gosLYrhkTZCxNIawhYxEfW0HhCKKgzT8lBAHLS809KX37Dwm+iIWZSEinjTRy51BFnvNofM7hGZfgXBYuaOlrG9wyiZMya1IxWRsnY4eDi2TONsK45IaghQbEkO4VIKlsaVE2AE8iQTxZN2WufCJMS7o6JSBKNQLp7CT35EKZw2wnDVDyCvBczDmg10NsbYyZS7aZHBNU58q8sTQgxnud+kVsjyQzHrTprDLh11DjY+AyjwFy00DQ2EaQBSLAPLIDGWRNw2BUiXHkwVCJeCAA */
 | 
			
		||||
    context: {
 | 
			
		||||
      commands: [] as Command[],
 | 
			
		||||
      selectedCommand: undefined as Command | undefined,
 | 
			
		||||
      currentArgument: undefined as
 | 
			
		||||
        | (CommandArgument<unknown> & { name: string })
 | 
			
		||||
        | undefined,
 | 
			
		||||
      selectionRanges: {
 | 
			
		||||
        otherSelections: [],
 | 
			
		||||
        codeBasedSelections: [],
 | 
			
		||||
      } as Selections,
 | 
			
		||||
      argumentsToSubmit: {} as { [x: string]: unknown },
 | 
			
		||||
    },
 | 
			
		||||
    id: 'Command Bar',
 | 
			
		||||
    initial: 'Closed',
 | 
			
		||||
    states: {
 | 
			
		||||
      Closed: {
 | 
			
		||||
        on: {
 | 
			
		||||
          Open: {
 | 
			
		||||
            target: 'Selecting command',
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Find and select command': {
 | 
			
		||||
            target: 'Command selected',
 | 
			
		||||
            actions: [
 | 
			
		||||
              'Find and select command',
 | 
			
		||||
              'Initialize arguments to submit',
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Add commands': {
 | 
			
		||||
            target: 'Closed',
 | 
			
		||||
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                commands: (context, event) =>
 | 
			
		||||
                  [...context.commands, ...event.data.commands].sort(
 | 
			
		||||
                    sortCommands
 | 
			
		||||
                  ),
 | 
			
		||||
              }),
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
            internal: true,
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Remove commands': {
 | 
			
		||||
            target: 'Closed',
 | 
			
		||||
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                commands: (context, event) =>
 | 
			
		||||
                  context.commands.filter(
 | 
			
		||||
                    (c) =>
 | 
			
		||||
                      !event.data.commands.some(
 | 
			
		||||
                        (c2) =>
 | 
			
		||||
                          c2.name === c.name &&
 | 
			
		||||
                          c2.ownerMachine === c.ownerMachine
 | 
			
		||||
                      )
 | 
			
		||||
                  ),
 | 
			
		||||
              }),
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
            internal: true,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Selecting command': {
 | 
			
		||||
        on: {
 | 
			
		||||
          'Select command': {
 | 
			
		||||
            target: 'Command selected',
 | 
			
		||||
            actions: ['Set selected command', 'Initialize arguments to submit'],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Command selected': {
 | 
			
		||||
        always: [
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Closed',
 | 
			
		||||
            cond: 'Command has no arguments',
 | 
			
		||||
            actions: ['Execute command'],
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            target: 'Gathering arguments',
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                currentArgument: (context, event) => {
 | 
			
		||||
                  const { selectedCommand } = context
 | 
			
		||||
                  if (!(selectedCommand && selectedCommand.args))
 | 
			
		||||
                    return undefined
 | 
			
		||||
                  const argName = Object.keys(selectedCommand.args)[0]
 | 
			
		||||
                  return {
 | 
			
		||||
                    ...selectedCommand.args[argName],
 | 
			
		||||
                    name: argName,
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
              }),
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Gathering arguments': {
 | 
			
		||||
        states: {
 | 
			
		||||
          'Awaiting input': {
 | 
			
		||||
            on: {
 | 
			
		||||
              'Submit argument': {
 | 
			
		||||
                target: 'Validating',
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          Validating: {
 | 
			
		||||
            invoke: {
 | 
			
		||||
              src: 'Validate argument',
 | 
			
		||||
              id: 'validateArgument',
 | 
			
		||||
              onDone: {
 | 
			
		||||
                target: '#Command Bar.Checking Arguments',
 | 
			
		||||
                actions: [
 | 
			
		||||
                  assign({
 | 
			
		||||
                    argumentsToSubmit: (context, event) => {
 | 
			
		||||
                      const [argName, argData] = Object.entries(event.data)[0]
 | 
			
		||||
                      const { currentArgument } = context
 | 
			
		||||
                      if (!currentArgument) return {}
 | 
			
		||||
                      return {
 | 
			
		||||
                        ...context.argumentsToSubmit,
 | 
			
		||||
                        [argName]: argData,
 | 
			
		||||
                      }
 | 
			
		||||
                    },
 | 
			
		||||
                  }),
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
              onError: [
 | 
			
		||||
                {
 | 
			
		||||
                  target: 'Awaiting input',
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        initial: 'Awaiting input',
 | 
			
		||||
 | 
			
		||||
        on: {
 | 
			
		||||
          'Change current argument': {
 | 
			
		||||
            target: 'Gathering arguments',
 | 
			
		||||
            internal: true,
 | 
			
		||||
            actions: ['Set current argument'],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Deselect command': {
 | 
			
		||||
            target: 'Selecting command',
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                selectedCommand: (_c, _e) => undefined,
 | 
			
		||||
              }),
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      Review: {
 | 
			
		||||
        entry: ['Clear current argument'],
 | 
			
		||||
        on: {
 | 
			
		||||
          'Submit command': {
 | 
			
		||||
            target: 'Closed',
 | 
			
		||||
            actions: ['Execute command'],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Add argument': {
 | 
			
		||||
            target: 'Gathering arguments',
 | 
			
		||||
            actions: ['Set current argument'],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Remove argument': {
 | 
			
		||||
            target: 'Review',
 | 
			
		||||
            actions: [
 | 
			
		||||
              assign({
 | 
			
		||||
                argumentsToSubmit: (context, event) => {
 | 
			
		||||
                  const argName = Object.keys(event.data)[0]
 | 
			
		||||
                  const { argumentsToSubmit } = context
 | 
			
		||||
                  const newArgumentsToSubmit = { ...argumentsToSubmit }
 | 
			
		||||
                  newArgumentsToSubmit[argName] = undefined
 | 
			
		||||
                  return newArgumentsToSubmit
 | 
			
		||||
                },
 | 
			
		||||
              }),
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Edit argument': {
 | 
			
		||||
            target: 'Gathering arguments',
 | 
			
		||||
            actions: ['Set current argument'],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      'Checking Arguments': {
 | 
			
		||||
        invoke: {
 | 
			
		||||
          src: 'Validate all arguments',
 | 
			
		||||
          id: 'validateArguments',
 | 
			
		||||
          onDone: [
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Review',
 | 
			
		||||
              cond: 'Command needs review',
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Closed',
 | 
			
		||||
              actions: 'Execute command',
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          onError: [
 | 
			
		||||
            {
 | 
			
		||||
              target: 'Gathering arguments',
 | 
			
		||||
              actions: ['Set current argument'],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    on: {
 | 
			
		||||
      Close: {
 | 
			
		||||
        target: '.Closed',
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      Clear: {
 | 
			
		||||
        target: '#Command Bar',
 | 
			
		||||
        internal: true,
 | 
			
		||||
        actions: ['Clear argument data'],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    schema: {
 | 
			
		||||
      events: {} as
 | 
			
		||||
        | { type: 'Open' }
 | 
			
		||||
        | { type: 'Close' }
 | 
			
		||||
        | { type: 'Clear' }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Select command'
 | 
			
		||||
            data: { command: Command }
 | 
			
		||||
          }
 | 
			
		||||
        | { type: 'Deselect command' }
 | 
			
		||||
        | { type: 'Submit command'; data: { [x: string]: unknown } }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Add argument'
 | 
			
		||||
            data: { argument: CommandArgumentWithName<unknown> }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Remove argument'
 | 
			
		||||
            data: { [x: string]: CommandArgumentWithName<unknown> }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Edit argument'
 | 
			
		||||
            data: { arg: CommandArgumentWithName<unknown> }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Add commands'
 | 
			
		||||
            data: { commands: Command[] }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Remove commands'
 | 
			
		||||
            data: { commands: Command[] }
 | 
			
		||||
          }
 | 
			
		||||
        | { type: 'Submit argument'; data: { [x: string]: unknown } }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'done.invoke.validateArguments'
 | 
			
		||||
            data: { [x: string]: unknown }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'error.platform.validateArguments'
 | 
			
		||||
            data: { message: string; arg: CommandArgumentWithName<unknown> }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Find and select command'
 | 
			
		||||
            data: { name: string; ownerMachine: string }
 | 
			
		||||
          }
 | 
			
		||||
        | {
 | 
			
		||||
            type: 'Change current argument'
 | 
			
		||||
            data: { arg: CommandArgumentWithName<unknown> }
 | 
			
		||||
          },
 | 
			
		||||
    },
 | 
			
		||||
    predictableActionArguments: true,
 | 
			
		||||
    preserveActionOrder: true,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    actions: {
 | 
			
		||||
      'Execute command': (context, event) => {
 | 
			
		||||
        const { selectedCommand } = context
 | 
			
		||||
        if (!selectedCommand) return
 | 
			
		||||
        if (selectedCommand?.args) {
 | 
			
		||||
          selectedCommand?.onSubmit(
 | 
			
		||||
            event.type === 'Submit command' ||
 | 
			
		||||
              event.type === 'done.invoke.validateArguments'
 | 
			
		||||
              ? event.data
 | 
			
		||||
              : undefined
 | 
			
		||||
          )
 | 
			
		||||
        } else {
 | 
			
		||||
          selectedCommand?.onSubmit()
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      'Clear current argument': assign({
 | 
			
		||||
        currentArgument: undefined,
 | 
			
		||||
      }),
 | 
			
		||||
      'Set current argument': assign({
 | 
			
		||||
        currentArgument: (context, event) => {
 | 
			
		||||
          switch (event.type) {
 | 
			
		||||
            case 'error.platform.validateArguments':
 | 
			
		||||
              return event.data.arg
 | 
			
		||||
            case 'Edit argument':
 | 
			
		||||
              return event.data.arg
 | 
			
		||||
            case 'Change current argument':
 | 
			
		||||
              return event.data.arg
 | 
			
		||||
            default:
 | 
			
		||||
              return context.currentArgument
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      'Clear argument data': assign({
 | 
			
		||||
        selectedCommand: undefined,
 | 
			
		||||
        currentArgument: undefined,
 | 
			
		||||
        argumentsToSubmit: {},
 | 
			
		||||
      }),
 | 
			
		||||
      'Set selected command': assign({
 | 
			
		||||
        selectedCommand: (c, e) =>
 | 
			
		||||
          e.type === 'Select command' ? e.data.command : c.selectedCommand,
 | 
			
		||||
      }),
 | 
			
		||||
      'Find and select command': assign({
 | 
			
		||||
        selectedCommand: (c, e) => {
 | 
			
		||||
          if (e.type !== 'Find and select command') return c.selectedCommand
 | 
			
		||||
          const found = c.commands.find(
 | 
			
		||||
            (cmd) =>
 | 
			
		||||
              cmd.name === e.data.name &&
 | 
			
		||||
              cmd.ownerMachine === e.data.ownerMachine
 | 
			
		||||
          )
 | 
			
		||||
 | 
			
		||||
          return !!found ? found : c.selectedCommand
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      'Initialize arguments to submit': assign({
 | 
			
		||||
        argumentsToSubmit: (c, e) => {
 | 
			
		||||
          if (
 | 
			
		||||
            e.type !== 'Select command' &&
 | 
			
		||||
            e.type !== 'Find and select command'
 | 
			
		||||
          )
 | 
			
		||||
            return c.argumentsToSubmit
 | 
			
		||||
          const command =
 | 
			
		||||
            'command' in e.data ? e.data.command : c.selectedCommand!
 | 
			
		||||
          if (!command.args) return {}
 | 
			
		||||
          const args: { [x: string]: unknown } = {}
 | 
			
		||||
          for (const [argName, arg] of Object.entries(command.args)) {
 | 
			
		||||
            args[argName] = arg.payload
 | 
			
		||||
          }
 | 
			
		||||
          return args
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
    },
 | 
			
		||||
    guards: {
 | 
			
		||||
      'Command needs review': (context, _) =>
 | 
			
		||||
        context.selectedCommand?.needsReview || false,
 | 
			
		||||
    },
 | 
			
		||||
    services: {
 | 
			
		||||
      'Validate argument': (context, event) => {
 | 
			
		||||
        if (event.type !== 'Submit argument') return Promise.reject()
 | 
			
		||||
        return new Promise((resolve, reject) => {
 | 
			
		||||
          // TODO: figure out if we should validate argument data here or in the form itself,
 | 
			
		||||
          // and if we should support people configuring a argument's validation function
 | 
			
		||||
 | 
			
		||||
          resolve(event.data)
 | 
			
		||||
        })
 | 
			
		||||
      },
 | 
			
		||||
      'Validate all arguments': (context, _) => {
 | 
			
		||||
        return new Promise((resolve, reject) => {
 | 
			
		||||
          for (const [argName, arg] of Object.entries(
 | 
			
		||||
            context.argumentsToSubmit
 | 
			
		||||
          )) {
 | 
			
		||||
            let argConfig = context.selectedCommand!.args![argName]
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
              typeof arg !== typeof argConfig.payload &&
 | 
			
		||||
              typeof arg !== typeof argConfig.defaultValue &&
 | 
			
		||||
              'options' in argConfig &&
 | 
			
		||||
              typeof arg !== typeof argConfig.options[0].value
 | 
			
		||||
            ) {
 | 
			
		||||
              return reject({
 | 
			
		||||
                message: 'Argument payload is of the wrong type',
 | 
			
		||||
                arg: {
 | 
			
		||||
                  ...argConfig,
 | 
			
		||||
                  name: argName,
 | 
			
		||||
                },
 | 
			
		||||
              })
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!arg && argConfig.required) {
 | 
			
		||||
              return reject({
 | 
			
		||||
                message: 'Argument payload is falsy but is required',
 | 
			
		||||
                arg: {
 | 
			
		||||
                  ...argConfig,
 | 
			
		||||
                  name: argName,
 | 
			
		||||
                },
 | 
			
		||||
              })
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return resolve(context.argumentsToSubmit)
 | 
			
		||||
        })
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    delays: {},
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
function sortCommands(a: Command, b: Command) {
 | 
			
		||||
  if (b.ownerMachine === 'auth') return -1
 | 
			
		||||
  if (a.ownerMachine === 'auth') return 1
 | 
			
		||||
  return a.name.localeCompare(b.name)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										74
									
								
								src/machines/commandBarMachine.typegen.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,74 @@
 | 
			
		||||
// This file was automatically generated. Edits will be overwritten
 | 
			
		||||
 | 
			
		||||
export interface Typegen0 {
 | 
			
		||||
  '@@xstate/typegen': true
 | 
			
		||||
  internalEvents: {
 | 
			
		||||
    '': { type: '' }
 | 
			
		||||
    'done.invoke.validateArgument': {
 | 
			
		||||
      type: 'done.invoke.validateArgument'
 | 
			
		||||
      data: unknown
 | 
			
		||||
      __tip: 'See the XState TS docs to learn how to strongly type this.'
 | 
			
		||||
    }
 | 
			
		||||
    'done.invoke.validateArguments': {
 | 
			
		||||
      type: 'done.invoke.validateArguments'
 | 
			
		||||
      data: unknown
 | 
			
		||||
      __tip: 'See the XState TS docs to learn how to strongly type this.'
 | 
			
		||||
    }
 | 
			
		||||
    'error.platform.validateArgument': {
 | 
			
		||||
      type: 'error.platform.validateArgument'
 | 
			
		||||
      data: unknown
 | 
			
		||||
    }
 | 
			
		||||
    'error.platform.validateArguments': {
 | 
			
		||||
      type: 'error.platform.validateArguments'
 | 
			
		||||
      data: unknown
 | 
			
		||||
    }
 | 
			
		||||
    'xstate.init': { type: 'xstate.init' }
 | 
			
		||||
  }
 | 
			
		||||
  invokeSrcNameMap: {
 | 
			
		||||
    'Validate all arguments': 'done.invoke.validateArguments'
 | 
			
		||||
    'Validate argument': 'done.invoke.validateArgument'
 | 
			
		||||
  }
 | 
			
		||||
  missingImplementations: {
 | 
			
		||||
    actions:
 | 
			
		||||
      | 'Add arguments'
 | 
			
		||||
      | 'Close dialog'
 | 
			
		||||
      | 'Execute command'
 | 
			
		||||
      | 'Open dialog'
 | 
			
		||||
    delays: never
 | 
			
		||||
    guards: never
 | 
			
		||||
    services: never
 | 
			
		||||
  }
 | 
			
		||||
  eventsCausingActions: {
 | 
			
		||||
    'Add arguments': 'done.invoke.validateArguments'
 | 
			
		||||
    'Add commands': 'Add commands'
 | 
			
		||||
    'Close dialog': 'Close'
 | 
			
		||||
    'Execute command': '' | 'Submit'
 | 
			
		||||
    'Open dialog': 'Open'
 | 
			
		||||
    'Remove argument': 'Remove argument'
 | 
			
		||||
    'Remove commands': 'Remove commands'
 | 
			
		||||
    'Set current argument':
 | 
			
		||||
      | 'Add argument'
 | 
			
		||||
      | 'Edit argument'
 | 
			
		||||
      | 'error.platform.validateArguments'
 | 
			
		||||
  }
 | 
			
		||||
  eventsCausingDelays: {}
 | 
			
		||||
  eventsCausingGuards: {
 | 
			
		||||
    'Arguments are ready': 'done.invoke.validateArguments'
 | 
			
		||||
    'Command has no arguments': ''
 | 
			
		||||
  }
 | 
			
		||||
  eventsCausingServices: {
 | 
			
		||||
    'Validate all arguments': 'done.invoke.validateArgument'
 | 
			
		||||
    'Validate argument': 'Submit'
 | 
			
		||||
  }
 | 
			
		||||
  matchesStates:
 | 
			
		||||
    | 'Checking Arguments'
 | 
			
		||||
    | 'Closed'
 | 
			
		||||
    | 'Command selected'
 | 
			
		||||
    | 'Gathering arguments'
 | 
			
		||||
    | 'Gathering arguments.Awaiting input'
 | 
			
		||||
    | 'Gathering arguments.Validating'
 | 
			
		||||
    | 'Review'
 | 
			
		||||
    | 'Selecting command'
 | 
			
		||||
    | { 'Gathering arguments'?: 'Awaiting input' | 'Validating' }
 | 
			
		||||
  tags: never
 | 
			
		||||
}
 | 
			
		||||
@ -1,56 +1,6 @@
 | 
			
		||||
import { assign, createMachine } from 'xstate'
 | 
			
		||||
import { ProjectWithEntryPointMetadata } from '../Router'
 | 
			
		||||
import { CommandBarConfig } from '../lib/commands'
 | 
			
		||||
 | 
			
		||||
export const homeCommandConfig: CommandBarConfig<typeof homeMachine> = {
 | 
			
		||||
  'Create project': {
 | 
			
		||||
    icon: 'folderPlus',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'name',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        getDefaultValueFromContext: 'defaultProjectName',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Open project': {
 | 
			
		||||
    icon: 'arrowRight',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'name',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        getOptionsFromContext: 'projects',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Delete project': {
 | 
			
		||||
    icon: 'close',
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'name',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        getOptionsFromContext: 'projects',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  'Rename project': {
 | 
			
		||||
    icon: 'folder',
 | 
			
		||||
    formatFunction: (args: string[]) =>
 | 
			
		||||
      `Rename project "${args[0]}" to "${args[1]}"`,
 | 
			
		||||
    args: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'oldName',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        getOptionsFromContext: 'projects',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'newName',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        getDefaultValueFromContext: 'defaultProjectName',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
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'
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -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 */
 | 
			
		||||
 | 
			
		||||
@ -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<typeof homeMachine>
 | 
			
		||||
      ) => {
 | 
			
		||||
        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<typeof homeMachine>({
 | 
			
		||||
    commands,
 | 
			
		||||
  useStateMachineCommands({
 | 
			
		||||
    machineId: 'home',
 | 
			
		||||
    send,
 | 
			
		||||
    state,
 | 
			
		||||
    commandBarConfig: homeCommandConfig,
 | 
			
		||||
    owner: 'home',
 | 
			
		||||
    commandBarConfig: homeCommandBarConfig,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
 | 
			
		||||
@ -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<Platform | ''>('')
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    async function getPlatform() {
 | 
			
		||||
      setPlatformName(await platform())
 | 
			
		||||
    }
 | 
			
		||||
    void getPlatform()
 | 
			
		||||
  }, [setPlatformName])
 | 
			
		||||
  const platformName = usePlatform()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="fixed inset-0 z-50 grid items-end justify-center pointer-events-none">
 | 
			
		||||
@ -29,13 +21,13 @@ export default function CmdK() {
 | 
			
		||||
        <h2 className="text-2xl">Command Bar</h2>
 | 
			
		||||
        <p className="my-4">
 | 
			
		||||
          Press{' '}
 | 
			
		||||
          {platformName === 'win32' ? (
 | 
			
		||||
          {platformName === 'darwin' ? (
 | 
			
		||||
            <>
 | 
			
		||||
              <kbd>Win</kbd> + <kbd>/</kbd>
 | 
			
		||||
              <kbd>⌘</kbd> + <kbd>K</kbd>
 | 
			
		||||
            </>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <>
 | 
			
		||||
              <kbd>OS</kbd> + <kbd>K</kbd>
 | 
			
		||||
              <kbd>Ctrl</kbd> + <kbd>/</kbd>
 | 
			
		||||
            </>
 | 
			
		||||
          )}{' '}
 | 
			
		||||
          to open the command bar. Try changing your theme with it.
 | 
			
		||||
 | 
			
		||||
@ -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 = () => {
 | 
			
		||||
          <ActionButton
 | 
			
		||||
            Element="button"
 | 
			
		||||
            onClick={signInTauri}
 | 
			
		||||
            icon={{ icon: faSignInAlt }}
 | 
			
		||||
            icon={{ icon: 'arrowRight' }}
 | 
			
		||||
            className="w-fit mt-4"
 | 
			
		||||
            data-testid="sign-in-button"
 | 
			
		||||
          >
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										276
									
								
								src/wasm-lib/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						@ -127,12 +127,6 @@ dependencies = [
 | 
			
		||||
 "backtrace",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "approx"
 | 
			
		||||
version = "0.1.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "08abcc3b4e9339e33a3d0a5ed15d84a687350c05689d825e0f6655eef9e76a94"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "arc-swap"
 | 
			
		||||
version = "1.6.0"
 | 
			
		||||
@ -249,7 +243,7 @@ dependencies = [
 | 
			
		||||
 "libm",
 | 
			
		||||
 "num-bigint",
 | 
			
		||||
 "num-integer",
 | 
			
		||||
 "num-traits 0.2.17",
 | 
			
		||||
 "num-traits",
 | 
			
		||||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -335,7 +329,7 @@ dependencies = [
 | 
			
		||||
 "indexmap 1.9.3",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "rand 0.8.5",
 | 
			
		||||
 "rand",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_bytes",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
@ -400,18 +394,6 @@ version = "1.0.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "cgmath"
 | 
			
		||||
version = "0.16.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "64a4b57c8f4e3a2e9ac07e0f6abc9c24b6fc9e1b54c3478cfb598f3d0023e51c"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "approx",
 | 
			
		||||
 "mint",
 | 
			
		||||
 "num-traits 0.1.43",
 | 
			
		||||
 "rand 0.4.6",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "chrono"
 | 
			
		||||
version = "0.4.31"
 | 
			
		||||
@ -420,10 +402,8 @@ checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "android-tzdata",
 | 
			
		||||
 "iana-time-zone",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "num-traits 0.2.17",
 | 
			
		||||
 "num-traits",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "windows-targets 0.48.5",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -600,7 +580,7 @@ dependencies = [
 | 
			
		||||
 "criterion-plot",
 | 
			
		||||
 "is-terminal",
 | 
			
		||||
 "itertools 0.10.5",
 | 
			
		||||
 "num-traits 0.2.17",
 | 
			
		||||
 "num-traits",
 | 
			
		||||
 "once_cell",
 | 
			
		||||
 "oorandom",
 | 
			
		||||
 "plotters",
 | 
			
		||||
@ -762,49 +742,6 @@ dependencies = [
 | 
			
		||||
 "syn 2.0.39",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "diesel"
 | 
			
		||||
version = "2.1.4"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "62c6fcf842f17f8c78ecf7c81d75c5ce84436b41ee07e03f490fbb5f5a8731d8"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "bigdecimal",
 | 
			
		||||
 "bitflags 2.4.1",
 | 
			
		||||
 "byteorder",
 | 
			
		||||
 "chrono",
 | 
			
		||||
 "diesel_derives",
 | 
			
		||||
 "mysqlclient-sys",
 | 
			
		||||
 "num-bigint",
 | 
			
		||||
 "num-integer",
 | 
			
		||||
 "num-traits 0.2.17",
 | 
			
		||||
 "percent-encoding",
 | 
			
		||||
 "r2d2",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "url",
 | 
			
		||||
 "uuid",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "diesel_derives"
 | 
			
		||||
version = "2.1.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "ef8337737574f55a468005a83499da720f20c65586241ffea339db9ecdfd2b44"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "diesel_table_macro_syntax",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 2.0.39",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "diesel_table_macro_syntax"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "syn 2.0.39",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "diff"
 | 
			
		||||
version = "0.1.13"
 | 
			
		||||
@ -869,26 +806,6 @@ dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "enum-iterator"
 | 
			
		||||
version = "1.4.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7add3873b5dd076766ee79c8e406ad1a472c385476b9e38849f8eec24f1be689"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "enum-iterator-derive",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "enum-iterator-derive"
 | 
			
		||||
version = "1.2.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "eecf8589574ce9b895052fa12d69af7a233f99e6107f5cb8dd1044f2a17bfdcb"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 2.0.39",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "equivalent"
 | 
			
		||||
version = "1.0.1"
 | 
			
		||||
@ -905,28 +822,6 @@ dependencies = [
 | 
			
		||||
 "windows-sys 0.48.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "euler"
 | 
			
		||||
version = "0.4.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "3f19d11568a4a46aee488bdab3a2963e5e2c3cfd6091aa0abceaddcea82c0bc1"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "approx",
 | 
			
		||||
 "cgmath",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "execution-plan"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "bytes",
 | 
			
		||||
 "kittycad",
 | 
			
		||||
 "kittycad-modeling-cmds",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "uuid",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "expectorate"
 | 
			
		||||
version = "1.1.0"
 | 
			
		||||
@ -1041,12 +936,6 @@ dependencies = [
 | 
			
		||||
 "unicode-segmentation",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "fuchsia-cprng"
 | 
			
		||||
version = "0.1.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "funty"
 | 
			
		||||
version = "2.0.0"
 | 
			
		||||
@ -1400,7 +1289,7 @@ dependencies = [
 | 
			
		||||
 "gif",
 | 
			
		||||
 "jpeg-decoder",
 | 
			
		||||
 "num-rational",
 | 
			
		||||
 "num-traits 0.2.17",
 | 
			
		||||
 "num-traits",
 | 
			
		||||
 "png",
 | 
			
		||||
 "qoi",
 | 
			
		||||
 "tiff",
 | 
			
		||||
@ -1439,12 +1328,6 @@ dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "inflections"
 | 
			
		||||
version = "1.1.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "insta"
 | 
			
		||||
version = "1.34.0"
 | 
			
		||||
@ -1611,7 +1494,7 @@ dependencies = [
 | 
			
		||||
 "log",
 | 
			
		||||
 "parse-display",
 | 
			
		||||
 "phonenumber",
 | 
			
		||||
 "rand 0.8.5",
 | 
			
		||||
 "rand",
 | 
			
		||||
 "reqwest",
 | 
			
		||||
 "reqwest-conditional-middleware",
 | 
			
		||||
 "reqwest-middleware",
 | 
			
		||||
@ -1628,42 +1511,6 @@ dependencies = [
 | 
			
		||||
 "uuid",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "kittycad-modeling-cmds"
 | 
			
		||||
version = "0.1.4"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c3217f9ffe9dce4b16303eeef539e7e27b743bc1c46eff8ce64657745a2b75ca"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "chrono",
 | 
			
		||||
 "data-encoding",
 | 
			
		||||
 "diesel",
 | 
			
		||||
 "diesel_derives",
 | 
			
		||||
 "enum-iterator",
 | 
			
		||||
 "enum-iterator-derive",
 | 
			
		||||
 "euler",
 | 
			
		||||
 "http",
 | 
			
		||||
 "kittycad-unit-conversion-derive",
 | 
			
		||||
 "measurements",
 | 
			
		||||
 "parse-display",
 | 
			
		||||
 "parse-display-derive",
 | 
			
		||||
 "schemars",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_bytes",
 | 
			
		||||
 "uuid",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "kittycad-unit-conversion-derive"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7001c46a92c1edce6722a3900539b198230980799035f02d92b4e7df3fc08738"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "inflections",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 1.0.109",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "lazy_static"
 | 
			
		||||
version = "1.4.0"
 | 
			
		||||
@ -1763,15 +1610,6 @@ version = "0.7.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "measurements"
 | 
			
		||||
version = "0.11.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f5b734b4e8187ea5777bc29c086f0970a27d8de42061b48f5af32cafc0ca904b"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "libm",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "memchr"
 | 
			
		||||
version = "2.6.4"
 | 
			
		||||
@ -1819,12 +1657,6 @@ dependencies = [
 | 
			
		||||
 "simd-adler32",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "mint"
 | 
			
		||||
version = "0.5.9"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "mio"
 | 
			
		||||
version = "0.8.9"
 | 
			
		||||
@ -1836,16 +1668,6 @@ dependencies = [
 | 
			
		||||
 "windows-sys 0.48.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "mysqlclient-sys"
 | 
			
		||||
version = "0.2.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "f61b381528ba293005c42a409dd73d034508e273bf90481f17ec2e964a6e969b"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "pkg-config",
 | 
			
		||||
 "vcpkg",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "newline-converter"
 | 
			
		||||
version = "0.3.0"
 | 
			
		||||
@ -1873,7 +1695,7 @@ checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "autocfg",
 | 
			
		||||
 "num-integer",
 | 
			
		||||
 "num-traits 0.2.17",
 | 
			
		||||
 "num-traits",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
@ -1883,7 +1705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "autocfg",
 | 
			
		||||
 "num-traits 0.2.17",
 | 
			
		||||
 "num-traits",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
@ -1894,16 +1716,7 @@ checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "autocfg",
 | 
			
		||||
 "num-integer",
 | 
			
		||||
 "num-traits 0.2.17",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "num-traits"
 | 
			
		||||
version = "0.1.43"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "num-traits 0.2.17",
 | 
			
		||||
 "num-traits",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
@ -1989,7 +1802,7 @@ dependencies = [
 | 
			
		||||
 "phonenumber",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "rand 0.8.5",
 | 
			
		||||
 "rand",
 | 
			
		||||
 "regex",
 | 
			
		||||
 "reqwest",
 | 
			
		||||
 "reqwest-middleware",
 | 
			
		||||
@ -2041,7 +1854,7 @@ dependencies = [
 | 
			
		||||
 "lazy_static",
 | 
			
		||||
 "percent-encoding",
 | 
			
		||||
 "pin-project",
 | 
			
		||||
 "rand 0.8.5",
 | 
			
		||||
 "rand",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -2196,7 +2009,7 @@ version = "0.3.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "num-traits 0.2.17",
 | 
			
		||||
 "num-traits",
 | 
			
		||||
 "plotters-backend",
 | 
			
		||||
 "plotters-svg",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
@ -2313,36 +2126,12 @@ dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "r2d2"
 | 
			
		||||
version = "0.8.10"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "log",
 | 
			
		||||
 "parking_lot 0.12.1",
 | 
			
		||||
 "scheduled-thread-pool",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "radium"
 | 
			
		||||
version = "0.7.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rand"
 | 
			
		||||
version = "0.4.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "fuchsia-cprng",
 | 
			
		||||
 "libc",
 | 
			
		||||
 "rand_core 0.3.1",
 | 
			
		||||
 "rdrand",
 | 
			
		||||
 "winapi",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rand"
 | 
			
		||||
version = "0.8.5"
 | 
			
		||||
@ -2351,7 +2140,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "libc",
 | 
			
		||||
 "rand_chacha",
 | 
			
		||||
 "rand_core 0.6.4",
 | 
			
		||||
 "rand_core",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
@ -2361,24 +2150,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "ppv-lite86",
 | 
			
		||||
 "rand_core 0.6.4",
 | 
			
		||||
 "rand_core",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rand_core"
 | 
			
		||||
version = "0.3.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "rand_core 0.4.2",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rand_core"
 | 
			
		||||
version = "0.4.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rand_core"
 | 
			
		||||
version = "0.6.4"
 | 
			
		||||
@ -2408,15 +2182,6 @@ dependencies = [
 | 
			
		||||
 "crossbeam-utils",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rdrand"
 | 
			
		||||
version = "0.4.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "rand_core 0.3.1",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "redox_syscall"
 | 
			
		||||
version = "0.2.16"
 | 
			
		||||
@ -2625,7 +2390,7 @@ checksum = "e09bbcb5003282bcb688f0bae741b278e9c7e8f378f561522c9806c58e075d9b"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "anyhow",
 | 
			
		||||
 "chrono",
 | 
			
		||||
 "rand 0.8.5",
 | 
			
		||||
 "rand",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
@ -2753,15 +2518,6 @@ dependencies = [
 | 
			
		||||
 "windows-sys 0.48.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "scheduled-thread-pool"
 | 
			
		||||
version = "0.2.7"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "parking_lot 0.12.1",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "schemars"
 | 
			
		||||
version = "0.8.16"
 | 
			
		||||
@ -3678,7 +3434,7 @@ dependencies = [
 | 
			
		||||
 "http",
 | 
			
		||||
 "httparse",
 | 
			
		||||
 "log",
 | 
			
		||||
 "rand 0.8.5",
 | 
			
		||||
 "rand",
 | 
			
		||||
 "rustls",
 | 
			
		||||
 "sha1",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 | 
			
		||||
@ -54,7 +54,6 @@ members = [
 | 
			
		||||
	"derive-docs",
 | 
			
		||||
	"kcl",
 | 
			
		||||
  "kcl-macros",
 | 
			
		||||
  "execution-plan",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[workspace.dependencies]
 | 
			
		||||
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "execution-plan"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
repository = "https://github.com/KittyCAD/modeling-app"
 | 
			
		||||
rust-version = "1.73"
 | 
			
		||||
description = "A DSL for composing KittyCAD API queries"
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
bytes = "1.5"
 | 
			
		||||
kittycad = { workspace = true, features = ["requests"] }
 | 
			
		||||
kittycad-modeling-cmds = "0.1.4"
 | 
			
		||||
serde = { version = "1", features = ["derive"] }
 | 
			
		||||
thiserror = "1"
 | 
			
		||||
uuid = "1.6.1"
 | 
			
		||||
@ -1,13 +0,0 @@
 | 
			
		||||
use crate::{ExecutionError, Value};
 | 
			
		||||
 | 
			
		||||
mod impls;
 | 
			
		||||
 | 
			
		||||
/// Types that can be written to or read from KCEP program memory,
 | 
			
		||||
/// but require multiple values to store.
 | 
			
		||||
/// They get laid out into multiple consecutive memory addresses.
 | 
			
		||||
pub trait Composite: Sized {
 | 
			
		||||
    /// Store the value in memory.
 | 
			
		||||
    fn into_parts(self) -> Vec<Value>;
 | 
			
		||||
    /// Read the value from memory.
 | 
			
		||||
    fn from_parts(values: &[Option<Value>]) -> Result<Self, ExecutionError>;
 | 
			
		||||
}
 | 
			
		||||
@ -1,98 +0,0 @@
 | 
			
		||||
use kittycad_modeling_cmds::{id::ModelingCmdId, shared::Point3d, MovePathPen};
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
use crate::{Address, ExecutionError, Value};
 | 
			
		||||
 | 
			
		||||
use super::Composite;
 | 
			
		||||
 | 
			
		||||
impl<T> Composite for kittycad_modeling_cmds::shared::Point3d<T>
 | 
			
		||||
where
 | 
			
		||||
    Value: From<T>,
 | 
			
		||||
    T: TryFrom<Value, Error = ExecutionError>,
 | 
			
		||||
{
 | 
			
		||||
    fn into_parts(self) -> Vec<Value> {
 | 
			
		||||
        let points = [self.x, self.y, self.z];
 | 
			
		||||
        points.into_iter().map(|component| component.into()).collect()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn from_parts(values: &[Option<Value>]) -> Result<Self, ExecutionError> {
 | 
			
		||||
        let err = ExecutionError::MemoryWrongSize { expected: 3 };
 | 
			
		||||
        let [x, y, z] = [0, 1, 2].map(|n| values.get(n).ok_or(err.clone()));
 | 
			
		||||
        let x = x?.to_owned().ok_or(err.clone())?.try_into()?;
 | 
			
		||||
        let y = y?.to_owned().ok_or(err.clone())?.try_into()?;
 | 
			
		||||
        let z = z?.to_owned().ok_or(err.clone())?.try_into()?;
 | 
			
		||||
        Ok(Self { x, y, z })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const START_PATH: &str = "StartPath";
 | 
			
		||||
const MOVE_PATH_PEN: &str = "MovePathPen";
 | 
			
		||||
 | 
			
		||||
impl Composite for MovePathPen {
 | 
			
		||||
    fn into_parts(self) -> Vec<Value> {
 | 
			
		||||
        let MovePathPen { path, to } = self;
 | 
			
		||||
        let to = to.into_parts();
 | 
			
		||||
        let mut vals = Vec::with_capacity(1 + to.len());
 | 
			
		||||
        vals.push(Value::Uuid(path.into()));
 | 
			
		||||
        vals.extend(to);
 | 
			
		||||
        vals
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn from_parts(values: &[Option<Value>]) -> Result<Self, ExecutionError> {
 | 
			
		||||
        let path = get_some(values, 0)?;
 | 
			
		||||
        let path = Uuid::try_from(path)?;
 | 
			
		||||
        let path = ModelingCmdId::from(path);
 | 
			
		||||
        let to = Point3d::from_parts(&values[1..])?;
 | 
			
		||||
        let params = MovePathPen { path, to };
 | 
			
		||||
        Ok(params)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Memory layout for modeling commands:
 | 
			
		||||
/// Field 0 is the command's name.
 | 
			
		||||
/// Fields 1 onwards are the command's fields.
 | 
			
		||||
impl Composite for kittycad_modeling_cmds::ModelingCmd {
 | 
			
		||||
    fn into_parts(self) -> Vec<Value> {
 | 
			
		||||
        let (endpoint_name, params) = match self {
 | 
			
		||||
            kittycad_modeling_cmds::ModelingCmd::StartPath => (START_PATH, vec![]),
 | 
			
		||||
            kittycad_modeling_cmds::ModelingCmd::MovePathPen(MovePathPen { path, to }) => {
 | 
			
		||||
                let to = to.into_parts();
 | 
			
		||||
                let mut vals = Vec::with_capacity(1 + to.len());
 | 
			
		||||
                vals.push(Value::Uuid(path.into()));
 | 
			
		||||
                vals.extend(to);
 | 
			
		||||
                (MOVE_PATH_PEN, vals)
 | 
			
		||||
            }
 | 
			
		||||
            _ => todo!(),
 | 
			
		||||
        };
 | 
			
		||||
        let mut out = Vec::with_capacity(params.len() + 1);
 | 
			
		||||
        out.push(endpoint_name.to_owned().into());
 | 
			
		||||
        out.extend(params);
 | 
			
		||||
        out
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn from_parts(values: &[Option<Value>]) -> Result<Self, ExecutionError> {
 | 
			
		||||
        // Check the array has an element at index 0
 | 
			
		||||
        let first_memory = values
 | 
			
		||||
            .get(0)
 | 
			
		||||
            .ok_or(ExecutionError::MemoryWrongSize { expected: 1 })?
 | 
			
		||||
            .to_owned();
 | 
			
		||||
        // The element should be Some
 | 
			
		||||
        let first_memory = first_memory.ok_or(ExecutionError::MemoryWrongSize { expected: 1 })?;
 | 
			
		||||
        let endpoint_name: String = first_memory.try_into()?;
 | 
			
		||||
        match endpoint_name.as_str() {
 | 
			
		||||
            START_PATH => Ok(Self::StartPath),
 | 
			
		||||
            MOVE_PATH_PEN => {
 | 
			
		||||
                let params = MovePathPen::from_parts(&values[1..])?;
 | 
			
		||||
                Ok(Self::MovePathPen(params))
 | 
			
		||||
            }
 | 
			
		||||
            other => Err(ExecutionError::UnrecognizedEndpoint(other.to_owned())),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_some(values: &[Option<Value>], i: usize) -> Result<Value, ExecutionError> {
 | 
			
		||||
    let addr = Address(0); // TODO: pass the `start` addr in
 | 
			
		||||
    let v = values.get(i).ok_or(ExecutionError::MemoryEmpty { addr })?.to_owned();
 | 
			
		||||
    let v = v.ok_or(ExecutionError::MemoryEmpty { addr })?.to_owned();
 | 
			
		||||
    Ok(v)
 | 
			
		||||
}
 | 
			
		||||
@ -1,323 +0,0 @@
 | 
			
		||||
//! A KittyCAD execution plan (KCEP) is a list of
 | 
			
		||||
//! - KittyCAD API requests to make
 | 
			
		||||
//! - Values to send in API requests
 | 
			
		||||
//! - Values to assign from API responses
 | 
			
		||||
//! - Computation to perform on values
 | 
			
		||||
//! You can think of it as a domain-specific language for making KittyCAD API calls and using
 | 
			
		||||
//! the results to make other API calls.
 | 
			
		||||
 | 
			
		||||
use composite::Composite;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use std::fmt;
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
mod composite;
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests;
 | 
			
		||||
 | 
			
		||||
/// KCEP's program memory. A flat, linear list of values.
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
#[cfg_attr(test, derive(PartialEq))]
 | 
			
		||||
pub struct Memory(Vec<Option<Value>>);
 | 
			
		||||
 | 
			
		||||
impl Default for Memory {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self(vec![None; 1024])
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// An address in KCEP's program memory.
 | 
			
		||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
 | 
			
		||||
pub struct Address(usize);
 | 
			
		||||
 | 
			
		||||
impl fmt::Display for Address {
 | 
			
		||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
        self.0.fmt(f)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<usize> for Address {
 | 
			
		||||
    fn from(value: usize) -> Self {
 | 
			
		||||
        Self(value)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Memory {
 | 
			
		||||
    /// Get a value from KCEP's program memory.
 | 
			
		||||
    pub fn get(&self, Address(addr): &Address) -> Option<&Value> {
 | 
			
		||||
        self.0[*addr].as_ref()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Store a value in KCEP's program memory.
 | 
			
		||||
    pub fn set(&mut self, Address(addr): Address, value: Value) {
 | 
			
		||||
        // If isn't big enough for this value, double the size of memory until it is.
 | 
			
		||||
        while addr > self.0.len() {
 | 
			
		||||
            self.0.extend(vec![None; self.0.len()]);
 | 
			
		||||
        }
 | 
			
		||||
        self.0[addr] = Some(value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Store a composite value (i.e. a value which takes up multiple addresses in memory).
 | 
			
		||||
    /// Store its parts in consecutive memory addresses starting at `start`.
 | 
			
		||||
    pub fn set_composite<T: Composite>(&mut self, composite_value: T, start: Address) {
 | 
			
		||||
        let parts = composite_value.into_parts().into_iter();
 | 
			
		||||
        for (value, addr) in parts.zip(start.0..) {
 | 
			
		||||
            self.0[addr] = Some(value);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get a composite value (i.e. a value which takes up multiple addresses in memory).
 | 
			
		||||
    /// Its parts are stored in consecutive memory addresses starting at `start`.
 | 
			
		||||
    pub fn get_composite<T: Composite>(&self, start: Address) -> Result<T, ExecutionError> {
 | 
			
		||||
        let values = &self.0[start.0..];
 | 
			
		||||
        T::from_parts(values)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// A value stored in KCEP program memory.
 | 
			
		||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub enum Value {
 | 
			
		||||
    String(String),
 | 
			
		||||
    NumericValue(NumericValue),
 | 
			
		||||
    Uuid(Uuid),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<Uuid> for Value {
 | 
			
		||||
    fn from(u: Uuid) -> Self {
 | 
			
		||||
        Self::Uuid(u)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<String> for Value {
 | 
			
		||||
    fn from(value: String) -> Self {
 | 
			
		||||
        Self::String(value)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<f64> for Value {
 | 
			
		||||
    fn from(value: f64) -> Self {
 | 
			
		||||
        Self::NumericValue(NumericValue::Float(value))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TryFrom<Value> for String {
 | 
			
		||||
    type Error = ExecutionError;
 | 
			
		||||
 | 
			
		||||
    fn try_from(value: Value) -> Result<Self, Self::Error> {
 | 
			
		||||
        if let Value::String(s) = value {
 | 
			
		||||
            Ok(s)
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(ExecutionError::MemoryWrongType {
 | 
			
		||||
                expected: "string",
 | 
			
		||||
                actual: format!("{value:?}"),
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TryFrom<Value> for Uuid {
 | 
			
		||||
    type Error = ExecutionError;
 | 
			
		||||
 | 
			
		||||
    fn try_from(value: Value) -> Result<Self, Self::Error> {
 | 
			
		||||
        if let Value::Uuid(u) = value {
 | 
			
		||||
            Ok(u)
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(ExecutionError::MemoryWrongType {
 | 
			
		||||
                expected: "uuid",
 | 
			
		||||
                actual: format!("{value:?}"),
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
impl TryFrom<Value> for f64 {
 | 
			
		||||
    type Error = ExecutionError;
 | 
			
		||||
 | 
			
		||||
    fn try_from(value: Value) -> Result<Self, Self::Error> {
 | 
			
		||||
        if let Value::NumericValue(NumericValue::Float(x)) = value {
 | 
			
		||||
            Ok(x)
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(ExecutionError::MemoryWrongType {
 | 
			
		||||
                expected: "float",
 | 
			
		||||
                actual: format!("{value:?}"),
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
impl From<usize> for Value {
 | 
			
		||||
    fn from(value: usize) -> Self {
 | 
			
		||||
        Self::NumericValue(NumericValue::Integer(value))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub enum NumericValue {
 | 
			
		||||
    Integer(usize),
 | 
			
		||||
    Float(f64),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// One step of the execution plan.
 | 
			
		||||
#[derive(Serialize, Deserialize)]
 | 
			
		||||
pub enum Instruction {
 | 
			
		||||
    /// Call the KittyCAD API.
 | 
			
		||||
    ApiRequest {
 | 
			
		||||
        /// Which ModelingCmd to call.
 | 
			
		||||
        /// It's a composite value starting at the given address.
 | 
			
		||||
        endpoint: Address,
 | 
			
		||||
        /// Which address should the response be stored in?
 | 
			
		||||
        store_response: Option<usize>,
 | 
			
		||||
        /// Look up each API request in this register number.
 | 
			
		||||
        arguments: Vec<Address>,
 | 
			
		||||
    },
 | 
			
		||||
    /// Set a value in memory.
 | 
			
		||||
    Set {
 | 
			
		||||
        /// Which memory address to set.
 | 
			
		||||
        address: Address,
 | 
			
		||||
        /// What value to set the memory address to.
 | 
			
		||||
        value: Value,
 | 
			
		||||
    },
 | 
			
		||||
    /// Perform arithmetic on values in memory.
 | 
			
		||||
    Arithmetic {
 | 
			
		||||
        /// What to do.
 | 
			
		||||
        arithmetic: Arithmetic,
 | 
			
		||||
        /// Write the output to this memory address.
 | 
			
		||||
        destination: Address,
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Instruction to perform arithmetic on values in memory.
 | 
			
		||||
#[derive(Deserialize, Serialize)]
 | 
			
		||||
pub struct Arithmetic {
 | 
			
		||||
    /// Apply this operation
 | 
			
		||||
    pub operation: Operation,
 | 
			
		||||
    /// First operand for the operation
 | 
			
		||||
    pub operand0: Operand,
 | 
			
		||||
    /// Second operand for the operation
 | 
			
		||||
    pub operand1: Operand,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
macro_rules! arithmetic_body {
 | 
			
		||||
    ($arith:ident, $mem:ident, $method:ident) => {
 | 
			
		||||
        match (
 | 
			
		||||
            $arith.operand0.eval(&$mem)?.clone(),
 | 
			
		||||
            $arith.operand1.eval(&$mem)?.clone(),
 | 
			
		||||
        ) {
 | 
			
		||||
            // If both operands are numeric, then do the arithmetic operation.
 | 
			
		||||
            (Value::NumericValue(x), Value::NumericValue(y)) => {
 | 
			
		||||
                let num = match (x, y) {
 | 
			
		||||
                    (NumericValue::Integer(x), NumericValue::Integer(y)) => NumericValue::Integer(x.$method(y)),
 | 
			
		||||
                    (NumericValue::Integer(x), NumericValue::Float(y)) => NumericValue::Float((x as f64).$method(y)),
 | 
			
		||||
                    (NumericValue::Float(x), NumericValue::Integer(y)) => NumericValue::Float(x.$method(y as f64)),
 | 
			
		||||
                    (NumericValue::Float(x), NumericValue::Float(y)) => NumericValue::Float(x.$method(y)),
 | 
			
		||||
                };
 | 
			
		||||
                Ok(Value::NumericValue(num))
 | 
			
		||||
            }
 | 
			
		||||
            // This operation can only be done on numeric types.
 | 
			
		||||
            _ => Err(ExecutionError::CannotApplyOperation {
 | 
			
		||||
                op: $arith.operation,
 | 
			
		||||
                operands: vec![
 | 
			
		||||
                    $arith.operand0.eval(&$mem)?.clone().to_owned(),
 | 
			
		||||
                    $arith.operand1.eval(&$mem)?.clone().to_owned(),
 | 
			
		||||
                ],
 | 
			
		||||
            }),
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
impl Arithmetic {
 | 
			
		||||
    /// Calculate the the arithmetic equation.
 | 
			
		||||
    /// May read values from the given memory.
 | 
			
		||||
    fn calculate(self, mem: &Memory) -> Result<Value, ExecutionError> {
 | 
			
		||||
        use std::ops::{Add, Div, Mul, Sub};
 | 
			
		||||
        match self.operation {
 | 
			
		||||
            Operation::Add => {
 | 
			
		||||
                arithmetic_body!(self, mem, add)
 | 
			
		||||
            }
 | 
			
		||||
            Operation::Mul => {
 | 
			
		||||
                arithmetic_body!(self, mem, mul)
 | 
			
		||||
            }
 | 
			
		||||
            Operation::Sub => {
 | 
			
		||||
                arithmetic_body!(self, mem, sub)
 | 
			
		||||
            }
 | 
			
		||||
            Operation::Div => {
 | 
			
		||||
                arithmetic_body!(self, mem, div)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Operations that can be applied to values in memory.
 | 
			
		||||
#[derive(Debug, Deserialize, Serialize, Clone, Copy)]
 | 
			
		||||
pub enum Operation {
 | 
			
		||||
    Add,
 | 
			
		||||
    Mul,
 | 
			
		||||
    Sub,
 | 
			
		||||
    Div,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl fmt::Display for Operation {
 | 
			
		||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
			
		||||
        match self {
 | 
			
		||||
            Operation::Add => "+",
 | 
			
		||||
            Operation::Mul => "*",
 | 
			
		||||
            Operation::Sub => "-",
 | 
			
		||||
            Operation::Div => "/",
 | 
			
		||||
        }
 | 
			
		||||
        .fmt(f)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Argument to an operation.
 | 
			
		||||
#[derive(Deserialize, Serialize)]
 | 
			
		||||
pub enum Operand {
 | 
			
		||||
    Literal(Value),
 | 
			
		||||
    Reference(Address),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Operand {
 | 
			
		||||
    /// Evaluate the operand, getting its value.
 | 
			
		||||
    fn eval(&self, mem: &Memory) -> Result<Value, ExecutionError> {
 | 
			
		||||
        match self {
 | 
			
		||||
            Operand::Literal(v) => Ok(v.to_owned()),
 | 
			
		||||
            Operand::Reference(addr) => match mem.get(addr) {
 | 
			
		||||
                None => Err(ExecutionError::MemoryEmpty { addr: *addr }),
 | 
			
		||||
                Some(v) => Ok(v.to_owned()),
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Execute the plan.
 | 
			
		||||
pub fn execute(mem: &mut Memory, plan: Vec<Instruction>) -> Result<(), ExecutionError> {
 | 
			
		||||
    for step in plan {
 | 
			
		||||
        match step {
 | 
			
		||||
            Instruction::ApiRequest { .. } => todo!("Execute API calls"),
 | 
			
		||||
            Instruction::Set { address, value } => {
 | 
			
		||||
                mem.set(address, value);
 | 
			
		||||
            }
 | 
			
		||||
            Instruction::Arithmetic {
 | 
			
		||||
                arithmetic,
 | 
			
		||||
                destination,
 | 
			
		||||
            } => {
 | 
			
		||||
                let out = arithmetic.calculate(mem)?;
 | 
			
		||||
                mem.set(destination, out);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Errors that could occur when executing a KittyCAD execution plan.
 | 
			
		||||
#[derive(Debug, thiserror::Error, Clone)]
 | 
			
		||||
pub enum ExecutionError {
 | 
			
		||||
    #[error("Memory address {addr} was not set")]
 | 
			
		||||
    MemoryEmpty { addr: Address },
 | 
			
		||||
    #[error("Cannot apply operation {op} to operands {operands:?}")]
 | 
			
		||||
    CannotApplyOperation { op: Operation, operands: Vec<Value> },
 | 
			
		||||
    #[error("Tried to read a '{expected}' from KCEP program memory, found an '{actual}' instead")]
 | 
			
		||||
    MemoryWrongType { expected: &'static str, actual: String },
 | 
			
		||||
    #[error("Wanted {expected} values but did not get enough")]
 | 
			
		||||
    MemoryWrongSize { expected: usize },
 | 
			
		||||
    #[error("No endpoint {0} recognized")]
 | 
			
		||||
    UnrecognizedEndpoint(String),
 | 
			
		||||
}
 | 
			
		||||
@ -1,117 +0,0 @@
 | 
			
		||||
use kittycad_modeling_cmds::{id::ModelingCmdId, shared::Point3d, ModelingCmd, MovePathPen};
 | 
			
		||||
 | 
			
		||||
use super::*;
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn write_addr_to_memory() {
 | 
			
		||||
    let plan = vec![Instruction::Set {
 | 
			
		||||
        address: Address(0),
 | 
			
		||||
        value: 3.4.into(),
 | 
			
		||||
    }];
 | 
			
		||||
    let mut mem = Memory::default();
 | 
			
		||||
    execute(&mut mem, plan).unwrap();
 | 
			
		||||
    assert_eq!(mem.get(&Address(0)), Some(&3.4.into()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn add_literals() {
 | 
			
		||||
    let plan = vec![Instruction::Arithmetic {
 | 
			
		||||
        arithmetic: Arithmetic {
 | 
			
		||||
            operation: Operation::Add,
 | 
			
		||||
            operand0: Operand::Literal(3.into()),
 | 
			
		||||
            operand1: Operand::Literal(2.into()),
 | 
			
		||||
        },
 | 
			
		||||
        destination: Address(1),
 | 
			
		||||
    }];
 | 
			
		||||
    let mut mem = Memory::default();
 | 
			
		||||
    execute(&mut mem, plan).unwrap();
 | 
			
		||||
    assert_eq!(mem.get(&Address(1)), Some(&5.into()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn add_literal_to_reference() {
 | 
			
		||||
    let plan = vec![
 | 
			
		||||
        // Memory addr 0 contains 450
 | 
			
		||||
        Instruction::Set {
 | 
			
		||||
            address: Address(0),
 | 
			
		||||
            value: 450.into(),
 | 
			
		||||
        },
 | 
			
		||||
        // Add 20 to addr 0
 | 
			
		||||
        Instruction::Arithmetic {
 | 
			
		||||
            arithmetic: Arithmetic {
 | 
			
		||||
                operation: Operation::Add,
 | 
			
		||||
                operand0: Operand::Reference(Address(0)),
 | 
			
		||||
                operand1: Operand::Literal(20.into()),
 | 
			
		||||
            },
 | 
			
		||||
            destination: Address(1),
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
    // 20 + 450 = 470
 | 
			
		||||
    let mut mem = Memory::default();
 | 
			
		||||
    execute(&mut mem, plan).unwrap();
 | 
			
		||||
    assert_eq!(mem.get(&Address(1)), Some(&470.into()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn add_to_composite_value() {
 | 
			
		||||
    let mut mem = Memory::default();
 | 
			
		||||
 | 
			
		||||
    // Write a point to memory.
 | 
			
		||||
    let point_before = Point3d {
 | 
			
		||||
        x: 2.0f64,
 | 
			
		||||
        y: 3.0,
 | 
			
		||||
        z: 4.0,
 | 
			
		||||
    };
 | 
			
		||||
    let start_addr = Address(0);
 | 
			
		||||
    mem.set_composite(point_before, start_addr);
 | 
			
		||||
    assert_eq!(mem.0[0], Some(2.0.into()));
 | 
			
		||||
    assert_eq!(mem.0[1], Some(3.0.into()));
 | 
			
		||||
    assert_eq!(mem.0[2], Some(4.0.into()));
 | 
			
		||||
 | 
			
		||||
    // Update the point's x-value in memory.
 | 
			
		||||
    execute(
 | 
			
		||||
        &mut mem,
 | 
			
		||||
        vec![Instruction::Arithmetic {
 | 
			
		||||
            arithmetic: Arithmetic {
 | 
			
		||||
                operation: Operation::Add,
 | 
			
		||||
                operand0: Operand::Reference(start_addr),
 | 
			
		||||
                operand1: Operand::Literal(40.into()),
 | 
			
		||||
            },
 | 
			
		||||
            destination: start_addr,
 | 
			
		||||
        }],
 | 
			
		||||
    )
 | 
			
		||||
    .unwrap();
 | 
			
		||||
 | 
			
		||||
    // Read the point out of memory, validate it.
 | 
			
		||||
    let point_after: Point3d<f64> = mem.get_composite(start_addr).unwrap();
 | 
			
		||||
    assert_eq!(
 | 
			
		||||
        point_after,
 | 
			
		||||
        Point3d {
 | 
			
		||||
            x: 42.0,
 | 
			
		||||
            y: 3.0,
 | 
			
		||||
            z: 4.0
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn api_types() {
 | 
			
		||||
    let mut mem = Memory::default();
 | 
			
		||||
    let start_addr = Address(0);
 | 
			
		||||
    let id = ModelingCmdId(Uuid::parse_str("6306afa2-3999-4b03-af30-1efad7cdc6fc").unwrap());
 | 
			
		||||
    let p = Point3d {
 | 
			
		||||
        x: 2.0f64,
 | 
			
		||||
        y: 3.0,
 | 
			
		||||
        z: 4.0,
 | 
			
		||||
    };
 | 
			
		||||
    let val_in = ModelingCmd::MovePathPen(MovePathPen { path: id, to: p });
 | 
			
		||||
    mem.set_composite(val_in, start_addr);
 | 
			
		||||
    let val_out: ModelingCmd = mem.get_composite(start_addr).unwrap();
 | 
			
		||||
    match val_out {
 | 
			
		||||
        ModelingCmd::MovePathPen(params) => {
 | 
			
		||||
            assert_eq!(params.to, p);
 | 
			
		||||
            assert_eq!(params.path, id);
 | 
			
		||||
        }
 | 
			
		||||
        _ => panic!("unexpected ModelingCmd variant"),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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"
 | 
			
		||||
 | 
			
		||||