lecodes-cli 0.18.2 → 0.19.2

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 (224) hide show
  1. package/dist/index.js +1866 -301
  2. package/package.json +14 -12
  3. package/runtime/scene-harness.json +1 -1
  4. package/runtime/sdk-types.json +1 -1
  5. package/src/api.ts +302 -0
  6. package/src/browserAuth.ts +87 -0
  7. package/src/cmgenTool.ts +104 -0
  8. package/src/commands/app.ts +891 -0
  9. package/src/commands/appAndroid.ts +603 -0
  10. package/src/commands/appDesktop.ts +326 -0
  11. package/src/commands/appDesktopMac.ts +470 -0
  12. package/src/commands/appIcon.ts +187 -0
  13. package/src/commands/appShared.ts +448 -0
  14. package/src/commands/appTemplates.ts +883 -0
  15. package/src/commands/appTemplatesAndroid.ts +599 -0
  16. package/src/commands/appTemplatesGradlew.ts +9 -0
  17. package/src/commands/assets.ts +28 -0
  18. package/src/commands/clone.ts +60 -0
  19. package/src/commands/compile.ts +253 -0
  20. package/src/commands/create.ts +59 -0
  21. package/src/commands/design.ts +863 -0
  22. package/src/commands/designTemplates.ts +11 -0
  23. package/src/commands/desktop.ts +122 -0
  24. package/src/commands/dev.ts +214 -0
  25. package/src/commands/diff.ts +59 -0
  26. package/src/commands/init.ts +191 -0
  27. package/src/commands/install.ts +198 -0
  28. package/src/commands/lightmap.ts +290 -0
  29. package/src/commands/link.ts +147 -0
  30. package/src/commands/login.ts +24 -0
  31. package/src/commands/navmesh.ts +225 -0
  32. package/src/commands/pn.ts +305 -0
  33. package/src/commands/projectTemplates.ts +129 -0
  34. package/src/commands/pull.ts +109 -0
  35. package/src/commands/push.ts +173 -0
  36. package/src/commands/render.ts +414 -0
  37. package/src/commands/scene.ts +148 -0
  38. package/src/commands/shaders.ts +157 -0
  39. package/src/commands/status.ts +29 -0
  40. package/src/commands/test.ts +370 -0
  41. package/src/commands/thumbs.ts +178 -0
  42. package/src/commands/types.ts +87 -0
  43. package/src/commands/update.ts +190 -0
  44. package/src/compile/assetIcons.ts +214 -0
  45. package/src/compile/collect.ts +46 -0
  46. package/src/compile/collectLocal.ts +37 -0
  47. package/src/compile/designCompile.ts +104 -0
  48. package/src/compile/fonts.ts +156 -0
  49. package/src/compile/headlessBundle.ts +129 -0
  50. package/src/compile/nativeStack.ts +21 -0
  51. package/src/compile/projectCompile.ts +109 -0
  52. package/src/compile/sceneCompile.ts +113 -0
  53. package/src/compile/screenEntry.ts +127 -0
  54. package/src/compile/shaders.ts +245 -0
  55. package/src/config.ts +42 -0
  56. package/src/designMeta.ts +35 -0
  57. package/src/desktopRenderer.ts +532 -0
  58. package/src/desktopScript.ts +276 -0
  59. package/src/dev/clientTemplates.ts +160 -0
  60. package/src/dev/devServer.ts +292 -0
  61. package/src/dev/wsServer.ts +144 -0
  62. package/src/distRoot.ts +20 -0
  63. package/src/ignore.ts +163 -0
  64. package/src/index.ts +491 -0
  65. package/src/lecodes-3d-editor.d.ts +41 -0
  66. package/src/lecodes-assets.d.ts +7 -0
  67. package/src/lecodes-design.d.ts +191 -0
  68. package/src/lecodes-renderer.d.ts +131 -0
  69. package/src/localFiles.ts +144 -0
  70. package/src/manifest.ts +46 -0
  71. package/src/matcTool.ts +137 -0
  72. package/src/peers.ts +40 -0
  73. package/src/project.ts +61 -0
  74. package/src/projectEnv.ts +94 -0
  75. package/src/qrcode-terminal.d.ts +9 -0
  76. package/src/releases.ts +125 -0
  77. package/src/serverDiff.ts +78 -0
  78. package/src/textDiff.ts +103 -0
  79. package/src/types.ts +0 -0
  80. package/src/util.ts +146 -0
  81. package/runtime/sdk/animate/animate.ts +0 -238
  82. package/runtime/sdk/animate/bezier.ts +0 -138
  83. package/runtime/sdk/animate/easings.ts +0 -126
  84. package/runtime/sdk/canvas/Canvas.ts +0 -305
  85. package/runtime/sdk/core/Aspect.ts +0 -512
  86. package/runtime/sdk/core/InspectorUI.ts +0 -212
  87. package/runtime/sdk/core/color.ts +0 -66
  88. package/runtime/sdk/core/compWrite.ts +0 -42
  89. package/runtime/sdk/core/events.ts +0 -38
  90. package/runtime/sdk/core/fields.ts +0 -120
  91. package/runtime/sdk/core/registry.ts +0 -23
  92. package/runtime/sdk/core/signals.ts +0 -277
  93. package/runtime/sdk/core/time.ts +0 -81
  94. package/runtime/sdk/g2/Camera2D.ts +0 -40
  95. package/runtime/sdk/g2/CharacterController2D.ts +0 -276
  96. package/runtime/sdk/g2/Node2D.ts +0 -267
  97. package/runtime/sdk/g2/OneWay2D.ts +0 -66
  98. package/runtime/sdk/g2/Physics2D.ts +0 -346
  99. package/runtime/sdk/g2/Scene2D.ts +0 -209
  100. package/runtime/sdk/g2/Shape2D.ts +0 -259
  101. package/runtime/sdk/g2/Sprite.ts +0 -89
  102. package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
  103. package/runtime/sdk/g2/SpriteSheet.ts +0 -166
  104. package/runtime/sdk/g2/Texture2D.ts +0 -47
  105. package/runtime/sdk/g2/Tilemap.ts +0 -41
  106. package/runtime/sdk/g2/Tileset.ts +0 -71
  107. package/runtime/sdk/g2/Trigger2D.ts +0 -77
  108. package/runtime/sdk/g2/autotile.ts +0 -433
  109. package/runtime/sdk/g2/cells.ts +0 -91
  110. package/runtime/sdk/g2/defineScene2d.ts +0 -381
  111. package/runtime/sdk/g2/groups2d.ts +0 -106
  112. package/runtime/sdk/g2/loop.ts +0 -50
  113. package/runtime/sdk/g2/scenarios2d.ts +0 -69
  114. package/runtime/sdk/g2/touch.ts +0 -83
  115. package/runtime/sdk/gl/Camera.ts +0 -160
  116. package/runtime/sdk/gl/CameraPlace.ts +0 -52
  117. package/runtime/sdk/gl/CharacterController.ts +0 -238
  118. package/runtime/sdk/gl/Gearbox.ts +0 -212
  119. package/runtime/sdk/gl/Geometry.ts +0 -279
  120. package/runtime/sdk/gl/IK.ts +0 -193
  121. package/runtime/sdk/gl/InstancedMesh.ts +0 -132
  122. package/runtime/sdk/gl/Light.ts +0 -99
  123. package/runtime/sdk/gl/Lightmap.ts +0 -179
  124. package/runtime/sdk/gl/Material.ts +0 -245
  125. package/runtime/sdk/gl/Mesh.ts +0 -83
  126. package/runtime/sdk/gl/Model.ts +0 -64
  127. package/runtime/sdk/gl/Node.ts +0 -350
  128. package/runtime/sdk/gl/Noise.ts +0 -30
  129. package/runtime/sdk/gl/Particles.ts +0 -676
  130. package/runtime/sdk/gl/Physics.ts +0 -222
  131. package/runtime/sdk/gl/Plane.ts +0 -53
  132. package/runtime/sdk/gl/Ray.ts +0 -16
  133. package/runtime/sdk/gl/Scene.ts +0 -479
  134. package/runtime/sdk/gl/Shape.ts +0 -377
  135. package/runtime/sdk/gl/Texture.ts +0 -46
  136. package/runtime/sdk/gl/Trigger.ts +0 -45
  137. package/runtime/sdk/gl/Vehicle.ts +0 -473
  138. package/runtime/sdk/gl/Wheel.ts +0 -240
  139. package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
  140. package/runtime/sdk/gl/animation/Animator.ts +0 -87
  141. package/runtime/sdk/gl/animation/Layer.ts +0 -29
  142. package/runtime/sdk/gl/animation/Loop.ts +0 -25
  143. package/runtime/sdk/gl/animation/Playback.ts +0 -43
  144. package/runtime/sdk/gl/animation/core.ts +0 -294
  145. package/runtime/sdk/gl/controls.ts +0 -95
  146. package/runtime/sdk/gl/physicsEvents.ts +0 -20
  147. package/runtime/sdk/gl/scenarios.ts +0 -291
  148. package/runtime/sdk/gl/state.ts +0 -6
  149. package/runtime/sdk/gl/touch.ts +0 -68
  150. package/runtime/sdk/inject.ts +0 -186
  151. package/runtime/sdk/math/Mathf.ts +0 -118
  152. package/runtime/sdk/math/mat4.ts +0 -278
  153. package/runtime/sdk/math/quat.ts +0 -232
  154. package/runtime/sdk/math/vec.ts +0 -255
  155. package/runtime/sdk/plugins/camera.ts +0 -81
  156. package/runtime/sdk/plugins/geolocation.ts +0 -123
  157. package/runtime/sdk/plugins/oauth.ts +0 -61
  158. package/runtime/sdk/plugins/permission.ts +0 -7
  159. package/runtime/sdk/plugins/push.ts +0 -132
  160. package/runtime/sdk/plugins/qr.ts +0 -73
  161. package/runtime/sdk/plugins/service.ts +0 -47
  162. package/runtime/sdk/runtime/app.ts +0 -101
  163. package/runtime/sdk/runtime/appEvents.ts +0 -54
  164. package/runtime/sdk/runtime/channel.ts +0 -61
  165. package/runtime/sdk/runtime/clipboard.ts +0 -20
  166. package/runtime/sdk/runtime/datetime.ts +0 -329
  167. package/runtime/sdk/runtime/device.ts +0 -293
  168. package/runtime/sdk/runtime/fetch.ts +0 -77
  169. package/runtime/sdk/runtime/files.ts +0 -23
  170. package/runtime/sdk/runtime/input.ts +0 -175
  171. package/runtime/sdk/runtime/media.ts +0 -111
  172. package/runtime/sdk/runtime/misc.ts +0 -16
  173. package/runtime/sdk/runtime/net.ts +0 -36
  174. package/runtime/sdk/runtime/rpc.ts +0 -218
  175. package/runtime/sdk/runtime/service.ts +0 -83
  176. package/runtime/sdk/runtime/share.ts +0 -9
  177. package/runtime/sdk/runtime/storage.ts +0 -13
  178. package/runtime/sdk/runtime/touch.ts +0 -76
  179. package/runtime/sdk/scene/defineScene.ts +0 -1227
  180. package/runtime/sdk/scene/editorPlugins.ts +0 -92
  181. package/runtime/sdk/scene/gizmos.ts +0 -148
  182. package/runtime/sdk/scene/grammar.ts +0 -120
  183. package/runtime/sdk/scene/material.ts +0 -188
  184. package/runtime/sdk/server/auth/appConfig.ts +0 -12
  185. package/runtime/sdk/server/auth/global.ts +0 -80
  186. package/runtime/sdk/server/auth/host.ts +0 -318
  187. package/runtime/sdk/server/auth/models.ts +0 -83
  188. package/runtime/sdk/server/auth/types.ts +0 -50
  189. package/runtime/sdk/server/channel.ts +0 -56
  190. package/runtime/sdk/server/context.ts +0 -36
  191. package/runtime/sdk/server/db/defineDb.ts +0 -237
  192. package/runtime/sdk/server/db/fields.ts +0 -132
  193. package/runtime/sdk/server/db/httpTransport.ts +0 -93
  194. package/runtime/sdk/server/db/index.ts +0 -7
  195. package/runtime/sdk/server/db/marci/query.ts +0 -412
  196. package/runtime/sdk/server/db/types.ts +0 -202
  197. package/runtime/sdk/server/errors.ts +0 -12
  198. package/runtime/sdk/server/host.ts +0 -74
  199. package/runtime/sdk/server/inject.ts +0 -13
  200. package/runtime/sdk/server/runtime.ts +0 -133
  201. package/runtime/sdk/server/validate.ts +0 -87
  202. package/runtime/sdk/ui/NativeView.ts +0 -142
  203. package/runtime/sdk/ui/UI.ts +0 -39
  204. package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
  205. package/runtime/sdk/ui/UIButton.ts +0 -101
  206. package/runtime/sdk/ui/UIContainer.ts +0 -60
  207. package/runtime/sdk/ui/UIImage.ts +0 -83
  208. package/runtime/sdk/ui/UIInput.ts +0 -185
  209. package/runtime/sdk/ui/UIModal.ts +0 -139
  210. package/runtime/sdk/ui/UINode.ts +0 -826
  211. package/runtime/sdk/ui/UIPager.ts +0 -362
  212. package/runtime/sdk/ui/UIPopover.ts +0 -100
  213. package/runtime/sdk/ui/UIScreen.ts +0 -123
  214. package/runtime/sdk/ui/UIScrollable.ts +0 -87
  215. package/runtime/sdk/ui/UISpacer.ts +0 -14
  216. package/runtime/sdk/ui/UITabs.ts +0 -236
  217. package/runtime/sdk/ui/UIText.ts +0 -51
  218. package/runtime/sdk/ui/UIVideo.ts +0 -88
  219. package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
  220. package/runtime/sdk/ui/UIWidget.ts +0 -127
  221. package/runtime/sdk/ui/fonts.ts +0 -13
  222. package/runtime/sdk/ui/presentable.ts +0 -117
  223. package/runtime/sdk/ui/router.ts +0 -132
  224. package/runtime/sdk/ui/theme.ts +0 -84
@@ -1,267 +0,0 @@
1
- // Base 2D scene node — an empty transform. Sprite and Tilemap extend it, so every node operation
2
- // (position, layer, visibility, events) works on them. Native (creator-2d) owns the authoritative
3
- // transform. JS caches it for free reads (JS is normally the sole writer); the exception is a node
4
- // driven by a dynamic/kinematic Physics2D body — there the native physics step also writes the
5
- // transform, so such nodes are flagged `_physicsDriven` and their transform getters sync the cache
6
- // from native first (lazy: only on read, only for moving bodies). So node.position stays correct
7
- // under physics without any per-frame bookkeeping.
8
- //
9
- // Empty Node2Ds are useful as: grouping parents, world-space markers/spawn points, and physics /
10
- // trigger objects — attach a Shape2D (geometry) plus a Physics2D (rigid body) or Trigger2D (sensor).
11
-
12
- import { AspectHost } from "../core/Aspect"
13
- import type { CompAxis, CompWriter } from "../core/compWrite"
14
- import { Registry } from "../core/registry"
15
- import { Vec2, cx, cy, type Vec2Like } from "../math/vec"
16
- import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
17
- import { ensurePointerEvents } from "./touch"
18
- import { ensurePhysicsEvents } from "./loop"
19
-
20
- // re-exported so existing `import { Vec2 } from "./Node2D"` sites keep working; the canonical home is math/.
21
- export { Vec2, type Vec2Like }
22
-
23
- export type Node2DEvents = {
24
- /** A looping animation clip wrapped around (fires each loop). */
25
- loopReached: (clip: string) => void
26
- /** A non-looping animation clip finished. */
27
- completed: (clip: string) => void
28
- /** A physics contact / sensor overlap began (the other node, plus where and how hard). The
29
- * contact's `normal` points AWAY from the other node, so `normal.y > 0.7` reads as "I landed on
30
- * top of it". Sensor overlaps and `exit` carry zeros. */
31
- enter: (other: Node2D, contact: Contact2D) => void
32
- exit: (other: Node2D, contact: Contact2D) => void
33
- /** Pointer up over this node's Physics2D shape (a tap/click). */
34
- click: (ev: ClickEvent<Node2D | null>) => void
35
- /** Pointer down on this node's Physics2D shape. Call ev.track(...) to capture the drag. */
36
- touchstart: (ev: TouchStartEvent<Node2D | null>) => void
37
- }
38
-
39
- /** Where a contact happened, delivered as the second argument of 'enter'. `speed` is the approach
40
- * speed at impact (world units/s) — scale an impact sound with it. Zeros on sensor and 'exit'
41
- * events, which have no manifold. */
42
- export type Contact2D = { point: Vec2, normal: Vec2, speed: number }
43
-
44
- /** id → Node2D, so host callbacks (animation events) route back to the owning JS object. */
45
- export const node2dRegistry = new Registry<Node2D>()
46
-
47
- export class Node2D extends AspectHost<Node2DEvents> implements CompWriter {
48
- /** Native entity handle. */
49
- readonly id: number
50
-
51
- /** Optional debug/editor name (plain JS data — scene2d files stamp their node names here). */
52
- name?: string
53
-
54
- protected _x = 0
55
- protected _y = 0
56
- protected _rotation = 0
57
- protected _sx = 1
58
- protected _sy = 1
59
- protected _layer = 0
60
- protected _z = 0
61
- protected _visible = true
62
-
63
- /** @internal — set by Physics2D for a dynamic/kinematic body: the native step owns this node's
64
- * transform, so the transform getters re-sync the cache from native before returning. */
65
- _physicsDriven = false
66
-
67
- /** @internal The engine object that mirrors this node's transform, if any — a physics body or a
68
- * character (0 = none). WRITES to position / x / y / rotation are routed to it as well, because
69
- * the engine is the authority: on a moving body a plain setPosition is overwritten by the next
70
- * sync, and on a static / sensor body it would leave the COLLIDER behind while the node moved.
71
- * Set by Physics2D, Trigger2D and CharacterController2D. This is what replaced `physics.moveTo`
72
- * and `trigger.moveTo`. */
73
- _xf = 0
74
- /** @internal What `_xf` is: 1 = character, 2 = physics body. */
75
- _xfKind = 0
76
-
77
- // Push this node's (already written) transform into the engine object that owns it. The world math
78
- // lives NATIVE — physicsSetFromEntity reads the entity's world matrix — so a parented body converts
79
- // through its parent chain without this file re-deriving it.
80
- //
81
- // NOTE: this fires on a DIRECT write only. A body parented to a node that moves does not follow;
82
- // keep bodies and sensors at the root and write their position (see docs/2d-physics-plan.md §6).
83
- private _pushXf(): void {
84
- if (this._xfKind === 2) _creator2d.physicsSetFromEntity(this._xf)
85
- else if (this._xfKind === 1) _creator2d.characterSetFromEntity(this._xf)
86
- }
87
-
88
- private _parent: Node2D | null = null
89
- private _children: Node2D[] = []
90
-
91
- constructor(internalId?: number) {
92
- super()
93
- this.id = internalId ?? _creator2d.createEntity()
94
- node2dRegistry.set(this.id, this)
95
- }
96
-
97
- // --- transform (Y-up, LOCAL to the parent; at zoom 1, units == logical pixels) ---
98
- // Pull the native transform into the cache iff physics owns it. No-op for the common case (plain
99
- // nodes keep free reads); one native read for a moving body, and only when actually read.
100
- private _pull(): void { if (this._physicsDriven) this._syncLocalFromNative() }
101
-
102
- get x(): number { this._pull(); return this._x }
103
- set x(v: number) { this._pull(); this._x = v; _creator2d.setPosition(this.id, v, this._y); if (this._xf) this._pushXf() }
104
- get y(): number { this._pull(); return this._y }
105
- set y(v: number) { this._pull(); this._y = v; _creator2d.setPosition(this.id, this._x, v); if (this._xf) this._pushXf() }
106
-
107
- /** @internal chisel `comp_write` (see core/compWrite.ts): `node.position.<axis> = v` compiles to a call
108
- * here; the scalar setters carry the physics routing. The list is compile-time data. */
109
- static _comps = [ "position" ]
110
- _writeComp(_prop: string, axis: CompAxis, v: number): void {
111
- if (axis === "x") this.x = v
112
- else if (axis === "y") this.y = v
113
- }
114
-
115
- get position(): Vec2 { this._pull(); return new Vec2(this._x, this._y) }
116
- set position(v: Vec2Like) {
117
- this._x = cx(v); this._y = cy(v)
118
- _creator2d.setPosition(this.id, this._x, this._y)
119
- if (this._xf) this._pushXf()
120
- }
121
-
122
- /** Rotation in degrees (CCW). */
123
- get rotation(): number { this._pull(); return this._rotation }
124
- set rotation(deg: number) {
125
- this._rotation = deg
126
- _creator2d.setRotation(this.id, deg)
127
- if (this._xf) this._pushXf()
128
- }
129
-
130
- get scale(): Vec2 { return new Vec2(this._sx, this._sy) }
131
- set scale(v: Vec2Like | number) {
132
- const sx = typeof v === "number" ? v : cx(v)
133
- const sy = typeof v === "number" ? v : cy(v)
134
- this._sx = sx; this._sy = sy
135
- _creator2d.setScale(this.id, sx, sy)
136
- }
137
-
138
- /** Draw layer (int). Higher layers render on top. */
139
- get layer(): number { return this._layer }
140
- set layer(v: number) { this._layer = v; _creator2d.setLayer(this.id, v) }
141
-
142
- /** Intra-layer depth for non-Y-sorted layers (higher z = on top). */
143
- get z(): number { return this._z }
144
- set z(v: number) { this._z = v; _creator2d.setZ(this.id, v) }
145
-
146
- get visible(): boolean { return this._visible }
147
- set visible(v: boolean) { this._visible = v; _creator2d.setVisible(this.id, v) }
148
-
149
- // --- hierarchy (parent/child; transforms above are LOCAL to the parent) ---
150
- get parent(): Node2D | null { return this._parent }
151
- set parent(p: Node2D | null) { this.setParent(p) }
152
-
153
- get children(): readonly Node2D[] { return this._children }
154
-
155
- /** Attach children to this node (each keeps its current world transform). Returns this. */
156
- add(...children: Node2D[]): this {
157
- for (const c of children) c.setParent(this)
158
- return this
159
- }
160
-
161
- /** Detach a direct child (reparents it to root, keeping its world transform). Returns this. */
162
- remove(child: Node2D): this {
163
- if (child._parent === this) child.setParent(null)
164
- return this
165
- }
166
-
167
- /**
168
- * Reparent this node. `keepWorld` (default true) recomputes the local transform so the on-screen
169
- * position/rotation/scale is unchanged; pass false to keep the local transform and move with the
170
- * new parent. Self/descendant parents are rejected natively (no cycles).
171
- */
172
- setParent(parent: Node2D | null, keepWorld = true): this {
173
- if (parent === this) return this
174
- if (this._parent) {
175
- const arr = this._parent._children
176
- const i = arr.indexOf(this)
177
- if (i >= 0) arr.splice(i, 1)
178
- }
179
- this._parent = parent
180
- if (parent) parent._children.push(this)
181
- _creator2d.setParent(this.id, parent?.id ?? 0, keepWorld)
182
- if (keepWorld) this._syncLocalFromNative() // native recomputed our local transform
183
- return this
184
- }
185
-
186
- /** World-space position (composes all ancestors). */
187
- get worldPosition(): Vec2 {
188
- const o = _creator2d.getWorldPosition(this.id)
189
- return new Vec2(o[0], o[1])
190
- }
191
-
192
- /** Convert a world-space point into this node's local space (and back). */
193
- worldToLocal(p: Vec2Like): Vec2 {
194
- const o = _creator2d.worldToLocal(this.id, cx(p), cy(p))
195
- return new Vec2(o[0], o[1])
196
- }
197
- localToWorld(p: Vec2Like): Vec2 {
198
- const o = _creator2d.localToWorld(this.id, cx(p), cy(p))
199
- return new Vec2(o[0], o[1])
200
- }
201
-
202
- private _syncLocalFromNative(): void {
203
- const t = _creator2d.getLocalTransform(this.id) // [x, y, rotDeg, sx, sy]
204
- this._x = t[0]; this._y = t[1]; this._rotation = t[2]; this._sx = t[3]; this._sy = t[4]
205
- }
206
-
207
- /** @internal — host animation events route here by id; the SpriteAnimation aspect installs an
208
- * instance override that translates clip ids to names and emits 'loopReached' / 'completed'. */
209
- _dispatchAnimEvent(_clipId: number, _type: number): void {}
210
-
211
- /** @internal — emit a translated animation event (the SpriteAnimation aspect can't reach the
212
- * protected dispatch directly). */
213
- _emitAnim(channel: "loopReached" | "completed", clip: string): void { this.dispatch(channel, clip) }
214
-
215
- /** @internal — physics contact/sensor events route here (loop.ts) → the node's 'enter'/'exit'. */
216
- _emitCollision(channel: "enter" | "exit", other: Node2D, contact: Contact2D): void {
217
- this.dispatch(channel, other, contact)
218
- }
219
-
220
- /** @internal — this node wants contact events. Set by the first 'enter'/'exit' listener; read by
221
- * Physics2D.onAttach for the case where the listener was added BEFORE the body existed. */
222
- _wantsContacts = false
223
-
224
- // Lazily wire the host callbacks the listener needs. Contact/sensor events used to be registered
225
- // only by Trigger2D.onAttach, so solid-vs-solid 'enter' never fired in a project with no triggers —
226
- // and, separately, every solid shape had contact events forced on. Both are fixed here: the channel
227
- // is registered by the listener, and the body opts in.
228
- override addEventListener<K extends keyof Node2DEvents>(channel: K, callback: Node2DEvents[K]): void {
229
- if (channel === "click" || channel === "touchstart") ensurePointerEvents()
230
- else if (channel === "enter" || channel === "exit") {
231
- ensurePhysicsEvents()
232
- this._wantsContacts = true
233
- if (this._xfKind === 2) _creator2d.physicsSetContactEvents(this._xf, true)
234
- }
235
- super.addEventListener(channel, callback)
236
- }
237
-
238
- /** @internal — the pointer system (g2/touch.ts) fires click/touchstart here. */
239
- _dispatchEvent<K extends keyof Node2DEvents>(channel: K, ...args: Parameters<Node2DEvents[K]>): void {
240
- this.dispatch(channel, ...args)
241
- }
242
-
243
- destroy(): void {
244
- // Aspects go first: onDetach runs (a body-owning aspect releases its handle), updaters are
245
- // unregistered so a destroyed node never ticks again, listeners are dropped. Children survive
246
- // (native reparents them to the root), so only this node's aspects detach.
247
- this._detachAll()
248
- // Native frees this entity's physics bodies inside destroyEntity (they used to leak — an invisible
249
- // collider stayed in the world, and recycled entity ids then delivered its contacts to whoever
250
- // inherited the id). Clear the routing so a stale aspect can't push a dead handle.
251
- this._xf = 0
252
- this._xfKind = 0
253
- this._physicsDriven = false
254
- // Detach from parent; native reparents our children to root (keepWorld), so resync their caches.
255
- if (this._parent) {
256
- const arr = this._parent._children
257
- const i = arr.indexOf(this)
258
- if (i >= 0) arr.splice(i, 1)
259
- this._parent = null
260
- }
261
- const orphans = this._children.slice()
262
- this._children.length = 0
263
- node2dRegistry.delete(this.id)
264
- _creator2d.destroyEntity(this.id)
265
- for (const c of orphans) { c._parent = null; c._syncLocalFromNative() }
266
- }
267
- }
@@ -1,66 +0,0 @@
1
- // A one-way surface, as an aspect on a Node2D that already carries a Shape2D and a Physics2D body:
2
- // solid from one side, passable from the other. The semisolid ledge of every platformer.
3
- //
4
- // const ledge = new Sprite({ texture: plank, size: [128, 8] })
5
- // .aspect(Shape2D, {})
6
- // .aspect(Physics2D, { motion: 'static' })
7
- // .aspect(OneWay2D) // jump up through it, stand on top
8
- //
9
- // It is an ASPECT and not a flag on Shape2D on purpose: Shape2D is geometry, and which side of a
10
- // surface is solid is behaviour. Keeping them apart is also what lets `normal` generalise the thing —
11
- // a one-way WALL you can only walk through from the left is the same aspect turned sideways.
12
- //
13
- // Honoured by the character's mover AND, through a native pre-solve callback, by ordinary rigid
14
- // bodies (a crate falls onto the ledge from above and passes up through it from below). Neither path
15
- // costs any JS per frame.
16
- //
17
- // Dropping DOWN through one is the character's business, not the surface's — see
18
- // `CharacterController2D.dropThrough`.
19
-
20
- import { Aspect } from "../core/Aspect"
21
- import { cx, cy, type Vec2Like } from "../math/vec"
22
- import { Physics2D } from "./Physics2D"
23
- import type { Node2D } from "./Node2D"
24
-
25
- export class OneWay2D extends Aspect<"oneWay", Node2D> {
26
- static readonly aspect = "oneWay"
27
-
28
- private _normal: Vec2Like = [ 0, 1 ]
29
- private _arc = 90
30
- private _enabled = true
31
-
32
- /** Which side is solid. Default `[0, 1]` — solid from above, i.e. a floor you pass through from
33
- * below. `[1, 0]` is a wall you can only enter from the left. */
34
- get normal(): Vec2Like { return this._normal }
35
- set normal(v: Vec2Like) { this._normal = v; this._push() }
36
-
37
- /** How far off `normal` an approach still counts as coming from the solid side, in degrees.
38
- * Default 90 — the whole half-plane, which is what a platform wants. */
39
- get arc(): number { return this._arc }
40
- set arc(v: number) { this._arc = v; this._push() }
41
-
42
- /** Turn the one-way behaviour off, making the surface solid from both sides again. */
43
- get enabled(): boolean { return this._enabled }
44
- set enabled(v: boolean) { this._enabled = v; this._push() }
45
-
46
- onAttach(): void {
47
- if (!this.node.has(Physics2D)) {
48
- throw new Error("OneWay2D needs a Physics2D body on the same node — add it first: node.aspect(Shape2D, {…}).aspect(Physics2D, { motion: 'static' }).aspect(OneWay2D)")
49
- }
50
- this._push()
51
- }
52
-
53
- onDetach(): void {
54
- const id = this.node.get(Physics2D)?.id
55
- if (id) _creator2d.physicsSetOneWay(id, 0, 1, 90, false)
56
- }
57
-
58
- onReconfigure(): void { this._push() }
59
-
60
- private _push(): void {
61
- const id = this.node.get(Physics2D)?.id
62
- // via the public accessors on purpose: an accessor pair that is only ever an object-literal key
63
- // gets tree-shaken away, and the option would silently do nothing (see Physics2D.onAttach).
64
- if (id) _creator2d.physicsSetOneWay(id, cx(this.normal), cy(this.normal), this.arc, this.enabled)
65
- }
66
- }
@@ -1,346 +0,0 @@
1
- // A rigid body, as an aspect on a Node2D, backed by Box2D v3 in creator-2d. Mirrors the 3D Physics
2
- // aspect. Requires a Shape2D aspect on the same node (its geometry becomes the body's solid fixture).
3
- //
4
- // ground.aspect(Shape2D, { box: [500, 8] }).aspect(Physics2D, { motion: 'static' })
5
- // crate.aspect(Shape2D, {}).aspect(Physics2D, { mass: 4, friction: 0.7, bounce: 0.1 })
6
- // crate.physics.applyImpulse([0, 400])
7
- // crate.addEventListener('enter', (other, c) => spark(c.point, c.normal))
8
- //
9
- // Physics2D.configure() is OPTIONAL — the world is created on the first body with sane defaults, and
10
- // calling configure later just changes gravity (it used to destroy and rebuild the world, orphaning
11
- // every handle that was already out).
12
- //
13
- // For a `dynamic` body, physics OWNS the transform — don't set node.position per frame; drive it via
14
- // velocity / applyImpulse. A `position` write is a TELEPORT that reaches the body (Node2D routes it),
15
- // which is also how a kinematic body is placed. Reading node.position always works.
16
- //
17
- // Keep bodies on ROOT nodes: a body parented to a moving node does not follow it (see
18
- // docs/2d-physics-plan.md §6).
19
-
20
- import { Aspect } from "../core/Aspect"
21
- import { Vec2, cx, cy, type Vec2Like } from "../math/vec"
22
- import { node2dRegistry, type Node2D } from "./Node2D"
23
- import { Shape2D } from "./Shape2D"
24
- import {
25
- PhysicsGroup2D, _addGroup, _watchGroupChanges, _unwatchGroupChanges,
26
- categoryOf, solidMask, defaultGroup,
27
- } from "./groups2d"
28
-
29
- export type MotionType = "static" | "kinematic" | "dynamic"
30
- const MOTION: Record<MotionType, number> = { static: 0, kinematic: 1, dynamic: 2 }
31
-
32
- /** A neutral solid surface. Deliberately the same default as 3D `Physics.friction`. */
33
- export const DEFAULT_FRICTION = 0.6
34
-
35
- export interface PhysicsConfig {
36
- /** Gravity in world units/s² (Y-up: down is negative). Default [0, -980]. */
37
- gravity?: Vec2Like
38
- /** Pixels per meter — tunes Box2D's internal tolerances; you still author in world units. Default 64.
39
- * Only applies before the world exists (it is a global solver tolerance). */
40
- pixelsPerMeter?: number
41
- /** Solver sub-steps per fixed step. Default 4. */
42
- subSteps?: number
43
- }
44
-
45
- /** Closest raycast hit. */
46
- export interface RayHit {
47
- node: Node2D | null
48
- point: Vec2
49
- normal: Vec2
50
- fraction: number
51
- }
52
-
53
- // Resolve a native entity id to its Node2D (0 = none).
54
- const node2dById = (id: number): Node2D | null => (id ? node2dRegistry.get(id) ?? null : null)
55
-
56
- export class Physics2D extends Aspect<"physics", Node2D> {
57
- static readonly aspect = "physics"
58
-
59
- private _motion: MotionType = "dynamic"
60
- /** Static (walls, floors), kinematic (script-driven platforms), or dynamic (fully simulated).
61
- * Default "dynamic", and LIVE: freeze a crate into `'static'`, hand a ragdoll to `'dynamic'`. */
62
- get motion(): MotionType { return this._motion }
63
- set motion(m: MotionType) {
64
- this._motion = m
65
- this.node._physicsDriven = m !== "static"
66
- if (this._h) _creator2d.physicsSetMotionType(this._h, MOTION[m])
67
- }
68
- /** Lock rotation (essential for platformer characters). */
69
- fixedRotation = false
70
- /** Continuous collision for fast-moving bodies (projectiles). */
71
- bullet = false
72
-
73
- private _mass = 0 // 0 = derive from the collider area
74
- private _friction = DEFAULT_FRICTION
75
- private _bounce = 0
76
- private _gravityScale = 1
77
- private _linearDamping = 0
78
- private _angularDamping = 0
79
- private _group: PhysicsGroup2D | undefined
80
- private _repush = (): void => {
81
- if (this._h) _creator2d.physicsSetFilter(this._h, categoryOf(this._group), solidMask(this._group))
82
- }
83
- private _h = 0 // native body id (0 = none / no physics support)
84
-
85
- /** Which collision group this body belongs to (see Physics2D.addGroup). Live. */
86
- get group(): PhysicsGroup2D | undefined { return this._group }
87
- set group(g: PhysicsGroup2D | undefined) {
88
- this._group = g
89
- this.node.get(Shape2D)?._setFilter(categoryOf(g), solidMask(g))
90
- this._repush()
91
- }
92
-
93
- /** Mass in world units. Omitted (or 0) = derived from the collider's AREA, so a big crate really is
94
- * heavier than a small one. Setting it scales the shape-derived mass data, keeping the rotational
95
- * inertia in the same ratio — a lopsided collider still tips the way its geometry says. */
96
- get mass(): number { return this._h ? _creator2d.physicsGetMass(this._h) : this._mass }
97
- set mass(v: number) { this._mass = v; if (this._h) _creator2d.physicsSetMass(this._h, v) }
98
-
99
- /** Surface friction. 0 is frictionless, ~1 grippy; two bodies COMBINE as sqrt(a×b), so the lower
100
- * value wins the argument. Default 0.6 — a neutral solid, matching 3D. */
101
- get friction(): number { return this._friction }
102
- set friction(v: number) { this._friction = v; this._pushMaterial() }
103
-
104
- /** Bounciness, 0..1 — the fraction of the approach speed returned on impact. Two bodies combine as
105
- * MAX (the bouncier one wins — the opposite of friction), and below ~64 units/s no bounce is
106
- * applied at all, which is what stops a ball jittering on the floor forever. */
107
- get bounce(): number { return this._bounce }
108
- set bounce(v: number) { this._bounce = v; this._pushMaterial() }
109
-
110
- // The live setters go to the BODY, but a later collider rebuild re-creates the fixture from the
111
- // Shape2D — so the shape keeps its own copy of the material or a crouch would silently reset it.
112
- private _pushMaterial(): void {
113
- if (!this._h) return
114
- _creator2d.physicsSetFriction(this._h, this._friction)
115
- _creator2d.physicsSetBounce(this._h, this._bounce)
116
- this.node.get(Shape2D)?._setMaterial(this._friction, this._bounce)
117
- }
118
-
119
- /** Native physics body id. 0 until attached, or if the build has no physics support. Mirrors 3D `Physics.id`. */
120
- get id(): number { return this._h }
121
-
122
- // NOTE — read every option through its PUBLIC accessor here, never the `_backing` field. chisel
123
- // tree-shakes class members and keeps `_*` unconditionally, but it DROPS an accessor pair whose
124
- // name no bundled code reads as a PROPERTY: an object-literal key (`aspect(Physics2D, { gravityScale: 0 })`)
125
- // does NOT count. With the pair gone, Object.assign writes a plain own property, the backing field
126
- // keeps its default, and the option silently does nothing — measured: a `gravityScale: 0` body fell.
127
- // One `this.gravityScale` read in bundled SDK code keeps the whole pair alive.
128
- onAttach(): void {
129
- if (!_creator2d.physicsHasSupport || !_creator2d.physicsHasSupport()) return
130
- const shape = this.node.get(Shape2D)
131
- if (!shape) {
132
- throw new Error("Physics2D requires a Shape2D aspect — add it first: node.aspect(Shape2D, {…}).aspect(Physics2D, {…})")
133
- }
134
- this._h = _creator2d.physicsCreateBody(this.node.id, MOTION[this.motion])
135
- if (!this._h) return
136
- shape._addTo(this._h, {
137
- sensor: false, friction: this.friction, bounce: this.bounce,
138
- category: categoryOf(this.group), mask: solidMask(this.group),
139
- staticBody: this.motion === "static",
140
- })
141
- // Only a body that named a group cares when the topology changes.
142
- if (this._group) _watchGroupChanges(this._repush)
143
- // dynamic/kinematic bodies are moved by the native step → the node must re-sync its transform
144
- // cache from native on read. Static bodies never move, so they keep the free cache.
145
- if (this.motion !== "static") this.node._physicsDriven = true
146
- // Route node.position / .rotation writes to the body (this is what replaced physics.moveTo).
147
- this.node._xf = this._h
148
- this.node._xfKind = 2
149
- if (this._mass > 0) _creator2d.physicsSetMass(this._h, this.mass)
150
- if (this.fixedRotation) _creator2d.physicsSetFixedRotation(this._h, true)
151
- if (this.gravityScale !== 1) _creator2d.physicsSetGravityScale(this._h, this.gravityScale)
152
- if (this.linearDamping !== 0) _creator2d.physicsSetLinearDamping(this._h, this.linearDamping)
153
- if (this.angularDamping !== 0) _creator2d.physicsSetAngularDamping(this._h, this.angularDamping)
154
- if (this.bullet) _creator2d.physicsSetBullet(this._h, true)
155
- // The listener may have been added before the body existed; contact events are per-body opt-in.
156
- if (this.node._wantsContacts) _creator2d.physicsSetContactEvents(this._h, true)
157
- }
158
-
159
- onDetach(): void {
160
- _unwatchGroupChanges(this._repush)
161
- if (this._h) { _creator2d.physicsRemoveBody(this._h); this._h = 0 }
162
- this.node._physicsDriven = false
163
- this.node._xf = 0
164
- this.node._xfKind = 0
165
- }
166
-
167
- /** Re-applying options (`node.aspect(Physics2D, {…})`) pushes the live ones through. */
168
- onReconfigure(): void {
169
- if (!this._h) return
170
- this._pushMaterial()
171
- _creator2d.physicsSetFixedRotation(this._h, this.fixedRotation)
172
- _creator2d.physicsSetBullet(this._h, this.bullet)
173
- _creator2d.physicsSetGravityScale(this._h, this.gravityScale)
174
- _creator2d.physicsSetLinearDamping(this._h, this.linearDamping)
175
- _creator2d.physicsSetAngularDamping(this._h, this.angularDamping)
176
- if (this._mass > 0) _creator2d.physicsSetMass(this._h, this.mass)
177
- }
178
-
179
- /** Linear velocity in world units/second (fresh Vec2 on read). */
180
- get velocity(): Vec2 {
181
- const o = this._h ? _creator2d.physicsGetLinearVelocity(this._h) : null
182
- return o ? new Vec2(o[0], o[1]) : new Vec2(0, 0)
183
- }
184
- set velocity(v: Vec2Like) { if (this._h) _creator2d.physicsSetLinearVelocity(this._h, cx(v), cy(v)) }
185
-
186
- /** Angular velocity in degrees/second. */
187
- get angularVelocity(): number { return this._h ? _creator2d.physicsGetAngularVelocity(this._h) : 0 }
188
- set angularVelocity(degPerSec: number) { if (this._h) _creator2d.physicsSetAngularVelocity(this._h, degPerSec) }
189
-
190
- /** Per-body gravity multiplier: 1 = full world gravity, 0 = floats, <0 = repelled. */
191
- get gravityScale(): number { return this._gravityScale }
192
- set gravityScale(scale: number) { this._gravityScale = scale; if (this._h) _creator2d.physicsSetGravityScale(this._h, scale) }
193
-
194
- get linearDamping(): number { return this._linearDamping }
195
- set linearDamping(d: number) { this._linearDamping = d; if (this._h) _creator2d.physicsSetLinearDamping(this._h, d) }
196
-
197
- get angularDamping(): number { return this._angularDamping }
198
- set angularDamping(d: number) { this._angularDamping = d; if (this._h) _creator2d.physicsSetAngularDamping(this._h, d) }
199
-
200
- /** Pause this body — it stops simulating and stops colliding until re-enabled. */
201
- set enabled(v: boolean) { if (this._h) _creator2d.physicsSetEnabled(this._h, v) }
202
-
203
- /** Box2D puts resting bodies to sleep; wake one to make it react to a change you made by hand. */
204
- set awake(v: boolean) { if (this._h) _creator2d.physicsSetAwake(this._h, v) }
205
-
206
- /** Apply an instantaneous impulse at the centre of mass (never spins the body) and wake it. */
207
- applyImpulse(v: Vec2Like): this { if (this._h) _creator2d.physicsApplyLinearImpulse(this._h, cx(v), cy(v)); return this }
208
- /** Apply an impulse at a WORLD point — the lever arm becomes spin. Use this when the push has a real
209
- * point of contact: a bullet's hit point, a blast pushing a prop at its base. */
210
- applyImpulseAt(v: Vec2Like, worldPoint: Vec2Like): this {
211
- if (this._h) _creator2d.physicsApplyImpulseAt(this._h, cx(v), cy(v), cx(worldPoint), cy(worldPoint))
212
- return this
213
- }
214
- /** Apply a continuous force (world units) and wake the body. */
215
- applyForce(v: Vec2Like): this { if (this._h) _creator2d.physicsApplyForce(this._h, cx(v), cy(v)); return this }
216
-
217
- // --- world (static) -------------------------------------------------------------------------
218
- /** Set the world's gravity / solver settings. OPTIONAL: the world is created on the first body with
219
- * gravity [0, -980], 64 px/m and 4 sub-steps. Calling this later changes gravity LIVE and leaves
220
- * bodies alone. */
221
- static configure(config: PhysicsConfig = {}): void {
222
- const g = config.gravity ?? [0, -980]
223
- _creator2d.physicsConfigure(cx(g), cy(g), config.pixelsPerMeter ?? 64, config.subSteps ?? 4)
224
- }
225
-
226
- /** World gravity, live. */
227
- static get gravity(): Vec2 { return new Vec2(Physics2D._gravity) }
228
- static set gravity(g: Vec2Like) {
229
- Physics2D._gravity = [ cx(g), cy(g) ]
230
- _creator2d.physicsConfigure(cx(g), cy(g), 0, 0)
231
- }
232
- private static _gravity: [number, number] = [ 0, -980 ]
233
-
234
- /** Whether this build has physics support (CREATOR_2D_PHYSICS). NOTE: this reports the BUILD, not
235
- * the world — there is nothing else to guard, since the world creates itself. */
236
- static get supported(): boolean { return !!_creator2d.physicsHasSupport && _creator2d.physicsHasSupport() }
237
-
238
- /** Render interpolation of body transforms between the fixed 60 Hz steps (global; default on). Turn
239
- * off to save per-frame transform writes when you have many moving bodies / don't need >60 Hz
240
- * smoothness — bodies then advance in discrete steps. */
241
- private static _interpolation = true
242
- static get interpolation(): boolean { return Physics2D._interpolation }
243
- static set interpolation(on: boolean) { Physics2D._interpolation = on; _creator2d.setInterpolation(on) }
244
-
245
- /**
246
- * Register a collision group. Groups are SUBTRACTIVE and SYMMETRIC (see groups2d.ts):
247
- *
248
- * const player = Physics2D.addGroup()
249
- * const bullets = Physics2D.addGroup().ignoreSelf().ignore(player)
250
- */
251
- static addGroup(label?: string): PhysicsGroup2D { return _addGroup(label) }
252
-
253
- /** The implicit group of every body that never named one - name it to exclude "everything else". */
254
- static get defaultGroup(): PhysicsGroup2D { return defaultGroup }
255
-
256
- // --- queries (static) -----------------------------------------------------------------------
257
- /** Closest body hit by the segment, or null. `node` resolves the hit entity. */
258
- static raycast(from: Vec2Like, to: Vec2Like, q?: Query2D): RayHit | null {
259
- const o = _creator2d.physicsRaycastClosest(cx(from), cy(from), cx(to), cy(to), maskOf(q), ignoreOf(q), ignoreCount(q))
260
- if (o[0] < 0.5) return null
261
- return { node: node2dById(o[1]), point: new Vec2(o[2], o[3]), normal: new Vec2(o[4], o[5]), fraction: o[6] }
262
- }
263
-
264
- /** Every body along the segment, NEAREST FIRST - a piercing bullet, a laser, "how many walls are
265
- * between us". Capped at 32 hits. */
266
- static raycastAll(from: Vec2Like, to: Vec2Like, q?: Query2D): RayHit[] {
267
- const o = _creator2d.physicsRaycastAll(cx(from), cy(from), cx(to), cy(to), maskOf(q), ignoreOf(q), ignoreCount(q), MAX_HITS)
268
- const hits: RayHit[] = []
269
- for (let i = 0; i + 5 < o.length; i += 6) {
270
- if (o[i] === 0) break
271
- hits.push({ node: node2dById(o[i]), point: new Vec2(o[i + 1], o[i + 2]), normal: new Vec2(o[i + 3], o[i + 4]), fraction: o[i + 5] })
272
- }
273
- return hits
274
- }
275
-
276
- /** Topmost body (solid or sensor) whose shape contains the world point, or null. "Topmost" means
277
- * by DRAW order - layer, then z - so picking agrees with what the player sees. */
278
- static overlapPoint(p: Vec2Like, q?: Query2D): Node2D | null {
279
- return node2dById(_creator2d.physicsOverlapPoint(cx(p), cy(p), maskOf(q), ignoreOf(q), ignoreCount(q)))
280
- }
281
-
282
- /** Everything within `radius` of a point - an explosion, an aura, an aggro range. */
283
- static overlapCircle(centre: Vec2Like, radius: number, q?: Query2D): Node2D[] {
284
- return overlapNodes([ cx(centre), cy(centre) ], radius, q)
285
- }
286
-
287
- /** Everything inside an axis-aligned box (centre + HALF-extents) - a room, a camera bound. */
288
- static overlapBox(centre: Vec2Like, half: Vec2Like, q?: Query2D): Node2D[] {
289
- const x = cx(centre), y = cy(centre), hw = cx(half), hh = cy(half)
290
- return overlapNodes([ x - hw, y - hh, x + hw, y - hh, x + hw, y + hh, x - hw, y + hh ], 0, q)
291
- }
292
-
293
- /**
294
- * Everything touching a capsule from-to of the given radius. A capsule is the shape a DIRECTED
295
- * question has: a beam with width, a sword arc, "does my radius fit through this gap" - none of
296
- * which an axis-aligned box can express.
297
- *
298
- * It is also the SWEPT test: a circle of radius r moving from A to B during a frame sweeps exactly
299
- * this capsule, so a fast projectile checked this way cannot tunnel through a thin wall the way two
300
- * separate position checks do.
301
- */
302
- static overlapCapsule(from: Vec2Like, to: Vec2Like, radius: number, q?: Query2D): Node2D[] {
303
- return overlapNodes([ cx(from), cy(from), cx(to), cy(to) ], radius, q)
304
- }
305
- }
306
-
307
- /** Which groups a query may hit, and what to skip. */
308
- export type Query2D = {
309
- /** Only hit bodies in these groups. Omitted = everything. */
310
- groups?: PhysicsGroup2D[]
311
- /** Skip these nodes - the "don't hit myself" case a ray fired from inside its own body needs. */
312
- ignore?: Node2D | Node2D[]
313
- }
314
-
315
- const MAX_HITS = 32
316
- const emptyIgnore = new Float32Array(0)
317
- let ignoreBuf = new Float32Array(8)
318
-
319
- const maskOf = (q?: Query2D): number => {
320
- if (!q?.groups || q.groups.length === 0) return 0 // 0 = everything, native-side
321
- let m = 0
322
- for (const g of q.groups) m |= g.category
323
- return m >>> 0
324
- }
325
- const ignoreCount = (q?: Query2D): number =>
326
- !q?.ignore ? 0 : Array.isArray(q.ignore) ? q.ignore.length : 1
327
- const ignoreOf = (q?: Query2D): Float32Array => {
328
- const n = ignoreCount(q)
329
- if (n === 0) return emptyIgnore
330
- if (ignoreBuf.length < n) ignoreBuf = new Float32Array(n)
331
- const ig = q!.ignore!
332
- if (Array.isArray(ig)) ig.forEach((node, i) => { ignoreBuf[i] = node.id })
333
- else ignoreBuf[0] = (ig as Node2D).id
334
- return ignoreBuf
335
- }
336
- const overlapNodes = (pts: number[], radius: number, q?: Query2D): Node2D[] => {
337
- const o = _creator2d.physicsOverlap(new Float32Array(pts), pts.length / 2, radius,
338
- maskOf(q), ignoreOf(q), ignoreCount(q), MAX_HITS)
339
- const out: Node2D[] = []
340
- for (let i = 0; i < o.length; i++) {
341
- if (o[i] === 0) break
342
- const n = node2dById(o[i])
343
- if (n) out.push(n)
344
- }
345
- return out
346
- }