lecodes-cli 0.3.0 → 0.5.1

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 (48) hide show
  1. package/README.md +3 -0
  2. package/dist/index.js +889 -507
  3. package/package.json +9 -4
  4. package/runtime/sdk/canvas/Canvas.ts +105 -0
  5. package/runtime/sdk/compile/aspectMacro.ts +42 -0
  6. package/runtime/sdk/compile/assetMacro.ts +44 -0
  7. package/runtime/sdk/compile/bundler.ts +173 -0
  8. package/runtime/sdk/compile/compileProject.ts +96 -0
  9. package/runtime/sdk/compile/header.ts +46 -0
  10. package/runtime/sdk/compile/index.ts +12 -0
  11. package/runtime/sdk/compile/sourcemap.ts +25 -0
  12. package/runtime/sdk/core/Aspect.ts +2 -2
  13. package/runtime/sdk/core/signals.ts +277 -0
  14. package/runtime/sdk/g2/CharacterController2D.ts +5 -4
  15. package/runtime/sdk/g2/Node2D.ts +1 -1
  16. package/runtime/sdk/g2/Physics2D.ts +22 -12
  17. package/runtime/sdk/g2/Scene2D.ts +12 -0
  18. package/runtime/sdk/g2/{Shape.ts → Shape2D.ts} +10 -10
  19. package/runtime/sdk/g2/Sprite.ts +7 -2
  20. package/runtime/sdk/g2/Texture2D.ts +4 -2
  21. package/runtime/sdk/g2/{Trigger.ts → Trigger2D.ts} +9 -9
  22. package/runtime/sdk/gl/CharacterController.ts +101 -0
  23. package/runtime/sdk/gl/Node.ts +11 -2
  24. package/runtime/sdk/gl/Physics.ts +65 -41
  25. package/runtime/sdk/gl/Scene.ts +18 -0
  26. package/runtime/sdk/gl/Shape.ts +98 -0
  27. package/runtime/sdk/gl/Texture.ts +2 -2
  28. package/runtime/sdk/gl/Trigger.ts +46 -0
  29. package/runtime/sdk/gl/physicsEvents.ts +20 -0
  30. package/runtime/sdk/inject.ts +12 -5
  31. package/runtime/sdk/runtime/camera.ts +31 -0
  32. package/runtime/sdk/runtime/device.ts +26 -0
  33. package/runtime/sdk/runtime/share.ts +9 -0
  34. package/runtime/sdk/ui/UI.ts +5 -1
  35. package/runtime/sdk/ui/UIButton.ts +11 -5
  36. package/runtime/sdk/ui/UIContainer.ts +28 -13
  37. package/runtime/sdk/ui/UIImage.ts +5 -0
  38. package/runtime/sdk/ui/UIInput.ts +10 -0
  39. package/runtime/sdk/ui/UINode.ts +206 -8
  40. package/runtime/sdk/ui/UIScreen.ts +10 -5
  41. package/runtime/sdk/ui/UIScrollable.ts +10 -5
  42. package/runtime/sdk/ui/UISpacer.ts +4 -0
  43. package/runtime/sdk/ui/UIText.ts +21 -5
  44. package/runtime/sdk/ui/UIVideo.ts +5 -0
  45. package/runtime/sdk/ui/UIVirtualizedList.ts +5 -0
  46. package/runtime/sdk/ui/UIWidget.ts +10 -5
  47. package/runtime/sdk/canvas/Label.ts +0 -57
  48. package/runtime/sdk/gl/Collider.ts +0 -29
@@ -2,7 +2,7 @@
2
2
  // on them. Native (creator-gl) owns the authoritative transform; reads decompose a cached local
3
3
  // matrix (re-synced lazily, since physics can write the transform from native each frame).
4
4
  //
5
- // Capabilities are aspects: node.aspect(Physics, …) (→ node.physics), node.aspect(Collider, …).
5
+ // Capabilities are aspects: node.aspect(Physics, …) (→ node.physics), node.aspect(Shape, …).
6
6
  // A GLB import is the Model node kind (with the ModelAnimation aspect). Hierarchy lives here.
7
7
 
8
8
  import { AspectHost } from "../core/Aspect"
@@ -12,6 +12,7 @@ import { Quat, cw, type QuatLike } from "../math/quat"
12
12
  import { Mat4, type Mat4Like } from "../math/mat4"
13
13
  import type { Geometry } from "./Geometry"
14
14
  import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
15
+ import { ensurePhysicsEvents } from "./physicsEvents"
15
16
  import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
16
17
 
17
18
  /** id → Node, so host callbacks (touch hits, animation events) route back to the owning object. */
@@ -28,6 +29,10 @@ export type NodeEvents = {
28
29
  track: () => void
29
30
  /** AR anchors only: the anchor lost tracking. */
30
31
  untrack: () => void
32
+ /** A physics contact/trigger overlap began (the other body's node). Needs a Shape + Physics/Trigger. */
33
+ enter: (other: Node) => void
34
+ /** A physics contact/trigger overlap ended. */
35
+ exit: (other: Node) => void
31
36
  }
32
37
 
33
38
  export class Node extends AspectHost<NodeEvents> {
@@ -171,7 +176,7 @@ export class Node extends AspectHost<NodeEvents> {
171
176
  _creator.traverse(this.id, (id) => callback(nodeRegistry.get(id) ?? new Node(id)))
172
177
  }
173
178
 
174
- // --- behaviors are aspects: node.aspect(Physics, …) / node.aspect(Collider, …) ---
179
+ // --- behaviors are aspects: node.aspect(Shape, …) / node.aspect(Physics, …) ---
175
180
 
176
181
  /** Orient the node so the given axis (default "-z", "forward") points at a world point. */
177
182
  lookAt(point: Vec3Like, mode: "z" | "-z" | "x" | "-x" | "y" | "-y" = "-z", ortho: Vec3Like = [ 0, 1, 0 ]): this {
@@ -207,6 +212,7 @@ export class Node extends AspectHost<NodeEvents> {
207
212
  override addEventListener<K extends keyof NodeEvents>(channel: K, callback: NodeEvents[K]): void {
208
213
  if (channel === "click") registerTouchEndEvent()
209
214
  else if (channel === "touchstart") registerTouchStartEvent()
215
+ else if (channel === "enter" || channel === "exit") ensurePhysicsEvents()
210
216
  super.addEventListener(channel, callback)
211
217
  }
212
218
 
@@ -215,6 +221,9 @@ export class Node extends AspectHost<NodeEvents> {
215
221
  this.dispatch(channel, ...args)
216
222
  }
217
223
 
224
+ /** @internal — emit a physics contact/trigger event (the physics-event router can't reach dispatch). */
225
+ _emitCollision(channel: "enter" | "exit", other: Node): void { this.dispatch(channel, other) }
226
+
218
227
  /** @internal — host animation-complete callback; forwards to the ModelAnimation aspect (if any).
219
228
  * Returns whether the clip should keep playing. */
220
229
  _onAnimationComplete(clipIndex: number): boolean {
@@ -1,47 +1,70 @@
1
- // Physics body as an aspect on a 3D Node, backed by a JoltPhysics body in creator-gl.
2
- // node.aspect(Physics, { motion: 'dynamic' }); node.physics.applyImpulse([0, 6, 0])
1
+ // Rigid body as an aspect on a 3D Node, backed by a JoltPhysics body in creator-gl. Requires a Shape
2
+ // aspect on the same node (its geometry becomes the body's collision shape).
3
3
  //
4
- // For a `dynamic` body, physics OWNS the transform — do NOT set node.position per frame; drive it
5
- // via velocity / applyImpulse. Reading node.position is free (native already wrote it). Kinematic
6
- // bodies are driven with moveTo. (Ported from the former Body class.)
4
+ // Physics.configure({ gravity: [0, -9.81, 0] }) // once, before creating bodies
5
+ // ground.aspect(Shape, { box: [20, 0.5, 20] }).aspect(Physics, { motion: 'static' })
6
+ // crate.aspect(Shape, {}).aspect(Physics, { mass: 2 }) // auto box from the mesh
7
+ // crate.physics.applyImpulse([0, 6, 0])
8
+ // crate.addEventListener('enter', other => …) // contact / trigger overlap began
9
+ //
10
+ // For a `dynamic` body, physics OWNS the transform — do NOT set node.position per frame; drive it via
11
+ // velocity / applyImpulse. Reading node.position is free (native already wrote it). Kinematic bodies
12
+ // are driven with moveTo.
7
13
 
8
14
  import { Aspect } from "../core/Aspect"
9
15
  import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
10
- import type { Node } from "./Node"
16
+ import { nodeRegistry, type Node } from "./Node"
17
+ import { Shape } from "./Shape"
11
18
 
12
19
  export type MotionType = "static" | "dynamic" | "kinematic"
13
20
  const MOTION: Record<MotionType, number> = { static: 0, kinematic: 1, dynamic: 2 }
14
21
 
15
- export type BodyShape =
16
- | { type: "auto" }
17
- | { type: "box", halfExtents: Vec3Like }
18
- | { type: "sphere", radius: number }
19
- | { type: "cylinder", halfHeight: number, radius: number }
22
+ export interface PhysicsConfig {
23
+ /** Gravity in world units/s² (Y-up: down is negative). Default [0, -9.81, 0]. */
24
+ gravity?: Vec3Like
25
+ /** Max simultaneous bodies (resizes the world; only before any body exists). Default 4096. */
26
+ maxBodies?: number
27
+ }
28
+
29
+ /** Closest raycast hit. */
30
+ export interface RayHit {
31
+ node: Node | null
32
+ point: Vec3
33
+ normal: Vec3
34
+ fraction: number
35
+ }
20
36
 
21
37
  export class Physics extends Aspect<"physics", Node> {
22
38
  static readonly aspect = "physics"
23
39
 
40
+ /** Render interpolation of body transforms between the fixed 60 Hz steps (global; default on). Turn
41
+ * off to save per-frame transform writes when you have many moving bodies / don't need >60 Hz
42
+ * smoothness — bodies then advance in discrete steps. */
43
+ private static _interpolation = true
44
+ static get interpolation(): boolean { return Physics._interpolation }
45
+ static set interpolation(on: boolean) { Physics._interpolation = on; _creator.setInterpolation(on) }
46
+
24
47
  /** Default "dynamic". */
25
48
  motion: MotionType = "dynamic"
26
49
  /** Mass in kg for dynamic bodies (default 1). Ignored for static/kinematic. */
27
50
  mass = 1
28
- /** Default { type: "auto" } — a box from the mesh AABB × world scale. */
29
- shape: BodyShape = { type: "auto" }
30
51
 
31
52
  private _bodyId = 0
32
53
 
33
54
  onAttach(): void {
34
- // No physics in this build (CREATOR_GL_PHYSICS=OFF) → bodyId stays 0; all methods no-op.
35
55
  if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
36
- const motion = MOTION[this.motion]
37
- const shape = resolveShape(this.node, this.shape)
38
- if (shape.type === "sphere") {
39
- this._bodyId = _creator.physicsCreateSphere(this.node.id, shape.radius, motion, this.mass)
40
- } else if (shape.type === "cylinder") {
41
- this._bodyId = _creator.physicsCreateCylinder(this.node.id, shape.halfHeight, shape.radius, motion, this.mass)
42
- } else {
43
- this._bodyId = _creator.physicsCreateBox(this.node.id, cx(shape.halfExtents), cy(shape.halfExtents), cz(shape.halfExtents), motion, this.mass)
56
+ const shape = this.node.get(Shape)
57
+ if (!shape) {
58
+ throw new Error("Physics requires a Shape aspect — add it first: node.aspect(Shape, {…}).aspect(Physics, {…})")
44
59
  }
60
+ const shapeId = shape._claim() // drop the pick-only body; reuse its shape
61
+ this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, MOTION[this.motion], this.mass, false, false)
62
+ shape._ownBody(this._bodyId)
63
+ }
64
+
65
+ onDetach(): void {
66
+ if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
67
+ this.node.get(Shape)?._recreatePickBody() // keep the node pickable if the Shape stays
45
68
  }
46
69
 
47
70
  /** Native Jolt body id. 0 until attached, or if the build has no physics support. */
@@ -69,27 +92,28 @@ export class Physics extends Aspect<"physics", Node> {
69
92
  return this
70
93
  }
71
94
 
72
- onDetach(): void {
73
- if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
95
+ // --- world (static) -------------------------------------------------------------------------
96
+ /** Configure gravity / limits. Call once before creating bodies. */
97
+ static configure(config: PhysicsConfig = {}): void {
98
+ const g = config.gravity ?? [0, -9.81, 0]
99
+ _creator.physicsConfigure(cx(g), cy(g), cz(g), config.maxBodies ?? 0)
100
+ }
101
+
102
+ /** Whether this build has physics support (CREATOR_GL_PHYSICS). */
103
+ static get supported(): boolean {
104
+ return !!_creator.physicsHasSupport && _creator.physicsHasSupport()
74
105
  }
75
- }
76
106
 
77
- // Resolve "auto" to a box from the mesh AABB × world scale (so the physical box matches the rendered
78
- // size). Falls back to a unit box if there is no geometry.
79
- const resolveShape = (node: Node, shape: BodyShape): Exclude<BodyShape, { type: "auto" }> => {
80
- if (shape.type !== "auto") return shape
81
- const scale = node.worldScale
82
- let hx = 0.5, hy = 0.5, hz = 0.5
83
- const geom = node.geometry
84
- if (geom) {
85
- const v = geom.vertices
86
- let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity
87
- for (let i = 0; i + 2 < v.length; i += 3) {
88
- if (v[i] < minX) minX = v[i]; if (v[i] > maxX) maxX = v[i]
89
- if (v[i + 1] < minY) minY = v[i + 1]; if (v[i + 1] > maxY) maxY = v[i + 1]
90
- if (v[i + 2] < minZ) minZ = v[i + 2]; if (v[i + 2] > maxZ) maxZ = v[i + 2]
107
+ /** Closest pickable body hit by the ray from `origin` along `dir` (up to `maxDist`), or null. */
108
+ static raycast(origin: Vec3Like, dir: Vec3Like, maxDist = 1000): RayHit | null {
109
+ const out = new Float32Array(7)
110
+ const id = _creator.physicsRaycast(cx(origin), cy(origin), cz(origin), cx(dir), cy(dir), cz(dir), maxDist, out)
111
+ if (!id) return null
112
+ return {
113
+ node: nodeRegistry.get(id) ?? null,
114
+ point: new Vec3(out[0], out[1], out[2]),
115
+ normal: new Vec3(out[3], out[4], out[5]),
116
+ fraction: out[6],
91
117
  }
92
- hx = (maxX - minX) * 0.5; hy = (maxY - minY) * 0.5; hz = (maxZ - minZ) * 0.5
93
118
  }
94
- return { type: "box", halfExtents: [ hx * scale.x, hy * scale.y, hz * scale.z ] }
95
119
  }
@@ -6,6 +6,7 @@
6
6
  // direct light + shadows.
7
7
 
8
8
  import { Color, type ColorInput } from "../core/color"
9
+ import { _installAspectFrames } from "../core/Aspect"
9
10
  import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
10
11
  import type { FetchResponse } from "../runtime/fetch"
11
12
  import { Camera } from "./Camera"
@@ -29,6 +30,22 @@ export type SceneOptions = {
29
30
  antialias?: boolean
30
31
  }
31
32
 
33
+ // Route aspect update(dt) through the render-synced native phases (render() early = before physics,
34
+ // late = after animations / before draw), so followers/cameras are in lockstep with the draw. Done from
35
+ // the 3D layer (not core) so a 2D-only bundle never pulls in the 3D bridge. Installed once, lazily on
36
+ // the first Scene. On an older wasm without the hooks the installer returns false → setLoop fallback.
37
+ let framesInstalled = false
38
+ const installRenderSyncedFrames = (): void => {
39
+ if (framesInstalled) return
40
+ framesInstalled = true
41
+ _installAspectFrames((early, late) => {
42
+ if (typeof _creator.setEarlyUpdate !== "function") return false
43
+ _creator.setEarlyUpdate(early)
44
+ _creator.setLateUpdate(late)
45
+ return true
46
+ })
47
+ }
48
+
32
49
  export class Scene {
33
50
  /** @internal native scene handle. */
34
51
  readonly _id: number
@@ -44,6 +61,7 @@ export class Scene {
44
61
  static get active(): Scene | null { return Scene._active }
45
62
 
46
63
  constructor(options: SceneOptions = {}) {
64
+ installRenderSyncedFrames()
47
65
  const parent = (options as any)._parent as Scene | undefined
48
66
  if (parent) {
49
67
  this.camera = parent.camera
@@ -0,0 +1,98 @@
1
+ // A collision shape, as an aspect on a 3D Node. PURE GEOMETRY: attaching a Shape does not by itself
2
+ // make the node rigid or a sensor — pair it with a Physics (rigid body) or a Trigger (sensor), both
3
+ // of which require a Shape. The Shape also makes the node pointer-pickable: on its own it creates a
4
+ // lightweight "pick-only" JoltPhysics body (collides with nothing, exists only for the raycast), so
5
+ // any node with a Shape can receive click/touchstart. Physics/Trigger REPLACE that body with the real
6
+ // one. Set `raycast = false` to opt a shape out of pointer picking.
7
+ //
8
+ // wall.aspect(Shape, { box: [1, 2, 1] }).aspect(Physics, { motion: 'static' })
9
+ // coin.aspect(Shape, { sphere: 0.5 }).aspect(Trigger)
10
+ // crate.aspect(Shape, {}).aspect(Physics) // auto box from the mesh AABB × world scale
11
+ //
12
+ // Explicit dims are world-space half-extents (not scaled by the node). Use {} ("auto") to track the
13
+ // mesh × world scale. Note: a Shape-only pick body is a STATIC snapshot at attach time — for a moving
14
+ // pickable object add a Physics body (which follows the sim), rather than relying on the pick body.
15
+
16
+ import { Aspect } from "../core/Aspect"
17
+ import { cx, cy, cz, type Vec3Like } from "../math/vec"
18
+ import type { Node } from "./Node"
19
+
20
+ export class Shape extends Aspect<"shape", Node> {
21
+ static readonly aspect = "shape"
22
+
23
+ /** Box half-extents [hx, hy, hz] (world units). */
24
+ box?: Vec3Like
25
+ /** Sphere radius. */
26
+ sphere?: number
27
+ /** Cylinder aligned on Y: half-height + radius. */
28
+ cylinder?: { halfHeight: number, radius: number }
29
+ /** Capsule aligned on Y: half-height of the cylinder section + radius. */
30
+ capsule?: { halfHeight: number, radius: number }
31
+ /** Whether pointer rays can hit this shape (click / touchstart). Default true. */
32
+ raycast = true
33
+
34
+ private _shapeId = 0
35
+ private _bodyId = 0
36
+
37
+ /** @internal native shape id (0 if the build has no physics). */
38
+ get shapeId(): number { return this._shapeId }
39
+ /** @internal current native body id (a pick-only body until Physics/Trigger claim it). */
40
+ get bodyId(): number { return this._bodyId }
41
+
42
+ onAttach(): void {
43
+ if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
44
+ this._shapeId = this._build()
45
+ if (this._shapeId) this._createPickBody()
46
+ }
47
+
48
+ onDetach(): void {
49
+ if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
50
+ if (this._shapeId) { _creator.physicsDestroyShape(this._shapeId); this._shapeId = 0 }
51
+ }
52
+
53
+ /** @internal Physics/Trigger take over: drop the pick body and hand back the shape id. */
54
+ _claim(): number {
55
+ if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
56
+ return this._shapeId
57
+ }
58
+ /** @internal Physics/Trigger report the body they created, so Shape owns its lifetime + pickability. */
59
+ _ownBody(bodyId: number): void {
60
+ this._bodyId = bodyId
61
+ if (!this.raycast && bodyId) _creator.physicsSetPickable(bodyId, false)
62
+ }
63
+ /** @internal restore a pick-only body when Physics/Trigger detach but the Shape remains. */
64
+ _recreatePickBody(): void {
65
+ if (!this._bodyId && this._shapeId) this._createPickBody()
66
+ }
67
+
68
+ private _createPickBody(): void {
69
+ this._bodyId = _creator.physicsCreateBody(this.node.id, this._shapeId, 0, 0, false, /*pickOnly*/ true)
70
+ if (!this.raycast && this._bodyId) _creator.physicsSetPickable(this._bodyId, false)
71
+ }
72
+
73
+ private _build(): number {
74
+ if (this.sphere !== undefined) return _creator.physicsBuildSphere(this.sphere)
75
+ if (this.cylinder) return _creator.physicsBuildCylinder(this.cylinder.halfHeight, this.cylinder.radius)
76
+ if (this.capsule) return _creator.physicsBuildCapsule(this.capsule.halfHeight, this.capsule.radius)
77
+ if (this.box) return _creator.physicsBuildBox(cx(this.box), cy(this.box), cz(this.box))
78
+
79
+ // auto: a box from the mesh AABB × world scale (so the physical box matches the rendered size).
80
+ const s = this.node.worldScale
81
+ let hx = 0.5, hy = 0.5, hz = 0.5
82
+ const geom = this.node.geometry
83
+ if (geom) {
84
+ const v = geom.vertices
85
+ let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity
86
+ for (let i = 0; i + 2 < v.length; i += 3) {
87
+ if (v[i] < minX) minX = v[i]
88
+ if (v[i] > maxX) maxX = v[i]
89
+ if (v[i + 1] < minY) minY = v[i + 1]
90
+ if (v[i + 1] > maxY) maxY = v[i + 1]
91
+ if (v[i + 2] < minZ) minZ = v[i + 2]
92
+ if (v[i + 2] > maxZ) maxZ = v[i + 2]
93
+ }
94
+ hx = (maxX - minX) * 0.5; hy = (maxY - minY) * 0.5; hz = (maxZ - minZ) * 0.5
95
+ }
96
+ return _creator.physicsBuildBox(hx * s.x, hy * s.y, hz * s.z)
97
+ }
98
+ }
@@ -1,6 +1,6 @@
1
1
  // A 3D GPU texture, decoded natively by creator-gl. Load from a URL or an already-fetched response.
2
2
 
3
- import { fetch, type FetchResponse } from "../runtime/fetch"
3
+ import { fetch, type FetchResponse, type File } from "../runtime/fetch"
4
4
 
5
5
  export class Texture {
6
6
  /** @internal native texture handle. */
@@ -30,7 +30,7 @@ export class Texture {
30
30
  return new Texture(Math.round(canvas.width * canvas.pixelRatio), Math.round(canvas.height * canvas.pixelRatio), canvas._tex3dId)
31
31
  }
32
32
 
33
- static load(source: string | FetchResponse): Promise<Texture> {
33
+ static load(source: string | FetchResponse | File): Promise<Texture> {
34
34
  if (typeof source === "string") {
35
35
  return fetch(source, { useOnce: true }).then((resp) => {
36
36
  if (resp.status >= 400) {
@@ -0,0 +1,46 @@
1
+ // A trigger zone, as an aspect on a 3D Node. Requires a Shape (its geometry); creates a static SENSOR
2
+ // body from it, so a physics body overlapping the zone fires the node's 'enter' / 'exit' events
3
+ // instead of colliding. A trigger does not block movement, and is still pointer-pickable.
4
+ //
5
+ // const goal = new Mesh(box(), material)
6
+ // .aspect(Shape, { box: [1, 2, 1] })
7
+ // .aspect(Trigger)
8
+ // goal.addEventListener('enter', other => win(other))
9
+ // goal.addEventListener('exit', other => …)
10
+
11
+ import { Aspect } from "../core/Aspect"
12
+ import { cx, cy, cz, type Vec3Like } from "../math/vec"
13
+ import { Shape } from "./Shape"
14
+ import { ensurePhysicsEvents } from "./physicsEvents"
15
+ import type { Node } from "./Node"
16
+
17
+ export class Trigger extends Aspect<"trigger", Node> {
18
+ static readonly aspect = "trigger"
19
+
20
+ private _bodyId = 0
21
+ /** Native body id (0 if no physics support). */
22
+ get id(): number { return this._bodyId }
23
+
24
+ onAttach(): void {
25
+ if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
26
+ const shape = this.node.get(Shape)
27
+ if (!shape) {
28
+ throw new Error("Trigger requires a Shape aspect — add it first: node.aspect(Shape, {…}).aspect(Trigger)")
29
+ }
30
+ const shapeId = shape._claim()
31
+ this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, 0 /* static */, 0, true /* sensor */, false)
32
+ shape._ownBody(this._bodyId)
33
+ ensurePhysicsEvents() // routes overlap events → the node's 'enter' / 'exit'
34
+ }
35
+
36
+ onDetach(): void {
37
+ if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
38
+ this.node.get(Shape)?._recreatePickBody()
39
+ }
40
+
41
+ /** Reposition the trigger zone to a world point. */
42
+ moveTo(p: Vec3Like): this {
43
+ if (this._bodyId) _creator.physicsSetBodyPosition(this._bodyId, cx(p), cy(p), cz(p))
44
+ return this
45
+ }
46
+ }
@@ -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
+ }
@@ -17,9 +17,11 @@ export { Color } from "./core/color"
17
17
  // ---- aspects (attachable node capabilities) ----
18
18
  export { Aspect, type With } from "./core/Aspect"
19
19
 
20
+ // ---- reactivity (signals; UI text/style props also accept `() => value` bindings) ----
21
+ export { signal, computed, effect, type Signal, type Computed } from "./core/signals"
22
+
20
23
  // ---- canvas (core 2D drawing that bakes to a texture; shared by 2D/3D/UI) ----
21
- export { Canvas } from "./canvas/Canvas"
22
- export { Label } from "./canvas/Label"
24
+ export { Canvas, Bitmap } from "./canvas/Canvas"
23
25
 
24
26
  // ---- platform runtime (injected globals; the host provides the lower-level _creator* bridges) ----
25
27
  export { toast, SvgSource } from "./runtime/misc"
@@ -29,7 +31,9 @@ export { device } from "./runtime/device"
29
31
  export { Input } from "./runtime/input"
30
32
  export { WebSocket } from "./runtime/net"
31
33
  export { AudioPlayer, VideoPlayer } from "./runtime/media"
34
+ export { CameraViewer } from "./runtime/camera"
32
35
  export { openFilePicker } from "./runtime/files"
36
+ export { share } from "./runtime/share"
33
37
  export { ClickEvent, TouchStartEvent } from "./runtime/touch"
34
38
 
35
39
  // ---- 2D engine (creator-2d) ----
@@ -38,9 +42,9 @@ export { Node2D } from "./g2/Node2D"
38
42
  export { Camera2D } from "./g2/Camera2D"
39
43
  export { Sprite } from "./g2/Sprite"
40
44
  export { SpriteAnimation } from "./g2/SpriteAnimation"
41
- export { Shape } from "./g2/Shape"
45
+ export { Shape2D } from "./g2/Shape2D"
42
46
  export { Physics2D } from "./g2/Physics2D"
43
- export { Trigger } from "./g2/Trigger"
47
+ export { Trigger2D } from "./g2/Trigger2D"
44
48
  export { CharacterController2D } from "./g2/CharacterController2D"
45
49
  export { Tilemap } from "./g2/Tilemap"
46
50
  export { Texture2D } from "./g2/Texture2D"
@@ -60,6 +64,7 @@ export {
60
64
  UIVirtualizedList,
61
65
  Router,
62
66
  registerFont,
67
+ __uiMap,
63
68
  } from "./ui/UI"
64
69
 
65
70
  // ---- animate ----
@@ -75,8 +80,10 @@ export { Scene, ARScene } from "./gl/Scene"
75
80
  export { Node } from "./gl/Node"
76
81
  export { Mesh } from "./gl/Mesh"
77
82
  export { Model } from "./gl/Model"
83
+ export { Shape } from "./gl/Shape"
78
84
  export { Physics } from "./gl/Physics"
79
- export { Collider } from "./gl/Collider"
85
+ export { Trigger } from "./gl/Trigger"
86
+ export { CharacterController } from "./gl/CharacterController"
80
87
  export { ModelAnimation } from "./gl/ModelAnimation"
81
88
  export { Light } from "./gl/Light"
82
89
  export { Material } from "./gl/Material"
@@ -0,0 +1,31 @@
1
+ // Device camera → a still-photo File. Ported from worker/src/utils/CameraViewer.ts. Permission is
2
+ // requested through the engine bridge's `requestCamera` (same as QRScanner); the live preview and the
3
+ // capture go through `_creatorUtils.camera*`. `takePhoto()` returns a File (a host buffer handle), so
4
+ // the shot can be used as a texture (`Texture2D.load(file)`), uploaded via FormData, or shared.
5
+
6
+ import { File } from "./fetch"
7
+
8
+ export class CameraViewer {
9
+ /** Ask for camera permission and start the live preview (default: the back camera). */
10
+ static async open(facingMode: "front" | "back" = "back"): Promise<void> {
11
+ await new Promise<void>((res, rej) => _creator.requestCamera(res, rej))
12
+ await new Promise<void>((res, rej) => _creatorUtils.cameraOpen(facingMode, res, rej))
13
+ }
14
+
15
+ /** Switch between the front and back camera while the preview is open. */
16
+ static setFacingMode(facingMode: "front" | "back"): void {
17
+ _creatorUtils.cameraSetFacingMode(facingMode)
18
+ }
19
+
20
+ /** Capture a still frame as an image File (host-encoded). */
21
+ static takePhoto(): Promise<File> {
22
+ return new Promise<File>((res, rej) => {
23
+ _creatorUtils.cameraTakePhoto((bufferId, name, size) => res(new File(bufferId, name, size)), rej)
24
+ })
25
+ }
26
+
27
+ /** Stop the preview and release the camera. */
28
+ static close(): void {
29
+ _creatorUtils.cameraClose()
30
+ }
31
+ }
@@ -17,6 +17,32 @@ export const device = {
17
17
  get language(): string {
18
18
  return _creatorUtils.language
19
19
  },
20
+ /** Display device-pixel ratio (physical px per logical/CSS px): 1 on standard displays, 2–3 on
21
+ * retina / iOS. Bake a Canvas at this ratio for crisp output without hardcoding, e.g.
22
+ * `new Canvas(w, h, { pixelRatio: device.pixelRatio })`. NOTE: the web 2D/GL surface currently
23
+ * renders at logical resolution, so on web this only helps UI canvases / is forward-looking for
24
+ * 2D; on iOS (physical surface) it makes 2D canvases crisp. Falls back to 1 if the host omits it. */
25
+ get pixelRatio(): number {
26
+ return _creatorUtils.pixelRatio || 1
27
+ },
28
+ /** Current display width in logical px — the same value the "resize" event delivers, but readable at
29
+ * any time (not only inside the listener). 0 until the host has reported a size. */
30
+ get width(): number {
31
+ return _creatorUI.getDisplaySize?.()?.[0] ?? 0
32
+ },
33
+ /** Current display height in logical px — the same value the "resize" event delivers, readable at any
34
+ * time. 0 until the host has reported a size. */
35
+ get height(): number {
36
+ return _creatorUI.getDisplaySize?.()?.[1] ?? 0
37
+ },
38
+ /** Opt into the precise-touch system where the platform supports it. OFF by default. When on, fast
39
+ * strokes are sampled at the touch digitizer's full rate (iOS coalesced touches, ≈120–240 Hz) instead
40
+ * of once per display frame (~60 Hz), so a pointer-heavy app (drawing, handwriting, dragging) gets more
41
+ * points and smoother lines. Leave it off for tap/button UIs. No-op on hosts without a coalesced-input
42
+ * concept (web already coalesces pointermove; headless). */
43
+ setPreciseTouch(enabled: boolean): void {
44
+ _creatorUtils.setPreciseTouch?.(enabled)
45
+ },
20
46
  addEventListener(channel: "resize", callback: ResizeCallback): void {
21
47
  if (channel !== "resize") return
22
48
  resizeListeners.push(callback)
@@ -0,0 +1,9 @@
1
+ // Share a media File (image/video) via the OS share sheet. Wraps `_creatorUtils.shareMedia`, which
2
+ // takes the host buffer id behind the File. On iOS the sheet also lets the user save to Files/Photos;
3
+ // on web it saves (downloads) the file. `text` is an optional caption/message.
4
+
5
+ import type { File, FetchResponse } from "./fetch"
6
+
7
+ export function share(media: File | FetchResponse, text?: string): void {
8
+ _creatorUtils.shareMedia((media as any).id ?? (media as any)._id, text)
9
+ }
@@ -16,4 +16,8 @@ export { UIScrollable, type UIScrollableStyle } from './UIScrollable'
16
16
 
17
17
  export { UIVirtualizedList } from './UIVirtualizedList'
18
18
 
19
- export { Router } from './router'
19
+ export { Router } from './router'
20
+
21
+ // Internal: memoized map for reactive children — the chisel `reactiveUi` pass rewrites
22
+ // `list.map(fn)` inside children bindings to `__uiMap(list, fn, slot)` calls.
23
+ export { __uiMap } from './UINode'
@@ -1,6 +1,6 @@
1
1
  import { ClickEvent, TouchStartEvent } from "../runtime/touch"
2
2
  import type { ClickEvent as ClickEventType, TouchStartEvent as TouchStartEventType } from "../runtime/touch"
3
- import { type ElementStyle, type ContainerStyle, type Style, ContainerElement, type DrawableStyle, type UINodeChild, type OnLayoutCallback, type BaseStyle, type AnimateStyle, type Color } from "./UINode"
3
+ import { type ElementStyle, type ContainerStyle, type Style, ContainerElement, type DrawableStyle, type UINodeChild, type ChildrenFn, type OnLayoutCallback, type BaseStyle, type AnimateStyle, type Color } from "./UINode"
4
4
 
5
5
  export type UIButtonStyle = ElementStyle & DrawableStyle & ContainerStyle & { rippleColor?: Color | "default" }
6
6
 
@@ -18,9 +18,15 @@ export interface UIButton {
18
18
  append(...nodes: UINodeChild[]): this
19
19
  insert(index: number, ...nodes: UINodeChild[]): this
20
20
  remove(...nodes: UINodeChild[]): this
21
- setContent(nodes: UINodeChild[]): this
21
+ setContent(nodes: UINodeChild[] | ChildrenFn): this
22
22
 
23
23
  onLayout(onLayout: OnLayoutCallback): this
24
+
25
+ setClass(name: string, enabled: boolean): this
26
+ toggleClass(name: string): this
27
+ hasClass(name: string): boolean
28
+ bindClass(name: string, fn: () => boolean): this
29
+
24
30
  readonly children: UINodeChild[],
25
31
  }
26
32
 
@@ -66,14 +72,14 @@ class ButtonElement extends ContainerElement<"button"> {
66
72
  }
67
73
 
68
74
  export function UIButton(): UIButton;
69
- export function UIButton(children: UINodeChild[]): UIButton;
75
+ export function UIButton(children: UINodeChild[] | ChildrenFn): UIButton;
70
76
  export function UIButton(style: UIButtonStyle): UIButton;
71
77
  export function UIButton(style: UIButtonStyle, children: UINodeChild[]): UIButton;
72
- export function UIButton(...args: [] | [UINodeChild[] | UIButtonStyle] | [UIButtonStyle, children: UINodeChild[]]): UIButton {
78
+ export function UIButton(...args: [] | [UINodeChild[] | ChildrenFn | UIButtonStyle] | [UIButtonStyle, children: UINodeChild[] | ChildrenFn]): UIButton {
73
79
  if (args.length === 0) {
74
80
  return new ButtonElement("button", {}, [])
75
81
  } else if (args.length === 1) {
76
- if (Array.isArray(args[0])) {
82
+ if (Array.isArray(args[0]) || typeof args[0] === "function") {
77
83
  return new ButtonElement("button", {}, args[0])
78
84
  } else {
79
85
  return new ButtonElement("button", args[0], [])