add in gestures and ctrl+space command tool (TBD add global LLM)
This commit is contained in:
parent
bc831c7516
commit
71a6b29165
|
|
@ -0,0 +1,75 @@
|
|||
# Gesture Recognition Tool
|
||||
|
||||
This document describes all available gestures in the Canvas application. Use the gesture tool (press `g` or select from toolbar) to draw these gestures and trigger their actions.
|
||||
|
||||
## How to Use
|
||||
|
||||
1. **Activate the Gesture Tool**: Press `g` or select the gesture tool from the toolbar
|
||||
2. **Draw a Gesture**: Use your mouse, pen, or finger to draw one of the gestures below
|
||||
3. **Release**: The gesture will be recognized and the corresponding action will be performed
|
||||
|
||||
## Available Gestures
|
||||
|
||||
### Basic Gestures (Default Mode)
|
||||
|
||||
| Gesture | Description | Action |
|
||||
|---------|-------------|---------|
|
||||
| **X** | Draw an "X" shape | Deletes selected shapes |
|
||||
| **Rectangle** | Draw a rectangle outline | Creates a rectangle shape at the gesture location |
|
||||
| **Circle** | Draw a circle/oval | Selects and highlights shapes under the gesture |
|
||||
| **Check** | Draw a checkmark (✓) | Changes color of shapes under the gesture to green |
|
||||
| **Caret** | Draw a caret (^) pointing up | Aligns selected shapes to the top |
|
||||
| **V** | Draw a "V" shape pointing down | Aligns selected shapes to the bottom |
|
||||
| **Delete** | Draw a delete symbol (similar to X) | Deletes selected shapes |
|
||||
| **Pigtail** | Draw a pigtail/spiral shape | Selects shapes under gesture and rotates them 90° counterclockwise |
|
||||
|
||||
### Layout Gestures (Hold Shift + Draw)
|
||||
|
||||
| Gesture | Description | Action |
|
||||
|---------|-------------|---------|
|
||||
| **Circle Layout** | Draw a circle while holding Shift | Arranges selected shapes in a circle around the gesture center |
|
||||
| **Triangle Layout** | Draw a triangle while holding Shift | Arranges selected shapes in a triangle around the gesture center |
|
||||
|
||||
## Gesture Tips
|
||||
|
||||
- **Accuracy**: Draw gestures clearly and completely for best recognition
|
||||
- **Size**: Gestures work at various sizes, but avoid extremely small or large drawings
|
||||
- **Speed**: Draw at a natural pace - not too fast or too slow
|
||||
- **Shift Key**: Hold Shift while drawing to access layout gestures
|
||||
- **Selection**: Most gestures work on selected shapes, so select shapes first if needed
|
||||
|
||||
## Keyboard Shortcut
|
||||
|
||||
- **`g`**: Activate the gesture tool
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If a gesture isn't recognized, try drawing it more clearly or at a different size
|
||||
- Make sure you're using the gesture tool (cursor should change to a cross)
|
||||
- For layout gestures, remember to hold Shift while drawing
|
||||
- Some gestures require shapes to be selected first
|
||||
|
||||
## Examples
|
||||
|
||||
### Deleting Shapes
|
||||
1. Select the shapes you want to delete
|
||||
2. Press `g` to activate gesture tool
|
||||
3. Draw an "X" over the shapes
|
||||
4. Release - the shapes will be deleted
|
||||
|
||||
### Creating a Rectangle
|
||||
1. Press `g` to activate gesture tool
|
||||
2. Draw a rectangle outline where you want the shape
|
||||
3. Release - a rectangle will be created
|
||||
|
||||
### Arranging Shapes in a Circle
|
||||
1. Select the shapes you want to arrange
|
||||
2. Press `g` to activate gesture tool
|
||||
3. Hold Shift and draw a circle
|
||||
4. Release - the shapes will be arranged in a circle
|
||||
|
||||
### Rotating Shapes
|
||||
1. Select the shapes you want to rotate
|
||||
2. Press `g` to activate gesture tool
|
||||
3. Draw a pigtail/spiral over the shapes
|
||||
4. Release - the shapes will rotate 90° counterclockwise
|
||||
|
|
@ -34,6 +34,7 @@
|
|||
"openai": "^4.79.3",
|
||||
"rbush": "^4.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-cmdk": "^1.3.9",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.0.2",
|
||||
|
|
@ -1239,6 +1240,32 @@
|
|||
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@headlessui/react": {
|
||||
"version": "1.7.19",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz",
|
||||
"integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/react-virtual": "^3.0.0-beta.60",
|
||||
"client-only": "^0.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16 || ^17 || ^18",
|
||||
"react-dom": "^16 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/@heroicons/react": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
|
||||
"integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">= 16 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
||||
|
|
@ -1623,7 +1650,6 @@
|
|||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
|
|
@ -1647,12 +1673,21 @@
|
|||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz",
|
||||
"integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
|
|
@ -1663,7 +1698,6 @@
|
|||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
|
|
@ -3013,6 +3047,33 @@
|
|||
"integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
|
||||
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.13.12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
||||
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tldraw/assets": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@tldraw/assets/-/assets-3.6.1.tgz",
|
||||
|
|
@ -3343,6 +3404,12 @@
|
|||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/html-minifier-terser": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
|
||||
"integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
|
|
@ -4462,6 +4529,12 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||
|
|
@ -4471,6 +4544,16 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/camel-case": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
|
||||
"integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pascal-case": "^3.1.2",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001690",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
|
||||
|
|
@ -4656,6 +4739,24 @@
|
|||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clean-css": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
|
||||
"integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"source-map": "~0.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
|
|
@ -4899,6 +5000,22 @@
|
|||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
|
||||
"integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.0.1",
|
||||
"domhandler": "^4.3.1",
|
||||
"domutils": "^2.8.0",
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/css-selector-parser": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.0.5.tgz",
|
||||
|
|
@ -4915,6 +5032,18 @@
|
|||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/cssom": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
|
||||
|
|
@ -5625,6 +5754,50 @@
|
|||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-converter": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
|
||||
"integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utila": "~0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
||||
"integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.0.1",
|
||||
"domhandler": "^4.2.0",
|
||||
"entities": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer/node_modules/entities": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
|
||||
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
|
||||
"license": "BSD-2-Clause",
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/domexception": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
|
||||
|
|
@ -5638,6 +5811,21 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
|
||||
"integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz",
|
||||
|
|
@ -5645,6 +5833,30 @@
|
|||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
|
||||
"integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^1.0.1",
|
||||
"domelementtype": "^2.2.0",
|
||||
"domhandler": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dot-case": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
|
||||
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"no-case": "^3.0.4",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/edge-runtime": {
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/edge-runtime/-/edge-runtime-2.5.9.tgz",
|
||||
|
|
@ -6937,6 +7149,15 @@
|
|||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/hotkeys-js": {
|
||||
"version": "3.13.9",
|
||||
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.9.tgz",
|
||||
|
|
@ -6958,6 +7179,36 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/html-minifier-terser": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
|
||||
"integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"camel-case": "^4.1.2",
|
||||
"clean-css": "^5.2.2",
|
||||
"commander": "^8.3.0",
|
||||
"he": "^1.2.0",
|
||||
"param-case": "^3.0.4",
|
||||
"relateurl": "^0.2.7",
|
||||
"terser": "^5.10.0"
|
||||
},
|
||||
"bin": {
|
||||
"html-minifier-terser": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/html-minifier-terser/node_modules/commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/html-url-attributes": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
|
||||
|
|
@ -6978,6 +7229,38 @@
|
|||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/html-webpack-plugin": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz",
|
||||
"integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/html-minifier-terser": "^6.0.0",
|
||||
"html-minifier-terser": "^6.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"pretty-error": "^4.0.0",
|
||||
"tapable": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/html-webpack-plugin"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rspack/core": "0.x || 1.x",
|
||||
"webpack": "^5.20.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@rspack/core": {
|
||||
"optional": true
|
||||
},
|
||||
"webpack": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
|
|
@ -6991,6 +7274,34 @@
|
|||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
|
||||
"integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.0.1",
|
||||
"domhandler": "^4.0.0",
|
||||
"domutils": "^2.5.2",
|
||||
"entities": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2/node_modules/entities": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
|
||||
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
|
||||
"license": "BSD-2-Clause",
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.4.0.tgz",
|
||||
|
|
@ -7514,7 +7825,6 @@
|
|||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
|
|
@ -7564,6 +7874,15 @@
|
|||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lower-case": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
|
||||
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
|
|
@ -8754,6 +9073,16 @@
|
|||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/no-case": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
||||
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lower-case": "^2.0.2",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
|
|
@ -8995,6 +9324,16 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/param-case": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
|
||||
"integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dot-case": "^3.0.4",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-entities": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
|
||||
|
|
@ -9041,6 +9380,16 @@
|
|||
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pascal-case": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
|
||||
"integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"no-case": "^3.0.4",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/path-browserify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||
|
|
@ -9158,6 +9507,16 @@
|
|||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-error": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
|
||||
"integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.20",
|
||||
"renderkid": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-ms": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz",
|
||||
|
|
@ -9333,6 +9692,21 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-cmdk": {
|
||||
"version": "1.3.9",
|
||||
"resolved": "https://registry.npmjs.org/react-cmdk/-/react-cmdk-1.3.9.tgz",
|
||||
"integrity": "sha512-MSVmAQZ9iqY7hO3r++XP6yWSHzGfMDGMvY3qlDT8k5RiWoRFwO1CGPlsWzhvcUbPilErzsMKK7uB4McEcX4B6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.6.4",
|
||||
"@heroicons/react": "^2.0.13",
|
||||
"html-webpack-plugin": "^5.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.x || ^17.x || ^18.x",
|
||||
"react-dom": "^16.x || ^17.x || ^18.x"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
|
|
@ -9776,6 +10150,15 @@
|
|||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/relateurl": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
|
||||
"integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-gfm": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
|
||||
|
|
@ -9857,6 +10240,19 @@
|
|||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/renderkid": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
|
||||
"integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-select": "^4.1.3",
|
||||
"dom-converter": "^0.2.0",
|
||||
"htmlparser2": "^6.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"strip-ansi": "^6.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
|
|
@ -10269,7 +10665,6 @@
|
|||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"devOptional": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
|
@ -10285,6 +10680,16 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sourcemap-codec": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
|
||||
|
|
@ -10531,6 +10936,15 @@
|
|||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
|
||||
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "4.4.18",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.18.tgz",
|
||||
|
|
@ -10549,6 +10963,30 @@
|
|||
"node": ">=4.5"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.43.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
|
||||
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.14.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/terser/node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
|
|
@ -11064,6 +11502,12 @@
|
|||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utila": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
|
||||
"integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
"openai": "^4.79.3",
|
||||
"rbush": "^4.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-cmdk": "^1.3.9",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.0.2",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,290 @@
|
|||
import CommandPalette, { filterItems, getItemIndex } from "react-cmdk"
|
||||
import { Fragment, useEffect, useState } from "react"
|
||||
import {
|
||||
Editor,
|
||||
TLShape,
|
||||
TLShapeId,
|
||||
unwrapLabel,
|
||||
useActions,
|
||||
useEditor,
|
||||
useLocalStorageState,
|
||||
useTranslation,
|
||||
useValue,
|
||||
} from "tldraw"
|
||||
// import { generateText } from "@/utils/llmUtils"
|
||||
import "@/css/style.css"
|
||||
|
||||
function toNearest(n: number, places = 2) {
|
||||
return Math.round(n * 10 ** places) / 10 ** places
|
||||
}
|
||||
|
||||
interface SimpleShape {
|
||||
type: string
|
||||
x: number
|
||||
y: number
|
||||
rotation: string
|
||||
properties: unknown
|
||||
}
|
||||
|
||||
function simplifiedShape(editor: Editor, shape: TLShape): SimpleShape {
|
||||
const bounds = editor.getShapePageBounds(shape.id)
|
||||
return {
|
||||
type: shape.type,
|
||||
x: toNearest(shape.x),
|
||||
y: toNearest(shape.y),
|
||||
rotation: `${toNearest(shape.rotation, 3)} radians`,
|
||||
properties: {
|
||||
...shape.props,
|
||||
w: toNearest(bounds?.width || 0),
|
||||
h: toNearest(bounds?.height || 0),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const CmdK = () => {
|
||||
const editor = useEditor()
|
||||
const actions = useActions()
|
||||
const trans = useTranslation()
|
||||
|
||||
const [inputRefs, setInputRefs] = useState<Set<string>>(new Set())
|
||||
const [response, setResponse] = useLocalStorageState("response", "")
|
||||
const [open, setOpen] = useState<boolean>(false)
|
||||
const [input, setInput] = useLocalStorageState("input", "")
|
||||
const [page, setPage] = useLocalStorageState<"search" | "llm">(
|
||||
"page",
|
||||
"search",
|
||||
)
|
||||
|
||||
const availableRefs = useValue<Map<string, TLShapeId[]>>(
|
||||
"avaiable refs",
|
||||
() => {
|
||||
const nameToShapeIdMap = new Map<string, TLShapeId[]>(
|
||||
editor
|
||||
.getCurrentPageShapes()
|
||||
.filter((shape) => shape.meta.name)
|
||||
.map((shape) => [shape.meta.name as string, [shape.id]]),
|
||||
)
|
||||
|
||||
const selected = editor.getSelectedShapeIds()
|
||||
const inView = editor
|
||||
.getShapesAtPoint(editor.getViewportPageBounds().center, {
|
||||
margin: 1200,
|
||||
})
|
||||
.map((o) => o.id)
|
||||
|
||||
return new Map([
|
||||
...nameToShapeIdMap,
|
||||
["selected", selected],
|
||||
["here", inView],
|
||||
])
|
||||
},
|
||||
[editor],
|
||||
)
|
||||
|
||||
/** Track the shapes we are referencing in the input */
|
||||
useEffect(() => {
|
||||
const namesInInput = input
|
||||
.split(" ")
|
||||
.filter((name) => name.startsWith("@"))
|
||||
.map((name) => name.slice(1).match(/^[a-zA-Z0-9]+/)?.[0])
|
||||
.filter(Boolean)
|
||||
|
||||
setInputRefs(new Set(namesInInput as string[]))
|
||||
}, [input])
|
||||
|
||||
/** Handle keyboard shortcuts for Opening and closing the command bar in search/llm mode */
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === " " && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setPage("search")
|
||||
setOpen(true)
|
||||
}
|
||||
if (e.key === "j" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setPage("llm")
|
||||
setOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => document.removeEventListener("keydown", down)
|
||||
}, [setPage])
|
||||
|
||||
const menuItems = filterItems(
|
||||
[
|
||||
{
|
||||
heading: "Actions",
|
||||
id: "actions",
|
||||
items: Object.entries(actions).map(([key, action]) => ({
|
||||
id: key,
|
||||
children: trans(unwrapLabel(action.label)),
|
||||
onClick: () => action.onSelect("unknown"),
|
||||
itemType: "foobar",
|
||||
})),
|
||||
},
|
||||
{
|
||||
heading: "Other",
|
||||
id: "other",
|
||||
items: [
|
||||
{
|
||||
id: "llm",
|
||||
children: "LLM",
|
||||
icon: "ArrowRightOnRectangleIcon",
|
||||
closeOnSelect: false,
|
||||
onClick: () => {
|
||||
setInput("")
|
||||
setPage("llm")
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
input,
|
||||
)
|
||||
|
||||
type ContextItem =
|
||||
| { name: string; shape: SimpleShape; shapes?: never }
|
||||
| { name: string; shape?: never; shapes: SimpleShape[] }
|
||||
|
||||
const handlePromptSubmit = () => {
|
||||
const cleanedPrompt = input.trim()
|
||||
const context: ContextItem[] = []
|
||||
|
||||
for (const name of inputRefs) {
|
||||
if (!availableRefs.has(name)) continue
|
||||
const shapes = availableRefs.get(name)?.map((id) => editor.getShape(id))
|
||||
if (!shapes || shapes.length < 1) continue
|
||||
|
||||
if (shapes.length === 1) {
|
||||
const contextShape: SimpleShape = simplifiedShape(editor, shapes[0]!)
|
||||
context.push({ name, shape: contextShape })
|
||||
} else {
|
||||
const contextShapes: SimpleShape[] = []
|
||||
for (const shape of shapes) {
|
||||
contextShapes.push(simplifiedShape(editor, shape!))
|
||||
}
|
||||
context.push({ name, shapes: contextShapes })
|
||||
}
|
||||
}
|
||||
|
||||
const systemPrompt = `You are a helpful assistant. Respond in plaintext.
|
||||
|
||||
Context:
|
||||
${JSON.stringify(context)}
|
||||
`
|
||||
|
||||
setResponse("🤖...")
|
||||
// generateText(cleanedPrompt, systemPrompt, (partialResponse, _) => {
|
||||
// setResponse(partialResponse)
|
||||
// })
|
||||
}
|
||||
|
||||
const ContextPrefix = ({ inputRefs }: { inputRefs: Set<string> }) => {
|
||||
return inputRefs.size > 0 ? (
|
||||
<span>Ask with: </span>
|
||||
) : (
|
||||
<span style={{ opacity: 0.5 }}>No references</span>
|
||||
)
|
||||
}
|
||||
|
||||
const LLMView = () => {
|
||||
return (
|
||||
<>
|
||||
<CommandPalette.ListItem
|
||||
className="references"
|
||||
index={0}
|
||||
showType={false}
|
||||
onClick={handlePromptSubmit}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<ContextPrefix inputRefs={inputRefs} />
|
||||
{Array.from(inputRefs).map((name, index, array) => {
|
||||
const refShapeIds = availableRefs.get(name)
|
||||
if (!refShapeIds) return null
|
||||
return (
|
||||
<Fragment key={name}>
|
||||
<span
|
||||
className={refShapeIds ? "reference" : "reference-missing"}
|
||||
onKeyDown={() => {}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
if (!refShapeIds) return
|
||||
editor.setSelectedShapes(refShapeIds)
|
||||
editor.zoomToSelection({
|
||||
animation: {
|
||||
duration: 200,
|
||||
easing: (t: number) => t * t * (3 - 2 * t),
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
{index < array.length - 1 && (
|
||||
<span style={{ marginLeft: "0em" }}>,</span>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</CommandPalette.ListItem>
|
||||
|
||||
{response && (
|
||||
<>
|
||||
<CommandPalette.ListItem
|
||||
disabled={true}
|
||||
className="llm-response"
|
||||
index={1}
|
||||
showType={false}
|
||||
>
|
||||
{response}
|
||||
</CommandPalette.ListItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const SearchView = () => {
|
||||
return (
|
||||
<>
|
||||
{menuItems.length ? (
|
||||
menuItems.map((list) => (
|
||||
<CommandPalette.List key={list.id} heading={list.heading}>
|
||||
{list.items.map(({ id, ...rest }) => (
|
||||
<CommandPalette.ListItem
|
||||
key={id}
|
||||
index={getItemIndex(menuItems, id)}
|
||||
{...rest}
|
||||
/>
|
||||
))}
|
||||
</CommandPalette.List>
|
||||
))
|
||||
) : (
|
||||
<CommandPalette.FreeSearchAction label="Search for" />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandPalette
|
||||
placeholder={page === "search" ? "Search..." : "Ask..."}
|
||||
onChangeSearch={setInput}
|
||||
onChangeOpen={setOpen}
|
||||
search={input}
|
||||
isOpen={open}
|
||||
page={page}
|
||||
>
|
||||
<CommandPalette.Page id="search">
|
||||
<SearchView />
|
||||
</CommandPalette.Page>
|
||||
<CommandPalette.Page id="llm">
|
||||
<LLMView />
|
||||
</CommandPalette.Page>
|
||||
</CommandPalette>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,498 @@
|
|||
import { DEFAULT_GESTURES, ALT_GESTURES } from "@/default_gestures"
|
||||
import { DollarRecognizer } from "@/gestures"
|
||||
import {
|
||||
StateNode,
|
||||
TLDefaultSizeStyle,
|
||||
TLDrawShape,
|
||||
TLDrawShapeSegment,
|
||||
TLEventHandlers,
|
||||
TLHighlightShape,
|
||||
TLPointerEventInfo,
|
||||
TLShapePartial,
|
||||
TLTextShape,
|
||||
Vec,
|
||||
createShapeId,
|
||||
uniqueId,
|
||||
} from "tldraw"
|
||||
|
||||
const STROKE_WIDTH = 10
|
||||
const SHOW_LABELS = true
|
||||
const PRESSURE = 0.5
|
||||
|
||||
export class GestureTool extends StateNode {
|
||||
static override id = "gesture"
|
||||
static override initial = "idle"
|
||||
static override children = () => [Idle, Drawing]
|
||||
static recognizer = new DollarRecognizer(DEFAULT_GESTURES)
|
||||
static recognizerAlt = new DollarRecognizer(ALT_GESTURES)
|
||||
|
||||
override shapeType = "draw"
|
||||
|
||||
override onExit = () => {
|
||||
const drawingState = this.children!.drawing as Drawing
|
||||
drawingState.initialShape = undefined
|
||||
}
|
||||
}
|
||||
|
||||
export class Idle extends StateNode {
|
||||
static override id = "idle"
|
||||
|
||||
tooltipElement?: HTMLDivElement
|
||||
tooltipTimeout?: NodeJS.Timeout
|
||||
mouseMoveHandler?: (e: MouseEvent) => void
|
||||
|
||||
override onPointerDown: TLEventHandlers["onPointerDown"] = (info) => {
|
||||
this.parent.transition("drawing", info)
|
||||
}
|
||||
|
||||
override onEnter = () => {
|
||||
this.editor.setCursor({ type: "cross", rotation: 0 })
|
||||
|
||||
// Create tooltip element
|
||||
this.tooltipElement = document.createElement('div')
|
||||
this.tooltipElement.style.cssText = `
|
||||
position: fixed;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-line;
|
||||
z-index: 10000;
|
||||
pointer-events: none;
|
||||
max-width: 300px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
`
|
||||
|
||||
// Set tooltip content
|
||||
this.tooltipElement.innerHTML = `
|
||||
<strong>Gesture Tool Active</strong><br><br>
|
||||
<strong>Basic Gestures:</strong><br>
|
||||
• X, Rectangle, Circle, Check<br>
|
||||
• Caret, V, Delete, Pigtail<br><br>
|
||||
<strong>Shift + Draw:</strong><br>
|
||||
• Circle Layout, Triangle Layout<br><br>
|
||||
Press 'g' again or select another tool to exit
|
||||
`
|
||||
|
||||
// Add tooltip to DOM
|
||||
document.body.appendChild(this.tooltipElement)
|
||||
|
||||
// Function to update tooltip position
|
||||
this.mouseMoveHandler = (e: MouseEvent) => {
|
||||
if (this.tooltipElement) {
|
||||
const x = e.clientX + 20
|
||||
const y = e.clientY - 20
|
||||
|
||||
// Keep tooltip within viewport bounds
|
||||
const rect = this.tooltipElement.getBoundingClientRect()
|
||||
const viewportWidth = window.innerWidth
|
||||
const viewportHeight = window.innerHeight
|
||||
|
||||
let finalX = x
|
||||
let finalY = y
|
||||
|
||||
// Adjust if tooltip would go off the right edge
|
||||
if (x + rect.width > viewportWidth) {
|
||||
finalX = e.clientX - rect.width - 20
|
||||
}
|
||||
|
||||
// Adjust if tooltip would go off the bottom edge
|
||||
if (y + rect.height > viewportHeight) {
|
||||
finalY = e.clientY - rect.height - 20
|
||||
}
|
||||
|
||||
// Ensure tooltip doesn't go off the top or left
|
||||
finalX = Math.max(10, finalX)
|
||||
finalY = Math.max(10, finalY)
|
||||
|
||||
this.tooltipElement.style.left = `${finalX}px`
|
||||
this.tooltipElement.style.top = `${finalY}px`
|
||||
}
|
||||
}
|
||||
|
||||
// Add mouse move listener
|
||||
document.addEventListener('mousemove', this.mouseMoveHandler)
|
||||
|
||||
// Set initial position
|
||||
if (this.mouseMoveHandler) {
|
||||
this.mouseMoveHandler({ clientX: 100, clientY: 100 } as MouseEvent)
|
||||
}
|
||||
|
||||
// Remove the tooltip after 5 seconds
|
||||
this.tooltipTimeout = setTimeout(() => {
|
||||
this.cleanupTooltip()
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
override onCancel = () => {
|
||||
this.editor.setCurrentTool("select")
|
||||
}
|
||||
|
||||
override onExit = () => {
|
||||
this.cleanupTooltip()
|
||||
}
|
||||
|
||||
private cleanupTooltip = () => {
|
||||
// Clear timeout
|
||||
if (this.tooltipTimeout) {
|
||||
clearTimeout(this.tooltipTimeout)
|
||||
this.tooltipTimeout = undefined
|
||||
}
|
||||
|
||||
// Remove mouse move listener
|
||||
if (this.mouseMoveHandler) {
|
||||
document.removeEventListener('mousemove', this.mouseMoveHandler)
|
||||
this.mouseMoveHandler = undefined
|
||||
}
|
||||
|
||||
// Remove tooltip element
|
||||
if (this.tooltipElement) {
|
||||
document.body.removeChild(this.tooltipElement)
|
||||
this.tooltipElement = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DrawableShape = TLDrawShape | TLHighlightShape
|
||||
|
||||
export class Drawing extends StateNode {
|
||||
static override id = "drawing"
|
||||
|
||||
info = {} as TLPointerEventInfo
|
||||
|
||||
initialShape?: DrawableShape
|
||||
|
||||
override shapeType =
|
||||
this.parent.id === "highlight" ? ("highlight" as const) : ("draw" as const)
|
||||
|
||||
util = this.editor.getShapeUtil(this.shapeType)
|
||||
|
||||
isPen = false
|
||||
isPenOrStylus = false
|
||||
|
||||
didJustShiftClickToExtendPreviousShapeLine = false
|
||||
|
||||
pagePointWhereCurrentSegmentChanged = {} as Vec
|
||||
|
||||
pagePointWhereNextSegmentChanged = null as Vec | null
|
||||
|
||||
lastRecordedPoint = {} as Vec
|
||||
mergeNextPoint = false
|
||||
currentLineLength = 0
|
||||
|
||||
canDraw = false
|
||||
|
||||
markId = null as null | string
|
||||
|
||||
override onEnter = (info: TLPointerEventInfo) => {
|
||||
this.markId = null
|
||||
this.info = info
|
||||
this.canDraw = !this.editor.getIsMenuOpen()
|
||||
this.lastRecordedPoint = this.editor.inputs.currentPagePoint.clone()
|
||||
if (this.canDraw) {
|
||||
this.startShape()
|
||||
}
|
||||
}
|
||||
|
||||
onGestureEnd = () => {
|
||||
const shape = this.editor.getShape(this.initialShape?.id!) as TLDrawShape
|
||||
const ps = shape.props.segments[0].points.map((s) => ({ x: s.x, y: s.y }))
|
||||
const gesture = this.editor.inputs.shiftKey ? GestureTool.recognizerAlt.recognize(ps) : GestureTool.recognizer.recognize(ps)
|
||||
const score_pass = gesture.score > 0.2
|
||||
const score_confident = gesture.score > 0.65
|
||||
let score_color: "green" | "red" | "yellow" = "green"
|
||||
if (!score_pass) {
|
||||
score_color = "red"
|
||||
} else if (!score_confident) {
|
||||
score_color = "yellow"
|
||||
}
|
||||
if (score_pass) {
|
||||
gesture.onComplete?.(this.editor, shape)
|
||||
}
|
||||
let opacity = 1
|
||||
const labelShape: TLShapePartial<TLTextShape> = {
|
||||
id: createShapeId(),
|
||||
type: "text",
|
||||
x: this.editor.inputs.currentPagePoint.x + 20,
|
||||
y: this.editor.inputs.currentPagePoint.y,
|
||||
props: {
|
||||
size: "xl",
|
||||
text: gesture.name,
|
||||
color: score_color,
|
||||
},
|
||||
}
|
||||
if (SHOW_LABELS) {
|
||||
this.editor.createShape(labelShape)
|
||||
}
|
||||
const intervalId = setInterval(() => {
|
||||
if (opacity > 0) {
|
||||
this.editor.updateShape({
|
||||
...shape,
|
||||
opacity: opacity,
|
||||
props: {
|
||||
...shape.props,
|
||||
color: score_color,
|
||||
},
|
||||
})
|
||||
this.editor.updateShape({
|
||||
...labelShape,
|
||||
opacity: opacity,
|
||||
props: {
|
||||
...labelShape.props,
|
||||
color: score_color,
|
||||
},
|
||||
})
|
||||
opacity = Math.max(0, opacity - 0.025)
|
||||
} else {
|
||||
clearInterval(intervalId)
|
||||
this.editor.deleteShape(shape.id)
|
||||
if (SHOW_LABELS) {
|
||||
this.editor.deleteShape(labelShape.id)
|
||||
}
|
||||
}
|
||||
}, 20)
|
||||
}
|
||||
|
||||
override onPointerMove: TLEventHandlers["onPointerMove"] = () => {
|
||||
const { inputs } = this.editor
|
||||
|
||||
if (this.isPen && !inputs.isPen) {
|
||||
// The user made a palm gesture before starting a pen gesture;
|
||||
// ideally we'd start the new shape here but we could also just bail
|
||||
// as the next interaction will work correctly
|
||||
if (this.markId) {
|
||||
this.editor.bailToMark(this.markId)
|
||||
this.startShape()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// If we came in from a menu but have no started dragging...
|
||||
if (!this.canDraw && inputs.isDragging) {
|
||||
this.startShape()
|
||||
this.canDraw = true // bad name
|
||||
}
|
||||
}
|
||||
|
||||
if (this.canDraw) {
|
||||
if (this.isPenOrStylus) {
|
||||
// Don't update the shape if we haven't moved far enough from the last time we recorded a point
|
||||
if (
|
||||
Vec.Dist(inputs.currentPagePoint, this.lastRecordedPoint) >=
|
||||
1 / this.editor.getZoomLevel()
|
||||
) {
|
||||
this.lastRecordedPoint = inputs.currentPagePoint.clone()
|
||||
this.mergeNextPoint = false
|
||||
} else {
|
||||
this.mergeNextPoint = true
|
||||
}
|
||||
} else {
|
||||
this.mergeNextPoint = false
|
||||
}
|
||||
|
||||
this.updateDrawingShape()
|
||||
}
|
||||
}
|
||||
|
||||
override onExit? = () => {
|
||||
this.onGestureEnd()
|
||||
this.editor.snaps.clearIndicators()
|
||||
this.pagePointWhereCurrentSegmentChanged =
|
||||
this.editor.inputs.currentPagePoint.clone()
|
||||
}
|
||||
|
||||
canClose() {
|
||||
return this.shapeType !== "highlight"
|
||||
}
|
||||
|
||||
getIsClosed(segments: TLDrawShapeSegment[], size: TLDefaultSizeStyle) {
|
||||
if (!this.canClose()) return false
|
||||
|
||||
const strokeWidth = STROKE_WIDTH
|
||||
const firstPoint = segments[0].points[0]
|
||||
const lastSegment = segments[segments.length - 1]
|
||||
const lastPoint = lastSegment.points[lastSegment.points.length - 1]
|
||||
|
||||
return (
|
||||
firstPoint !== lastPoint &&
|
||||
this.currentLineLength > strokeWidth * 4 &&
|
||||
Vec.DistMin(firstPoint, lastPoint, strokeWidth * 2)
|
||||
)
|
||||
}
|
||||
|
||||
private startShape() {
|
||||
const {
|
||||
inputs: { originPagePoint },
|
||||
} = this.editor
|
||||
|
||||
this.markId = this.editor.markHistoryStoppingPoint()
|
||||
|
||||
this.didJustShiftClickToExtendPreviousShapeLine = false
|
||||
|
||||
this.lastRecordedPoint = originPagePoint.clone()
|
||||
|
||||
this.pagePointWhereCurrentSegmentChanged = originPagePoint.clone()
|
||||
const id = createShapeId()
|
||||
|
||||
this.editor.createShapes<DrawableShape>([
|
||||
{
|
||||
id,
|
||||
type: this.shapeType,
|
||||
x: originPagePoint.x,
|
||||
y: originPagePoint.y,
|
||||
opacity: 0.5,
|
||||
props: {
|
||||
isPen: this.isPenOrStylus,
|
||||
segments: [
|
||||
{
|
||||
type: "free",
|
||||
points: [
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: PRESSURE,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
this.currentLineLength = 0
|
||||
this.initialShape = this.editor.getShape<DrawableShape>(id)
|
||||
}
|
||||
|
||||
private updateDrawingShape() {
|
||||
const { initialShape } = this
|
||||
const { inputs } = this.editor
|
||||
|
||||
if (!initialShape) return
|
||||
|
||||
const {
|
||||
id,
|
||||
props: { size },
|
||||
} = initialShape
|
||||
|
||||
const shape = this.editor.getShape<DrawableShape>(id)!
|
||||
|
||||
if (!shape) return
|
||||
|
||||
const { segments } = shape.props
|
||||
|
||||
const { x, y, z } = this.editor
|
||||
.getPointInShapeSpace(shape, inputs.currentPagePoint)
|
||||
.toFixed()
|
||||
|
||||
const newPoint = {
|
||||
x,
|
||||
y,
|
||||
z: this.isPenOrStylus ? +(z! * 1.25).toFixed(2) : 0.5,
|
||||
}
|
||||
|
||||
const newSegments = segments.slice()
|
||||
const newSegment = newSegments[newSegments.length - 1]
|
||||
const newPoints = [...newSegment.points]
|
||||
|
||||
if (newPoints.length && this.mergeNextPoint) {
|
||||
const { z } = newPoints[newPoints.length - 1]
|
||||
newPoints[newPoints.length - 1] = {
|
||||
x: newPoint.x,
|
||||
y: newPoint.y,
|
||||
z: z ? Math.max(z, newPoint.z) : newPoint.z,
|
||||
}
|
||||
} else {
|
||||
this.currentLineLength += Vec.Dist(
|
||||
newPoints[newPoints.length - 1],
|
||||
newPoint,
|
||||
)
|
||||
newPoints.push(newPoint)
|
||||
}
|
||||
|
||||
newSegments[newSegments.length - 1] = {
|
||||
...newSegment,
|
||||
points: newPoints,
|
||||
}
|
||||
|
||||
if (this.currentLineLength < STROKE_WIDTH * 4) {
|
||||
this.currentLineLength = this.getLineLength(newSegments)
|
||||
}
|
||||
|
||||
const shapePartial: TLShapePartial<DrawableShape> = {
|
||||
id,
|
||||
type: this.shapeType,
|
||||
props: {
|
||||
segments: newSegments,
|
||||
},
|
||||
}
|
||||
|
||||
if (this.canClose()) {
|
||||
; (shapePartial as TLShapePartial<TLDrawShape>).props!.isClosed =
|
||||
this.getIsClosed(newSegments, size)
|
||||
}
|
||||
|
||||
this.editor.updateShapes([shapePartial])
|
||||
}
|
||||
|
||||
private getLineLength(segments: TLDrawShapeSegment[]) {
|
||||
let length = 0
|
||||
|
||||
for (const segment of segments) {
|
||||
for (let i = 0; i < segment.points.length - 1; i++) {
|
||||
const A = segment.points[i]
|
||||
const B = segment.points[i + 1]
|
||||
length += Vec.Dist2(B, A)
|
||||
}
|
||||
}
|
||||
|
||||
return Math.sqrt(length)
|
||||
}
|
||||
|
||||
override onPointerUp: TLEventHandlers["onPointerUp"] = () => {
|
||||
this.complete()
|
||||
}
|
||||
|
||||
override onCancel: TLEventHandlers["onCancel"] = () => {
|
||||
this.cancel()
|
||||
}
|
||||
|
||||
override onComplete: TLEventHandlers["onComplete"] = () => {
|
||||
this.complete()
|
||||
}
|
||||
|
||||
override onInterrupt: TLEventHandlers["onInterrupt"] = () => {
|
||||
if (this.editor.inputs.isDragging) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.markId) {
|
||||
this.editor.bailToMark(this.markId)
|
||||
}
|
||||
this.cancel()
|
||||
}
|
||||
|
||||
complete() {
|
||||
if (!this.canDraw) {
|
||||
this.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
const { initialShape } = this
|
||||
if (!initialShape) return
|
||||
this.editor.updateShapes([
|
||||
{
|
||||
id: initialShape.id,
|
||||
type: initialShape.type,
|
||||
props: { isComplete: true },
|
||||
},
|
||||
])
|
||||
|
||||
this.parent.transition("idle")
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.parent.transition("idle", this.info)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
@import url("reset.css");
|
||||
|
||||
:root {
|
||||
--border-radius: 10px;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
|
|
@ -632,4 +636,164 @@ p:has(+ ol) {
|
|||
margin-left: -1rem;
|
||||
margin-right: -1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Command Palette Styles */
|
||||
[cmdk-root] {
|
||||
z-index: 9999 !important;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
}
|
||||
|
||||
[cmdk-dialog] {
|
||||
padding: 0.5em;
|
||||
width: 100%;
|
||||
max-width: 35em;
|
||||
border: 1px solid #c7c7c7;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||
background-color: white;
|
||||
position: fixed;
|
||||
top: 30%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
z-index: 9999 !important;
|
||||
|
||||
& input {
|
||||
font-size: 1.4em;
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0.2em;
|
||||
background-color: #f8f8f8;
|
||||
margin-bottom: 0.2em;
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-radius: 3px;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[cmdk-group-heading] {
|
||||
font-size: 1.2em;
|
||||
opacity: 0.5;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
[cmdk-item] {
|
||||
padding: 0.2em;
|
||||
font-size: 1.2em;
|
||||
& .tlui-kbd {
|
||||
border: 1px solid #c7c7c7;
|
||||
border-radius: 3px;
|
||||
padding: 0.2em;
|
||||
padding-bottom: 0.1em;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
[cmdk-item]:hover {
|
||||
border-radius: 3px;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
[cmdk-empty] {
|
||||
font-size: 1.2em;
|
||||
opacity: 0.5;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
[cmdk-overlay] {
|
||||
z-index: 9998 !important;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
background: rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
|
||||
/* Ensure command palette renders above Tldraw canvas */
|
||||
.tldraw__editor [cmdk-root] {
|
||||
position: fixed !important;
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
.tldraw__editor [cmdk-dialog] {
|
||||
position: fixed !important;
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
.tldraw__editor [cmdk-overlay] {
|
||||
position: fixed !important;
|
||||
z-index: 9998 !important;
|
||||
}
|
||||
|
||||
/* Command Palette Specific Styles */
|
||||
.command-palette .duration-300 {
|
||||
transition-duration: 0s; /* Set your desired duration */
|
||||
}
|
||||
|
||||
.command-palette .duration-200 {
|
||||
transition-duration: 0s; /* Set your desired duration */
|
||||
}
|
||||
|
||||
.command-palette .bg-opacity-80 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.command-palette .llm-response {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.llm-response {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.references {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.command-palette .llm-response div {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.command-palette .llm-response span {
|
||||
height: 500px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.references * {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.reference {
|
||||
color: #40cf66;
|
||||
margin-left: 0.2em !important;
|
||||
padding-right: 0.1em;
|
||||
padding-left: 0.1em;
|
||||
&:hover {
|
||||
background-color: #40cf664d;
|
||||
}
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.reference-missing {
|
||||
margin-left: 0.2em !important;
|
||||
padding-right: 0.1em;
|
||||
padding-left: 0.1em;
|
||||
color: #fc8958;
|
||||
}
|
||||
|
|
@ -0,0 +1,802 @@
|
|||
import { Gesture } from "@/gestures"
|
||||
import { Editor, TLDrawShape, TLShape, VecLike, createShapeId } from "tldraw"
|
||||
|
||||
const getShapesUnderGesture = (editor: Editor, gesture: TLDrawShape) => {
|
||||
const bounds = editor.getShapePageBounds(gesture.id)
|
||||
return editor.getShapesAtPoint(bounds?.center!, {
|
||||
margin: (bounds?.width! + bounds?.height!) / 4,
|
||||
}).filter((shape) => shape.id !== gesture.id)
|
||||
}
|
||||
|
||||
/** Returns shapes arranged in a circle around the given origin */
|
||||
const circleDistribution = (editor: Editor, shapes: TLShape[], origin: VecLike, radius: number): TLShape[] => {
|
||||
const angleStep = (2 * Math.PI) / shapes.length;
|
||||
return shapes.map((shape, index) => {
|
||||
const { w, h } = editor.getShapeGeometry(shape.id).bounds
|
||||
const angle = index * angleStep;
|
||||
const pointOnCircle = {
|
||||
x: origin.x + radius * Math.cos(angle),
|
||||
y: origin.y + radius * Math.sin(angle),
|
||||
};
|
||||
const shapeAngle = angle + Math.PI / 2;
|
||||
const pos = posFromRotatedCenter(pointOnCircle, w, h, shapeAngle);
|
||||
return {
|
||||
...shape,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
rotation: shapeAngle,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Returns shapes arranged in a triangle around the given origin */
|
||||
const triangleDistribution = (editor: Editor, shapes: TLShape[], origin: VecLike, radius: number): TLShape[] => {
|
||||
const vertices = [
|
||||
{ x: origin.x - radius, y: origin.y + radius }, // Bottom left
|
||||
{ x: origin.x + radius, y: origin.y + radius }, // Bottom right
|
||||
{ x: origin.x, y: origin.y - radius }, // Top middle
|
||||
];
|
||||
|
||||
const totalShapes = shapes.length;
|
||||
const shapesPerEdge = Math.ceil(totalShapes / 3);
|
||||
|
||||
return shapes.map((shape, index) => {
|
||||
const edgeIndex = Math.floor(index / shapesPerEdge);
|
||||
const edgeStart = vertices[edgeIndex];
|
||||
const edgeEnd = vertices[(edgeIndex + 1) % 3];
|
||||
|
||||
const t = (index % shapesPerEdge) / shapesPerEdge;
|
||||
const pointOnEdge = {
|
||||
x: edgeStart.x + t * (edgeEnd.x - edgeStart.x),
|
||||
y: edgeStart.y + t * (edgeEnd.y - edgeStart.y),
|
||||
};
|
||||
|
||||
let shapeAngle;
|
||||
if (index % shapesPerEdge === 0) {
|
||||
// Shape is at a vertex, adjust angle to face away from the triangle
|
||||
const vertex = vertices[edgeIndex];
|
||||
shapeAngle = Math.atan2(vertex.y - origin.y, vertex.x - origin.x);
|
||||
} else {
|
||||
// Shape is on an edge
|
||||
shapeAngle = Math.atan2(edgeEnd.y - edgeStart.y, edgeEnd.x - edgeStart.x);
|
||||
}
|
||||
|
||||
const { w, h } = editor.getShapeGeometry(shape.id).bounds;
|
||||
const pos = posFromRotatedCenter(pointOnEdge, w, h, shapeAngle);
|
||||
|
||||
return {
|
||||
...shape,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
rotation: shapeAngle,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Calculates the top-left position of a shape given a center point, width, height, and rotation (radians) */
|
||||
/** its origin x/y and rotation are around the top-left corner of the shape */
|
||||
const posFromRotatedCenter = (center: VecLike, w: number, h: number, rotation: number): VecLike => {
|
||||
const halfWidth = w / 2;
|
||||
const halfHeight = h / 2;
|
||||
|
||||
const cosTheta = Math.cos(rotation);
|
||||
const sinTheta = Math.sin(rotation);
|
||||
|
||||
const topLeftX = center.x - (halfWidth * cosTheta - halfHeight * sinTheta);
|
||||
const topLeftY = center.y - (halfWidth * sinTheta + halfHeight * cosTheta);
|
||||
|
||||
return { x: topLeftX, y: topLeftY };
|
||||
}
|
||||
|
||||
export const DEFAULT_GESTURES: Gesture[] = [
|
||||
{
|
||||
name: "x",
|
||||
onComplete(editor) {
|
||||
editor.deleteShapes(editor.getSelectedShapes())
|
||||
},
|
||||
points: [
|
||||
{ x: 87, y: 142 },
|
||||
{ x: 89, y: 145 },
|
||||
{ x: 91, y: 148 },
|
||||
{ x: 93, y: 151 },
|
||||
{ x: 96, y: 155 },
|
||||
{ x: 98, y: 157 },
|
||||
{ x: 100, y: 160 },
|
||||
{ x: 102, y: 162 },
|
||||
{ x: 106, y: 167 },
|
||||
{ x: 108, y: 169 },
|
||||
{ x: 110, y: 171 },
|
||||
{ x: 115, y: 177 },
|
||||
{ x: 119, y: 183 },
|
||||
{ x: 123, y: 189 },
|
||||
{ x: 127, y: 193 },
|
||||
{ x: 129, y: 196 },
|
||||
{ x: 133, y: 200 },
|
||||
{ x: 137, y: 206 },
|
||||
{ x: 140, y: 209 },
|
||||
{ x: 143, y: 212 },
|
||||
{ x: 146, y: 215 },
|
||||
{ x: 151, y: 220 },
|
||||
{ x: 153, y: 222 },
|
||||
{ x: 155, y: 223 },
|
||||
{ x: 157, y: 225 },
|
||||
{ x: 158, y: 223 },
|
||||
{ x: 157, y: 218 },
|
||||
{ x: 155, y: 211 },
|
||||
{ x: 154, y: 208 },
|
||||
{ x: 152, y: 200 },
|
||||
{ x: 150, y: 189 },
|
||||
{ x: 148, y: 179 },
|
||||
{ x: 147, y: 170 },
|
||||
{ x: 147, y: 158 },
|
||||
{ x: 147, y: 148 },
|
||||
{ x: 147, y: 141 },
|
||||
{ x: 147, y: 136 },
|
||||
{ x: 144, y: 135 },
|
||||
{ x: 142, y: 137 },
|
||||
{ x: 140, y: 139 },
|
||||
{ x: 135, y: 145 },
|
||||
{ x: 131, y: 152 },
|
||||
{ x: 124, y: 163 },
|
||||
{ x: 116, y: 177 },
|
||||
{ x: 108, y: 191 },
|
||||
{ x: 100, y: 206 },
|
||||
{ x: 94, y: 217 },
|
||||
{ x: 91, y: 222 },
|
||||
{ x: 89, y: 225 },
|
||||
{ x: 87, y: 226 },
|
||||
{ x: 87, y: 224 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "rectangle",
|
||||
onComplete(editor, gesture?: TLDrawShape) {
|
||||
const bounds = editor.getShapePageBounds(gesture?.id!)
|
||||
const { w, h, center } = bounds!
|
||||
editor.createShape({
|
||||
id: createShapeId(),
|
||||
type: "geo",
|
||||
x: center?.x! - w / 2,
|
||||
y: center?.y! - h / 2,
|
||||
props: {
|
||||
fill: "solid",
|
||||
w: w,
|
||||
h: h,
|
||||
},
|
||||
})
|
||||
},
|
||||
points: [
|
||||
{ x: 78, y: 149 },
|
||||
{ x: 78, y: 153 },
|
||||
{ x: 78, y: 157 },
|
||||
{ x: 78, y: 160 },
|
||||
{ x: 79, y: 162 },
|
||||
{ x: 79, y: 164 },
|
||||
{ x: 79, y: 167 },
|
||||
{ x: 79, y: 169 },
|
||||
{ x: 79, y: 173 },
|
||||
{ x: 79, y: 178 },
|
||||
{ x: 79, y: 183 },
|
||||
{ x: 80, y: 189 },
|
||||
{ x: 80, y: 193 },
|
||||
{ x: 80, y: 198 },
|
||||
{ x: 80, y: 202 },
|
||||
{ x: 81, y: 208 },
|
||||
{ x: 81, y: 210 },
|
||||
{ x: 81, y: 216 },
|
||||
{ x: 82, y: 222 },
|
||||
{ x: 82, y: 224 },
|
||||
{ x: 82, y: 227 },
|
||||
{ x: 83, y: 229 },
|
||||
{ x: 83, y: 231 },
|
||||
{ x: 85, y: 230 },
|
||||
{ x: 88, y: 232 },
|
||||
{ x: 90, y: 233 },
|
||||
{ x: 92, y: 232 },
|
||||
{ x: 94, y: 233 },
|
||||
{ x: 99, y: 232 },
|
||||
{ x: 102, y: 233 },
|
||||
{ x: 106, y: 233 },
|
||||
{ x: 109, y: 234 },
|
||||
{ x: 117, y: 235 },
|
||||
{ x: 123, y: 236 },
|
||||
{ x: 126, y: 236 },
|
||||
{ x: 135, y: 237 },
|
||||
{ x: 142, y: 238 },
|
||||
{ x: 145, y: 238 },
|
||||
{ x: 152, y: 238 },
|
||||
{ x: 154, y: 239 },
|
||||
{ x: 165, y: 238 },
|
||||
{ x: 174, y: 237 },
|
||||
{ x: 179, y: 236 },
|
||||
{ x: 186, y: 235 },
|
||||
{ x: 191, y: 235 },
|
||||
{ x: 195, y: 233 },
|
||||
{ x: 197, y: 233 },
|
||||
{ x: 200, y: 233 },
|
||||
{ x: 201, y: 235 },
|
||||
{ x: 201, y: 233 },
|
||||
{ x: 199, y: 231 },
|
||||
{ x: 198, y: 226 },
|
||||
{ x: 198, y: 220 },
|
||||
{ x: 196, y: 207 },
|
||||
{ x: 195, y: 195 },
|
||||
{ x: 195, y: 181 },
|
||||
{ x: 195, y: 173 },
|
||||
{ x: 195, y: 163 },
|
||||
{ x: 194, y: 155 },
|
||||
{ x: 192, y: 145 },
|
||||
{ x: 192, y: 143 },
|
||||
{ x: 192, y: 138 },
|
||||
{ x: 191, y: 135 },
|
||||
{ x: 191, y: 133 },
|
||||
{ x: 191, y: 130 },
|
||||
{ x: 190, y: 128 },
|
||||
{ x: 188, y: 129 },
|
||||
{ x: 186, y: 129 },
|
||||
{ x: 181, y: 132 },
|
||||
{ x: 173, y: 131 },
|
||||
{ x: 162, y: 131 },
|
||||
{ x: 151, y: 132 },
|
||||
{ x: 149, y: 132 },
|
||||
{ x: 138, y: 132 },
|
||||
{ x: 136, y: 132 },
|
||||
{ x: 122, y: 131 },
|
||||
{ x: 120, y: 131 },
|
||||
{ x: 109, y: 130 },
|
||||
{ x: 107, y: 130 },
|
||||
{ x: 90, y: 132 },
|
||||
{ x: 81, y: 133 },
|
||||
{ x: 76, y: 133 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "circle",
|
||||
onComplete(editor, gesture?: TLDrawShape) {
|
||||
const selection = getShapesUnderGesture(editor, gesture!)
|
||||
editor.setSelectedShapes(selection)
|
||||
editor.setHintingShapes(selection)
|
||||
},
|
||||
points: [
|
||||
{ x: 127, y: 141 },
|
||||
{ x: 124, y: 140 },
|
||||
{ x: 120, y: 139 },
|
||||
{ x: 118, y: 139 },
|
||||
{ x: 116, y: 139 },
|
||||
{ x: 111, y: 140 },
|
||||
{ x: 109, y: 141 },
|
||||
{ x: 104, y: 144 },
|
||||
{ x: 100, y: 147 },
|
||||
{ x: 96, y: 152 },
|
||||
{ x: 93, y: 157 },
|
||||
{ x: 90, y: 163 },
|
||||
{ x: 87, y: 169 },
|
||||
{ x: 85, y: 175 },
|
||||
{ x: 83, y: 181 },
|
||||
{ x: 82, y: 190 },
|
||||
{ x: 82, y: 195 },
|
||||
{ x: 83, y: 200 },
|
||||
{ x: 84, y: 205 },
|
||||
{ x: 88, y: 213 },
|
||||
{ x: 91, y: 216 },
|
||||
{ x: 96, y: 219 },
|
||||
{ x: 103, y: 222 },
|
||||
{ x: 108, y: 224 },
|
||||
{ x: 111, y: 224 },
|
||||
{ x: 120, y: 224 },
|
||||
{ x: 133, y: 223 },
|
||||
{ x: 142, y: 222 },
|
||||
{ x: 152, y: 218 },
|
||||
{ x: 160, y: 214 },
|
||||
{ x: 167, y: 210 },
|
||||
{ x: 173, y: 204 },
|
||||
{ x: 178, y: 198 },
|
||||
{ x: 179, y: 196 },
|
||||
{ x: 182, y: 188 },
|
||||
{ x: 182, y: 177 },
|
||||
{ x: 178, y: 167 },
|
||||
{ x: 170, y: 150 },
|
||||
{ x: 163, y: 138 },
|
||||
{ x: 152, y: 130 },
|
||||
{ x: 143, y: 129 },
|
||||
{ x: 140, y: 131 },
|
||||
{ x: 129, y: 136 },
|
||||
{ x: 126, y: 139 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "check",
|
||||
onComplete(editor, gesture?: TLDrawShape) {
|
||||
const originPoint = { x: gesture?.x!, y: gesture?.y! }
|
||||
const shapeAtOrigin = editor.getShapesAtPoint(originPoint, {
|
||||
hitInside: true,
|
||||
margin: 10,
|
||||
})
|
||||
for (const shape of shapeAtOrigin) {
|
||||
if (shape.id === gesture?.id) continue
|
||||
editor.updateShape({
|
||||
...shape,
|
||||
props: {
|
||||
...shape.props,
|
||||
color: "green",
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
points: [
|
||||
{ x: 91, y: 185 },
|
||||
{ x: 93, y: 185 },
|
||||
{ x: 95, y: 185 },
|
||||
{ x: 97, y: 185 },
|
||||
{ x: 100, y: 188 },
|
||||
{ x: 102, y: 189 },
|
||||
{ x: 104, y: 190 },
|
||||
{ x: 106, y: 193 },
|
||||
{ x: 108, y: 195 },
|
||||
{ x: 110, y: 198 },
|
||||
{ x: 112, y: 201 },
|
||||
{ x: 114, y: 204 },
|
||||
{ x: 115, y: 207 },
|
||||
{ x: 117, y: 210 },
|
||||
{ x: 118, y: 212 },
|
||||
{ x: 120, y: 214 },
|
||||
{ x: 121, y: 217 },
|
||||
{ x: 122, y: 219 },
|
||||
{ x: 123, y: 222 },
|
||||
{ x: 124, y: 224 },
|
||||
{ x: 126, y: 226 },
|
||||
{ x: 127, y: 229 },
|
||||
{ x: 129, y: 231 },
|
||||
{ x: 130, y: 233 },
|
||||
{ x: 129, y: 231 },
|
||||
{ x: 129, y: 228 },
|
||||
{ x: 129, y: 226 },
|
||||
{ x: 129, y: 224 },
|
||||
{ x: 129, y: 221 },
|
||||
{ x: 129, y: 218 },
|
||||
{ x: 129, y: 212 },
|
||||
{ x: 129, y: 208 },
|
||||
{ x: 130, y: 198 },
|
||||
{ x: 132, y: 189 },
|
||||
{ x: 134, y: 182 },
|
||||
{ x: 137, y: 173 },
|
||||
{ x: 143, y: 164 },
|
||||
{ x: 147, y: 157 },
|
||||
{ x: 151, y: 151 },
|
||||
{ x: 155, y: 144 },
|
||||
{ x: 161, y: 137 },
|
||||
{ x: 165, y: 131 },
|
||||
{ x: 171, y: 122 },
|
||||
{ x: 174, y: 118 },
|
||||
{ x: 176, y: 114 },
|
||||
{ x: 177, y: 112 },
|
||||
{ x: 177, y: 114 },
|
||||
{ x: 175, y: 116 },
|
||||
{ x: 173, y: 118 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "caret",
|
||||
onComplete(editor) {
|
||||
editor.alignShapes(editor.getSelectedShapes(), "top")
|
||||
},
|
||||
points: [
|
||||
{ x: 79, y: 245 },
|
||||
{ x: 79, y: 242 },
|
||||
{ x: 79, y: 239 },
|
||||
{ x: 80, y: 237 },
|
||||
{ x: 80, y: 234 },
|
||||
{ x: 81, y: 232 },
|
||||
{ x: 82, y: 230 },
|
||||
{ x: 84, y: 224 },
|
||||
{ x: 86, y: 220 },
|
||||
{ x: 86, y: 218 },
|
||||
{ x: 87, y: 216 },
|
||||
{ x: 88, y: 213 },
|
||||
{ x: 90, y: 207 },
|
||||
{ x: 91, y: 202 },
|
||||
{ x: 92, y: 200 },
|
||||
{ x: 93, y: 194 },
|
||||
{ x: 94, y: 192 },
|
||||
{ x: 96, y: 189 },
|
||||
{ x: 97, y: 186 },
|
||||
{ x: 100, y: 179 },
|
||||
{ x: 102, y: 173 },
|
||||
{ x: 105, y: 165 },
|
||||
{ x: 107, y: 160 },
|
||||
{ x: 109, y: 158 },
|
||||
{ x: 112, y: 151 },
|
||||
{ x: 115, y: 144 },
|
||||
{ x: 117, y: 139 },
|
||||
{ x: 119, y: 136 },
|
||||
{ x: 119, y: 134 },
|
||||
{ x: 120, y: 132 },
|
||||
{ x: 121, y: 129 },
|
||||
{ x: 122, y: 127 },
|
||||
{ x: 124, y: 125 },
|
||||
{ x: 126, y: 124 },
|
||||
{ x: 129, y: 125 },
|
||||
{ x: 131, y: 127 },
|
||||
{ x: 132, y: 130 },
|
||||
{ x: 136, y: 139 },
|
||||
{ x: 141, y: 154 },
|
||||
{ x: 145, y: 166 },
|
||||
{ x: 151, y: 182 },
|
||||
{ x: 156, y: 193 },
|
||||
{ x: 157, y: 196 },
|
||||
{ x: 161, y: 209 },
|
||||
{ x: 162, y: 211 },
|
||||
{ x: 167, y: 223 },
|
||||
{ x: 169, y: 229 },
|
||||
{ x: 170, y: 231 },
|
||||
{ x: 173, y: 237 },
|
||||
{ x: 176, y: 242 },
|
||||
{ x: 177, y: 244 },
|
||||
{ x: 179, y: 250 },
|
||||
{ x: 181, y: 255 },
|
||||
{ x: 182, y: 257 },
|
||||
],
|
||||
},
|
||||
// {
|
||||
// name: "zig-zag",
|
||||
// points: [
|
||||
// { x: 307, y: 216 },
|
||||
// { x: 333, y: 186 },
|
||||
// { x: 356, y: 215 },
|
||||
// { x: 375, y: 186 },
|
||||
// { x: 399, y: 216 },
|
||||
// { x: 418, y: 186 },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
name: "v",
|
||||
onComplete(editor) {
|
||||
editor.alignShapes(editor.getSelectedShapes(), "bottom")
|
||||
},
|
||||
points: [
|
||||
{ x: 89, y: 164 },
|
||||
{ x: 90, y: 162 },
|
||||
{ x: 92, y: 162 },
|
||||
{ x: 94, y: 164 },
|
||||
{ x: 95, y: 166 },
|
||||
{ x: 96, y: 169 },
|
||||
{ x: 97, y: 171 },
|
||||
{ x: 99, y: 175 },
|
||||
{ x: 101, y: 178 },
|
||||
{ x: 103, y: 182 },
|
||||
{ x: 106, y: 189 },
|
||||
{ x: 108, y: 194 },
|
||||
{ x: 111, y: 199 },
|
||||
{ x: 114, y: 204 },
|
||||
{ x: 117, y: 209 },
|
||||
{ x: 119, y: 214 },
|
||||
{ x: 122, y: 218 },
|
||||
{ x: 124, y: 222 },
|
||||
{ x: 126, y: 225 },
|
||||
{ x: 128, y: 228 },
|
||||
{ x: 130, y: 229 },
|
||||
{ x: 133, y: 233 },
|
||||
{ x: 134, y: 236 },
|
||||
{ x: 136, y: 239 },
|
||||
{ x: 138, y: 240 },
|
||||
{ x: 139, y: 242 },
|
||||
{ x: 140, y: 244 },
|
||||
{ x: 142, y: 242 },
|
||||
{ x: 142, y: 240 },
|
||||
{ x: 142, y: 237 },
|
||||
{ x: 143, y: 235 },
|
||||
{ x: 143, y: 233 },
|
||||
{ x: 145, y: 229 },
|
||||
{ x: 146, y: 226 },
|
||||
{ x: 148, y: 217 },
|
||||
{ x: 149, y: 208 },
|
||||
{ x: 149, y: 205 },
|
||||
{ x: 151, y: 196 },
|
||||
{ x: 151, y: 193 },
|
||||
{ x: 153, y: 182 },
|
||||
{ x: 155, y: 172 },
|
||||
{ x: 157, y: 165 },
|
||||
{ x: 159, y: 160 },
|
||||
{ x: 162, y: 155 },
|
||||
{ x: 164, y: 150 },
|
||||
{ x: 165, y: 148 },
|
||||
{ x: 166, y: 146 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "delete",
|
||||
onComplete(editor) {
|
||||
editor.deleteShapes(editor.getSelectedShapes())
|
||||
},
|
||||
points: [
|
||||
{ x: 123, y: 129 },
|
||||
{ x: 123, y: 131 },
|
||||
{ x: 124, y: 133 },
|
||||
{ x: 125, y: 136 },
|
||||
{ x: 127, y: 140 },
|
||||
{ x: 129, y: 142 },
|
||||
{ x: 133, y: 148 },
|
||||
{ x: 137, y: 154 },
|
||||
{ x: 143, y: 158 },
|
||||
{ x: 145, y: 161 },
|
||||
{ x: 148, y: 164 },
|
||||
{ x: 153, y: 170 },
|
||||
{ x: 158, y: 176 },
|
||||
{ x: 160, y: 178 },
|
||||
{ x: 164, y: 183 },
|
||||
{ x: 168, y: 188 },
|
||||
{ x: 171, y: 191 },
|
||||
{ x: 175, y: 196 },
|
||||
{ x: 178, y: 200 },
|
||||
{ x: 180, y: 202 },
|
||||
{ x: 181, y: 205 },
|
||||
{ x: 184, y: 208 },
|
||||
{ x: 186, y: 210 },
|
||||
{ x: 187, y: 213 },
|
||||
{ x: 188, y: 215 },
|
||||
{ x: 186, y: 212 },
|
||||
{ x: 183, y: 211 },
|
||||
{ x: 177, y: 208 },
|
||||
{ x: 169, y: 206 },
|
||||
{ x: 162, y: 205 },
|
||||
{ x: 154, y: 207 },
|
||||
{ x: 145, y: 209 },
|
||||
{ x: 137, y: 210 },
|
||||
{ x: 129, y: 214 },
|
||||
{ x: 122, y: 217 },
|
||||
{ x: 118, y: 218 },
|
||||
{ x: 111, y: 221 },
|
||||
{ x: 109, y: 222 },
|
||||
{ x: 110, y: 219 },
|
||||
{ x: 112, y: 217 },
|
||||
{ x: 118, y: 209 },
|
||||
{ x: 120, y: 207 },
|
||||
{ x: 128, y: 196 },
|
||||
{ x: 135, y: 187 },
|
||||
{ x: 138, y: 183 },
|
||||
{ x: 148, y: 167 },
|
||||
{ x: 157, y: 153 },
|
||||
{ x: 163, y: 145 },
|
||||
{ x: 165, y: 142 },
|
||||
{ x: 172, y: 133 },
|
||||
{ x: 177, y: 127 },
|
||||
{ x: 179, y: 127 },
|
||||
{ x: 180, y: 125 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "pigtail",
|
||||
onComplete(editor, gesture?: TLDrawShape) {
|
||||
const shapes = getShapesUnderGesture(editor, gesture!)
|
||||
editor.setSelectedShapes(shapes)
|
||||
editor.setHintingShapes(shapes)
|
||||
editor.animateShapes(shapes.map((shape) => ({
|
||||
...shape,
|
||||
rotation: shape.rotation + (Math.PI / -2),
|
||||
})),
|
||||
{
|
||||
animation: {
|
||||
duration: 600,
|
||||
easing: (t) => t * t * (3 - 2 * t),
|
||||
},
|
||||
},
|
||||
)
|
||||
},
|
||||
points: [
|
||||
{ x: 81, y: 219 },
|
||||
{ x: 84, y: 218 },
|
||||
{ x: 86, y: 220 },
|
||||
{ x: 88, y: 220 },
|
||||
{ x: 90, y: 220 },
|
||||
{ x: 92, y: 219 },
|
||||
{ x: 95, y: 220 },
|
||||
{ x: 97, y: 219 },
|
||||
{ x: 99, y: 220 },
|
||||
{ x: 102, y: 218 },
|
||||
{ x: 105, y: 217 },
|
||||
{ x: 107, y: 216 },
|
||||
{ x: 110, y: 216 },
|
||||
{ x: 113, y: 214 },
|
||||
{ x: 116, y: 212 },
|
||||
{ x: 118, y: 210 },
|
||||
{ x: 121, y: 208 },
|
||||
{ x: 124, y: 205 },
|
||||
{ x: 126, y: 202 },
|
||||
{ x: 129, y: 199 },
|
||||
{ x: 132, y: 196 },
|
||||
{ x: 136, y: 191 },
|
||||
{ x: 139, y: 187 },
|
||||
{ x: 142, y: 182 },
|
||||
{ x: 144, y: 179 },
|
||||
{ x: 146, y: 174 },
|
||||
{ x: 148, y: 170 },
|
||||
{ x: 149, y: 168 },
|
||||
{ x: 151, y: 162 },
|
||||
{ x: 152, y: 160 },
|
||||
{ x: 152, y: 157 },
|
||||
{ x: 152, y: 155 },
|
||||
{ x: 152, y: 151 },
|
||||
{ x: 152, y: 149 },
|
||||
{ x: 152, y: 146 },
|
||||
{ x: 149, y: 142 },
|
||||
{ x: 148, y: 139 },
|
||||
{ x: 145, y: 137 },
|
||||
{ x: 141, y: 135 },
|
||||
{ x: 139, y: 135 },
|
||||
{ x: 134, y: 136 },
|
||||
{ x: 130, y: 140 },
|
||||
{ x: 128, y: 142 },
|
||||
{ x: 126, y: 145 },
|
||||
{ x: 122, y: 150 },
|
||||
{ x: 119, y: 158 },
|
||||
{ x: 117, y: 163 },
|
||||
{ x: 115, y: 170 },
|
||||
{ x: 114, y: 175 },
|
||||
{ x: 117, y: 184 },
|
||||
{ x: 120, y: 190 },
|
||||
{ x: 125, y: 199 },
|
||||
{ x: 129, y: 203 },
|
||||
{ x: 133, y: 208 },
|
||||
{ x: 138, y: 213 },
|
||||
{ x: 145, y: 215 },
|
||||
{ x: 155, y: 218 },
|
||||
{ x: 164, y: 219 },
|
||||
{ x: 166, y: 219 },
|
||||
{ x: 177, y: 219 },
|
||||
{ x: 182, y: 218 },
|
||||
{ x: 192, y: 216 },
|
||||
{ x: 196, y: 213 },
|
||||
{ x: 199, y: 212 },
|
||||
{ x: 201, y: 211 },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const ALT_GESTURES: Gesture[] = [
|
||||
{
|
||||
name: "circle layout",
|
||||
onComplete(editor, gesture?: TLDrawShape) {
|
||||
const bounds = editor.getShapePageBounds(gesture?.id!)
|
||||
const center = bounds?.center
|
||||
const radius = Math.max(bounds?.width || 0, bounds?.height || 0) / 2
|
||||
const selected = editor.getSelectedShapes()
|
||||
const radialShapes = circleDistribution(editor, selected, center!, radius)
|
||||
editor.animateShapes(radialShapes, {
|
||||
animation: {
|
||||
duration: 600,
|
||||
easing: (t) => t * t * (3 - 2 * t),
|
||||
},
|
||||
})
|
||||
},
|
||||
points: [
|
||||
{ x: 127, y: 141 },
|
||||
{ x: 124, y: 140 },
|
||||
{ x: 120, y: 139 },
|
||||
{ x: 118, y: 139 },
|
||||
{ x: 116, y: 139 },
|
||||
{ x: 111, y: 140 },
|
||||
{ x: 109, y: 141 },
|
||||
{ x: 104, y: 144 },
|
||||
{ x: 100, y: 147 },
|
||||
{ x: 96, y: 152 },
|
||||
{ x: 93, y: 157 },
|
||||
{ x: 90, y: 163 },
|
||||
{ x: 87, y: 169 },
|
||||
{ x: 85, y: 175 },
|
||||
{ x: 83, y: 181 },
|
||||
{ x: 82, y: 190 },
|
||||
{ x: 82, y: 195 },
|
||||
{ x: 83, y: 200 },
|
||||
{ x: 84, y: 205 },
|
||||
{ x: 88, y: 213 },
|
||||
{ x: 91, y: 216 },
|
||||
{ x: 96, y: 219 },
|
||||
{ x: 103, y: 222 },
|
||||
{ x: 108, y: 224 },
|
||||
{ x: 111, y: 224 },
|
||||
{ x: 120, y: 224 },
|
||||
{ x: 133, y: 223 },
|
||||
{ x: 142, y: 222 },
|
||||
{ x: 152, y: 218 },
|
||||
{ x: 160, y: 214 },
|
||||
{ x: 167, y: 210 },
|
||||
{ x: 173, y: 204 },
|
||||
{ x: 178, y: 198 },
|
||||
{ x: 179, y: 196 },
|
||||
{ x: 182, y: 188 },
|
||||
{ x: 182, y: 177 },
|
||||
{ x: 178, y: 167 },
|
||||
{ x: 170, y: 150 },
|
||||
{ x: 163, y: 138 },
|
||||
{ x: 152, y: 130 },
|
||||
{ x: 143, y: 129 },
|
||||
{ x: 140, y: 131 },
|
||||
{ x: 129, y: 136 },
|
||||
{ x: 126, y: 139 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "triangle layout",
|
||||
onComplete(editor, gesture?: TLDrawShape) {
|
||||
const bounds = editor.getShapePageBounds(gesture?.id!)
|
||||
const center = bounds?.center
|
||||
const radius = Math.max(bounds?.width || 0, bounds?.height || 0) / 2
|
||||
const selected = editor.getSelectedShapes()
|
||||
const radialShapes = triangleDistribution(editor, selected, center!, radius)
|
||||
editor.animateShapes(radialShapes, {
|
||||
animation: {
|
||||
duration: 600,
|
||||
easing: (t) => t * t * (3 - 2 * t),
|
||||
},
|
||||
})
|
||||
},
|
||||
points: [
|
||||
{ x: 137, y: 139 },
|
||||
{ x: 135, y: 141 },
|
||||
{ x: 133, y: 144 },
|
||||
{ x: 132, y: 146 },
|
||||
{ x: 130, y: 149 },
|
||||
{ x: 128, y: 151 },
|
||||
{ x: 126, y: 155 },
|
||||
{ x: 123, y: 160 },
|
||||
{ x: 120, y: 166 },
|
||||
{ x: 116, y: 171 },
|
||||
{ x: 112, y: 177 },
|
||||
{ x: 107, y: 183 },
|
||||
{ x: 102, y: 188 },
|
||||
{ x: 100, y: 191 },
|
||||
{ x: 95, y: 195 },
|
||||
{ x: 90, y: 199 },
|
||||
{ x: 86, y: 203 },
|
||||
{ x: 82, y: 206 },
|
||||
{ x: 80, y: 209 },
|
||||
{ x: 75, y: 213 },
|
||||
{ x: 73, y: 213 },
|
||||
{ x: 70, y: 216 },
|
||||
{ x: 67, y: 219 },
|
||||
{ x: 64, y: 221 },
|
||||
{ x: 61, y: 223 },
|
||||
{ x: 60, y: 225 },
|
||||
{ x: 62, y: 226 },
|
||||
{ x: 65, y: 225 },
|
||||
{ x: 67, y: 226 },
|
||||
{ x: 74, y: 226 },
|
||||
{ x: 77, y: 227 },
|
||||
{ x: 85, y: 229 },
|
||||
{ x: 91, y: 230 },
|
||||
{ x: 99, y: 231 },
|
||||
{ x: 108, y: 232 },
|
||||
{ x: 116, y: 233 },
|
||||
{ x: 125, y: 233 },
|
||||
{ x: 134, y: 234 },
|
||||
{ x: 145, y: 233 },
|
||||
{ x: 153, y: 232 },
|
||||
{ x: 160, y: 233 },
|
||||
{ x: 170, y: 234 },
|
||||
{ x: 177, y: 235 },
|
||||
{ x: 179, y: 236 },
|
||||
{ x: 186, y: 237 },
|
||||
{ x: 193, y: 238 },
|
||||
{ x: 198, y: 239 },
|
||||
{ x: 200, y: 237 },
|
||||
{ x: 202, y: 239 },
|
||||
{ x: 204, y: 238 },
|
||||
{ x: 206, y: 234 },
|
||||
{ x: 205, y: 230 },
|
||||
{ x: 202, y: 222 },
|
||||
{ x: 197, y: 216 },
|
||||
{ x: 192, y: 207 },
|
||||
{ x: 186, y: 198 },
|
||||
{ x: 179, y: 189 },
|
||||
{ x: 174, y: 183 },
|
||||
{ x: 170, y: 178 },
|
||||
{ x: 164, y: 171 },
|
||||
{ x: 161, y: 168 },
|
||||
{ x: 154, y: 160 },
|
||||
{ x: 148, y: 155 },
|
||||
{ x: 143, y: 150 },
|
||||
{ x: 138, y: 148 },
|
||||
{ x: 136, y: 148 },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
/** Modified $1 for TS & tldraw */
|
||||
|
||||
/**
|
||||
* The $1 Unistroke Recognizer (JavaScript version)
|
||||
*
|
||||
* Jacob O. Wobbrock, Ph.D.
|
||||
* The Information School
|
||||
* University of Washington
|
||||
* Seattle, WA 98195-2840
|
||||
* wobbrock@uw.edu
|
||||
*
|
||||
* Andrew D. Wilson, Ph.D.
|
||||
* Microsoft Research
|
||||
* One Microsoft Way
|
||||
* Redmond, WA 98052
|
||||
* awilson@microsoft.com
|
||||
*
|
||||
* Yang Li, Ph.D.
|
||||
* Department of Computer Science and Engineering
|
||||
* University of Washington
|
||||
* Seattle, WA 98195-2840
|
||||
* yangli@cs.washington.edu
|
||||
*
|
||||
* The academic publication for the $1 recognizer, and what should be
|
||||
* used to cite it, is:
|
||||
*
|
||||
* Wobbrock, J.O., Wilson, A.D. and Li, Y. (2007). Gestures without
|
||||
* libraries, toolkits or training: A $1 recognizer for user interface
|
||||
* prototypes. Proceedings of the ACM Symposium on User Interface
|
||||
* Software and Technology (UIST '07). Newport, Rhode Island (October
|
||||
* 7-10, 2007). New York: ACM Press, pp. 159-168.
|
||||
* https://dl.acm.org/citation.cfm?id=1294238
|
||||
*
|
||||
* The Protractor enhancement was separately published by Yang Li and programmed
|
||||
* here by Jacob O. Wobbrock:
|
||||
*
|
||||
* Li, Y. (2010). Protractor: A fast and accurate gesture
|
||||
* recognizer. Proceedings of the ACM Conference on Human
|
||||
* Factors in Computing Systems (CHI '10). Atlanta, Georgia
|
||||
* (April 10-15, 2010). New York: ACM Press, pp. 2169-2172.
|
||||
* https://dl.acm.org/citation.cfm?id=1753654
|
||||
*
|
||||
* This software is distributed under the "New BSD License" agreement:
|
||||
*
|
||||
* Copyright (C) 2007-2012, Jacob O. Wobbrock, Andrew D. Wilson and Yang Li.
|
||||
* All rights reserved. Last updated July 14, 2018.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the names of the University of Washington nor Microsoft,
|
||||
* nor the names of its contributors may be used to endorse or promote
|
||||
* products derived from this software without specific prior written
|
||||
* permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Jacob O. Wobbrock OR Andrew D. Wilson
|
||||
* OR Yang Li BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
**/
|
||||
|
||||
import { Editor, TLDrawShape, VecLike, BoxModel } from "tldraw"
|
||||
|
||||
const NUM_POINTS = 64
|
||||
const SQUARE_SIZE = 250.0
|
||||
const ORIGIN = { x: 0, y: 0 }
|
||||
|
||||
interface Result {
|
||||
name: string
|
||||
score: number
|
||||
time: number
|
||||
onComplete?: (editor: Editor, gesture?: TLDrawShape) => void
|
||||
}
|
||||
|
||||
export interface Gesture {
|
||||
name: string
|
||||
points: VecLike[]
|
||||
onComplete?: (editor: Editor, gesture?: TLDrawShape) => void
|
||||
}
|
||||
|
||||
class Unistroke {
|
||||
name: string
|
||||
points: VecLike[]
|
||||
vector: number[]
|
||||
private _originalPoints: VecLike[]
|
||||
onComplete?: (editor: Editor, gesture?: TLDrawShape) => void
|
||||
constructor(
|
||||
name: string,
|
||||
points: VecLike[],
|
||||
onComplete?: (editor: Editor, gesture?: TLDrawShape) => void,
|
||||
) {
|
||||
this.name = name
|
||||
this.onComplete = onComplete
|
||||
this._originalPoints = points
|
||||
this.points = Resample(points, NUM_POINTS)
|
||||
const radians = IndicativeAngle(this.points)
|
||||
this.points = RotateBy(this.points, -radians)
|
||||
this.points = ScaleTo(this.points, SQUARE_SIZE)
|
||||
this.points = TranslateTo(this.points, ORIGIN)
|
||||
this.vector = Vectorize(this.points) // for Protractor
|
||||
}
|
||||
originalPoints(): VecLike[] {
|
||||
return this._originalPoints
|
||||
}
|
||||
}
|
||||
|
||||
export class DollarRecognizer {
|
||||
unistrokes: Unistroke[] = []
|
||||
|
||||
constructor(gestures: Gesture[]) {
|
||||
for (const gesture of gestures) {
|
||||
this.unistrokes.push(
|
||||
new Unistroke(gesture.name, gesture.points, gesture.onComplete),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recognize a gesture
|
||||
* @param points The points of the gesture
|
||||
* @returns The result
|
||||
*/
|
||||
recognize(points: VecLike[]): Result {
|
||||
const t0 = Date.now()
|
||||
const candidate = new Unistroke("", points)
|
||||
|
||||
let u = -1
|
||||
let b = +Infinity
|
||||
for (
|
||||
let i = 0;
|
||||
i < this.unistrokes.length;
|
||||
i++ // for each unistroke template
|
||||
) {
|
||||
const d = OptimalCosineDistance(
|
||||
this.unistrokes[i].vector,
|
||||
candidate.vector,
|
||||
) // Protractor
|
||||
if (d < b) {
|
||||
b = d // best (least) distance
|
||||
u = i // unistroke index
|
||||
}
|
||||
}
|
||||
const t1 = Date.now()
|
||||
return u === -1
|
||||
? { name: "No match.", score: 0.0, time: t1 - t0 }
|
||||
: {
|
||||
name: this.unistrokes[u].name,
|
||||
score: 1.0 - b,
|
||||
time: t1 - t0,
|
||||
onComplete: this.unistrokes[u].onComplete,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a gesture to the recognizer
|
||||
* @param name The name of the gesture
|
||||
* @param points The points of the gesture
|
||||
* @returns The number of gestures
|
||||
*/
|
||||
addGesture(name: string, points: VecLike[]): number {
|
||||
this.unistrokes[this.unistrokes.length] = new Unistroke(name, points) // append new unistroke
|
||||
let num = 0
|
||||
for (let i = 0; i < this.unistrokes.length; i++) {
|
||||
if (this.unistrokes[i].name === name) num++
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a gesture from the recognizer
|
||||
* @param name The name of the gesture
|
||||
* @returns The number of gestures after removal
|
||||
*/
|
||||
removeGesture(name: string): number {
|
||||
this.unistrokes = this.unistrokes.filter((gesture) => gesture.name !== name)
|
||||
return this.unistrokes.length
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Private helper functions from here on down
|
||||
//
|
||||
function Resample(points: VecLike[], n: number): VecLike[] {
|
||||
const I = PathLength(points) / (n - 1) // interval length
|
||||
let D = 0.0
|
||||
const newpoints = new Array(points[0])
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const d = Distance(points[i - 1], points[i])
|
||||
if (D + d >= I) {
|
||||
const qx =
|
||||
points[i - 1].x + ((I - D) / d) * (points[i].x - points[i - 1].x)
|
||||
const qy =
|
||||
points[i - 1].y + ((I - D) / d) * (points[i].y - points[i - 1].y)
|
||||
const q = { x: qx, y: qy }
|
||||
newpoints[newpoints.length] = q // append new point 'q'
|
||||
points.splice(i, 0, q) // insert 'q' at position i in points s.t. 'q' will be the next i
|
||||
D = 0.0
|
||||
} else D += d
|
||||
}
|
||||
if (newpoints.length === n - 1)
|
||||
// somtimes we fall a rounding-error short of adding the last point, so add it if so
|
||||
newpoints[newpoints.length] = {
|
||||
x: points[points.length - 1].x,
|
||||
y: points[points.length - 1].y,
|
||||
}
|
||||
return newpoints
|
||||
}
|
||||
|
||||
function IndicativeAngle(points: VecLike[]): number {
|
||||
const c = Centroid(points)
|
||||
return Math.atan2(c.y - points[0].y, c.x - points[0].x)
|
||||
}
|
||||
|
||||
function RotateBy(points: VecLike[], radians: number): VecLike[] {
|
||||
// rotates points around centroid
|
||||
const c = Centroid(points)
|
||||
const cos = Math.cos(radians)
|
||||
const sin = Math.sin(radians)
|
||||
const newpoints = new Array()
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const qx = (points[i].x - c.x) * cos - (points[i].y - c.y) * sin + c.x
|
||||
const qy = (points[i].x - c.x) * sin + (points[i].y - c.y) * cos + c.y
|
||||
newpoints[newpoints.length] = { x: qx, y: qy }
|
||||
}
|
||||
return newpoints
|
||||
}
|
||||
|
||||
function ScaleTo(points: VecLike[], size: number): VecLike[] {
|
||||
// non-uniform scale; assumes 2D gestures (i.e., no lines)
|
||||
const B = BoundingBox(points)
|
||||
const newpoints = new Array()
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const qx = points[i].x * (size / B.w)
|
||||
const qy = points[i].y * (size / B.h)
|
||||
newpoints[newpoints.length] = { x: qx, y: qy }
|
||||
}
|
||||
return newpoints
|
||||
}
|
||||
function TranslateTo(points: VecLike[], pt: VecLike): VecLike[] {
|
||||
// translates points' centroid
|
||||
const c = Centroid(points)
|
||||
const newpoints = new Array()
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const qx = points[i].x + pt.x - c.x
|
||||
const qy = points[i].y + pt.y - c.y
|
||||
newpoints[newpoints.length] = { x: qx, y: qy }
|
||||
}
|
||||
return newpoints
|
||||
}
|
||||
|
||||
function Vectorize(points: VecLike[]): number[] {
|
||||
let sum = 0.0
|
||||
const vector = new Array()
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
vector[vector.length] = points[i].x
|
||||
vector[vector.length] = points[i].y
|
||||
sum += points[i].x * points[i].x + points[i].y * points[i].y
|
||||
}
|
||||
const magnitude = Math.sqrt(sum)
|
||||
for (let i = 0; i < vector.length; i++) vector[i] /= magnitude
|
||||
return vector
|
||||
}
|
||||
|
||||
function OptimalCosineDistance(v1: number[], v2: number[]): number {
|
||||
let a = 0.0
|
||||
let b = 0.0
|
||||
for (let i = 0; i < v1.length; i += 2) {
|
||||
a += v1[i] * v2[i] + v1[i + 1] * v2[i + 1]
|
||||
b += v1[i] * v2[i + 1] - v1[i + 1] * v2[i]
|
||||
}
|
||||
const angle = Math.atan(b / a)
|
||||
return Math.acos(a * Math.cos(angle) + b * Math.sin(angle))
|
||||
}
|
||||
|
||||
function Centroid(points: VecLike[]): VecLike {
|
||||
let x = 0.0
|
||||
let y = 0.0
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
x += points[i].x
|
||||
y += points[i].y
|
||||
}
|
||||
x /= points.length
|
||||
y /= points.length
|
||||
return { x: x, y: y }
|
||||
}
|
||||
|
||||
function BoundingBox(points: VecLike[]): BoxModel {
|
||||
let minX = +Infinity
|
||||
let maxX = -Infinity
|
||||
let minY = +Infinity
|
||||
let maxY = -Infinity
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
minX = Math.min(minX, points[i].x)
|
||||
minY = Math.min(minY, points[i].y)
|
||||
maxX = Math.max(maxX, points[i].x)
|
||||
maxY = Math.max(maxY, points[i].y)
|
||||
}
|
||||
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY }
|
||||
}
|
||||
|
||||
function PathLength(points: VecLike[]): number {
|
||||
let d = 0.0
|
||||
for (let i = 1; i < points.length; i++)
|
||||
d += Distance(points[i - 1], points[i])
|
||||
return d
|
||||
}
|
||||
|
||||
function Distance(p1: VecLike, p2: VecLike): number {
|
||||
const dx = p2.x - p1.x
|
||||
const dy = p2.y - p1.y
|
||||
return Math.sqrt(dx * dx + dy * dy)
|
||||
}
|
||||
|
|
@ -38,6 +38,12 @@ import {
|
|||
} from "@/ui/cameraUtils"
|
||||
import { Collection, initializeGlobalCollections } from "@/collections"
|
||||
import { GraphLayoutCollection } from "@/graph/GraphLayoutCollection"
|
||||
import { GestureTool } from "@/GestureTool"
|
||||
import { CmdK } from "@/CmdK"
|
||||
|
||||
|
||||
import "react-cmdk/dist/cmdk.css"
|
||||
import "@/css/style.css"
|
||||
|
||||
const collections: Collection[] = [GraphLayoutCollection]
|
||||
|
||||
|
|
@ -61,6 +67,7 @@ const customTools = [
|
|||
MycrozineTemplateTool,
|
||||
MarkdownTool,
|
||||
PromptShapeTool,
|
||||
GestureTool,
|
||||
]
|
||||
|
||||
export function Board() {
|
||||
|
|
@ -151,7 +158,9 @@ export function Board() {
|
|||
// Initialize global collections
|
||||
initializeGlobalCollections(editor, collections)
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<CmdK />
|
||||
</Tldraw>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
import { CSSProperties, useEffect, useRef, useState } from "react"
|
||||
import {
|
||||
TLComponents,
|
||||
useEditor,
|
||||
useValue,
|
||||
stopEventPropagation,
|
||||
TLUiOverrides,
|
||||
TLUiActionsContextType,
|
||||
TLDrawShape,
|
||||
TLShapePartial,
|
||||
} from "tldraw"
|
||||
import { DollarRecognizer } from "@/gestures"
|
||||
import { DEFAULT_GESTURES } from "@/default_gestures"
|
||||
|
||||
export const overrides: TLUiOverrides = {
|
||||
tools(editor, tools) {
|
||||
return {
|
||||
...tools,
|
||||
gesture: {
|
||||
id: "gesture",
|
||||
name: "Gesture",
|
||||
icon: "👆",
|
||||
kbd: "g",
|
||||
label: "Gesture",
|
||||
onSelect: () => {
|
||||
editor.setCurrentTool("gesture")
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
actions(editor, actions): TLUiActionsContextType {
|
||||
const R = new DollarRecognizer(DEFAULT_GESTURES)
|
||||
return {
|
||||
...actions,
|
||||
recognize: {
|
||||
id: "recognize",
|
||||
kbd: "c",
|
||||
onSelect: () => {
|
||||
const onlySelectedShape = editor.getOnlySelectedShape()
|
||||
if (!onlySelectedShape || onlySelectedShape.type !== "draw") return
|
||||
console.log("recognizing")
|
||||
const verts = editor.getShapeGeometry(onlySelectedShape).vertices
|
||||
const result = R.recognize(verts)
|
||||
console.log(result)
|
||||
},
|
||||
},
|
||||
addGesture: {
|
||||
id: "addGesture",
|
||||
kbd: "x",
|
||||
onSelect: () => {
|
||||
const onlySelectedShape = editor.getOnlySelectedShape()
|
||||
if (!onlySelectedShape || onlySelectedShape.type !== "draw") return
|
||||
const name = onlySelectedShape.meta.name
|
||||
if (!name) return
|
||||
console.log("adding gesture:", name)
|
||||
const points = editor.getShapeGeometry(onlySelectedShape).vertices
|
||||
R.addGesture(name as string, points)
|
||||
},
|
||||
},
|
||||
recognizeAndSnap: {
|
||||
id: "recognizeAndSnap",
|
||||
kbd: "z",
|
||||
onSelect: () => {
|
||||
const onlySelectedShape = editor.getOnlySelectedShape()
|
||||
if (!onlySelectedShape || onlySelectedShape.type !== "draw") return
|
||||
const points = editor.getShapeGeometry(onlySelectedShape).vertices
|
||||
const result = R.recognize(points)
|
||||
console.log("morphing to closest:", result.name)
|
||||
const newShape: TLShapePartial<TLDrawShape> = {
|
||||
...onlySelectedShape,
|
||||
type: "draw",
|
||||
props: {
|
||||
...onlySelectedShape.props,
|
||||
segments: [
|
||||
{
|
||||
points: R.unistrokes
|
||||
.find((u) => u.name === result.name)
|
||||
?.originalPoints() || [],
|
||||
type: "free",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
editor.animateShape(newShape)
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const components: TLComponents = {
|
||||
OnTheCanvas: () => {
|
||||
const editor = useEditor()
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [shouldFocus, setShouldFocus] = useState(false)
|
||||
const onlySelectedShape = useValue(
|
||||
"onlySelectedShape",
|
||||
() => editor.getOnlySelectedShape(),
|
||||
[editor],
|
||||
)
|
||||
const isInSelectMode = useValue(
|
||||
"isInSelectMode",
|
||||
() => editor.isIn("select"),
|
||||
[editor],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldFocus) {
|
||||
inputRef.current?.focus()
|
||||
setShouldFocus(false)
|
||||
}
|
||||
}, [shouldFocus])
|
||||
|
||||
if (!isInSelectMode) return null
|
||||
|
||||
const shapes = editor.getRenderingShapes()
|
||||
return shapes.map((_shape, i) => {
|
||||
const shape = editor.getShape(_shape.id)
|
||||
const isSelected = editor.getOnlySelectedShapeId() === shape?.id
|
||||
|
||||
const offset = 35
|
||||
const x = shape?.x! - offset * Math.sin(-shape?.rotation!)
|
||||
const y = shape?.y! - offset * Math.cos(shape?.rotation!)
|
||||
|
||||
const labelStyle: CSSProperties = {
|
||||
fontFamily: "Inter, sans-serif",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
transformOrigin: "top left",
|
||||
transform: `translate(${x}px, ${y}px) rotate(${shape?.rotation}rad)`,
|
||||
pointerEvents: "all",
|
||||
opacity: 0.8,
|
||||
fontSize: "1.2rem",
|
||||
}
|
||||
|
||||
const inputStyle: CSSProperties = {
|
||||
fontFamily: "Inter, sans-serif",
|
||||
fontSize: "1.2rem",
|
||||
backgroundColor: "transparent",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
border: "none",
|
||||
outline: "none",
|
||||
}
|
||||
|
||||
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = event.target.value
|
||||
if (/^[a-z0-9]+$/i.test(newValue) || newValue === "") {
|
||||
editor.updateShape({
|
||||
...onlySelectedShape!,
|
||||
meta: {
|
||||
...onlySelectedShape!.meta,
|
||||
name: newValue,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
return (
|
||||
<div
|
||||
key={`${shape.id}-${i}`}
|
||||
style={labelStyle}
|
||||
onPointerDown={stopEventPropagation}
|
||||
>
|
||||
<span style={{ display: "flex", alignItems: "center" }}>
|
||||
<span>@</span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
style={inputStyle}
|
||||
type="text"
|
||||
placeholder="name"
|
||||
onChange={handleNameChange}
|
||||
value={(onlySelectedShape?.meta?.name as string) ?? ""}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
editor.setSelectedShapes([shape?.id!])
|
||||
setShouldFocus(true)
|
||||
}}
|
||||
onKeyDown={() => {}}
|
||||
style={labelStyle}
|
||||
key={`${shape?.id}-${i}`}
|
||||
>
|
||||
{shape?.meta?.name && `@${shape?.meta.name}`}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
|
@ -151,6 +151,15 @@ export const overrides: TLUiOverrides = {
|
|||
readonlyOk: true,
|
||||
onSelect: () => editor.setCurrentTool("Prompt"),
|
||||
},
|
||||
gesture: {
|
||||
id: "gesture",
|
||||
icon: "draw",
|
||||
label: "Gesture",
|
||||
kbd: "g",
|
||||
readonlyOk: true,
|
||||
type: "gesture",
|
||||
onSelect: () => editor.setCurrentTool("gesture"),
|
||||
},
|
||||
hand: {
|
||||
...tools.hand,
|
||||
onDoubleClick: (info: any) => {
|
||||
|
|
@ -312,7 +321,7 @@ export const overrides: TLUiOverrides = {
|
|||
llm: {
|
||||
id: "llm",
|
||||
label: "Run LLM Prompt",
|
||||
kbd: "g",
|
||||
kbd: "alt+g",
|
||||
readonlyOk: true,
|
||||
onSelect: () => {
|
||||
const selectedShapes = editor.getSelectedShapes()
|
||||
|
|
|
|||
Loading…
Reference in New Issue