lecodes-sdk 0.19.0

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 (367) hide show
  1. package/README.md +114 -0
  2. package/dist/global.d.ts +503 -0
  3. package/dist/host.d.ts +61 -0
  4. package/dist/inject.js +4730 -0
  5. package/dist/types/animate/animate.d.ts +20 -0
  6. package/dist/types/animate/bezier.d.ts +2 -0
  7. package/dist/types/animate/easings.d.ts +21 -0
  8. package/dist/types/canvas/Canvas.d.ts +168 -0
  9. package/dist/types/core/Aspect.d.ts +171 -0
  10. package/dist/types/core/InspectorUI.d.ts +88 -0
  11. package/dist/types/core/StateMachine.d.ts +81 -0
  12. package/dist/types/core/color.d.ts +13 -0
  13. package/dist/types/core/compWrite.d.ts +11 -0
  14. package/dist/types/core/events.d.ts +11 -0
  15. package/dist/types/core/fields.d.ts +58 -0
  16. package/dist/types/core/registry.d.ts +7 -0
  17. package/dist/types/core/signals.d.ts +29 -0
  18. package/dist/types/core/time.d.ts +31 -0
  19. package/dist/types/g2/Camera2D.d.ts +19 -0
  20. package/dist/types/g2/CharacterController2D.d.ts +134 -0
  21. package/dist/types/g2/Node2D.d.ts +91 -0
  22. package/dist/types/g2/OneWay2D.d.ts +24 -0
  23. package/dist/types/g2/Physics2D.d.ts +147 -0
  24. package/dist/types/g2/Scene2D.d.ts +72 -0
  25. package/dist/types/g2/Shape2D.d.ts +58 -0
  26. package/dist/types/g2/Sprite.d.ts +51 -0
  27. package/dist/types/g2/SpriteAnimation.d.ts +50 -0
  28. package/dist/types/g2/SpriteSheet.d.ts +69 -0
  29. package/dist/types/g2/Texture2D.d.ts +16 -0
  30. package/dist/types/g2/Tilemap.d.ts +20 -0
  31. package/dist/types/g2/Tileset.d.ts +32 -0
  32. package/dist/types/g2/Trigger2D.d.ts +20 -0
  33. package/dist/types/g2/autotile.d.ts +133 -0
  34. package/dist/types/g2/cells.d.ts +14 -0
  35. package/dist/types/g2/defineScene2d.d.ts +141 -0
  36. package/dist/types/g2/groups2d.d.ts +16 -0
  37. package/dist/types/g2/loop.d.ts +4 -0
  38. package/dist/types/g2/scenarios2d.d.ts +30 -0
  39. package/dist/types/g2/touch.d.ts +2 -0
  40. package/dist/types/gl/Camera.d.ts +79 -0
  41. package/dist/types/gl/CameraPlace.d.ts +26 -0
  42. package/dist/types/gl/CharacterController.d.ts +98 -0
  43. package/dist/types/gl/Gearbox.d.ts +86 -0
  44. package/dist/types/gl/Geometry.d.ts +39 -0
  45. package/dist/types/gl/IK.d.ts +53 -0
  46. package/dist/types/gl/InstancedMesh.d.ts +46 -0
  47. package/dist/types/gl/Light.d.ts +73 -0
  48. package/dist/types/gl/Lightmap.d.ts +85 -0
  49. package/dist/types/gl/Locomotion.d.ts +279 -0
  50. package/dist/types/gl/Material.d.ts +100 -0
  51. package/dist/types/gl/Mesh.d.ts +33 -0
  52. package/dist/types/gl/Model.d.ts +53 -0
  53. package/dist/types/gl/NavAgent.d.ts +99 -0
  54. package/dist/types/gl/NavMesh.d.ts +169 -0
  55. package/dist/types/gl/Node.d.ts +99 -0
  56. package/dist/types/gl/Noise.d.ts +20 -0
  57. package/dist/types/gl/Particles.d.ts +277 -0
  58. package/dist/types/gl/Physics.d.ts +91 -0
  59. package/dist/types/gl/Plane.d.ts +19 -0
  60. package/dist/types/gl/Ragdoll.d.ts +86 -0
  61. package/dist/types/gl/Ray.d.ts +8 -0
  62. package/dist/types/gl/Scene.d.ts +221 -0
  63. package/dist/types/gl/Shape.d.ts +122 -0
  64. package/dist/types/gl/Terrain.d.ts +280 -0
  65. package/dist/types/gl/Texture.d.ts +27 -0
  66. package/dist/types/gl/Trigger.d.ts +10 -0
  67. package/dist/types/gl/Vehicle.d.ts +191 -0
  68. package/dist/types/gl/Wheel.d.ts +95 -0
  69. package/dist/types/gl/animation/AnimationClip.d.ts +60 -0
  70. package/dist/types/gl/animation/Animator.d.ts +219 -0
  71. package/dist/types/gl/animation/Layer.d.ts +31 -0
  72. package/dist/types/gl/animation/Loop.d.ts +17 -0
  73. package/dist/types/gl/animation/Playback.d.ts +36 -0
  74. package/dist/types/gl/animation/core.d.ts +367 -0
  75. package/dist/types/gl/controls.d.ts +19 -0
  76. package/dist/types/gl/physicsEvents.d.ts +1 -0
  77. package/dist/types/gl/scenarios.d.ts +98 -0
  78. package/dist/types/gl/state.d.ts +5 -0
  79. package/dist/types/gl/terrainMesh.d.ts +46 -0
  80. package/dist/types/gl/touch.d.ts +2 -0
  81. package/dist/types/inject.d.ts +123 -0
  82. package/dist/types/math/Mathf.d.ts +39 -0
  83. package/dist/types/math/mat4.d.ts +66 -0
  84. package/dist/types/math/quat.d.ts +53 -0
  85. package/dist/types/math/vec.d.ts +129 -0
  86. package/dist/types/net/codec.d.ts +25 -0
  87. package/dist/types/net/core.d.ts +106 -0
  88. package/dist/types/net/index.d.ts +80 -0
  89. package/dist/types/net/replication.d.ts +101 -0
  90. package/dist/types/plugins/camera.d.ts +25 -0
  91. package/dist/types/plugins/geolocation.d.ts +39 -0
  92. package/dist/types/plugins/oauth.d.ts +25 -0
  93. package/dist/types/plugins/permission.d.ts +1 -0
  94. package/dist/types/plugins/push.d.ts +46 -0
  95. package/dist/types/plugins/qr.d.ts +21 -0
  96. package/dist/types/plugins/service.d.ts +33 -0
  97. package/dist/types/runtime/app.d.ts +67 -0
  98. package/dist/types/runtime/appEvents.d.ts +7 -0
  99. package/dist/types/runtime/channel.d.ts +1 -0
  100. package/dist/types/runtime/clipboard.d.ts +7 -0
  101. package/dist/types/runtime/datetime.d.ts +80 -0
  102. package/dist/types/runtime/device.d.ts +126 -0
  103. package/dist/types/runtime/fetch.d.ts +31 -0
  104. package/dist/types/runtime/files.d.ts +33 -0
  105. package/dist/types/runtime/input.d.ts +121 -0
  106. package/dist/types/runtime/media.d.ts +46 -0
  107. package/dist/types/runtime/misc.d.ts +11 -0
  108. package/dist/types/runtime/net.d.ts +15 -0
  109. package/dist/types/runtime/rpc.d.ts +44 -0
  110. package/dist/types/runtime/service.d.ts +1 -0
  111. package/dist/types/runtime/share.d.ts +2 -0
  112. package/dist/types/runtime/storage.d.ts +5 -0
  113. package/dist/types/runtime/touch.d.ts +56 -0
  114. package/dist/types/scene/defineScene.d.ts +296 -0
  115. package/dist/types/scene/editorPlugins.d.ts +82 -0
  116. package/dist/types/scene/gizmos.d.ts +56 -0
  117. package/dist/types/scene/grammar.d.ts +56 -0
  118. package/dist/types/scene/level.d.ts +98 -0
  119. package/dist/types/scene/material.d.ts +76 -0
  120. package/dist/types/server/auth/appConfig.d.ts +16 -0
  121. package/dist/types/server/auth/global.d.ts +56 -0
  122. package/dist/types/server/auth/models.d.ts +164 -0
  123. package/dist/types/server/auth/types.d.ts +56 -0
  124. package/dist/types/server/channel.d.ts +28 -0
  125. package/dist/types/server/context.d.ts +27 -0
  126. package/dist/types/server/db/defineDb.d.ts +36 -0
  127. package/dist/types/server/db/fields.d.ts +146 -0
  128. package/dist/types/server/db/index.d.ts +6 -0
  129. package/dist/types/server/db/marci/query.d.ts +326 -0
  130. package/dist/types/server/db/types.d.ts +163 -0
  131. package/dist/types/server/errors.d.ts +8 -0
  132. package/dist/types/server/inject.d.ts +5 -0
  133. package/dist/types/ui/NativeView.d.ts +38 -0
  134. package/dist/types/ui/UI.d.ts +27 -0
  135. package/dist/types/ui/UIBottomSheet.d.ts +71 -0
  136. package/dist/types/ui/UIButton.d.ts +30 -0
  137. package/dist/types/ui/UIContainer.d.ts +33 -0
  138. package/dist/types/ui/UIImage.d.ts +35 -0
  139. package/dist/types/ui/UIInput.d.ts +110 -0
  140. package/dist/types/ui/UIModal.d.ts +70 -0
  141. package/dist/types/ui/UINode.d.ts +363 -0
  142. package/dist/types/ui/UIPager.d.ts +123 -0
  143. package/dist/types/ui/UIPopover.d.ts +39 -0
  144. package/dist/types/ui/UIScreen.d.ts +57 -0
  145. package/dist/types/ui/UIScrollable.d.ts +40 -0
  146. package/dist/types/ui/UISpacer.d.ts +9 -0
  147. package/dist/types/ui/UITabs.d.ts +96 -0
  148. package/dist/types/ui/UIText.d.ts +19 -0
  149. package/dist/types/ui/UIVideo.d.ts +28 -0
  150. package/dist/types/ui/UIVirtualizedList.d.ts +97 -0
  151. package/dist/types/ui/UIWidget.d.ts +65 -0
  152. package/dist/types/ui/fonts.d.ts +9 -0
  153. package/dist/types/ui/presentable.d.ts +68 -0
  154. package/dist/types/ui/router.d.ts +40 -0
  155. package/dist/types/ui/theme.d.ts +26 -0
  156. package/dist/types.json +1 -0
  157. package/package.json +46 -0
  158. package/prompts/2d.md +395 -0
  159. package/prompts/3d-scene-files.md +113 -0
  160. package/prompts/3d-scene.md +119 -0
  161. package/prompts/3d.md +303 -0
  162. package/prompts/README.md +142 -0
  163. package/prompts/ar.md +109 -0
  164. package/prompts/canvas.md +27 -0
  165. package/prompts/compose.ts +46 -0
  166. package/prompts/concept.md +49 -0
  167. package/prompts/core-design.md +75 -0
  168. package/prompts/core.md +264 -0
  169. package/prompts/design.md +218 -0
  170. package/prompts/dist/2d-game.md +1530 -0
  171. package/prompts/dist/3d-app.md +1672 -0
  172. package/prompts/dist/ar-app.md +1548 -0
  173. package/prompts/dist/concept.md +49 -0
  174. package/prompts/dist/design.md +498 -0
  175. package/prompts/dist/ui-app.md +1134 -0
  176. package/prompts/index.md +37 -0
  177. package/prompts/intake-prompt.md +33 -0
  178. package/prompts/namer-prompt.md +12 -0
  179. package/prompts/router-prompt.md +50 -0
  180. package/prompts/select.ts +111 -0
  181. package/prompts/ui-design.md +203 -0
  182. package/prompts/ui.md +803 -0
  183. package/src/animate/animate.ts +238 -0
  184. package/src/animate/bezier.ts +138 -0
  185. package/src/animate/easings.ts +126 -0
  186. package/src/bridges.d.ts +1345 -0
  187. package/src/canvas/Canvas.ts +305 -0
  188. package/src/compile/__tests__/assetIconMacro.test.ts +219 -0
  189. package/src/compile/__tests__/assetMacro.test.ts +74 -0
  190. package/src/compile/__tests__/assetName.test.ts +55 -0
  191. package/src/compile/__tests__/compile.test.ts +287 -0
  192. package/src/compile/__tests__/detectEntry.test.ts +132 -0
  193. package/src/compile/__tests__/fontMacro.test.ts +199 -0
  194. package/src/compile/aspectMacro.ts +86 -0
  195. package/src/compile/assetIconMacro.ts +384 -0
  196. package/src/compile/assetMacro.ts +146 -0
  197. package/src/compile/assetName.ts +50 -0
  198. package/src/compile/bundler.ts +287 -0
  199. package/src/compile/compileProject.ts +127 -0
  200. package/src/compile/detectEntry.ts +128 -0
  201. package/src/compile/fontMacro.ts +459 -0
  202. package/src/compile/fontRegistry.ts +78 -0
  203. package/src/compile/header.ts +67 -0
  204. package/src/compile/index.ts +100 -0
  205. package/src/compile/libraryImports.ts +52 -0
  206. package/src/compile/liteMaterial.ts +247 -0
  207. package/src/compile/sceneEditor.ts +88 -0
  208. package/src/compile/serverSplit.ts +233 -0
  209. package/src/compile/serverTypes.ts +227 -0
  210. package/src/compile/sfnt.ts +98 -0
  211. package/src/compile/shaderSchema.ts +202 -0
  212. package/src/compile/shaderTargets.ts +77 -0
  213. package/src/compile/sourcemap.ts +25 -0
  214. package/src/core/Aspect.ts +568 -0
  215. package/src/core/InspectorUI.ts +212 -0
  216. package/src/core/StateMachine.ts +308 -0
  217. package/src/core/__tests__/stateMachine.test.ts +132 -0
  218. package/src/core/color.ts +66 -0
  219. package/src/core/compWrite.ts +42 -0
  220. package/src/core/events.ts +38 -0
  221. package/src/core/fields.ts +120 -0
  222. package/src/core/registry.ts +23 -0
  223. package/src/core/signals.ts +277 -0
  224. package/src/core/time.ts +84 -0
  225. package/src/g2/Camera2D.ts +40 -0
  226. package/src/g2/CharacterController2D.ts +276 -0
  227. package/src/g2/Node2D.ts +267 -0
  228. package/src/g2/OneWay2D.ts +66 -0
  229. package/src/g2/Physics2D.ts +346 -0
  230. package/src/g2/Scene2D.ts +209 -0
  231. package/src/g2/Shape2D.ts +259 -0
  232. package/src/g2/Sprite.ts +89 -0
  233. package/src/g2/SpriteAnimation.ts +171 -0
  234. package/src/g2/SpriteSheet.ts +166 -0
  235. package/src/g2/Texture2D.ts +47 -0
  236. package/src/g2/Tilemap.ts +41 -0
  237. package/src/g2/Tileset.ts +71 -0
  238. package/src/g2/Trigger2D.ts +77 -0
  239. package/src/g2/autotile.ts +433 -0
  240. package/src/g2/cells.ts +91 -0
  241. package/src/g2/defineScene2d.ts +381 -0
  242. package/src/g2/groups2d.ts +106 -0
  243. package/src/g2/loop.ts +50 -0
  244. package/src/g2/scenarios2d.ts +69 -0
  245. package/src/g2/touch.ts +83 -0
  246. package/src/gl/Camera.ts +160 -0
  247. package/src/gl/CameraPlace.ts +52 -0
  248. package/src/gl/CharacterController.ts +238 -0
  249. package/src/gl/Gearbox.ts +212 -0
  250. package/src/gl/Geometry.ts +279 -0
  251. package/src/gl/IK.ts +193 -0
  252. package/src/gl/InstancedMesh.ts +132 -0
  253. package/src/gl/Light.ts +135 -0
  254. package/src/gl/Lightmap.ts +402 -0
  255. package/src/gl/Locomotion.ts +498 -0
  256. package/src/gl/Material.ts +279 -0
  257. package/src/gl/Mesh.ts +83 -0
  258. package/src/gl/Model.ts +124 -0
  259. package/src/gl/NavAgent.ts +337 -0
  260. package/src/gl/NavMesh.ts +397 -0
  261. package/src/gl/Node.ts +350 -0
  262. package/src/gl/Noise.ts +30 -0
  263. package/src/gl/Particles.ts +676 -0
  264. package/src/gl/Physics.ts +222 -0
  265. package/src/gl/Plane.ts +53 -0
  266. package/src/gl/Ragdoll.ts +270 -0
  267. package/src/gl/Ray.ts +16 -0
  268. package/src/gl/Scene.ts +505 -0
  269. package/src/gl/Shape.ts +409 -0
  270. package/src/gl/Terrain.ts +1092 -0
  271. package/src/gl/Texture.ts +63 -0
  272. package/src/gl/Trigger.ts +45 -0
  273. package/src/gl/Vehicle.ts +473 -0
  274. package/src/gl/Wheel.ts +240 -0
  275. package/src/gl/animation/AnimationClip.ts +204 -0
  276. package/src/gl/animation/Animator.ts +329 -0
  277. package/src/gl/animation/Layer.ts +39 -0
  278. package/src/gl/animation/Loop.ts +31 -0
  279. package/src/gl/animation/Playback.ts +52 -0
  280. package/src/gl/animation/core.ts +670 -0
  281. package/src/gl/controls.ts +95 -0
  282. package/src/gl/physicsEvents.ts +20 -0
  283. package/src/gl/scenarios.ts +291 -0
  284. package/src/gl/state.ts +6 -0
  285. package/src/gl/terrainMesh.ts +219 -0
  286. package/src/gl/touch.ts +68 -0
  287. package/src/host.d.ts +61 -0
  288. package/src/inject.ts +203 -0
  289. package/src/math/Mathf.ts +118 -0
  290. package/src/math/mat4.ts +278 -0
  291. package/src/math/quat.ts +232 -0
  292. package/src/math/vec.ts +255 -0
  293. package/src/net/codec.ts +119 -0
  294. package/src/net/core.ts +384 -0
  295. package/src/net/index.ts +181 -0
  296. package/src/net/replication.ts +622 -0
  297. package/src/plugins/camera.ts +81 -0
  298. package/src/plugins/geolocation.ts +123 -0
  299. package/src/plugins/oauth.ts +61 -0
  300. package/src/plugins/permission.ts +7 -0
  301. package/src/plugins/push.ts +132 -0
  302. package/src/plugins/qr.ts +73 -0
  303. package/src/plugins/service.ts +47 -0
  304. package/src/runtime/app.ts +101 -0
  305. package/src/runtime/appEvents.ts +54 -0
  306. package/src/runtime/channel.ts +61 -0
  307. package/src/runtime/clipboard.ts +20 -0
  308. package/src/runtime/datetime.ts +329 -0
  309. package/src/runtime/device.ts +293 -0
  310. package/src/runtime/fetch.ts +77 -0
  311. package/src/runtime/files.ts +108 -0
  312. package/src/runtime/input.ts +175 -0
  313. package/src/runtime/media.ts +111 -0
  314. package/src/runtime/misc.ts +16 -0
  315. package/src/runtime/net.ts +36 -0
  316. package/src/runtime/rpc.ts +218 -0
  317. package/src/runtime/service.ts +83 -0
  318. package/src/runtime/share.ts +9 -0
  319. package/src/runtime/storage.ts +13 -0
  320. package/src/runtime/touch.ts +76 -0
  321. package/src/scene/defineScene.ts +1396 -0
  322. package/src/scene/editorPlugins.ts +110 -0
  323. package/src/scene/gizmos.ts +148 -0
  324. package/src/scene/grammar.ts +120 -0
  325. package/src/scene/level.ts +296 -0
  326. package/src/scene/material.ts +188 -0
  327. package/src/server/auth/appConfig.ts +12 -0
  328. package/src/server/auth/global.ts +80 -0
  329. package/src/server/auth/host.ts +318 -0
  330. package/src/server/auth/models.ts +83 -0
  331. package/src/server/auth/types.ts +50 -0
  332. package/src/server/channel.ts +56 -0
  333. package/src/server/context.ts +36 -0
  334. package/src/server/db/defineDb.ts +237 -0
  335. package/src/server/db/fields.ts +132 -0
  336. package/src/server/db/httpTransport.ts +93 -0
  337. package/src/server/db/index.ts +7 -0
  338. package/src/server/db/marci/query.ts +412 -0
  339. package/src/server/db/types.ts +202 -0
  340. package/src/server/errors.ts +12 -0
  341. package/src/server/host.ts +74 -0
  342. package/src/server/inject.ts +13 -0
  343. package/src/server/runtime.ts +133 -0
  344. package/src/server/validate.ts +87 -0
  345. package/src/ui/NativeView.ts +142 -0
  346. package/src/ui/UI.ts +39 -0
  347. package/src/ui/UIBottomSheet.ts +139 -0
  348. package/src/ui/UIButton.ts +101 -0
  349. package/src/ui/UIContainer.ts +60 -0
  350. package/src/ui/UIImage.ts +83 -0
  351. package/src/ui/UIInput.ts +185 -0
  352. package/src/ui/UIModal.ts +139 -0
  353. package/src/ui/UINode.ts +830 -0
  354. package/src/ui/UIPager.ts +362 -0
  355. package/src/ui/UIPopover.ts +100 -0
  356. package/src/ui/UIScreen.ts +123 -0
  357. package/src/ui/UIScrollable.ts +87 -0
  358. package/src/ui/UISpacer.ts +14 -0
  359. package/src/ui/UITabs.ts +236 -0
  360. package/src/ui/UIText.ts +51 -0
  361. package/src/ui/UIVideo.ts +88 -0
  362. package/src/ui/UIVirtualizedList.ts +241 -0
  363. package/src/ui/UIWidget.ts +127 -0
  364. package/src/ui/fonts.ts +13 -0
  365. package/src/ui/presentable.ts +117 -0
  366. package/src/ui/router.ts +132 -0
  367. package/src/ui/theme.ts +84 -0
@@ -0,0 +1,95 @@
1
+ // AR object-placement controls — the classic "drop a model in the room and manipulate it" gesture
2
+ // recipe: ONE finger drags the target across the ground plane (camera-relative), TWO fingers pinch to
3
+ // scale and twist to rotate (yaw). Lives on ARScene (see Scene.ts) because it orchestrates the scene's
4
+ // camera + touch input against a target node; it's built entirely on the public API (no native calls
5
+ // the user couldn't make), so it's a convenience, not a primitive.
6
+
7
+ import { Vec2, Vec3 } from "../math/vec"
8
+ import { Quat } from "../math/quat"
9
+ import type { Node } from "./Node"
10
+ import type { Scene } from "./Scene"
11
+ import type { TouchStartEvent } from "../runtime/touch"
12
+
13
+ export type ControlsOptions = {
14
+ /** Smallest uniform scale the pinch gesture allows (default 0.08). */
15
+ minScale?: number
16
+ /** Largest uniform scale the pinch gesture allows (default 2). */
17
+ maxScale?: number
18
+ /** One-finger drag moves the target across the ground plane (default true). */
19
+ pan?: boolean
20
+ /** Two-finger pinch scales the target (default true). */
21
+ pinch?: boolean
22
+ /** Two-finger twist rotates the target around its Y axis (default true). */
23
+ twist?: boolean
24
+ }
25
+
26
+ /** Returned by `addControls` — call `remove()` to detach the gesture listeners. */
27
+ export type ControlsHandle = { remove(): void }
28
+
29
+ type ActiveTouch = { pointerId: number, x: number, y: number }
30
+
31
+ const clamp = (v: number, lo: number, hi: number): number => (v < lo ? lo : v > hi ? hi : v)
32
+
33
+ export const attachControls = (scene: Scene, target: Node, options: ControlsOptions = {}): ControlsHandle => {
34
+ const minScale = options.minScale ?? 0.08
35
+ const maxScale = options.maxScale ?? 2
36
+ const pan = options.pan ?? true
37
+ const pinch = options.pinch ?? true
38
+ const twist = options.twist ?? true
39
+
40
+ const active: ActiveTouch[] = []
41
+
42
+ const onTouchStart = (e: TouchStartEvent<Node | null>): void => {
43
+ const touch: ActiveTouch = { pointerId: e.pointerId, x: e.clientX, y: e.clientY }
44
+ active.push(touch)
45
+ e.track({
46
+ onMove(m) {
47
+ if (active.length === 2) {
48
+ // --- two-finger: pinch-scale + twist-rotate about the gesture axis ---
49
+ const other = active.find((t) => t !== touch)
50
+ if (other) {
51
+ const axis = new Vec2(other.x - touch.x, other.y - touch.y)
52
+ const span = axis.length()
53
+ if (span > 1e-3) {
54
+ const dir = axis.scale(1 / span)
55
+ const delta = new Vec2(m.deltaX, m.deltaY)
56
+ if (pinch) {
57
+ const along = -dir.dot(delta) // movement along the pinch axis
58
+ target.scale = clamp(target.scale.x * (span + along) / span, minScale * 0.4, maxScale * 1.5)
59
+ }
60
+ if (twist) {
61
+ const tangent = dir.perp().dot(delta) // movement across the axis
62
+ target.quaternion = target.quaternion.mul(Quat.fromAxisAngle(Vec3.up, Math.atan2(tangent, span)))
63
+ }
64
+ }
65
+ }
66
+ } else if (active.length === 1 && pan) {
67
+ // --- one-finger: drag across the ground plane, relative to where the camera looks ---
68
+ let dir = scene.camera.forward
69
+ const parent = target.parent
70
+ if (parent) dir = dir.rotate(parent.quaternion.invert()) // into the parent's local frame
71
+ dir = dir.withY(0)
72
+ const k = Math.hypot(dir.x, dir.z)
73
+ const screen = Math.max(...scene.camera.displaySize)
74
+ if (k > 1e-4 && screen > 0) {
75
+ const dist = target.worldPosition.distanceTo(scene.camera.worldPosition)
76
+ const right = new Vec3(-dir.z, 0, dir.x).scale(m.deltaX / k / screen * dist)
77
+ const fwd = dir.scale(-m.deltaY / k / screen * dist)
78
+ const move = right.add(fwd)
79
+ const p = target.position
80
+ target.position = new Vec3(p.x + move.x, p.y, p.z + move.z) // keep height fixed
81
+ }
82
+ }
83
+ touch.x = m.clientX
84
+ touch.y = m.clientY
85
+ },
86
+ onEnd() {
87
+ const i = active.indexOf(touch)
88
+ if (i >= 0) active.splice(i, 1)
89
+ },
90
+ })
91
+ }
92
+
93
+ scene.addEventListener("touchstart", onTouchStart)
94
+ return { remove() { scene.removeEventListener("touchstart", onTouchStart); active.length = 0 } }
95
+ }
@@ -0,0 +1,20 @@
1
+ // Routes native physics contact/sensor events to the owning nodes' 'enter'/'exit' events. Each event
2
+ // is delivered to BOTH nodes with the other as the argument. type: 0 = enter (contact/overlap began),
3
+ // 1 = exit (ended). Registered lazily on the first trigger/physics body or 'enter'/'exit' listener.
4
+
5
+ import { nodeRegistry } from "./Node"
6
+
7
+ let registered = false
8
+ export const ensurePhysicsEvents = (): void => {
9
+ if (registered) return
10
+ if (!_creator.setOnPhysicsEvent) return // physics-less build / bridge without the hook
11
+ registered = true
12
+ _creator.setOnPhysicsEvent((a, b, type) => {
13
+ const na = nodeRegistry.get(a)
14
+ const nb = nodeRegistry.get(b)
15
+ if (!na || !nb) return
16
+ const channel = type === 1 ? "exit" : "enter"
17
+ na._emitCollision(channel, nb)
18
+ nb._emitCollision(channel, na)
19
+ })
20
+ }
@@ -0,0 +1,291 @@
1
+ // No-code scenario aspects: ready-made behaviors a scene builder attaches in the editor without
2
+ // writing any code — move to a target, travel a waypoint path, spin, face something, start a GLB
3
+ // animation. They are ordinary aspects (work from hand-written code too); in the scene editor the
4
+ // movement ones double as `static editor` generators that draw their paths as editor gizmos
5
+ // (`Gizmos.*` in rebuild() — overlay lines, never scene content; play mode never calls rebuild(),
6
+ // so they cost nothing at runtime).
7
+ //
8
+ // Conventions shared by the family:
9
+ // • targets/waypoints are scene nodes — place an Empty, then pick it in the inspector (`ref()`);
10
+ // • times are seconds, angles degrees; all motion runs in `update(dt)` (play mode only);
11
+ // • orientation is quaternion-composed — never euler-incremented (see math/quat.ts).
12
+
13
+ import { Aspect } from "../core/Aspect"
14
+ import type { FieldMeta } from "../core/fields"
15
+ import type { InspectorUI } from "../core/InspectorUI"
16
+ import { Vec3 } from "../math/vec"
17
+ import { Quat } from "../math/quat"
18
+ import { Node } from "./Node"
19
+ import { Model } from "./Model"
20
+ import type { Playback } from "./animation/Playback"
21
+ import { Gizmos } from "../scene/gizmos"
22
+
23
+ const MOVE_LINE = "#5b8ef0"
24
+ const LOOK_LINE = "#d9a13f"
25
+ const DEG2RAD = Math.PI / 180
26
+
27
+ type LoopMode = "once" | "loop" | "pingpong"
28
+
29
+ /** Normalized phase for elapsed time `t` over one trip of `duration`: once clamps at 1, loop
30
+ * wraps, pingpong triangles 0→1→0. */
31
+ const phase = (t: number, duration: number, mode: LoopMode): number => {
32
+ if (duration <= 0) return 1
33
+ const u = t / duration
34
+ if (mode === "once") return Math.min(u, 1)
35
+ if (mode === "loop") return u - Math.floor(u)
36
+ const v = u % 2
37
+ return v <= 1 ? v : 2 - v
38
+ }
39
+
40
+ const ease = (k: number, easing: "smooth" | "linear"): number =>
41
+ easing === "smooth" ? k * k * (3 - 2 * k) : k
42
+
43
+ /** A world point in `node`'s PARENT space — what `node.position` is expressed in. */
44
+ const toParentLocal = (node: Node, world: Vec3): Vec3 => {
45
+ const parent = node.parent
46
+ return parent ? parent.worldMatrix.invert().transformPoint(world) : world
47
+ }
48
+
49
+ /** A path node's waypoints: its children in scene-FILE order (`_sceneChildOrder`, stamped by
50
+ * defineScene — the engine's live child order is insertion-based and may differ), editor helper
51
+ * nodes (`__`-prefixed) excluded. Runtime-added children append in engine order. */
52
+ const pathWaypoints = (path: Node): Node[] => {
53
+ const children = path.children.filter((c) => !(c.name ?? "").startsWith("__"))
54
+ const order = (path as { _sceneChildOrder?: string[] })._sceneChildOrder
55
+ if (!order) return children
56
+ const byName = new Map(children.map((c) => [ c.name, c ]))
57
+ const sorted: Node[] = []
58
+ for (const n of order) { const c = byName.get(n); if (c) sorted.push(c) }
59
+ const seen = new Set(sorted)
60
+ for (const c of children) if (!seen.has(c)) sorted.push(c)
61
+ return sorted
62
+ }
63
+
64
+ // ---- the aspects ---------------------------------------------------------------------------------
65
+
66
+ /** Travel from the node's starting position to a target node in `duration` seconds. */
67
+ export class MoveTo extends Aspect<"moveTo"> {
68
+ /** Where to travel — place an Empty node and pick it. */
69
+ target: Node | null = null
70
+
71
+ /** Seconds for the full trip. */
72
+ duration = 2
73
+
74
+ /** Seconds to wait before starting. */
75
+ delay = 0
76
+
77
+ /** once = stop at the target · loop = restart from the start point · pingpong = back and forth. */
78
+ mode: LoopMode = "once"
79
+
80
+ /** Motion curve. */
81
+ easing: "smooth" | "linear" = "smooth"
82
+
83
+ static readonly aspect = "moveTo"
84
+ static editor = { rebuild: true }
85
+ static fields: FieldMeta<MoveTo> = {
86
+ target: { editor: "node" },
87
+ duration: { min: 0.05, max: 120, step: 0.05 },
88
+ delay: { min: 0, max: 120, step: 0.05 },
89
+ mode: { options: [ "once", "loop", "pingpong" ] },
90
+ easing: { options: [ "smooth", "linear" ] },
91
+ }
92
+
93
+ private _t = 0
94
+ private _start: Vec3 | null = null
95
+
96
+ update(dt: number): void {
97
+ if (!this.target) return
98
+ this._start ??= this.node.position
99
+ this._t += dt
100
+ const k = ease(phase(Math.max(0, this._t - this.delay), this.duration, this.mode), this.easing)
101
+ const end = toParentLocal(this.node, this.target.worldPosition)
102
+ this.node.position = new Vec3(this._start).add(end.sub(this._start).scale(k))
103
+ }
104
+
105
+ /** Editor-only path line (edit mode; play mode never calls this). */
106
+ rebuild(): void {
107
+ if (!this.target) return
108
+ // the target is an empty — its own anchored marker shows the endpoint
109
+ Gizmos.line(this.node.worldPosition, this.target.worldPosition, { color: MOVE_LINE })
110
+ }
111
+ }
112
+
113
+ /** Travel through the children of a path node (add an Empty per waypoint) in `duration` seconds.
114
+ * `loop` runs the circuit closed (last → first); `pingpong` goes back and forth along the open
115
+ * path; `once` stops at the last waypoint. */
116
+ export class FollowPath extends Aspect<"followPath"> {
117
+ /** A node whose CHILDREN are the waypoints, in file order. */
118
+ path: Node | null = null
119
+
120
+ /** Seconds for one full pass of the path. */
121
+ duration = 6
122
+
123
+ /** Seconds to wait before starting. */
124
+ delay = 0
125
+
126
+ mode: LoopMode = "loop"
127
+
128
+ /** Turn to face the direction of travel. */
129
+ orient = true
130
+
131
+ /** Which local axis leads when orienting. */
132
+ forward: "-z" | "z" | "x" | "-x" = "-z"
133
+
134
+ static readonly aspect = "followPath"
135
+ static editor = { rebuild: true }
136
+ static fields: FieldMeta<FollowPath> = {
137
+ path: { editor: "node" },
138
+ duration: { min: 0.05, max: 300, step: 0.05 },
139
+ delay: { min: 0, max: 120, step: 0.05 },
140
+ mode: { options: [ "loop", "once", "pingpong" ] },
141
+ forward: { options: [ "-z", "z", "x", "-x" ] },
142
+ }
143
+
144
+ private _t = 0
145
+
146
+ private _points(): Vec3[] {
147
+ if (!this.path) return []
148
+ const points = pathWaypoints(this.path).map((c) => c.worldPosition)
149
+ if (this.mode === "loop" && points.length > 1) points.push(points[0]!.clone())
150
+ return points
151
+ }
152
+
153
+ update(dt: number): void {
154
+ const points = this._points()
155
+ if (points.length < 2) return
156
+ this._t += dt
157
+ const k = phase(Math.max(0, this._t - this.delay), this.duration, this.mode === "loop" ? "loop" : this.mode)
158
+
159
+ const lengths: number[] = []
160
+ let total = 0
161
+ for (let i = 0; i < points.length - 1; i++) {
162
+ const l = points[i + 1]!.sub(points[i]!).length()
163
+ lengths.push(l)
164
+ total += l
165
+ }
166
+ if (total <= 0) return
167
+
168
+ let dist = k * total
169
+ let i = 0
170
+ while (i < lengths.length - 1 && dist > lengths[i]!) { dist -= lengths[i]!; i++ }
171
+ const a = points[i]!
172
+ const b = points[i + 1]!
173
+ const seg = b.sub(a)
174
+ const u = lengths[i]! > 0 ? Math.min(dist / lengths[i]!, 1) : 0
175
+ const world = a.add(seg.scale(u))
176
+ this.node.position = toParentLocal(this.node, world)
177
+ if (this.orient && seg.lengthSq() > 1e-12) this.node.lookAt(world.add(seg), this.forward)
178
+ }
179
+
180
+ /** Editor-only waypoint polyline (edit mode; play mode never calls this). */
181
+ rebuild(): void {
182
+ const points = this._points() // loop mode already appends the first point again
183
+ Gizmos.polyline(points, { color: MOVE_LINE }) // waypoints are empties: they carry their own markers
184
+ }
185
+ }
186
+
187
+ /** Continuous rotation about one of the node's local axes, degrees per second. */
188
+ export class Spin extends Aspect<"spin"> {
189
+ /** Degrees per second. */
190
+ speed = 45
191
+
192
+ /** Local axis to spin around. */
193
+ axis: "x" | "y" | "z" = "y"
194
+
195
+ static readonly aspect = "spin"
196
+ static fields: FieldMeta<Spin> = {
197
+ speed: { min: -720, max: 720, step: 5 },
198
+ axis: { options: [ "x", "y", "z" ] },
199
+ }
200
+
201
+ update(dt: number): void {
202
+ const axis: [number, number, number] = this.axis === "x" ? [ 1, 0, 0 ] : this.axis === "y" ? [ 0, 1, 0 ] : [ 0, 0, 1 ]
203
+ // compose in quaternion space about the LOCAL axis (q ⊗ dq) — never euler-increment
204
+ this.node.quaternion = this.node.quaternion.mul(Quat.fromAxisAngle(axis, this.speed * dt * DEG2RAD))
205
+ }
206
+ }
207
+
208
+ /** Keep the node facing a target node (a turret tracking, a signpost, a camera aimed at a hero).
209
+ * Accessor `node.facing` — NOT `lookAt`, which would shadow the `Node.lookAt()` method. */
210
+ export class LookAt extends Aspect<"facing"> {
211
+ /** The node to face. */
212
+ target: Node | null = null
213
+
214
+ /** Which local axis points at the target. */
215
+ forward: "-z" | "z" | "x" | "-x" = "-z"
216
+
217
+ /** 0 = snap instantly; otherwise seconds of turn lag (bigger = slower, smoother). */
218
+ smoothing = 0
219
+
220
+ static readonly aspect = "facing"
221
+ static editor = { rebuild: true }
222
+ static fields: FieldMeta<LookAt> = {
223
+ target: { editor: "node" },
224
+ forward: { options: [ "-z", "z", "x", "-x" ] },
225
+ smoothing: { min: 0, max: 5, step: 0.05 },
226
+ }
227
+
228
+ update(dt: number): void {
229
+ if (!this.target) return
230
+ const point = this.target.worldPosition
231
+ if (this.smoothing <= 0) { this.node.lookAt(point, this.forward); return }
232
+ const q0 = this.node.quaternion
233
+ this.node.lookAt(point, this.forward)
234
+ const q1 = this.node.quaternion
235
+ this.node.quaternion = q0.slerp(q1, 1 - Math.exp(-dt / this.smoothing))
236
+ }
237
+
238
+ /** Editor-only sight line (edit mode; play mode never calls this). */
239
+ rebuild(): void {
240
+ if (!this.target) return
241
+ Gizmos.line(this.node.worldPosition, this.target.worldPosition, { color: LOOK_LINE })
242
+ }
243
+ }
244
+
245
+ /** Start a GLB animation clip when the scene runs. Attach to a `model:` node; the inspector card
246
+ * lists the model's clips live and can preview them while editing. */
247
+ export class PlayAnimation extends Aspect<"playAnimation", Model> {
248
+ /** Clip name; empty = the model's first clip. */
249
+ clip = ""
250
+
251
+ loop = true
252
+
253
+ /** Playback rate (1 = authored speed). */
254
+ speed = 1
255
+
256
+ static readonly aspect = "playAnimation"
257
+ static fields: FieldMeta<PlayAnimation> = {
258
+ speed: { min: 0.05, max: 5, step: 0.05 },
259
+ }
260
+
261
+ private _preview?: Playback
262
+
263
+ onAttach(): void {
264
+ const anim = (this.node as Model | undefined)?.anim
265
+ if (!anim) return
266
+ if (this.loop) anim.playLoop(this.clip === "" ? 0 : this.clip, { speed: this.speed })
267
+ else anim.play(this.clip === "" ? 0 : this.clip, { speed: this.speed })
268
+ }
269
+
270
+ static inspector(ui: InspectorUI, a: PlayAnimation): void {
271
+ const anim = (a.node as Model | undefined)?.anim
272
+ const clips = anim ? anim.clips : []
273
+ if (clips.length === 0) {
274
+ ui.warn("No animation clips — attach this to a GLB model node")
275
+ return
276
+ }
277
+ ui.select("clip", clips.map((c) => c.name))
278
+ ui.auto("loop", "speed")
279
+ if (anim) {
280
+ const clip = a.clip === "" ? clips[0]?.name : a.clip
281
+ const playing = a._preview?.playing === true
282
+ if (ui.button(playing ? "Restart preview" : "Preview")) {
283
+ if (a.loop) { anim.playLoop(clip, { speed: a.speed }); a._preview = undefined }
284
+ else a._preview = anim.play(clip, { speed: a.speed, restart: true })
285
+ }
286
+ if (playing && ui.button("Stop")) { anim.stop(); a._preview = undefined }
287
+ const duration = clips.find((c) => c.name === clip)?.duration
288
+ ui.info(playing ? "previewing…" : duration !== undefined ? `${duration.toFixed(2)}s` : "")
289
+ }
290
+ }
291
+ }
@@ -0,0 +1,6 @@
1
+ // Tiny shared module to break the Node ↔ Scene ↔ touch dependency cycle. The active 3D scene is set
2
+ // here by Scene.open()/close() and read by the touch dispatch system.
3
+
4
+ import type { Scene } from "./Scene"
5
+
6
+ export const glState: { activeScene: Scene | null, frame: number } = { activeScene: null, frame: 0 }
@@ -0,0 +1,219 @@
1
+ // The terrain grid as geometry, in TypeScript — used two ways (docs/terrain-plan.md §1.3, §3.5):
2
+ // • `heightAt` / `normalAt` — the SDK's own queries, identical on every host (bilinear on the SAME
3
+ // triangle split the engines draw and collide);
4
+ // • `buildTerrainChunk` — chunk meshes for hosts WITHOUT `_creator.terrainCreate` (web-lite, headless,
5
+ // a wasm/AAR/Apple build predating the feature): the same grid → chunk → triangles rule
6
+ // creator-gl/src/terrain.cpp applies (the i00–i11 diagonal, CCW seen from +Y, a triangle only when
7
+ // its three samples are valid), as plain Geometry for setMesh. Normals use central differences over
8
+ // the FULL grid (seamless chunks); the tangent frame is then the host's arbitrary one, so normal maps
9
+ // read wrong on those hosts — the accepted fallback cost.
10
+ // Keep the hole / split rule in step with terrain.cpp buildChunk and Jolt's HeightFieldShape.
11
+
12
+ import { Geometry } from "./Geometry"
13
+
14
+ export type TerrainGrid = {
15
+ sizeX: number
16
+ sizeZ: number
17
+ cellSize: number
18
+ /** sizeX × sizeZ heights, row-major (z rows). */
19
+ heights: Float32Array
20
+ /** One byte per sample, 1 = hole; null = no holes. */
21
+ holes: Uint8Array | null
22
+ }
23
+
24
+ const clampI = (v: number, hi: number): number => (v < 0 ? 0 : v > hi ? hi : v)
25
+
26
+ /** The sample at integer (x, z), clamped to the grid. */
27
+ export const sampleAt = (g: TerrainGrid, x: number, z: number): number =>
28
+ g.heights[clampI(z, g.sizeZ - 1) * g.sizeX + clampI(x, g.sizeX - 1)]
29
+
30
+ export const isHole = (g: TerrainGrid, x: number, z: number): boolean =>
31
+ g.holes !== null && g.holes[z * g.sizeX + x] !== 0
32
+
33
+ /** Surface normal at a sample from central differences (one-sided at the borders). Writes `out` (xyz). */
34
+ export const normalAt = (g: TerrainGrid, x: number, z: number, out: Float32Array | number[] = [ 0, 0, 0 ]): typeof out => {
35
+ const x0 = x > 0 ? x - 1 : x, x1 = x + 1 < g.sizeX ? x + 1 : x
36
+ const z0 = z > 0 ? z - 1 : z, z1 = z + 1 < g.sizeZ ? z + 1 : z
37
+ const dx = (sampleAt(g, x1, z) - sampleAt(g, x0, z)) / ((x1 - x0) * g.cellSize)
38
+ const dz = (sampleAt(g, x, z1) - sampleAt(g, x, z0)) / ((z1 - z0) * g.cellSize)
39
+ const len = Math.hypot(dx, 1, dz)
40
+ out[0] = -dx / len; out[1] = 1 / len; out[2] = -dz / len
41
+ return out
42
+ }
43
+
44
+ /**
45
+ * Height at a LOCAL (x, z) in metres, interpolated on the drawn triangles (the i00–i11 diagonal), so a
46
+ * character's feet and a raycast agree with what is rendered to the millimetre. NaN inside a hole
47
+ * triangle or outside the grid.
48
+ */
49
+ export const heightAt = (g: TerrainGrid, x: number, z: number): number => {
50
+ const fx = x / g.cellSize, fz = z / g.cellSize
51
+ if (!(fx >= 0 && fz >= 0 && fx <= g.sizeX - 1 && fz <= g.sizeZ - 1)) return NaN
52
+ let ix = Math.floor(fx), iz = Math.floor(fz)
53
+ if (ix >= g.sizeX - 1) ix = g.sizeX - 2
54
+ if (iz >= g.sizeZ - 1) iz = g.sizeZ - 2
55
+ const u = fx - ix, v = fz - iz
56
+ const i00 = iz * g.sizeX + ix
57
+ const h00 = g.heights[i00], h10 = g.heights[i00 + 1], h01 = g.heights[i00 + g.sizeX], h11 = g.heights[i00 + g.sizeX + 1]
58
+ if (g.holes !== null) {
59
+ const holes = g.holes
60
+ if (holes[i00] || holes[i00 + g.sizeX + 1]) return NaN
61
+ // lower-left triangle (i00, i01, i11) when v >= u, else upper-right (i00, i11, i10)
62
+ if (v >= u ? holes[i00 + g.sizeX] !== 0 : holes[i00 + 1] !== 0) return NaN
63
+ }
64
+ // plane of the triangle the point falls in
65
+ return v >= u
66
+ ? h00 + (h11 - h01) * u + (h01 - h00) * v
67
+ : h00 + (h10 - h00) * u + (h11 - h10) * v
68
+ }
69
+
70
+ /** The geometric normal at a LOCAL (x, z): the drawn triangle's normal (not the smoothed vertex one). */
71
+ export const faceNormalAt = (g: TerrainGrid, x: number, z: number, out: Float32Array | number[] = [ 0, 0, 0 ]): typeof out => {
72
+ const fx = x / g.cellSize, fz = z / g.cellSize
73
+ let ix = Math.floor(fx), iz = Math.floor(fz)
74
+ ix = clampI(ix, g.sizeX - 2); iz = clampI(iz, g.sizeZ - 2)
75
+ const u = fx - ix, v = fz - iz
76
+ const i00 = iz * g.sizeX + ix
77
+ const h00 = g.heights[i00], h10 = g.heights[i00 + 1], h01 = g.heights[i00 + g.sizeX], h11 = g.heights[i00 + g.sizeX + 1]
78
+ const c = g.cellSize
79
+ // gradients of the two planes (see heightAt)
80
+ const dx = (v >= u ? h11 - h01 : h10 - h00) / c
81
+ const dz = (v >= u ? h01 - h00 : h11 - h10) / c
82
+ const len = Math.hypot(dx, 1, dz)
83
+ out[0] = -dx / len; out[1] = 1 / len; out[2] = -dz / len
84
+ return out
85
+ }
86
+
87
+ /** Chunks along X and Z for a grid: a chunk covers `chunk` cells (chunk + 1 samples). */
88
+ export const chunkCounts = (g: TerrainGrid, chunk: number): [number, number] =>
89
+ [ Math.floor((g.sizeX - 2) / chunk) + 1, Math.floor((g.sizeZ - 2) / chunk) + 1 ]
90
+
91
+ /**
92
+ * One chunk as a Geometry (positions, smooth normals, UV0 = local XZ metres, UV1 = the terrain's unit
93
+ * square, u16 indices), or null when every cell of it is a hole.
94
+ */
95
+ export const buildTerrainChunk = (g: TerrainGrid, cx: number, cz: number, chunk: number): Geometry | null => {
96
+ const x0 = cx * chunk, z0 = cz * chunk
97
+ const x1 = Math.min(x0 + chunk, g.sizeX - 1), z1 = Math.min(z0 + chunk, g.sizeZ - 1)
98
+ const nx = x1 - x0 + 1, nz = z1 - z0 + 1
99
+ const idx: number[] = []
100
+ for (let z = z0; z < z1; z++) {
101
+ for (let x = x0; x < x1; x++) {
102
+ const i00 = (z - z0) * nx + (x - x0), i10 = i00 + 1, i01 = i00 + nx, i11 = i01 + 1
103
+ const v00 = !isHole(g, x, z), v10 = !isHole(g, x + 1, z), v01 = !isHole(g, x, z + 1), v11 = !isHole(g, x + 1, z + 1)
104
+ if (!(v00 && v11)) continue
105
+ if (v01) idx.push(i00, i01, i11)
106
+ if (v10) idx.push(i00, i11, i10)
107
+ }
108
+ }
109
+ if (idx.length === 0) return null
110
+ const count = nx * nz
111
+ const vertices = new Float32Array(count * 3)
112
+ const normals = new Float32Array(count * 3)
113
+ const uv = new Float32Array(count * 2)
114
+ const uv1 = new Float32Array(count * 2)
115
+ const n = [ 0, 0, 0 ]
116
+ const invX = 1 / (g.sizeX - 1), invZ = 1 / (g.sizeZ - 1)
117
+ for (let z = z0; z <= z1; z++) {
118
+ for (let x = x0; x <= x1; x++) {
119
+ const i = (z - z0) * nx + (x - x0)
120
+ vertices[i * 3] = x * g.cellSize
121
+ vertices[i * 3 + 1] = g.heights[z * g.sizeX + x]
122
+ vertices[i * 3 + 2] = z * g.cellSize
123
+ normalAt(g, x, z, n)
124
+ normals[i * 3] = n[0]; normals[i * 3 + 1] = n[1]; normals[i * 3 + 2] = n[2]
125
+ uv[i * 2] = x * g.cellSize; uv[i * 2 + 1] = z * g.cellSize
126
+ uv1[i * 2] = x * invX; uv1[i * 2 + 1] = z * invZ
127
+ }
128
+ }
129
+ const geom = new Geometry(vertices, normals, Uint16Array.from(idx), uv)
130
+ geom.uv1 = uv1
131
+ return geom
132
+ }
133
+
134
+ // ---- ray queries ---------------------------------------------------------------------------------
135
+
136
+ export type GridHit = { x: number, y: number, z: number, distance: number, nx: number, ny: number, nz: number }
137
+
138
+ /** Möller–Trumbore against one triangle; the distance along the ray, or -1. */
139
+ const rayTriangle = (
140
+ ox: number, oy: number, oz: number, dx: number, dy: number, dz: number,
141
+ ax: number, ay: number, az: number, bx: number, by: number, bz: number, qx: number, qy: number, qz: number,
142
+ ): number => {
143
+ const e1x = bx - ax, e1y = by - ay, e1z = bz - az
144
+ const e2x = qx - ax, e2y = qy - ay, e2z = qz - az
145
+ const px = dy * e2z - dz * e2y, py = dz * e2x - dx * e2z, pz = dx * e2y - dy * e2x
146
+ const det = e1x * px + e1y * py + e1z * pz
147
+ if (Math.abs(det) < 1e-12) return -1
148
+ const inv = 1 / det
149
+ const tx = ox - ax, ty = oy - ay, tz = oz - az
150
+ const u = (tx * px + ty * py + tz * pz) * inv
151
+ if (u < -1e-6 || u > 1 + 1e-6) return -1
152
+ const qxx = ty * e1z - tz * e1y, qyy = tz * e1x - tx * e1z, qzz = tx * e1y - ty * e1x
153
+ const v = (dx * qxx + dy * qyy + dz * qzz) * inv
154
+ if (v < -1e-6 || u + v > 1 + 1e-6) return -1
155
+ const t = (e2x * qxx + e2y * qyy + e2z * qzz) * inv
156
+ return t >= 0 ? t : -1
157
+ }
158
+
159
+ /**
160
+ * Nearest hit of a LOCAL ray (origin + direction, metres) on the drawn triangles — the same
161
+ * i00–i11 split and hole rule as `heightAt`, so a pick lands on what is rendered. Walks the cells
162
+ * the ray crosses (2D DDA on XZ), testing each cell's two triangles; holes let the ray through.
163
+ * Null when it misses (or never enters the grid within `maxDistance`).
164
+ */
165
+ export const gridRaycast = (g: TerrainGrid, ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, maxDistance = 1e5): GridHit | null => {
166
+ const c = g.cellSize, W = (g.sizeX - 1) * c, D = (g.sizeZ - 1) * c
167
+ let t0 = 0, t1 = maxDistance
168
+ const clip = (o: number, d: number, hi: number): boolean => {
169
+ if (Math.abs(d) < 1e-12) return o >= 0 && o <= hi
170
+ let a = -o / d, b = (hi - o) / d
171
+ if (a > b) { const s = a; a = b; b = s }
172
+ if (a > t0) t0 = a
173
+ if (b < t1) t1 = b
174
+ return t0 <= t1
175
+ }
176
+ if (!clip(ox, dx, W) || !clip(oz, dz, D)) return null
177
+ const sx = ox + dx * t0, sz = oz + dz * t0
178
+ let ix = Math.min(g.sizeX - 2, Math.max(0, Math.floor(sx / c)))
179
+ let iz = Math.min(g.sizeZ - 2, Math.max(0, Math.floor(sz / c)))
180
+ const stepX = dx > 1e-12 ? 1 : dx < -1e-12 ? -1 : 0
181
+ const stepZ = dz > 1e-12 ? 1 : dz < -1e-12 ? -1 : 0
182
+ const tDeltaX = stepX ? Math.abs(c / dx) : Infinity, tDeltaZ = stepZ ? Math.abs(c / dz) : Infinity
183
+ let tMaxX = stepX > 0 ? t0 + ((ix + 1) * c - sx) / dx : stepX < 0 ? t0 + (ix * c - sx) / dx : Infinity
184
+ let tMaxZ = stepZ > 0 ? t0 + ((iz + 1) * c - sz) / dz : stepZ < 0 ? t0 + (iz * c - sz) / dz : Infinity
185
+ const holes = g.holes
186
+ let t = t0
187
+ for (let guard = 0; guard < g.sizeX + g.sizeZ + 2 && t <= t1 + 1e-9; guard++) {
188
+ const i00 = iz * g.sizeX + ix, i10 = i00 + 1, i01 = i00 + g.sizeX, i11 = i01 + 1
189
+ const x0 = ix * c, x1 = x0 + c, z0 = iz * c, z1 = z0 + c
190
+ const h00 = g.heights[i00], h10 = g.heights[i10], h01 = g.heights[i01], h11 = g.heights[i11]
191
+ const ok00 = !holes || holes[i00] === 0, ok11 = !holes || holes[i11] === 0
192
+ let best = -1, tri = 0
193
+ if (ok00 && ok11) {
194
+ // lower-left (i00, i01, i11) and upper-right (i00, i11, i10) — both CCW seen from +Y
195
+ if (!holes || holes[i01] === 0) {
196
+ const d = rayTriangle(ox, oy, oz, dx, dy, dz, x0, h00, z0, x0, h01, z1, x1, h11, z1)
197
+ if (d >= 0 && (best < 0 || d < best)) { best = d; tri = 0 }
198
+ }
199
+ if (!holes || holes[i10] === 0) {
200
+ const d = rayTriangle(ox, oy, oz, dx, dy, dz, x0, h00, z0, x1, h11, z1, x1, h10, z0)
201
+ if (d >= 0 && (best < 0 || d < best)) { best = d; tri = 1 }
202
+ }
203
+ }
204
+ if (best >= 0 && best <= t1 + 1e-6) {
205
+ // the hit triangle's normal
206
+ let ax: number, ay: number, az: number, bx: number, by: number, bz: number
207
+ if (tri === 0) { ax = 0; ay = h01 - h00; az = c; bx = c; by = h11 - h00; bz = c }
208
+ else { ax = c; ay = h11 - h00; az = c; bx = c; by = h10 - h00; bz = 0 }
209
+ let nx = ay * bz - az * by, ny = az * bx - ax * bz, nz = ax * by - ay * bx
210
+ const l = Math.hypot(nx, ny, nz) || 1
211
+ nx /= l; ny /= l; nz /= l
212
+ return { x: ox + dx * best, y: oy + dy * best, z: oz + dz * best, distance: best, nx, ny, nz }
213
+ }
214
+ if (tMaxX < tMaxZ) { t = tMaxX; ix += stepX; tMaxX += tDeltaX }
215
+ else { t = tMaxZ; iz += stepZ; tMaxZ += tDeltaZ }
216
+ if (ix < 0 || iz < 0 || ix > g.sizeX - 2 || iz > g.sizeZ - 2) break
217
+ }
218
+ return null
219
+ }