castle-web-cli 0.4.168 → 0.4.170

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/agent-prompts.js +17 -0
  2. package/dist/agent.js +23 -13
  3. package/dist/atomicFile.d.ts +29 -0
  4. package/dist/atomicFile.js +120 -0
  5. package/dist/castle-host/host.js +37 -0
  6. package/dist/get-deck.js +2 -11
  7. package/dist/headlessCover.d.ts +4 -0
  8. package/dist/headlessCover.js +7 -5
  9. package/dist/ide.js +14 -6
  10. package/dist/index.js +10 -6
  11. package/dist/init.d.ts +1 -1
  12. package/dist/init.js +11 -1
  13. package/dist/remix.js +2 -14
  14. package/dist/save-deck.d.ts +3 -0
  15. package/dist/save-deck.js +51 -74
  16. package/dist/saveCover.d.ts +18 -0
  17. package/dist/saveCover.js +61 -0
  18. package/dist/serve.js +18 -5
  19. package/dist/serveSecurity.d.ts +11 -1
  20. package/dist/serveSecurity.js +38 -4
  21. package/dist/shell/assets/index-BvQmVwlO.css +1 -0
  22. package/dist/shell/assets/index-CV5sBby1.js +445 -0
  23. package/dist/shell/index.html +2 -2
  24. package/dist/versionStore.js +13 -4
  25. package/kits/base/CLAUDE.md +9 -0
  26. package/kits/base/castle.json +16 -11
  27. package/kits/base/sdk/README.md +66 -1
  28. package/kits/base/sdk/commands.d.ts +23 -1
  29. package/kits/base/sdk/commands.js +9 -0
  30. package/kits/base/sdk/lifecycle.d.ts +20 -0
  31. package/kits/base/sdk/lifecycle.js +7 -1
  32. package/kits/base/sdk/resumeState.d.ts +2 -0
  33. package/kits/base/sdk/resumeState.js +84 -0
  34. package/kits/base/sdk/runtime.js +65 -0
  35. package/kits/base/sdk/transport.d.ts +3 -0
  36. package/kits/base/sdk/transport.js +39 -5
  37. package/kits/base/sdk/unloadHandshake.d.ts +6 -0
  38. package/kits/base/sdk/unloadHandshake.js +47 -0
  39. package/kits/multiplayer-2d/castle.json +3 -3
  40. package/kits/multiplayer-2d/package-lock.json +0 -27
  41. package/kits/multiplayer-2d/package.json +0 -1
  42. package/kits/multiplayer-3d/castle.json +4 -4
  43. package/kits/multiplayer-3d/package-lock.json +0 -27
  44. package/kits/multiplayer-3d/package.json +0 -1
  45. package/kits/physics-2d/CLAUDE.md +75 -43
  46. package/kits/physics-2d/behaviors/AnalogStick.jsx +75 -45
  47. package/kits/physics-2d/behaviors/Slingshot.jsx +1 -2
  48. package/kits/physics-2d/castle.json +6 -6
  49. package/kits/physics-2d/editors/PxArtEditor.jsx +83 -180
  50. package/kits/physics-2d/editors/SceneEditor.jsx +25 -4
  51. package/kits/physics-2d/editors/StyleEditor.jsx +6 -2
  52. package/kits/physics-2d/editors/overlayCanvas.js +77 -0
  53. package/kits/physics-2d/editors/pathOverlay.js +5 -13
  54. package/kits/physics-2d/editors/pixelEditorChrome.jsx +5 -1
  55. package/kits/physics-2d/editors/pixelInspector.jsx +4 -4
  56. package/kits/physics-2d/editors/pxArtTimeline.jsx +8 -36
  57. package/kits/physics-2d/editors/pxArtTimeline.module.css +4 -28
  58. package/kits/physics-2d/editors/styleEditor.module.css +23 -4
  59. package/kits/physics-2d/editors/useArtboardZoomPan.js +322 -0
  60. package/kits/physics-2d/engine/SceneUI.jsx +6 -12
  61. package/kits/physics-2d/engine/icons.js +56 -0
  62. package/kits/physics-2d/engine/panelSorting.jsx +7 -14
  63. package/kits/physics-2d/engine/physics/PhysicsSystem.js +66 -11
  64. package/kits/physics-2d/engine/physics/controls.js +76 -12
  65. package/kits/physics-2d/engine/scene.js +47 -9
  66. package/kits/physics-2d/engine/ui.jsx +13 -125
  67. package/kits/physics-2d/engine/ui.module.css +53 -28
  68. package/kits/physics-2d/package-lock.json +0 -29
  69. package/kits/physics-2d/package.json +3 -1
  70. package/kits/physics-2d/pnpm-lock.yaml +1 -17
  71. package/kits/physics-3d/castle.json +5 -5
  72. package/kits/physics-3d/package-lock.json +0 -29
  73. package/kits/physics-3d/package.json +0 -1
  74. package/kits/physics-3d/pnpm-lock.yaml +0 -17
  75. package/kits/turn-based/castle.json +3 -3
  76. package/kits/turn-based/package-lock.json +0 -27
  77. package/kits/turn-based/package.json +0 -1
  78. package/package.json +5 -4
  79. package/dist/shell/assets/index-CVHj503j.css +0 -1
  80. package/dist/shell/assets/index-D-qJ5VGp.js +0 -425
  81. package/kits/blank/.castle/agent/children.json +0 -1
  82. package/kits/blank/.castle/agent/messages.json +0 -9
  83. package/kits/blank/.castle/agent/pending-sends.json +0 -1
  84. package/kits/blank/.castle/agent/tasks/index.md +0 -2
  85. package/kits/blank/.castle/docs/platform.md +0 -36
  86. package/kits/blank/node_modules/.vite/deps/_metadata.json +0 -8
  87. package/kits/blank/node_modules/.vite/deps/package.json +0 -3
@@ -0,0 +1,56 @@
1
+ // Generated by scripts/build-icons.mjs from @phosphor-icons/core regular SVGs.
2
+ // Do not edit. Run `npm run icons` to regenerate.
3
+
4
+ export const icons = {
5
+ bars: { icon: [256, 256, [], '', "M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM40,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16ZM216,184H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Z"] },
6
+ "bezier-curve": { icon: [256, 256, [], '', "M221.07,144.41A96.68,96.68,0,0,0,181,88h59a8,8,0,0,0,0-16H159a32,32,0,0,0-62,0H16a8,8,0,0,0,0,16H75a96.68,96.68,0,0,0-40.07,56.41A32,32,0,1,0,51.08,146,80.6,80.6,0,0,1,99,93.44a32,32,0,0,0,58.06,0A80.6,80.6,0,0,1,204.92,146a32,32,0,1,0,16.15-1.57ZM56,176a16,16,0,1,1-16-16A16,16,0,0,1,56,176Zm72-80a16,16,0,1,1,16-16A16,16,0,0,1,128,96Zm88,96a16,16,0,1,1,16-16A16,16,0,0,1,216,192Z"] },
7
+ "bounding-box": { icon: [256, 256, [], '', "M208,96a16,16,0,0,0,16-16V48a16,16,0,0,0-16-16H176a16,16,0,0,0-16,16v8H96V48A16,16,0,0,0,80,32H48A16,16,0,0,0,32,48V80A16,16,0,0,0,48,96h8v64H48a16,16,0,0,0-16,16v32a16,16,0,0,0,16,16H80a16,16,0,0,0,16-16v-8h64v8a16,16,0,0,0,16,16h32a16,16,0,0,0,16-16V176a16,16,0,0,0-16-16h-8V96ZM176,48h32V80H176ZM48,48H80V63.9a.51.51,0,0,0,0,.2V80H48ZM80,208H48V176H80v15.9a.51.51,0,0,0,0,.2V208Zm128,0H176V176h32Zm-24-48h-8a16,16,0,0,0-16,16v8H96v-8a16,16,0,0,0-16-16H72V96h8A16,16,0,0,0,96,80V72h64v8a16,16,0,0,0,16,16h8Z"] },
8
+ check: { icon: [256, 256, [], '', "M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z"] },
9
+ "chevron-down": { icon: [256, 256, [], '', "M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z"] },
10
+ "chevron-left": { icon: [256, 256, [], '', "M165.66,202.34a8,8,0,0,1-11.32,11.32l-80-80a8,8,0,0,1,0-11.32l80-80a8,8,0,0,1,11.32,11.32L91.31,128Z"] },
11
+ "chevron-right": { icon: [256, 256, [], '', "M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"] },
12
+ "chevron-up": { icon: [256, 256, [], '', "M213.66,165.66a8,8,0,0,1-11.32,0L128,91.31,53.66,165.66a8,8,0,0,1-11.32-11.32l80-80a8,8,0,0,1,11.32,0l80,80A8,8,0,0,1,213.66,165.66Z"] },
13
+ circle: { icon: [256, 256, [], '', "M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Z"] },
14
+ checkerboard: { icon: [256, 256, [], '', "M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32Zm-12.69,88L136,60.69V48h12.69L208,107.32V120ZM136,83.31,172.69,120H136Zm72,1.38L171.31,48H208ZM120,48v72H48V48ZM107.31,208,48,148.69V136H60.69L120,195.31V208ZM120,172.69,83.31,136H120Zm-72-1.38L84.69,208H48ZM208,208H136V136h72v72Z"] },
15
+ clone: { icon: [256, 256, [], '', "M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"] },
16
+ "code-fork": { icon: [256, 256, [], '', "M224,64a32,32,0,1,0-40,31v17a8,8,0,0,1-8,8H80a8,8,0,0,1-8-8V95a32,32,0,1,0-16,0v17a24,24,0,0,0,24,24h40v25a32,32,0,1,0,16,0V136h40a24,24,0,0,0,24-24V95A32.06,32.06,0,0,0,224,64ZM48,64A16,16,0,1,1,64,80,16,16,0,0,1,48,64Zm96,128a16,16,0,1,1-16-16A16,16,0,0,1,144,192ZM192,80a16,16,0,1,1,16-16A16,16,0,0,1,192,80Z"] },
17
+ "dot-circle": { icon: [256, 256, [], '', "M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm0-144a56,56,0,1,0,56,56A56.06,56.06,0,0,0,128,72Zm0,96a40,40,0,1,1,40-40A40,40,0,0,1,128,168Z"] },
18
+ eraser: { icon: [256, 256, [], '', "M225,80.4,183.6,39a24,24,0,0,0-33.94,0L31,157.66a24,24,0,0,0,0,33.94l30.06,30.06A8,8,0,0,0,66.74,224H216a8,8,0,0,0,0-16h-84.7L225,114.34A24,24,0,0,0,225,80.4ZM108.68,208H70.05L42.33,180.28a8,8,0,0,1,0-11.31L96,115.31,148.69,168Zm105-105L160,156.69,107.31,104,161,50.34a8,8,0,0,1,11.32,0l41.38,41.38a8,8,0,0,1,0,11.31Z"] },
19
+ eye: { icon: [256, 256, [], '', "M247.31,124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57,61.26,162.88,48,128,48S61.43,61.26,36.34,86.35C17.51,105.18,9,124,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208s66.57-13.26,91.66-38.34c18.83-18.83,27.3-37.61,27.65-38.4A8,8,0,0,0,247.31,124.76ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.47,133.47,0,0,1,25,128,133.33,133.33,0,0,1,48.07,97.25C70.33,75.19,97.22,64,128,64s57.67,11.19,79.93,33.25A133.46,133.46,0,0,1,231.05,128C223.84,141.46,192.43,192,128,192Zm0-112a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Z"] },
20
+ "eye-closed": { icon: [256, 256, [], '', "M228,175a8,8,0,0,1-10.92-3l-19-33.2A123.23,123.23,0,0,1,162,155.46l5.87,35.22a8,8,0,0,1-6.58,9.21A8.4,8.4,0,0,1,160,200a8,8,0,0,1-7.88-6.69l-5.77-34.58a133.06,133.06,0,0,1-36.68,0l-5.77,34.58A8,8,0,0,1,96,200a8.4,8.4,0,0,1-1.32-.11,8,8,0,0,1-6.58-9.21L94,155.46a123.23,123.23,0,0,1-36.06-16.69L39,172A8,8,0,1,1,25.06,164l20-35a153.47,153.47,0,0,1-19.3-20A8,8,0,1,1,38.22,99c16.6,20.54,45.64,45,89.78,45s73.18-24.49,89.78-45A8,8,0,1,1,230.22,109a153.47,153.47,0,0,1-19.3,20l20,35A8,8,0,0,1,228,175Z"] },
21
+ eyedropper: { icon: [256, 256, [], '', "M224,67.3a35.79,35.79,0,0,0-11.26-25.66c-14-13.28-36.72-12.78-50.62,1.13L142.8,62.2a24,24,0,0,0-33.14.77l-9,9a16,16,0,0,0,0,22.64l2,2.06-51,51a39.75,39.75,0,0,0-10.53,38l-8,18.41A13.68,13.68,0,0,0,36,219.3a15.92,15.92,0,0,0,17.71,3.35L71.23,215a39.89,39.89,0,0,0,37.06-10.75l51-51,2.06,2.06a16,16,0,0,0,22.62,0l9-9a24,24,0,0,0,.74-33.18l19.75-19.87A35.75,35.75,0,0,0,224,67.3ZM97,193a24,24,0,0,1-24,6,8,8,0,0,0-5.55.31l-18.1,7.91L57,189.41a8,8,0,0,0,.25-5.75A23.88,23.88,0,0,1,63,159l51-51,33.94,34ZM202.13,82l-25.37,25.52a8,8,0,0,0,0,11.3l4.89,4.89a8,8,0,0,1,0,11.32l-9,9L112,83.26l9-9a8,8,0,0,1,11.31,0l4.89,4.89a8,8,0,0,0,11.33,0l24.94-25.09c7.81-7.82,20.5-8.18,28.29-.81a20,20,0,0,1,.39,28.7Z"] },
22
+ fill: { icon: [256, 256, [], '', "M234.53,139.07a8,8,0,0,0,3.13-13.24L122.17,10.34a8,8,0,0,0-11.31,0L70.25,51,45.65,26.34A8,8,0,0,0,34.34,37.66l24.6,24.6L15,106.17a24,24,0,0,0,0,33.94L99.89,225a24,24,0,0,0,33.94,0l78.49-78.49Zm-32.19-5.24-79.83,79.83a8,8,0,0,1-11.31,0L26.34,128.8a8,8,0,0,1,0-11.31L70.25,73.57l29.12,29.12a28,28,0,1,0,11.31-11.32L81.57,62.26l35-34.95L217.19,128l-11.72,3.9A8.09,8.09,0,0,0,202.34,133.83Zm-86.83-26.31,0,0a13.26,13.26,0,1,1-.05.06S115.51,107.53,115.51,107.52Zm123.15,56a8,8,0,0,0-13.32,0C223.57,166.23,208,190.09,208,208a24,24,0,0,0,48,0C256,190.09,240.43,166.23,238.66,163.56ZM232,216a8,8,0,0,1-8-8c0-6.8,4-16.32,8-24.08,4,7.76,8,17.34,8,24.08A8,8,0,0,1,232,216Z"] },
23
+ film: { icon: [256, 256, [], '', "M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40ZM40,88h80v80H40Zm96-16V56h32V72Zm-16,0H88V56h32Zm0,112v16H88V184Zm16,0h32v16H136Zm0-16V88h80v80Zm80-96H184V56h32ZM72,56V72H40V56ZM40,184H72v16H40Zm176,16H184V184h32v16Z"] },
24
+ fit: { icon: [256, 256, [], '', "M216,48V88a8,8,0,0,1-16,0V56H168a8,8,0,0,1,0-16h40A8,8,0,0,1,216,48ZM88,200H56V168a8,8,0,0,0-16,0v40a8,8,0,0,0,8,8H88a8,8,0,0,0,0-16Zm120-40a8,8,0,0,0-8,8v32H168a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V168A8,8,0,0,0,208,160ZM88,40H48a8,8,0,0,0-8,8V88a8,8,0,0,0,16,0V56H88a8,8,0,0,0,0-16Z"] },
25
+ "flip-h": { icon: [256, 256, [], '', "M107.18,24.33a15.86,15.86,0,0,0-17.92,9.45l-.06.14-64,159.93A16,16,0,0,0,40,216h64a16,16,0,0,0,16-16V40A15.85,15.85,0,0,0,107.18,24.33ZM104,200H40l.06-.15L104,40Zm126.77-6.15-64-159.93-.06-.14A16,16,0,0,0,136,40V200a16,16,0,0,0,16,16h64a16,16,0,0,0,14.78-22.15ZM152,200V40l63.93,159.84.06.15Z"] },
26
+ "flip-v": { icon: [256, 256, [], '', "M56,120H216a16,16,0,0,0,6.23-30.74l-.14-.06-159.93-64A16,16,0,0,0,40,40v64A16,16,0,0,0,56,120Zm0-80,.15.06L216,104H56l0-64Zm160,96H56a16,16,0,0,0-16,16v64a16,16,0,0,0,22.15,14.78l159.93-64,.14-.06A16,16,0,0,0,216,136ZM56.15,215.93,56,216V152H216Z"] },
27
+ grip: { icon: [256, 256, [], '', "M104,60A12,12,0,1,1,92,48,12,12,0,0,1,104,60Zm60,12a12,12,0,1,0-12-12A12,12,0,0,0,164,72ZM92,116a12,12,0,1,0,12,12A12,12,0,0,0,92,116Zm72,0a12,12,0,1,0,12,12A12,12,0,0,0,164,116ZM92,184a12,12,0,1,0,12,12A12,12,0,0,0,92,184Zm72,0a12,12,0,1,0,12,12A12,12,0,0,0,164,184Z"] },
28
+ image: { icon: [256, 256, [], '', "M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm0,16V158.75l-26.07-26.06a16,16,0,0,0-22.63,0l-20,20-44-44a16,16,0,0,0-22.62,0L40,149.37V56ZM40,172l52-52,80,80H40Zm176,28H194.63l-36-36,20-20L216,181.38V200ZM144,100a12,12,0,1,1,12,12A12,12,0,0,1,144,100Z"] },
29
+ "layer-group": { icon: [256, 256, [], '', "M230.91,172A8,8,0,0,1,228,182.91l-96,56a8,8,0,0,1-8.06,0l-96-56A8,8,0,0,1,36,169.09l92,53.65,92-53.65A8,8,0,0,1,230.91,172ZM220,121.09l-92,53.65L36,121.09A8,8,0,0,0,28,134.91l96,56a8,8,0,0,0,8.06,0l96-56A8,8,0,1,0,220,121.09ZM24,80a8,8,0,0,1,4-6.91l96-56a8,8,0,0,1,8.06,0l96,56a8,8,0,0,1,0,13.82l-96,56a8,8,0,0,1-8.06,0l-96-56A8,8,0,0,1,24,80Zm23.88,0L128,126.74,208.12,80,128,33.26Z"] },
30
+ link: { icon: [256, 256, [], '', "M240,88.23a54.43,54.43,0,0,1-16,37L189.25,160a54.27,54.27,0,0,1-38.63,16h-.05A54.63,54.63,0,0,1,96,119.84a8,8,0,0,1,16,.45A38.62,38.62,0,0,0,150.58,160h0a38.39,38.39,0,0,0,27.31-11.31l34.75-34.75a38.63,38.63,0,0,0-54.63-54.63l-11,11A8,8,0,0,1,135.7,59l11-11A54.65,54.65,0,0,1,224,48,54.86,54.86,0,0,1,240,88.23ZM109,185.66l-11,11A38.41,38.41,0,0,1,70.6,208h0a38.63,38.63,0,0,1-27.29-65.94L78,107.31A38.63,38.63,0,0,1,144,135.71a8,8,0,0,0,16,.45A54.86,54.86,0,0,0,144,96a54.65,54.65,0,0,0-77.27,0L32,130.75A54.62,54.62,0,0,0,70.56,224h0a54.28,54.28,0,0,0,38.64-16l11-11A8,8,0,0,0,109,185.66Z"] },
31
+ marquee: { icon: [256, 256, [], '', "M152,40a8,8,0,0,1-8,8H112a8,8,0,0,1,0-16h32A8,8,0,0,1,152,40Zm-8,168H112a8,8,0,0,0,0,16h32a8,8,0,0,0,0-16ZM208,32H184a8,8,0,0,0,0,16h24V72a8,8,0,0,0,16,0V48A16,16,0,0,0,208,32Zm8,72a8,8,0,0,0-8,8v32a8,8,0,0,0,16,0V112A8,8,0,0,0,216,104Zm0,72a8,8,0,0,0-8,8v24H184a8,8,0,0,0,0,16h24a16,16,0,0,0,16-16V184A8,8,0,0,0,216,176ZM40,152a8,8,0,0,0,8-8V112a8,8,0,0,0-16,0v32A8,8,0,0,0,40,152Zm32,56H48V184a8,8,0,0,0-16,0v24a16,16,0,0,0,16,16H72a8,8,0,0,0,0-16ZM72,32H48A16,16,0,0,0,32,48V72a8,8,0,0,0,16,0V48H72a8,8,0,0,0,0-16Z"] },
32
+ move: { icon: [256, 256, [], '', "M90.34,61.66a8,8,0,0,1,0-11.32l32-32a8,8,0,0,1,11.32,0l32,32a8,8,0,0,1-11.32,11.32L136,43.31V96a8,8,0,0,1-16,0V43.31L101.66,61.66A8,8,0,0,1,90.34,61.66Zm64,132.68L136,212.69V160a8,8,0,0,0-16,0v52.69l-18.34-18.35a8,8,0,0,0-11.32,11.32l32,32a8,8,0,0,0,11.32,0l32-32a8,8,0,0,0-11.32-11.32Zm83.32-72-32-32a8,8,0,0,0-11.32,11.32L212.69,120H160a8,8,0,0,0,0,16h52.69l-18.35,18.34a8,8,0,0,0,11.32,11.32l32-32A8,8,0,0,0,237.66,122.34ZM43.31,136H96a8,8,0,0,0,0-16H43.31l18.35-18.34A8,8,0,0,0,50.34,90.34l-32,32a8,8,0,0,0,0,11.32l32,32a8,8,0,0,0,11.32-11.32Z"] },
33
+ "navigation-arrow": { icon: [256, 256, [], '', "M237.33,106.21,61.41,41l-.16-.05A16,16,0,0,0,40.9,61.25a1,1,0,0,0,.05.16l65.26,175.92A15.77,15.77,0,0,0,121.28,248h.3a15.77,15.77,0,0,0,15-11.29l.06-.2,21.84-78,78-21.84.2-.06a16,16,0,0,0,.62-30.38ZM149.84,144.3a8,8,0,0,0-5.54,5.54L121.3,232l-.06-.17L56,56l175.82,65.22.16.06Z"] },
34
+ "object-group": { icon: [256, 256, [], '', "M152,40a8,8,0,0,1-8,8H112a8,8,0,0,1,0-16h32A8,8,0,0,1,152,40Zm-8,168H112a8,8,0,0,0,0,16h32a8,8,0,0,0,0-16ZM208,48V72a8,8,0,0,0,16,0V48a16,16,0,0,0-16-16H184a8,8,0,0,0,0,16Zm8,56a8,8,0,0,0-8,8v32a8,8,0,0,0,16,0V112A8,8,0,0,0,216,104ZM40,152a8,8,0,0,0,8-8V112a8,8,0,0,0-16,0v32A8,8,0,0,0,40,152Zm32,56H48V184a8,8,0,0,0-16,0v24a16,16,0,0,0,16,16H72a8,8,0,0,0,0-16ZM72,32H48A16,16,0,0,0,32,48V72a8,8,0,0,0,16,0V48H72a8,8,0,0,0,0-16ZM240,208H224V192a8,8,0,0,0-16,0v16H192a8,8,0,0,0,0,16h16v16a8,8,0,0,0,16,0V224h16a8,8,0,0,0,0-16Z"] },
35
+ pencil: { icon: [256, 256, [], '', "M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z"] },
36
+ "pen-nib": { icon: [256, 256, [], '', "M248,92.68a15.86,15.86,0,0,0-4.69-11.31L174.63,12.68a16,16,0,0,0-22.63,0L123.57,41.11l-58,21.77A16.06,16.06,0,0,0,55.35,75.23L32.11,214.68A8,8,0,0,0,40,224a8.4,8.4,0,0,0,1.32-.11l139.44-23.24a16,16,0,0,0,12.35-10.17l21.77-58L243.31,104A15.87,15.87,0,0,0,248,92.68Zm-69.87,92.19L63.32,204l47.37-47.37a28,28,0,1,0-11.32-11.32L52,192.7,71.13,77.86,126,57.29,198.7,130ZM112,132a12,12,0,1,1,12,12A12,12,0,0,1,112,132Zm96-15.32L139.31,48l24-24L232,92.68Z"] },
37
+ play: { icon: [256, 256, [], '', "M232.4,114.49,88.32,26.35a16,16,0,0,0-16.2-.3A15.86,15.86,0,0,0,64,39.87V216.13A15.94,15.94,0,0,0,80,232a16.07,16.07,0,0,0,8.36-2.35L232.4,141.51a15.81,15.81,0,0,0,0-27ZM80,215.94V40l143.83,88Z"] },
38
+ plus: { icon: [256, 256, [], '', "M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"] },
39
+ redo: { icon: [256, 256, [], '', "M170.34,130.34,204.69,96H88a48,48,0,0,0,0,96h88a8,8,0,0,1,0,16H88A64,64,0,0,1,88,80H204.69L170.34,45.66a8,8,0,0,1,11.32-11.32l48,48a8,8,0,0,1,0,11.32l-48,48a8,8,0,0,1-11.32-11.32Z"] },
40
+ rotate: { icon: [256, 256, [], '', "M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64l-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,0,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60L224,85.8V56a8,8,0,1,1,16,0Z"] },
41
+ scribble: { icon: [256, 256, [], '', "M205.67,189.66a14.61,14.61,0,0,0,0,20.68,8,8,0,0,1-11.32,11.32,30.64,30.64,0,0,1,0-43.32l9.38-9.37A14.63,14.63,0,0,0,183,148.28L116.28,215A30.63,30.63,0,0,1,73,171.72L171.72,73A14.63,14.63,0,0,0,151,52.28L84.28,119A30.63,30.63,0,0,1,41,75.72L82.34,34.34A8,8,0,0,1,93.65,45.66L52.27,87A14.63,14.63,0,0,0,73,107.72L139.72,41A30.63,30.63,0,0,1,183,84.28L84.28,183A14.63,14.63,0,0,0,105,203.72L171.72,137A30.63,30.63,0,0,1,215,180.28Z"] },
42
+ shapes: { icon: [256, 256, [], '', "M71.59,61.47a8,8,0,0,0-15.18,0l-40,120A8,8,0,0,0,24,192h80a8,8,0,0,0,7.59-10.53ZM35.1,176,64,89.3,92.9,176ZM208,76a52,52,0,1,0-52,52A52.06,52.06,0,0,0,208,76Zm-88,0a36,36,0,1,1,36,36A36,36,0,0,1,120,76Zm104,68H136a8,8,0,0,0-8,8v56a8,8,0,0,0,8,8h88a8,8,0,0,0,8-8V152A8,8,0,0,0,224,144Zm-8,56H144V160h72Z"] },
43
+ slash: { icon: [256, 256, [], '', "M214.64,41.36a32,32,0,0,0-50.2,38.89L80.25,164.44a32.06,32.06,0,0,0-38.89,4.94h0a32,32,0,1,0,50.2,6.37l84.19-84.19a32,32,0,0,0,38.89-50.2Zm-139.33,162a16,16,0,0,1-22.64-22.64h0a16,16,0,0,1,22.63,0h0A16,16,0,0,1,75.31,203.33Zm128-128a16,16,0,1,1,0-22.63A16,16,0,0,1,203.33,75.3Z"] },
44
+ square: { icon: [256, 256, [], '', "M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32Zm0,176H48V48H208V208Z"] },
45
+ stop: { icon: [256, 256, [], '', "M200,40H56A16,16,0,0,0,40,56V200a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40Zm0,160H56V56H200V200Z"] },
46
+ stopwatch: { icon: [256, 256, [], '', "M128,40a96,96,0,1,0,96,96A96.11,96.11,0,0,0,128,40Zm0,176a80,80,0,1,1,80-80A80.09,80.09,0,0,1,128,216ZM173.66,90.34a8,8,0,0,1,0,11.32l-40,40a8,8,0,0,1-11.32-11.32l40-40A8,8,0,0,1,173.66,90.34ZM96,16a8,8,0,0,1,8-8h48a8,8,0,0,1,0,16H104A8,8,0,0,1,96,16Z"] },
47
+ subtract: { icon: [256, 256, [], '', "M174.63,81.35a80,80,0,1,0-93.28,93.28,80,80,0,1,0,93.28-93.28ZM224,160c0,1.52-.07,3-.18,4.51l-50-50A80.14,80.14,0,0,0,176,98,63.81,63.81,0,0,1,224,160Zm-77.4-2.09,52.61,52.62A64,64,0,0,1,183,219.7l-51.86-51.86A80.5,80.5,0,0,0,146.6,157.91Zm11.31-11.31a80.5,80.5,0,0,0,9.93-15.44L219.7,183a64,64,0,0,1-9.17,16.19ZM32,96a64,64,0,1,1,64,64A64.07,64.07,0,0,1,32,96ZM98,176a80.14,80.14,0,0,0,16.5-2.13l50,50c-1.49.11-3,.18-4.51.18A63.81,63.81,0,0,1,98,176Z"] },
48
+ times: { icon: [256, 256, [], '', "M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"] },
49
+ triangle: { icon: [256, 256, [], '', "M236.8,188.09,149.35,36.22a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.34,24.34,0,0,0,40.55,224h174.9a24.34,24.34,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8Z"] },
50
+ "toggle-off": { icon: [256, 256, [], '', "M176,56H80a72,72,0,0,0,0,144h96a72,72,0,0,0,0-144Zm0,128H80A56,56,0,0,1,80,72h96a56,56,0,0,1,0,112ZM80,88a40,40,0,1,0,40,40A40,40,0,0,0,80,88Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,80,152Z"] },
51
+ "toggle-on": { icon: [256, 256, [], '', "M176,56H80a72,72,0,0,0,0,144h96a72,72,0,0,0,0-144Zm0,128H80A56,56,0,0,1,80,72h96a56,56,0,0,1,0,112Zm0-96a40,40,0,1,0,40,40A40,40,0,0,0,176,88Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,176,152Z"] },
52
+ trash: { icon: [256, 256, [], '', "M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"] },
53
+ undo: { icon: [256, 256, [], '', "M232,144a64.07,64.07,0,0,1-64,64H80a8,8,0,0,1,0-16h88a48,48,0,0,0,0-96H51.31l34.35,34.34a8,8,0,0,1-11.32,11.32l-48-48a8,8,0,0,1,0-11.32l48-48A8,8,0,0,1,85.66,45.66L51.31,80H168A64.07,64.07,0,0,1,232,144Z"] },
54
+ "zoom-in": { icon: [256, 256, [], '', "M152,112a8,8,0,0,1-8,8H120v24a8,8,0,0,1-16,0V120H80a8,8,0,0,1,0-16h24V80a8,8,0,0,1,16,0v24h24A8,8,0,0,1,152,112Zm77.66,117.66a8,8,0,0,1-11.32,0l-50.06-50.07a88.11,88.11,0,1,1,11.31-11.31l50.07,50.06A8,8,0,0,1,229.66,229.66ZM112,184a72,72,0,1,0-72-72A72.08,72.08,0,0,0,112,184Z"] },
55
+ "zoom-out": { icon: [256, 256, [], '', "M152,112a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h64A8,8,0,0,1,152,112Zm77.66,117.66a8,8,0,0,1-11.32,0l-50.06-50.07a88.11,88.11,0,1,1,11.31-11.31l50.07,50.06A8,8,0,0,1,229.66,229.66ZM112,184a72,72,0,1,0-72-72A72.08,72.08,0,0,0,112,184Z"] },
56
+ };
@@ -20,17 +20,12 @@ import {
20
20
  } from '@dnd-kit/sortable';
21
21
  import { CSS } from '@dnd-kit/utilities';
22
22
 
23
- // Hold, then drag -- on both input kinds, because the drag target is the whole
24
- // title row rather than a handle. Arming on movement alone would turn any click
25
- // that drifts a pixel into a reorder, and the row carries a disclosure toggle
26
- // and an enable switch that must stay clickable.
27
- //
28
- // `tolerance` is the drift allowed during the hold: past it dnd-kit abandons the
29
- // gesture, so a press that was really the start of a scroll stays a scroll.
30
- // A finger waits longer than a mouse -- a finger resting on a surface is
31
- // ordinary, a held mouse button is deliberate.
32
- const MOUSE_ACTIVATION = { delay: 200, tolerance: 6 };
33
- const TOUCH_ACTIVATION = { delay: 380, tolerance: 8 };
23
+ // Hold / move to reorder via the grip only -- the rest of the title row must
24
+ // stay free for scrolling (especially on phones) and for the disclosure /
25
+ // enable controls. Arming on distance rather than a long press: the grip has
26
+ // `touch-action: none`, so there is no scroll gesture to yield to there.
27
+ const MOUSE_ACTIVATION = { distance: 4 };
28
+ const TOUCH_ACTIVATION = { distance: 8 };
34
29
 
35
30
  export function PanelSortContext({ names, onReorder, children }) {
36
31
  const sensors = useSensors(
@@ -61,9 +56,7 @@ export function PanelSortContext({ names, onReorder, children }) {
61
56
  );
62
57
  }
63
58
 
64
- // One sortable row. Returns the props for the row element and the handle, so
65
- // the caller decides what the handle looks like -- here it is the grip in the
66
- // panel header, which is several components away.
59
+ // One sortable row. Returns the props for the row element and the grip handle.
67
60
  export function usePanelSortable(id) {
68
61
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
69
62
  id,
@@ -2,8 +2,9 @@ import { isBehaviorEnabled } from '../scene';
2
2
  // The physics system: one matter-js engine per scene runtime. Registered as a
3
3
  // runtime system (see engine/scene.js `registerSystem`), it runs once per frame
4
4
  // in `afterBehaviors` -- after behaviors have expressed intent (velocities,
5
- // forces) -- to reconcile bodies, step the simulation on a fixed timestep, and
6
- // write results back into each dynamic actor's Layout.
5
+ // forces) -- to reconcile bodies, push any Layout writes onto the matching
6
+ // bodies, step the simulation on a fixed timestep, and write results back into
7
+ // each dynamic actor's Layout.
7
8
  //
8
9
  // The matter engine is created lazily on first step, so draw-only editor
9
10
  // previews (which never call update) pay nothing.
@@ -13,6 +14,7 @@ import { getColliderRect } from '../collider';
13
14
  import { buildJointConstraints, jointSignature, patchJoint } from './joints';
14
15
  import {
15
16
  DEFAULT_GRAVITY,
17
+ DEG_TO_RAD,
16
18
  FIXED_STEP_MS,
17
19
  FIXED_STEP_S,
18
20
  MAX_STEPS_PER_FRAME,
@@ -31,9 +33,9 @@ import {
31
33
  export class PhysicsSystem {
32
34
  constructor() {
33
35
  this.engine = null;
34
- this.bodies = new Map(); // actorId -> { body, sig, offset: {dx, dy} }
36
+ this.bodies = new Map(); // actorId -> { body, sig, offset: {dx, dy}, last }
35
37
  this.joints = new Map(); // `${ownerId}#${listIndex}` -> { constraints, bodyA, bodyB, sig }
36
- this.intents = new Map(); // actorId -> { velocity?, impulse?, force? }
38
+ this.intents = new Map(); // actorId -> { velocity?, impulse?, force?, angularVelocity? }
37
39
  this.grabs = new Map(); // actorId -> { constraint, localPoint }
38
40
  this.gravity = { ...DEFAULT_GRAVITY };
39
41
  this.acc = 0;
@@ -93,7 +95,7 @@ export class PhysicsSystem {
93
95
  this.reconcile(scene);
94
96
  this.reconcileJoints(scene);
95
97
  this.applyIntents();
96
- this.syncStaticBodies(scene);
98
+ this.syncBodiesFromLayout(scene);
97
99
  this.step(dt);
98
100
  this.writeBack(scene);
99
101
  this.detectNonDynamicContacts();
@@ -186,7 +188,10 @@ export class PhysicsSystem {
186
188
  const center = rectCenter(rect);
187
189
  const layout = actor.components.Layout;
188
190
  const offset = { dx: center.x - layout.x, dy: center.y - layout.y };
189
- this.bodies.set(actor.id, { body, sig: bodySignature(actor, rect), offset });
191
+ // Cache what physics last wrote so a later Layout write can be detected
192
+ // with !== (raw values: an omitted rotation stays undefined).
193
+ const last = { x: layout.x, y: layout.y, rotation: layout.rotation };
194
+ this.bodies.set(actor.id, { body, sig: bodySignature(actor, rect), offset, last });
190
195
  Matter.Composite.add(this.engine.world, body);
191
196
  }
192
197
 
@@ -223,19 +228,39 @@ export class PhysicsSystem {
223
228
  });
224
229
  }
225
230
  if (intent.force) Matter.Body.applyForce(body, body.position, intent.force);
231
+ if (intent.angularVelocity != null) {
232
+ Matter.Body.setAngularVelocity(body, intent.angularVelocity * DEG_TO_RAD);
233
+ }
226
234
  }
227
235
  this.intents.clear();
228
236
  }
229
237
 
230
- // Keep static + kinematic bodies wherever Layout says (editor edits, or a
231
- // behavior moving a platform by writing Layout).
232
- syncStaticBodies(scene) {
238
+ // Push Layout writes onto bodies before the step. Static/kinematic always
239
+ // follow Layout (editor edits, or a behavior moving a platform). Dynamic
240
+ // bodies only sync fields that differ from what writeBack last wrote — an
241
+ // untouched field must not get a float-roundtrip micro-delta.
242
+ syncBodiesFromLayout(scene) {
233
243
  for (const [id, entry] of this.bodies) {
234
- if (!entry.body.isStatic) continue;
235
244
  const actor = scene.getActor(id);
236
245
  if (!actor) continue;
237
246
  const layout = actor.components.Layout;
238
- syncBodyToPoint(entry.body, { x: layout.x + entry.offset.dx, y: layout.y + entry.offset.dy }, layout.rotation);
247
+ if (entry.body.isStatic) {
248
+ syncBodyToPoint(entry.body, { x: layout.x + entry.offset.dx, y: layout.y + entry.offset.dy }, layout.rotation);
249
+ continue;
250
+ }
251
+ const { last } = entry;
252
+ if (layout.x !== last.x || layout.y !== last.y) {
253
+ Matter.Body.setPosition(entry.body, {
254
+ x: layout.x + entry.offset.dx,
255
+ y: layout.y + entry.offset.dy,
256
+ });
257
+ }
258
+ if (layout.rotation !== last.rotation) {
259
+ Matter.Body.setAngle(entry.body, (layout.rotation ?? 0) * DEG_TO_RAD);
260
+ }
261
+ last.x = layout.x;
262
+ last.y = layout.y;
263
+ last.rotation = layout.rotation;
239
264
  }
240
265
  }
241
266
 
@@ -263,9 +288,18 @@ export class PhysicsSystem {
263
288
  layout.x = entry.body.position.x - entry.offset.dx;
264
289
  layout.y = entry.body.position.y - entry.offset.dy;
265
290
  layout.rotation = entry.body.angle * RAD_TO_DEG;
291
+ entry.last.x = layout.x;
292
+ entry.last.y = layout.y;
293
+ entry.last.rotation = layout.rotation;
266
294
  }
267
295
  }
268
296
 
297
+ layoutOf(actorOrId) {
298
+ const actor =
299
+ typeof actorOrId === 'string' ? this.lastScene?.getActor(actorOrId) : actorOrId;
300
+ return actor?.components?.Layout ?? null;
301
+ }
302
+
269
303
  // matter-js only generates contacts when at least one body is dynamic, so it
270
304
  // never reports overlaps between two non-dynamic bodies (static-static, or
271
305
  // any pair involving an emulated-kinematic body, which is matter-static).
@@ -449,6 +483,27 @@ export class PhysicsSystem {
449
483
  const body = this.bodyOf(actor);
450
484
  return body ? { x: body.velocity.x, y: body.velocity.y } : { x: 0, y: 0 };
451
485
  },
486
+ // Degrees per fixed step (matches RigidBody.angularVelocity). Set to 0
487
+ // to stop a spin before driving rotation by hand.
488
+ setAngularVelocity: (actor, degPerStep) =>
489
+ this.queue(actor, 'angularVelocity', degPerStep),
490
+ getAngularVelocity: (actor) => {
491
+ const body = this.bodyOf(actor);
492
+ return body ? body.angularVelocity * RAD_TO_DEG : 0;
493
+ },
494
+ // Thin sugar over Layout writes — teleport the actor (velocity kept).
495
+ // Works for dynamic, kinematic, static, and body-less actors alike.
496
+ setRotation: (actor, deg) => {
497
+ const layout = this.layoutOf(actor);
498
+ if (layout) layout.rotation = deg;
499
+ },
500
+ // Layout top-left convention (not body center).
501
+ setPosition: (actor, { x, y }) => {
502
+ const layout = this.layoutOf(actor);
503
+ if (!layout) return;
504
+ layout.x = x;
505
+ layout.y = y;
506
+ },
452
507
  // True while two actors are touching (updated by collision events).
453
508
  isColliding: (a, b) => {
454
509
  const aId = typeof a === 'string' ? a : a?.id;
@@ -4,6 +4,11 @@
4
4
  // thin wrappers: read input + runtime state, call these, push the result
5
5
  // through scene.physics. (Controls are touch/pointer-first; keyboard, when
6
6
  // added, should only DUPLICATE an on-screen control, never be the only input.)
7
+ //
8
+ // AnalogStick force mode: stickDrive (held) and stickBrake (idle) return
9
+ // velocity deltas applied via applyImpulse — mass-independent, composable,
10
+ // and restricted to the stick's driven axes so gravity on an undriven axis
11
+ // is never clamped or braked.
7
12
 
8
13
  // Authored Draggable `stiffness` (0..1, 0 = floppy .. 1 = rigid) -> the matter
9
14
  // constraint values that hold the object. Geometric, like joints.js's rope map
@@ -68,9 +73,10 @@ export function acquirePress(scene, actorId, priorityFor) {
68
73
  }
69
74
 
70
75
  // Run `draw(ctx)` in world space, undoing the actor's Layout rotation that the
71
- // runtime applies about the actor's center. Overlay controls (Slingshot,
72
- // AnalogStick) use this so their on-screen UI stays anchored where the user
73
- // pressed instead of rotating/sliding with the actor.
76
+ // runtime applies about the actor's center. Overlays anchored to world objects
77
+ // (Slingshot, Draggable, Joints) use this so their art stays aligned with the
78
+ // actor's position without rotating/sliding with it. Still moves with the
79
+ // camera -- for screen-fixed HUD use `drawUi` instead.
74
80
  export function inActorWorldSpace(ctx, layout, draw) {
75
81
  const rot = layout?.rotation ?? 0;
76
82
  ctx.save();
@@ -103,15 +109,73 @@ export function stickVector(pointer, origin, maxRadius = 60) {
103
109
  return clampLength({ x: pointer.x - origin.x, y: pointer.y - origin.y }, maxRadius);
104
110
  }
105
111
 
106
- // Velocity a virtual stick commands: proportional to displacement/maxRadius,
107
- // up to `speed`, restricted to the enabled axes.
108
- export function analogVelocity(pointer, origin, props = {}) {
112
+ // Card coords before the camera -- for screen-fixed controls reading a pointer.
113
+ export function screenPoint(pointer) {
114
+ return { x: pointer.screenX, y: pointer.screenY };
115
+ }
116
+
117
+ // Keep only the axes the stick is allowed to drive. An undriven axis (gravity
118
+ // in a platformer) must never be clamped or braked by the stick.
119
+ function drivenAxes(v, axes) {
120
+ return {
121
+ x: axes === 'vertical' ? 0 : v.x,
122
+ y: axes === 'horizontal' ? 0 : v.y,
123
+ };
124
+ }
125
+
126
+ // Smallest signed angle from `from` to `to`, wrapped into [−π, π].
127
+ function angleDelta(from, to) {
128
+ let d = Math.atan2(to.y, to.x) - Math.atan2(from.y, from.x);
129
+ if (d > Math.PI) d -= 2 * Math.PI;
130
+ if (d < -Math.PI) d += 2 * Math.PI;
131
+ return d;
132
+ }
133
+
134
+ // Velocity delta while the stick is held: accelerate toward deflection × speed
135
+ // with OG turnFriction boost on reversals, then clamp the driven axes to `speed`.
136
+ // Returns an impulse (add to velocity); never touches an undriven axis.
137
+ export function stickDrive(velocity, deflection, props = {}, dt) {
109
138
  const speed = props.speed ?? 6;
110
- const maxRadius = props.maxRadius ?? 60;
139
+ const rampTime = props.rampTime ?? 0.2;
140
+ const turnBoost = props.turnBoost ?? 3;
141
+ const axes = props.axes ?? 'both';
142
+ const vDriven = drivenAxes(velocity, axes);
143
+
144
+ let boost = 1;
145
+ if (turnBoost > 0 && length(velocity) > 0 && length(deflection) > 0) {
146
+ boost = 1 + turnBoost * (Math.abs(angleDelta(velocity, deflection)) / Math.PI);
147
+ }
148
+
149
+ let delta;
150
+ if (rampTime <= 0) {
151
+ // Instant: one-frame snap toward the capped target on the driven axes.
152
+ const target = drivenAxes(
153
+ { x: deflection.x * speed, y: deflection.y * speed },
154
+ axes,
155
+ );
156
+ delta = { x: target.x - vDriven.x, y: target.y - vDriven.y };
157
+ } else {
158
+ const accel = speed / rampTime;
159
+ delta = drivenAxes(
160
+ { x: deflection.x * accel * boost * dt, y: deflection.y * accel * boost * dt },
161
+ axes,
162
+ );
163
+ const capped = clampLength({ x: vDriven.x + delta.x, y: vDriven.y + delta.y }, speed);
164
+ delta = { x: capped.x - vDriven.x, y: capped.y - vDriven.y };
165
+ }
166
+ return delta;
167
+ }
168
+
169
+ // Velocity delta while the stick is idle: Coulomb brake on the driven axes only.
170
+ // `slowDown` is speed units lost per second (px/step per second). 0 = coast.
171
+ export function stickBrake(velocity, props = {}, dt) {
172
+ const slowDown = props.slowDown ?? 20;
173
+ if (slowDown <= 0) return { x: 0, y: 0 };
111
174
  const axes = props.axes ?? 'both';
112
- const v = stickVector(pointer, origin, maxRadius);
113
- const out = { x: (v.x / maxRadius) * speed, y: (v.y / maxRadius) * speed };
114
- if (axes === 'horizontal') out.y = 0;
115
- if (axes === 'vertical') out.x = 0;
116
- return out;
175
+ const vDriven = drivenAxes(velocity, axes);
176
+ const len = length(vDriven);
177
+ if (len === 0) return { x: 0, y: 0 };
178
+ const reduce = Math.min(len, slowDown * dt);
179
+ const s = -reduce / len;
180
+ return { x: vDriven.x * s, y: vDriven.y * s };
117
181
  }
@@ -60,15 +60,16 @@ export class SceneRuntime {
60
60
  // an in-progress drag. `pointer` stays the primary (first-down) pointer for
61
61
  // single-touch behaviors and for decks reading the documented API.
62
62
  this.pointers = new Map();
63
- this.pointer = { x: 0, y: 0, down: false };
63
+ this.pointer = { x: 0, y: 0, screenX: 0, screenY: 0, down: false };
64
64
  this.data = { actors: [] };
65
65
  this.actors = new Map();
66
66
  this.camera = undefined;
67
67
  this.status = undefined;
68
68
  // The deck's font as a CSS font-family value, always usable: the face
69
69
  // picked in the Style editor, or a plain sans stack when there is none.
70
- // For `draw` hooks -- `ctx.font = `20px ${scene.font}`` -- since canvas
71
- // takes a family name, not a class. HUD `ui` output inherits it already.
70
+ // For `draw` / `drawUi` hooks -- `ctx.font = `20px ${scene.font}`` -- since
71
+ // canvas takes a family name, not a class. Prefer `drawUi` for HUD text;
72
+ // the deprecated React `ui` overlay still inherits this face via CSS.
72
73
  this.font = deckFontFamily;
73
74
  // Registered systems run once per frame after all behavior `update`s (see
74
75
  // `update`). A system is a plain object with an optional
@@ -256,22 +257,36 @@ export class SceneRuntime {
256
257
  const point = screenToCard(canvas, clientX, clientY);
257
258
  const x = point.x + (this.camera?.x ?? 0);
258
259
  const y = point.y + (this.camera?.y ?? 0);
260
+ const screenX = point.x;
261
+ const screenY = point.y;
259
262
  let p = this.pointers.get(pointerId);
260
263
  if (!p) {
261
264
  // Only a press creates a pointer. A move with no press is a mouse hover,
262
265
  // and a stray event after release must not resurrect a finished contact.
263
- if (down !== true) return this.syncPrimaryPointer(x, y);
264
- p = { id: pointerId, x, y, down: false, justPressed: false, claimedBy: null, claimPriority: 0 };
266
+ if (down !== true) return this.syncPrimaryPointer({ x, y, screenX, screenY });
267
+ p = {
268
+ id: pointerId,
269
+ x,
270
+ y,
271
+ screenX,
272
+ screenY,
273
+ down: false,
274
+ justPressed: false,
275
+ claimedBy: null,
276
+ claimPriority: 0,
277
+ };
265
278
  this.pointers.set(pointerId, p);
266
279
  }
267
280
  p.x = x;
268
281
  p.y = y;
282
+ p.screenX = screenX;
283
+ p.screenY = screenY;
269
284
  // The press edge is per-pointer, so a finger landing while another is
270
285
  // already held still registers as a new press (a shared `down` flag would
271
286
  // show no rising edge and the second press would be invisible).
272
287
  if (down === true && !p.down) p.justPressed = true;
273
288
  if (down !== undefined) p.down = down;
274
- this.syncPrimaryPointer(x, y);
289
+ this.syncPrimaryPointer({ x, y, screenX, screenY });
275
290
  }
276
291
 
277
292
  // Claim a pointer for one gesture, so two controls can't both act on the same
@@ -318,16 +333,23 @@ export class SceneRuntime {
318
333
  // `pointer` mirrors the primary -- the oldest pointer still held (Map keeps
319
334
  // insertion order) -- so single-touch behaviors and decks reading the
320
335
  // documented `scene.pointer` API behave exactly as before.
321
- syncPrimaryPointer(fallbackX, fallbackY) {
336
+ // `fallback` is `{ x, y, screenX, screenY }` for the hover path (no press yet).
337
+ syncPrimaryPointer(fallback) {
322
338
  for (const p of this.pointers.values()) {
323
339
  if (!p.down) continue;
324
340
  this.pointer.x = p.x;
325
341
  this.pointer.y = p.y;
342
+ this.pointer.screenX = p.screenX;
343
+ this.pointer.screenY = p.screenY;
326
344
  this.pointer.down = true;
327
345
  return;
328
346
  }
329
- if (fallbackX !== undefined) this.pointer.x = fallbackX;
330
- if (fallbackY !== undefined) this.pointer.y = fallbackY;
347
+ if (fallback) {
348
+ this.pointer.x = fallback.x;
349
+ this.pointer.y = fallback.y;
350
+ this.pointer.screenX = fallback.screenX;
351
+ this.pointer.screenY = fallback.screenY;
352
+ }
331
353
  this.pointer.down = false;
332
354
  }
333
355
 
@@ -454,6 +476,22 @@ export class SceneRuntime {
454
476
  if (options.marquee) drawMarquee(ctx, options.marquee);
455
477
  if (options.showCropOutline) drawCropOutline(ctx);
456
478
  ctx.restore();
479
+
480
+ // Screen-space pass after the camera block so HUD / on-screen controls
481
+ // composite above the world and stay put while the camera scrolls.
482
+ this.#drawUiPass(ctx, viewport, options);
483
+ }
484
+
485
+ // Screen-space pass: card units, no camera, no actor rotation. Preferred
486
+ // path for HUD and on-screen controls that must stay put while the world
487
+ // scrolls (the React `ui()` hook is deprecated).
488
+ #drawUiPass(ctx, viewport, options) {
489
+ ctx.save();
490
+ ctx.translate(viewport.originX, viewport.originY);
491
+ for (const actor of this.getActors()) {
492
+ this.forEachBehavior(actor, (instance) => instance.drawUi?.(actor, this, ctx, options));
493
+ }
494
+ ctx.restore();
457
495
  }
458
496
 
459
497
  actorAt(x, y) {
@@ -1,58 +1,6 @@
1
1
  import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
2
2
  import { createPortal } from 'react-dom';
3
- import {
4
- faArrowsAlt,
5
- faArrowsAltH,
6
- faArrowsAltV,
7
- faBars,
8
- faBezierCurve,
9
- faChevronDown,
10
- faChevronLeft,
11
- faChevronRight,
12
- faChevronUp,
13
- faCircle,
14
- faClone,
15
- faCode,
16
- faCodeBranch,
17
- faDotCircle,
18
- faDrawPolygon,
19
- faEraser,
20
- faExpand,
21
- faExternalLinkAlt,
22
- faEyeDropper,
23
- faFile,
24
- faFilm,
25
- faFillDrip,
26
- faGlobe,
27
- faGripVertical,
28
- faToggleOff,
29
- faToggleOn,
30
- faImage,
31
- faLayerGroup,
32
- faLocationArrow,
33
- faObjectGroup,
34
- faPaintBrush,
35
- faPalette,
36
- faPencilAlt,
37
- faPenNib,
38
- faPlay,
39
- faPlus,
40
- faRedo,
41
- faSearchMinus,
42
- faSearchPlus,
43
- faShapes,
44
- faSlash,
45
- faVectorSquare,
46
- faSquare,
47
- faStamp,
48
- faStop,
49
- faSyncAlt,
50
- faTh,
51
- faTimes,
52
- faTrash,
53
- faUndo,
54
- faVideo,
55
- } from '@fortawesome/free-solid-svg-icons';
3
+ import { icons } from './icons';
56
4
  import styles from './ui.module.css';
57
5
  export { styles };
58
6
  export const theme = {
@@ -219,72 +167,8 @@ export function IconButton({ icon, label, active = false, variant = '', ...props
219
167
  </Button>
220
168
  );
221
169
  }
222
- const icons = {
223
- bars: faBars,
224
- 'bezier-curve': faBezierCurve,
225
- camera: faVideo,
226
- 'chevron-down': faChevronDown,
227
- 'chevron-left': faChevronLeft,
228
- 'chevron-right': faChevronRight,
229
- 'chevron-up': faChevronUp,
230
- circle: faCircle,
231
- clone: faClone,
232
- code: faCode,
233
- // FA5 has no faCodeFork (that's the FA6 name); faCodeBranch is the fork glyph.
234
- //
235
- // Reserved for forking a FILE -- taking this deck's own version of one an
236
- // import owns -- which is what it means in the shell's file tree and editor
237
- // banner too. Nothing in this kit draws it since the actor action became
238
- // `clone`; kept because a deck or a kit pinned to an older physics-2d may
239
- // still ask for it by name, and an unused entry costs less than a missing
240
- // glyph on somebody else's screen.
241
- 'code-fork': faCodeBranch,
242
- 'dot-circle': faDotCircle,
243
- 'draw-polygon': faDrawPolygon,
244
- eraser: faEraser,
245
- expand: faExpand,
246
- external: faExternalLinkAlt,
247
- eyedropper: faEyeDropper,
248
- fill: faFillDrip,
249
- 'flip-h': faArrowsAltH,
250
- 'flip-v': faArrowsAltV,
251
- file: faFile,
252
- film: faFilm,
253
- globe: faGlobe,
254
- grip: faGripVertical,
255
- 'toggle-off': faToggleOff,
256
- 'toggle-on': faToggleOn,
257
- grid: faTh,
258
- image: faImage,
259
- 'layer-group': faLayerGroup,
260
- // FA's location-arrow points up-right; flip it for a path-select cursor feel.
261
- 'location-arrow': { ...faLocationArrow, flipX: true },
262
- marquee: faVectorSquare,
263
- move: faArrowsAlt,
264
- 'object-group': faObjectGroup,
265
- paintbrush: faPaintBrush,
266
- palette: faPalette,
267
- pencil: faPencilAlt,
268
- 'pen-nib': faPenNib,
269
- play: faPlay,
270
- plus: faPlus,
271
- redo: faRedo,
272
- rotate: faSyncAlt,
273
- 'zoom-in': faSearchPlus,
274
- 'zoom-out': faSearchMinus,
275
- fit: faExpand,
276
- shapes: faShapes,
277
- slash: faSlash,
278
- square: faSquare,
279
- stamp: faStamp,
280
- stop: faStop,
281
- times: faTimes,
282
- // Hand-rolled equilateral-ish triangle glyph (FA free-solid has no plain
283
- // triangle); shaped to match the filled-glyph look of the imported icons.
284
- triangle: { icon: [512, 512, [], '', 'M256 64 L496 448 L16 448 Z'] },
285
- trash: faTrash,
286
- undo: faUndo,
287
- };
170
+ // `code-fork` is generated and unused in this kit (the actor action is `clone`);
171
+ // kept so a deck or older kit still asking for it by name gets a glyph.
288
172
  export function Icon({ name }) {
289
173
  const def = icons[name];
290
174
  if (!def) return null;
@@ -383,14 +267,18 @@ export function Panel({ title, action, children, overridden = false }) {
383
267
  <section className={cx(styles.panel, behavior?.dragging && styles.panelDragging)}>
384
268
  {title ? (
385
269
  <div
386
- className={cx(styles.panelHeader, collapsed && styles.panelHeaderCollapsed)}
387
- {...(behavior?.handleProps ?? {})}>
270
+ className={cx(styles.panelHeader, collapsed && styles.panelHeaderCollapsed)}>
388
271
  {collapsible && behavior.handleProps ? (
389
- // Affordance only -- the whole header row carries the drag
390
- // listeners, so there is nothing to hit precisely.
391
- <span className={styles.panelGrip} aria-hidden="true">
272
+ // Real drag handle: listeners live here so the rest of the header
273
+ // can scroll on touch. `touch-action: none` is on the grip only.
274
+ <button
275
+ type="button"
276
+ className={styles.panelGrip}
277
+ title="Drag to reorder"
278
+ aria-label={`Reorder ${title}`}
279
+ {...behavior.handleProps}>
392
280
  <Icon name="grip" />
393
- </span>
281
+ </button>
394
282
  ) : null}
395
283
  {collapsible ? (
396
284
  // The whole title row is the hit target, not just the chevron --