lecodes-sdk 2.2.11 → 2.2.13
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.
- package/dist/global.d.ts +2 -0
- package/dist/host.d.ts +8 -2
- package/dist/types/core/Aspect.d.ts +5 -0
- package/dist/types/g2/Physics2D.d.ts +3 -2
- package/dist/types/g2/Scene2D.d.ts +6 -0
- package/dist/types/gl/Scene.d.ts +6 -0
- package/dist/types/gl/Texture.d.ts +2 -7
- package/dist/types/gl/animation/DynamicBone.d.ts +3 -0
- package/dist/types/gl/animation/Locomotion.d.ts +3 -0
- package/dist/types/gl/physics/Physics.d.ts +2 -1
- package/dist/types/gl/physics/Ragdoll.d.ts +4 -0
- package/dist/types/gl/vehicle/Vehicle.d.ts +4 -0
- package/dist/types/inject.d.ts +1 -1
- package/dist/types/runtime/app.d.ts +4 -2
- package/dist/types/runtime/fetch.d.ts +3 -0
- package/dist/types/runtime/storage.d.ts +6 -1
- package/dist/types/server/db/marci/query.d.ts +8 -2
- package/dist/types/ui/UIButton.d.ts +2 -2
- package/dist/types/ui/UIContainer.d.ts +4 -2
- package/dist/types/ui/UINode.d.ts +1 -1
- package/dist/types/ui/UIScreen.d.ts +2 -2
- package/dist/types/ui/UIVideo.d.ts +3 -2
- package/dist/types/ui/router.d.ts +1 -1
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +2 -5
- package/src/compile/assetIconMacro.ts +2 -1
- package/src/compile/assetMacro.ts +2 -1
- package/src/compile/compileProject.ts +28 -4
- package/src/compile/header.ts +17 -0
- package/src/compile/index.ts +2 -0
- package/src/core/Aspect.ts +2 -1
- package/src/g2/Physics2D.ts +6 -3
- package/src/g2/Scene2D.ts +9 -1
- package/src/gl/Scene.ts +9 -1
- package/src/gl/Texture.ts +2 -1
- package/src/gl/animation/DynamicBone.ts +6 -2
- package/src/gl/animation/Locomotion.ts +11 -1
- package/src/gl/physics/Physics.ts +3 -2
- package/src/gl/physics/Ragdoll.ts +8 -3
- package/src/gl/vehicle/Vehicle.ts +7 -2
- package/src/host.d.ts +8 -2
- package/src/inject.ts +1 -1
- package/src/runtime/app.ts +4 -2
- package/src/runtime/fetch.ts +6 -0
- package/src/runtime/storage.ts +14 -4
- package/src/server/db/marci/query.ts +8 -2
- package/src/ui/UIButton.ts +2 -2
- package/src/ui/UIContainer.ts +4 -2
- package/src/ui/UINode.ts +1 -1
- package/src/ui/UIScreen.ts +2 -2
- package/src/ui/UIVideo.ts +3 -2
- package/src/ui/router.ts +4 -3
- package/src/version.ts +1 -1
- package/prompts/2d.md +0 -391
- package/prompts/3d-scene-files.md +0 -113
- package/prompts/3d-scene.md +0 -122
- package/prompts/3d.md +0 -299
- package/prompts/README.md +0 -142
- package/prompts/ar.md +0 -142
- package/prompts/canvas.md +0 -28
- package/prompts/compose.ts +0 -46
- package/prompts/concept.md +0 -49
- package/prompts/core-design.md +0 -98
- package/prompts/core.md +0 -294
- package/prompts/design.md +0 -218
- package/prompts/dist/2d-game.md +0 -1567
- package/prompts/dist/3d-app.md +0 -1712
- package/prompts/dist/ar-app.md +0 -1585
- package/prompts/dist/concept.md +0 -49
- package/prompts/dist/design.md +0 -521
- package/prompts/dist/ui-app.md +0 -1175
- package/prompts/index.md +0 -39
- package/prompts/intake-prompt.md +0 -33
- package/prompts/namer-prompt.md +0 -12
- package/prompts/router-prompt.md +0 -50
- package/prompts/select.ts +0 -126
- package/prompts/ui-design.md +0 -203
- package/prompts/ui.md +0 -813
package/prompts/3d.md
DELETED
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
## 3D engine
|
|
2
|
-
|
|
3
|
-
Filament-rendered 3D content with Jolt physics. Units are world units ≈ METERS, Y is up, forward is −Z. All `eulerAngles` are DEGREES. Everything here is a global — no imports.
|
|
4
|
-
|
|
5
|
-
Scene construction and lifecycle are mode-specific — see the Scene (or AR) section further below. Everything in THIS section applies to any scene:
|
|
6
|
-
|
|
7
|
-
// ===== SCENE CONTENT (any scene kind) =====
|
|
8
|
-
|
|
9
|
-
scene.add(...nodes: Node[]): this // register with the draw set (≠ parenting — see hierarchy)
|
|
10
|
-
scene.remove(...nodes: Node[]): this
|
|
11
|
-
|
|
12
|
-
// Scene-level pointer events — fire for EVERY tap; ev.target = hit Node (needs a Shape) or null:
|
|
13
|
-
scene.addEventListener('click' | 'touchstart', (ev) => { ev.target; ev.clientX; ev.clientY })
|
|
14
|
-
|
|
15
|
-
// 2D HUD over a 3D view: open a UIScreen (no bgColor → transparent) AFTER the scene opens — see UI section.
|
|
16
|
-
|
|
17
|
-
// ===== NODES =====
|
|
18
|
-
// Node = empty transform. Mesh, Model, Light, Camera, Particles all extend it.
|
|
19
|
-
// The NATIVE engine owns the transform (physics rewrites it) — getters return fresh COPIES (see math).
|
|
20
|
-
|
|
21
|
-
const pivot = new Node()
|
|
22
|
-
pivot.position = [0, 1, -2] // Vec3Like (raw arrays fine); getters return Vec3 copies
|
|
23
|
-
pivot.x = 3 // scalar setters x / y / z — the way to move one axis
|
|
24
|
-
pivot.quaternion = Quat.fromEuler(0, 90, 0)
|
|
25
|
-
pivot.eulerAngles = [0, 90, 0] // DEGREES
|
|
26
|
-
pivot.scale = 2 // uniform number or [sx, sy, sz]
|
|
27
|
-
pivot.matrix = Mat4.compose([0, 1, 0], Quat.identity, 1)
|
|
28
|
-
|
|
29
|
-
node.worldMatrix // get/set full world transform
|
|
30
|
-
node.worldPosition / node.worldQuaternion / node.worldEulerAngles / node.worldScale // read-only
|
|
31
|
-
node.forward // Vec3: world-space −Z axis (read-only)
|
|
32
|
-
|
|
33
|
-
node.name = 'door' // visible/name; traverse lookups match on name
|
|
34
|
-
node.visible = false
|
|
35
|
-
node.destroy() // frees the native entity — the ONLY teardown; node unusable after
|
|
36
|
-
|
|
37
|
-
// Hierarchy (separate from scene membership — scene.add puts it in the world, node.add parents it):
|
|
38
|
-
node.add(...children): this
|
|
39
|
-
node.setParent(parent: Node | null, worldPositionStays = false): this
|
|
40
|
-
node.parent / node.children / node.childCount / node.getChild(i)
|
|
41
|
-
node.traverse(cb: (node: Node) => void) // this node, then all descendants
|
|
42
|
-
|
|
43
|
-
node.lookAt(point: Vec3Like, mode = '-z', up = [0, 1, 0]): this // aim a local axis at a WORLD point
|
|
44
|
-
|
|
45
|
-
// Node events — node.addEventListener(channel, cb) / removeEventListener:
|
|
46
|
-
// 'click' pointer-up over the node — REQUIRES a Shape aspect (pick body)
|
|
47
|
-
// 'touchstart' pointer-down over the node (ev.track() drags) — requires Shape
|
|
48
|
-
// 'enter' / 'exit' (other: Node) physics contact / trigger overlap began/ended — Shape + Physics/Trigger
|
|
49
|
-
// Without a Shape a node is INVISIBLE to taps — only scene listeners fire (ev.target === null).
|
|
50
|
-
|
|
51
|
-
// ===== ASPECTS =====
|
|
52
|
-
// Capabilities attach to nodes as aspects and chain; each adds a named accessor:
|
|
53
|
-
node.aspect(Class, opts?) // attach + configure; returns the node (typed with the accessor)
|
|
54
|
-
node.get(Class) / node.has(Class) / node.removeAspect(Class)
|
|
55
|
-
// Named accessors: node.physics, node.trigger, node.controller, node.shape, node.anim
|
|
56
|
-
// Custom game logic = your own aspect with per-frame update(dt seconds):
|
|
57
|
-
class Spin extends Aspect<'spin', Node> { // <accessor name, node kind>
|
|
58
|
-
speed = 90 // class fields = configurable defaults (opts override)
|
|
59
|
-
update(dt: number) { this.node.eulerAngles = this.node.eulerAngles.add([0, this.speed * dt, 0]) }
|
|
60
|
-
}
|
|
61
|
-
mesh.aspect(Spin, { speed: 45 }) // never `new Spin()`; init in onAttach(), clean in onDetach()
|
|
62
|
-
// Phases: default = LATE (after physics — for cameras/followers reading final positions);
|
|
63
|
-
// set `updateBeforePhysics = true` on aspects that WRITE velocity/kinematic transforms.
|
|
64
|
-
|
|
65
|
-
// ===== MESHES & MATERIALS =====
|
|
66
|
-
|
|
67
|
-
// Primitive factories — all take: { material? (default Material.unlit()), position?, eulerAngles?,
|
|
68
|
-
// scale?, name?, castShadows?, receiveShadows? }. Defaults are UNIT-sized (1 m).
|
|
69
|
-
Mesh.box({ size?: Vec3Like | number }) // full edge lengths; default 1×1×1
|
|
70
|
-
Mesh.sphere({ radius? }) // default 0.5 (unit diameter)
|
|
71
|
-
Mesh.cylinder({ radius?, radiusTop?, radiusBottom?, edges? }) // height fixed 1; radiusTop: 0 = cone
|
|
72
|
-
Mesh.plane({ normal?: Vec3Like }) // 1×1 quad; default normal [0,0,1] = STANDS UPRIGHT facing camera.
|
|
73
|
-
// A ground/floor plane NEEDS normal: [0, 1, 0].
|
|
74
|
-
|
|
75
|
-
mesh.material // get/set slot 0; mesh.setMaterial(mat, index = 0)
|
|
76
|
-
mesh.castShadows = true // write-only render flags (or set in factory options)
|
|
77
|
-
mesh.receiveShadows = true
|
|
78
|
-
|
|
79
|
-
// Materials:
|
|
80
|
-
Material.lit({ color?, map?: Texture }) // PBR — needs light (IBL and/or sun) or renders black
|
|
81
|
-
Material.unlit({ color?, map? }) // flat, ignores all lighting
|
|
82
|
-
Material.video(player.texture) // samples a VideoPlayer's texture
|
|
83
|
-
Material.shadow(color = '#000000aa') // shadow-catcher: invisible except where shadows fall (AR staple)
|
|
84
|
-
material.color = '#ff8800' // convenience setters on lit/unlit (no-op on custom shaders)
|
|
85
|
-
material.map = tex // Texture | Canvas | null
|
|
86
|
-
|
|
87
|
-
// Custom compiled shaders (.mat files) — load, then set uniforms by name (chainable):
|
|
88
|
-
const mat = new Material(await fetch(asset('./hologram.mat'), { useOnce: true }))
|
|
89
|
-
.set('baseColor', '#ffa200') // color STRINGS only (any CSS color); a number is a float
|
|
90
|
-
.set('roughness', 0.54) // number → float, boolean → bool
|
|
91
|
-
.set('glowMap', tex) // Texture → sampler
|
|
92
|
-
.set('dir', [0, 1, 0]) // number[] / Float32Array → vector. NOT a Vec3 — spread it: [...v]
|
|
93
|
-
// material.uniforms.key = value — same thing property-style. `color`/`map` setters don't know a
|
|
94
|
-
// custom shader's uniform names — always .set() the real names there.
|
|
95
|
-
|
|
96
|
-
// Textures:
|
|
97
|
-
const tex = await Texture.load(asset('./crate.jpg')) // url string | FetchResponse | File
|
|
98
|
-
tex.wrapS = 1; tex.wrapT = 1 // set BEFORE assigning to a material
|
|
99
|
-
Texture.fromCanvas(c) // bake a Canvas (see CANVAS section) — nameplates, generated art;
|
|
100
|
-
// material.map = c does the same implicitly; c.update() refreshes it
|
|
101
|
-
// No dispose() — textures live until the engine tears down. Load once, reuse everywhere.
|
|
102
|
-
|
|
103
|
-
// Light — Light.sun() is the ONLY light (no point/spot lights). Ambient comes from the scene's IBL.
|
|
104
|
-
scene.add(Light.sun({
|
|
105
|
-
direction?: Vec3Like, // where light TRAVELS; default a key-light angle
|
|
106
|
-
intensity?: number, // default 100000
|
|
107
|
-
color?: ColorInput,
|
|
108
|
-
shadowsQuality?: 0 | 1 | 2 | 3 // 0 = none, 1 = default 1024 map, 2 = soft 2048, 3 = PCSS 4096
|
|
109
|
-
}))
|
|
110
|
-
// Creation-time only — no setters; to change the sun, destroy() it and add a new one.
|
|
111
|
-
// Shadows also need castShadows on the caster + receiveShadows on the ground.
|
|
112
|
-
|
|
113
|
-
// Custom geometry — tweak a primitive before wrapping (moves pivot, tiles UVs), then Mesh.from(geo):
|
|
114
|
-
const geo = Geometry.cylinder({ radiusTop: 0, edges: 6 }).scale(1, 3, 1).translate(0, 1.5, 0)
|
|
115
|
-
const spike = Mesh.from(geo, { material }) // finish geometry BEFORE building the mesh (buffers upload once)
|
|
116
|
-
|
|
117
|
-
// ===== MODELS (GLB) =====
|
|
118
|
-
|
|
119
|
-
const hero = await Model.load(asset('./hero.glb')) // asset path | https URL | FetchResponse; rejects on error
|
|
120
|
-
scene.add(hero) // a Model IS a Node — transform/events/aspects all apply
|
|
121
|
-
hero.traverse(n => { if (n.name === 'Sword') n.visible = false }) // GLB internals are plain child Nodes
|
|
122
|
-
|
|
123
|
-
// Animation — every Model has an Animator pre-attached at model.anim (clip list = the glb's clips). A layer
|
|
124
|
-
// has a LOOP (what it rests on) and ONE-SHOTS played over it; any call overrides what's there over its own fade:
|
|
125
|
-
hero.anim.clips / hero.anim.clip('Run') / hero.anim.clip(0) // AnimationClip[] (file order) / by name / by index; unnamed clips = 'animation_0', …
|
|
126
|
-
hero.anim.playLoop('Idle', { fade: 0.2 }) // rest on a looping clip (crossfades from whatever plays); hero.anim.loop reads it
|
|
127
|
-
const loco = hero.anim.playLoop({ Idle: 0, Walking: 2, 'Fast Run': 6 }) // a BLEND as the loop (1D; [x, y] positions = 2D)
|
|
128
|
-
loco.value = speed // position on the axis — members stay in phase (no foot sliding)
|
|
129
|
-
const ok = await hero.anim.play('slash', { fadeIn: 0.1, fadeOut: 0.3 }) // one-shot over the loop, returns to it
|
|
130
|
-
// fades default to 0 (cut): fade = both ends, fadeIn / fadeOut = one end. Unknown name warns, plays nothing.
|
|
131
|
-
// The await resolves at the HAND-OVER (end − fadeOut) with true (false = cut short) — what you start right after is what
|
|
132
|
-
// the clip fades into: chains crossfade: if (ok) await hero.anim.play('slash2', { fadeIn: 0.3, fadeOut: 0.3 })
|
|
133
|
-
// then hero.anim.playLoop('Crouch', { fade: 0.3 }) — or nothing = back to the current loop.
|
|
134
|
-
if (!hero.anim.busy) hero.anim.play('kick') // busy = a one-shot hasn't handed over; play() always takes over otherwise
|
|
135
|
-
hero.anim.stop({ fade: 0.2 }) // everything off → rest; hero.anim.busy; hero.anim.speed = 0.3 (0 = pause)
|
|
136
|
-
// p = play(): p.done (return THIS from async fns) / p.playing / p.progress / p.weight / p.stop({ fade }) / p.seek(t)
|
|
137
|
-
// Clips from other files (Mixamo: one GLB per animation), procedural, sliced — add by name:
|
|
138
|
-
const [idle, slash] = await Promise.all([AnimationClip.load(asset('./idle.glb')), AnimationClip.load(asset('./attacks.glb'), 'Slash')])
|
|
139
|
-
hero.anim.addClip('slash', slash).addClip('kick', hero.anim.clip('Kick').slice(0.2, 1.1)) // slice = a sub-range, re-timed
|
|
140
|
-
AnimationClip.from({ tracks: { Hips: { position: [[0, [0,0,0]], [1, [0,0.05,0]]] } } }) // curves in code, binds by node NAME
|
|
141
|
-
// Clip events live on the CLIP, in SECONDS: slash.addEvent(0.4, 'hit') → hero.anim.on('hit', (clip, layer) => dealDamage())
|
|
142
|
-
// Layers (legs walk, arms aim): const upper = hero.anim.addLayer({ mask: 'Spine1' }); upper.playLoop('Aim', { fade: 0.3 }); upper.stop()
|
|
143
|
-
// { additive: true } = each clip's DELTA vs its first frame on top (recoil, breathe); upper.weight = 0.5
|
|
144
|
-
// Root motion (clips whose hips travel — Mixamo without 'In Place', rolls): hero.anim.rootMotion = true → the node moves
|
|
145
|
-
// (or its CharacterController, as a velocity — it collides); the bone stays put
|
|
146
|
-
hero.bone('RightHand')?.add(sword) // bones are Nodes — sockets
|
|
147
|
-
// FBX assets (Mixamo exports FBX): `lecodes assets convert hero.fbx --clips Idle.fbx Run.fbx -o hero.glb` → one GLB, clips merged by bone name
|
|
148
|
-
// IK — late-phase aspects ON BONES (over the Animator's pose): TwoBone on the END bone, LookAt on the bone itself
|
|
149
|
-
hero.bone('LeftFoot').aspect(IK.TwoBone, { target: footPoint, pole: kneeHint }).ik.weight = grounded ? 1 : 0
|
|
150
|
-
hero.bone('Head').aspect(IK.LookAt, { target: camera, limit: 70 }) // axis = bone-local forward, default [0,0,-1]
|
|
151
|
-
|
|
152
|
-
// ===== PHYSICS (Jolt) =====
|
|
153
|
-
|
|
154
|
-
Physics.supported // static boolean — without it EVERYTHING here silently no-ops
|
|
155
|
-
// (aspects attach fine, velocity reads [0,0,0], no events) — gate gameplay
|
|
156
|
-
Physics.configure({ gravity?: [0, -9.81, 0], maxBodies?: 4096 }) // ONCE, BEFORE any body exists
|
|
157
|
-
Physics.interpolation = true // static; default on — smooths bodies between fixed 60 Hz steps
|
|
158
|
-
|
|
159
|
-
// Shape — collision geometry AND pickability. MUST be attached BEFORE Physics/Trigger/CharacterController
|
|
160
|
-
// (they throw "requires a Shape aspect" otherwise). Pick exactly one kind:
|
|
161
|
-
node.aspect(Shape, {
|
|
162
|
-
box?: Vec3Like, // HALF-extents, WORLD units, NOT scaled by the node
|
|
163
|
-
sphere?: number, // radius
|
|
164
|
-
cylinder?: { halfHeight: number, radius: number }, // Y-aligned
|
|
165
|
-
capsule?: { halfHeight: number, radius: number }, // halfHeight = cylinder section only
|
|
166
|
-
mesh?: true | 'convex', // the node's OWN triangles (Mesh geometry or Model GLB, × world scale)
|
|
167
|
-
origin?: Vec3Like, // collider centre, offset from the node's pivot (world units, node rotation, NOT scaled)
|
|
168
|
-
raycast?: boolean, // default true; false = invisible to taps & raycasts
|
|
169
|
-
})
|
|
170
|
-
// mesh: true = exact triangle mesh → level geometry / terrain / ramps: static or kinematic Physics, Trigger,
|
|
171
|
-
// pick bodies, character ground. NEVER dynamic (Physics throws). mesh: 'convex' = convex hull → dynamic props.
|
|
172
|
-
// A Model collider is its bind pose (skinned parts skipped) — animated characters keep a capsule.
|
|
173
|
-
node.aspect(Shape, {}) // auto: box from the mesh AABB × world scale — right for most meshes
|
|
174
|
-
// origin: art modelled ABOVE its pivot (a character on its feet, a barrel on its base) needs the
|
|
175
|
-
// collider lifted: { capsule: {…}, origin: [0, 0.9, 0] }. An auto shape already centres on its mesh.
|
|
176
|
-
node.shape.fit(kind?) // measure dimensions + origin from what the node RENDERS (subtree,
|
|
177
|
-
// so a Model root works where {} can't) and rebuild the live collider. Fit AFTER Model.load resolves.
|
|
178
|
-
// A bare Shape (no Physics) creates a STATIC pick-only body → the node gets 'click'/'touchstart'.
|
|
179
|
-
// It snapshots the transform at attach time — a MOVING pickable object needs a Physics body too.
|
|
180
|
-
|
|
181
|
-
// Physics — rigid body:
|
|
182
|
-
node.aspect(Shape, {}).aspect(Physics, {
|
|
183
|
-
motion?: 'static' | 'dynamic' | 'kinematic', // default 'dynamic'
|
|
184
|
-
mass?: number, // kg, dynamic only; default 1
|
|
185
|
-
})
|
|
186
|
-
node.physics.velocity // Vec3 world units/s — get (fresh copy) / set
|
|
187
|
-
node.physics.angularVelocity // Vec3 DEGREES/s about each world axis — get / set
|
|
188
|
-
node.physics.applyImpulse(v): this // instant impulse, wakes the body
|
|
189
|
-
node.position = [x, y, z] // place / teleport — moves the node AND its body (no moveTo())
|
|
190
|
-
node.eulerAngles = [0, 90, 0] // orientation is routed to the body too (NOT for a CharacterController)
|
|
191
|
-
// A teleport keeps BOTH velocities: putting an object down is velocity = 0, angularVelocity = 0, position = p
|
|
192
|
-
// PHYSICS OWNS a dynamic body's transform: a position write teleports, but the sim takes over again —
|
|
193
|
-
// drive motion with velocity / applyImpulse. READING node.position/worldPosition is always correct.
|
|
194
|
-
// static = never moves (floors, walls); kinematic = write position each frame, pushes but isn't pushed.
|
|
195
|
-
|
|
196
|
-
// Contact events — on BOTH nodes of a contact/overlap, arg = the other node:
|
|
197
|
-
crate.addEventListener('enter', (other: Node) => {})
|
|
198
|
-
crate.addEventListener('exit', (other: Node) => {})
|
|
199
|
-
|
|
200
|
-
// Trigger — static sensor zone (needs a Shape; no options). Overlapping bodies fire 'enter'/'exit'
|
|
201
|
-
// instead of colliding; never blocks movement:
|
|
202
|
-
const goal = Mesh.box({ position: [0, 1, -6] }).aspect(Shape, { box: [1, 1, 0.2] }).aspect(Trigger)
|
|
203
|
-
goal.addEventListener('enter', other => win(other))
|
|
204
|
-
goal.trigger.moveTo(p)
|
|
205
|
-
|
|
206
|
-
// Raycast — closest PICKABLE body (any Shape with raycast: true, incl. pick-only & triggers):
|
|
207
|
-
Physics.raycast(origin: Vec3Like, dir: Vec3Like, maxDist = 1000)
|
|
208
|
-
// → { node: Node | null, point: Vec3, normal: Vec3, fraction: number /* 0..1 of maxDist×|dir| */ } | null
|
|
209
|
-
|
|
210
|
-
// ===== PARTICLES =====
|
|
211
|
-
// GPU particle emitter node — move/rotate the node to move the emitter. Every option is a live setter.
|
|
212
|
-
new Particles({
|
|
213
|
-
material: Material, // REQUIRED — Material.unlit() is the usual choice
|
|
214
|
-
rate?: number, // particles/second (plain number only — a range is ignored)
|
|
215
|
-
shape?: { type: 'point', v: Vec3Like } | { type: 'box', min: Vec3Like, max: Vec3Like },
|
|
216
|
-
startVelocity?: Vec3Like | { // fixed vector, or a direction mode:
|
|
217
|
-
dir?: Vec3Like, from?: Vec3Like, to?: Vec3Like, // along / away-from / toward
|
|
218
|
-
speed?: number | { min, max },
|
|
219
|
-
randomizeAngle?: { min: Vec3Like, max: Vec3Like }, // jitter, degrees per axis
|
|
220
|
-
},
|
|
221
|
-
gravity?: number, drag?: number,
|
|
222
|
-
lifetime?: number | { min, max }, // seconds; range = random per particle
|
|
223
|
-
color?: string | { min, max } | DynamicColor,
|
|
224
|
-
size?: number | { min, max } | DynamicValue,
|
|
225
|
-
rotation?: number | { min, max } | DynamicValue,
|
|
226
|
-
noise?: { strength?, frequency?, speed? } | null, // turbulence
|
|
227
|
-
})
|
|
228
|
-
sparks.spawn(count) // burst on top of rate; one-shot effect = rate: 0 + spawn(n)
|
|
229
|
-
// Curves over each particle's life (t runs 0..1): flat (t, value, t, value, …) stop lists:
|
|
230
|
-
sparks.size = dynamic(0.1, 'multiply', 0, 1, 1, 4) // grow to 4× by death
|
|
231
|
-
sparks.color = dynamicColor('#ffaa33', 0.7, '#ffffff', 1, '#000000') // multiplies base — fade to black
|
|
232
|
-
sparks.destroy() // from Node — removes the system
|
|
233
|
-
|
|
234
|
-
// ===== CAMERA / RAY / PLANE / NOISE =====
|
|
235
|
-
|
|
236
|
-
scene.camera // a Node — never constructed; every scene owns one.
|
|
237
|
-
// (Who MOVES it is mode-specific — see the Scene/AR section.)
|
|
238
|
-
camera.fov / .near / .far // lens: vertical FOV degrees (60), clip 0.01 / 1000 — SETTABLE.
|
|
239
|
-
camera.setProjection({ fov, near, far }) // any subset; .horizontalFov, .displaySize stay read-only
|
|
240
|
-
camera.getViewDirection(screenX, screenY): Vec3 // world dir through a screen point (logical px = ev.clientX/Y)
|
|
241
|
-
camera.getRay(screenX, screenY): Ray // origin = camera.worldPosition + that direction
|
|
242
|
-
|
|
243
|
-
new Ray(origin, dir) // .origin, .dir, .getPoint(t) → origin + t·dir
|
|
244
|
-
new Plane(normal, point) // infinite math plane (NOT a physics body)
|
|
245
|
-
plane.intersectRay(ray): number | null // returns the PARAMETER t, not the point → ray.getPoint(t)
|
|
246
|
-
plane.signedDistance(p) / plane.projectPoint(p)
|
|
247
|
-
|
|
248
|
-
// Drag-on-ground recipe — tap/drag to a world point on the floor:
|
|
249
|
-
const ground = new Plane([0, 1, 0], [0, 0, 0])
|
|
250
|
-
scene.addEventListener('click', ev => {
|
|
251
|
-
const ray = scene.camera.getRay(ev.clientX, ev.clientY)
|
|
252
|
-
const t = ground.intersectRay(ray)
|
|
253
|
-
if (t !== null) marker.position = ray.getPoint(t)
|
|
254
|
-
})
|
|
255
|
-
// Against real bodies instead: Physics.raycast(ray.origin, ray.dir, 100)
|
|
256
|
-
|
|
257
|
-
const noise = new Noise() // native Perlin/fractal; deterministic — animate by moving the sample point
|
|
258
|
-
noise.get(x, y) / noise.get3d(x, y, z)
|
|
259
|
-
noise.frequency = 0.01; noise.octaves = 1 // + fractalLacunarity, fractalGain
|
|
260
|
-
|
|
261
|
-
// ===== COMMON MISTAKES — DO NOT DO THESE =====
|
|
262
|
-
|
|
263
|
-
// ❌ wrong accessor name
|
|
264
|
-
node.body.applyImpulse(...) // WRONG — the accessor is node.physics
|
|
265
|
-
// ✅ node.physics / node.trigger / node.controller / model.anim
|
|
266
|
-
|
|
267
|
-
// ❌ writing a dynamic body's transform per frame — physics owns it and fights back
|
|
268
|
-
setLoop(() => { crate.position = target })
|
|
269
|
-
// ✅ drive the simulation: crate.physics.velocity = ... / applyImpulse(...); kinematic → moveTo(...)
|
|
270
|
-
|
|
271
|
-
// ❌ Physics/Trigger/CharacterController before Shape — throws
|
|
272
|
-
node.aspect(Physics).aspect(Shape, {})
|
|
273
|
-
// ✅ Shape FIRST
|
|
274
|
-
node.aspect(Shape, {}).aspect(Physics)
|
|
275
|
-
|
|
276
|
-
// ❌ full extents in Shape box — collider twice the mesh
|
|
277
|
-
Mesh.box().aspect(Shape, { box: [1, 1, 1] }) // 2×2×2 collider around a 1×1×1 box
|
|
278
|
-
// ✅ HALF-extents, or {} to auto-fit
|
|
279
|
-
Mesh.box().aspect(Shape, { box: [0.5, 0.5, 0.5] })
|
|
280
|
-
|
|
281
|
-
// ❌ cloning a Model
|
|
282
|
-
const copy = model.clone() // doesn't exist
|
|
283
|
-
// ✅ pool: Promise.all(Array.from({ length: N }, () => Model.load(url))) up front, recycle
|
|
284
|
-
|
|
285
|
-
// ❌ a ground plane with the default normal — it stands upright facing the camera
|
|
286
|
-
Mesh.plane({ scale: 20 })
|
|
287
|
-
// ✅ Mesh.plane({ scale: 20, normal: [0, 1, 0] })
|
|
288
|
-
|
|
289
|
-
// ❌ Vec3/Quat as a custom-shader uniform value — lowers to NULL
|
|
290
|
-
mat.set('lightDir', dir)
|
|
291
|
-
// ✅ spread to a plain array: mat.set('lightDir', [...dir])
|
|
292
|
-
|
|
293
|
-
// ❌ point/spot lights — they don't exist
|
|
294
|
-
Light.point(...)
|
|
295
|
-
// ✅ Light.sun() + scene IBL is the whole lighting model; fake glows with unlit/bloom/particles
|
|
296
|
-
|
|
297
|
-
// ❌ a fresh Material per frame (or per particle) to animate a look
|
|
298
|
-
setLoop(() => { mesh.material = Material.lit({ color: next() }) })
|
|
299
|
-
// ✅ mutate the one material: mesh.material.color = next() / mat.set('progress', t)
|
package/prompts/README.md
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
# Codegen prompts
|
|
2
|
-
|
|
3
|
-
> **Scope: the le.codes PLATFORM assistant only.** These prompts are bundled into `dist/` and served
|
|
4
|
-
> by claude-proxy to the in-editor chat. They are NOT used by local development — Claude Code and
|
|
5
|
-
> `lecodes-cli` never read them; those get the SDK surface from `.lecodes/types` (the d.ts bundle),
|
|
6
|
-
> the generated project `CLAUDE.md` (`lecodes-cli/src/commands/init/templates.ts`) and `sdk/docs`.
|
|
7
|
-
> A change meant for local development belongs there, not here.
|
|
8
|
-
|
|
9
|
-
The system prompt(s) sent to Claude for LeCodes code generation, split into composable modules so
|
|
10
|
-
a request only pays for the API areas it can actually use. Facts come from `../docs/` (the source
|
|
11
|
-
of truth) — when the SDK changes, update docs first, then mirror the change here.
|
|
12
|
-
|
|
13
|
-
**Consumers:**
|
|
14
|
-
- `claude-proxy/prompts/*.md` — deployed copies of `dist/` + `router.md` (see its README); the
|
|
15
|
-
proxy injects them per the `x-lecodes-bundle` header (`claude-proxy/src/proxy/bundles.ts`). An
|
|
16
|
-
unknown/missing header is a **400** (no silent fallback), and the proxy's `/health` exposes a
|
|
17
|
-
sha256-12 per bundle — `bun run check:prompts` (repo root) compares them against `dist/` to
|
|
18
|
-
catch deploy drift (`--dir <proxy>/prompts` checks a local checkout instead).
|
|
19
|
-
- `packages/frontend/.../chat/engine/bundleSelect.ts` — a thin adapter over `select.ts` (imported
|
|
20
|
-
as `sdk/prompts/select`); the selection logic itself is not duplicated anywhere.
|
|
21
|
-
|
|
22
|
-
## Layout
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
core.md shared foundation: response format, entry-point rule, runtime, math, tweens
|
|
26
|
-
index.md one-line map of EVERY global — always included (anti-hallucination anchor)
|
|
27
|
-
canvas.md Canvas drawing surface — engine-independent, in every bundle (each engine
|
|
28
|
-
module documents its own hookup: Sprite texture / UIImage src / Texture)
|
|
29
|
-
ui.md UI module (screens, router, styling, widgets, lists)
|
|
30
|
-
2d.md 2D engine module (sprites, tilemaps, Box2D physics, aspects, Canvas)
|
|
31
|
-
3d.md SHARED 3D content (nodes, meshes, models, materials, physics, particles)
|
|
32
|
-
3d-scene.md non-AR scenes: Scene construction, camera control, CharacterController, example
|
|
33
|
-
3d-scene-files.md declarative .scene.ts files (defineScene/use/ref/make, SceneHandle, HUD-over-scene)
|
|
34
|
-
— the visual scene editor's format; non-AR only (camera nodes, skybox env)
|
|
35
|
-
ar.md AR scenes: ARScene, anchors, placement controls, example — never with 3d-scene.md
|
|
36
|
-
core-design.md design mode's core: directives/context/reports, no runtime APIs (no timers/network)
|
|
37
|
-
ui-design.md design mode's UI: curated static-mockup subset of ui.md (KEEP IN SYNC with it)
|
|
38
|
-
design.md design mode: canvas conventions — kit-first, states, design_map tool, tab bar
|
|
39
|
-
concept.md concept mode: product-shaping conversation, writes ONLY design/spec.md
|
|
40
|
-
compose.ts builds dist/<bundle>.md from the modules
|
|
41
|
-
select.ts deterministic bundle detection from project code
|
|
42
|
-
router-prompt.md fallback classifier prompt (empty projects only)
|
|
43
|
-
namer-prompt.md project name + icon-emoji generator (first request of a new project)
|
|
44
|
-
intake-prompt.md new-project intake: clarifying questions + confirmable brief (two Haiku turns)
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Bundles (`bun run prompts/compose.ts` → `dist/`)
|
|
48
|
-
|
|
49
|
-
| Bundle | Modules | For |
|
|
50
|
-
|---|---|---|
|
|
51
|
-
| `ui-app` | core + index + canvas + ui | apps: screens, forms, lists, chat, commerce |
|
|
52
|
-
| `2d-game` | core + index + canvas + 2d + ui | flat games, drawing toys |
|
|
53
|
-
| `3d-app` | core + index + canvas + 3d + 3d-scene + 3d-scene-files + ui | 3D games/viewers, scene-editor projects |
|
|
54
|
-
| `ar-app` | core + index + canvas + 3d + ar + ui | AR experiences |
|
|
55
|
-
| `design` | core-design + ui-design + design | design mode: the design/ prototyping canvas |
|
|
56
|
-
| `concept` | concept | concept mode: shape the idea, maintain design/spec.md (~0.8k tokens) |
|
|
57
|
-
|
|
58
|
-
Fixed bundles (not per-request assembly) are deliberate: **each bundle is a stable prompt prefix**,
|
|
59
|
-
so Anthropic prompt caching hits on every generation after the first (~10× cheaper input, lower
|
|
60
|
-
latency). Per-request cherry-picking would produce near-unique prefixes and always pay full price.
|
|
61
|
-
Game bundles include the full UI module because games need HUDs/menus; the ui-app bundle omits the
|
|
62
|
-
engines entirely.
|
|
63
|
-
|
|
64
|
-
## Selecting the bundle
|
|
65
|
-
|
|
66
|
-
`design` and `concept` are different from the engine bundles: they are selected EXPLICITLY by the
|
|
67
|
-
client from the editor's mode switcher (Concept / Design / Build — `x-lecodes-bundle: design` or
|
|
68
|
-
`concept`) — never routed, detected from code, or reachable via the `<bundle>` escalation. The
|
|
69
|
-
engine ladder below applies to Build mode only. Concept turns also carry no code context (design
|
|
70
|
-
excerpts + file names only) and the client enforces each mode's write boundary (concept:
|
|
71
|
-
design/spec.md only; design: design/ only). Any bundle may end a reply with `<mode>x</mode>` — the
|
|
72
|
-
client strips it and renders a "Switch to …" button; the mode never switches itself.
|
|
73
|
-
|
|
74
|
-
For the engine bundles — three signals, in this order:
|
|
75
|
-
|
|
76
|
-
**1. Project code — `select.ts` (primary, deterministic).**
|
|
77
|
-
`detectBundle(files)` scans the project's `.ts` sources for engine globals (comments and string
|
|
78
|
-
literals stripped) and returns the bundle. This is language-independent — it doesn't matter what
|
|
79
|
-
language the user writes in, because it never reads the user's words at all. It covers every
|
|
80
|
-
request against an existing project, i.e. the vast majority of traffic. Priority: AR > 3D > 2D;
|
|
81
|
-
sources with no engine references → `ui-app`. This mirrors the engine-kind detection the compiler
|
|
82
|
-
already does for project icons — if that detection is exposed server-side, reuse it instead.
|
|
83
|
-
|
|
84
|
-
**0. Intake — `intake-prompt.md` (new projects, preferred over the router).**
|
|
85
|
-
The session's first request against a project with no app code runs the intake instead of building:
|
|
86
|
-
one Haiku call returns 2–4 clarifying questions with tappable options (including the app kind when
|
|
87
|
-
the request is ambiguous between kinds), a second call turns the answers into a short brief the
|
|
88
|
-
user confirms. The confirmed kind sets the first build turn's bundle explicitly (`x-lecodes-bundle`
|
|
89
|
-
sent by the client — the router below never runs), and the brief seeds `design/spec.md` as the
|
|
90
|
-
project concept. Intake failure or the user's Skip degrades to the flow below — it never blocks.
|
|
91
|
-
Client side: `frontend/.../chat/engine/intake.ts` + the intake card in `chatController.ts`.
|
|
92
|
-
|
|
93
|
-
**2. Router model — `router-prompt.md` (fallback: intake skipped/failed on empty projects).**
|
|
94
|
-
When `detectBundle` returns `null` (no code yet), classify the user's first message with one cheap
|
|
95
|
-
call: `claude-haiku-4-5`, temperature 0, `max_tokens: 8`, reply is a single word `ui|2d|3d|ar`.
|
|
96
|
-
The prompt is meaning-based and multilingual (EN/RU examples included). Parse defensively; any
|
|
97
|
-
garbage → `ui`. Cost/latency: ~1k input tokens, a few output tokens, ~200–400 ms — negligible, and
|
|
98
|
-
it runs at most once per project's life.
|
|
99
|
-
|
|
100
|
-
**Stickiness.** Cache the chosen bundle per conversation and only move UP
|
|
101
|
-
(`upgradeBundle(current, detected)`: ui → 2d/3d → ar), re-running `detectBundle` over files the
|
|
102
|
-
assistant itself just generated. Never downgrade mid-conversation — it would invalidate the prompt
|
|
103
|
-
cache and confuse in-flight context. Wrong-but-bigger is harmless; wrong-but-smaller causes
|
|
104
|
-
hallucinated APIs.
|
|
105
|
-
|
|
106
|
-
**3. Escalation — the `<bundle>` directive (recovery from a misroute).**
|
|
107
|
-
Code-based detection can never recover from a wrong-but-smaller bundle on its own: a model that
|
|
108
|
-
wasn't shown an engine never emits its globals, so the upgrade never triggers. core.md ("Missing
|
|
109
|
-
engine") therefore instructs the generator to reply with ONLY `<bundle>ar</bundle>` (or `3d`/`2d`)
|
|
110
|
-
when the request needs engine APIs its prompt doesn't document. The client (`parseBundleDirective`
|
|
111
|
-
in `select.ts`) discards that reply, upgrades the pinned bundle, and re-sends the same request
|
|
112
|
-
once — upgrade-only here too; a non-upgradable directive (e.g. a 3D project asking for the 2D
|
|
113
|
-
engine) surfaces as a plain explanation instead. Relatedly, the router's classifier input must be
|
|
114
|
-
ONLY the user's words: the proxy cuts everything before the frontend's `[Request]` marker, so e.g.
|
|
115
|
-
a `.glb` in the project snapshot can't drag an explicit AR request toward `3d`.
|
|
116
|
-
|
|
117
|
-
## Naming new projects
|
|
118
|
-
|
|
119
|
-
`namer-prompt.md` is unrelated to bundle selection but lives here with the other model prompts:
|
|
120
|
-
an assistant request made while the project has NO CODE yet (no non-empty `.ts` file — uploaded
|
|
121
|
-
assets like a `.glb` model don't count as "started") also fires one extra Haiku call in parallel
|
|
122
|
-
(`x-lecodes-bundle: namer`, `chat/engine/projectNamer.ts`) that returns bare JSON `{name, emoji}`;
|
|
123
|
-
the result is saved via the normal project-update API (name + `iconEmoji`, drawn by
|
|
124
|
-
`VProjectIcon`). Uncached — the prompt is far below Haiku's minimum cacheable prefix. Parsing is
|
|
125
|
-
defensive: a garbled reply must never rename the project, so it's rejected rather than truncated
|
|
126
|
-
into place; a rename while the call is in flight also wins over the generated name.
|
|
127
|
-
|
|
128
|
-
**Why not route every request with a model?** It adds a failure mode (misroute → the generator
|
|
129
|
-
confidently invents APIs it wasn't shown), latency, and cost on 100% of traffic to solve a problem
|
|
130
|
-
that exists only for the ~one request per project where no code exists. Residual misroutes are
|
|
131
|
-
recoverable: the index.md map means the generator always knows what exists, and the `<bundle>`
|
|
132
|
-
escalation directive (above) lets it swap in the right engine module instead of guessing.
|
|
133
|
-
|
|
134
|
-
## Keeping prompts honest
|
|
135
|
-
|
|
136
|
-
- `index.md` must list every global exported from `sdk/src/inject.ts` — same contract as
|
|
137
|
-
`docs/check.mjs`. When a global is added, update docs, then the module, then index.md.
|
|
138
|
-
- Known corrections baked into these prompts (do not regress; see `docs/AUTHORING.md`):
|
|
139
|
-
`onEndReached(thresholdPx, cb)` (threshold first), `bgSize: cover|contain|fill|tile`,
|
|
140
|
-
track `deltaX/deltaY` are per-move, colors are CSS strings / 0xRRGGBB / [r,g,b(,a)] everywhere, `animate` can't
|
|
141
|
-
tween strings/colors, `node.physics` (not `.body`), no `Camera2D.follow`, no
|
|
142
|
-
`UIScrollable.scrollTo`, models can't be cloned.
|
package/prompts/ar.md
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
## AR
|
|
2
|
-
|
|
3
|
-
ARScene = a scene over the device camera with world tracking, from the "ar" plugin — the app lists it
|
|
4
|
-
in app.json: `"plugins": { "ar": "*" }` (without it `ARScene.isSupported` is false and open() rejects).
|
|
5
|
-
Everything from the 3D-engine section applies (nodes, meshes, models, materials, physics, events);
|
|
6
|
-
THIS section is how an AR scene is created and driven. The session moves scene.camera every frame and
|
|
7
|
-
draws the camera image behind everything (no skybox shows); lit materials still need the default IBL
|
|
8
|
-
and/or a Light.sun.
|
|
9
|
-
|
|
10
|
-
// ===== ARSCENE =====
|
|
11
|
-
|
|
12
|
-
new ARScene(options?: {
|
|
13
|
-
ibl?: boolean // image-based ambient light; default TRUE
|
|
14
|
-
environmentIntensity?: number // IBL strength; default 20000
|
|
15
|
-
mode?: 'world' | 'images' // 'world' (default): planes, hitTest, anchors, images.
|
|
16
|
-
// 'images': image targets only — no planes, no hitTest, no root
|
|
17
|
-
})
|
|
18
|
-
ARScene.isSupported // static: the host has the plugin — check before an AR button
|
|
19
|
-
|
|
20
|
-
await scene.open(options?: { // ASYNC: camera permission → warm render → session → on screen.
|
|
21
|
-
planes?: 'horizontal' | 'vertical' | 'both' | 'none' // default 'horizontal'
|
|
22
|
-
lightEstimation?: boolean // 'light' events
|
|
23
|
-
followLight?: boolean // the scene's ambient follows the estimated light
|
|
24
|
-
transition?: Transition
|
|
25
|
-
}) // REJECTS: camera denied, 'unavailable' (no ARKit/ARCore), superseded —
|
|
26
|
-
// always await and handle failure (fall back / show a message).
|
|
27
|
-
scene.close() // stops the session (the camera is let go)
|
|
28
|
-
|
|
29
|
-
// ===== TRACKING =====
|
|
30
|
-
scene.trackingState // 'notAvailable' | 'limited' | 'normal'
|
|
31
|
-
scene.trackingReason // with 'limited': 'initializing' | 'excessiveMotion' | 'insufficientFeatures' | 'relocalizing'
|
|
32
|
-
scene.addEventListener('tracking', (state, reason) => {})
|
|
33
|
-
scene.addEventListener('error', ({ code, message }) => {}) // the session failed and is over
|
|
34
|
-
|
|
35
|
-
// ===== ANCHORS =====
|
|
36
|
-
// An Anchor is a Node the session moves — parent content under one (it joins the scene with it).
|
|
37
|
-
|
|
38
|
-
scene.root // Anchor: pinned on the FIRST HORIZONTAL PLANE found ('track' fires then);
|
|
39
|
-
// until then at the world's origin. Not in 'images' mode.
|
|
40
|
-
scene.anchor(pose) // Anchor at a pose ({ position, rotation }, e.g. a hitTest result's)
|
|
41
|
-
scene.createAnchor(source: FetchResponse, physicalWidth = 0.2): ImageAnchor
|
|
42
|
-
// Image target: `source` = the marker image (fetch it), `physicalWidth` = its printed width in
|
|
43
|
-
// METERS. Hidden until the image is seen. A RIGID pose in meters (no scale): its centre, Y out of
|
|
44
|
-
// the image, the image in XZ (Z toward its bottom). Its size: anchor.extent = [width, height] m
|
|
45
|
-
// (refined while tracked; [physicalWidth, 0] before the first 'track'). A plane on it:
|
|
46
|
-
// const s = Mesh.plane({ material, eulerAngles: [-90, 0, 0] }); anchor.add(s)
|
|
47
|
-
// anchor.addEventListener('track', () => s.scale = [anchor.extent[0], anchor.extent[1], 1])
|
|
48
|
-
anchor.tracked // the session holds it now
|
|
49
|
-
anchor.addEventListener('track', () => {})
|
|
50
|
-
anchor.addEventListener('untrack', () => {})
|
|
51
|
-
anchor.remove() // let it go; it leaves the scene
|
|
52
|
-
|
|
53
|
-
// ===== PLANES & HIT TEST =====
|
|
54
|
-
scene.planes // ARPlane[]: plane.type ('horizontal' | 'vertical'), plane.extent [w, d] m,
|
|
55
|
-
// plane.polygon (x,z pairs in the plane's space), plane.center (Vec3)
|
|
56
|
-
scene.addEventListener('plane', ({ plane, change, mergedInto }) => {}) // change: 'added' | 'updated' | 'removed'
|
|
57
|
-
scene.showPlanes = true // a grid drawn on every plane
|
|
58
|
-
const hits = await scene.hitTest(ev.clientX, ev.clientY, 'planes') // a scene touch's point → [{ pose, type, plane, distance }], nearest first; [] = nothing
|
|
59
|
-
|
|
60
|
-
// ===== LIGHT & CAPTURE =====
|
|
61
|
-
scene.lightEstimate // { intensity (1000 = neutral), temperature (K) } | null
|
|
62
|
-
const photo = await scene.captureFrame() // the camera image alone: a JPEG File
|
|
63
|
-
const shot = await scene.capture() // what the scene shows (camera + 3D): a PNG File
|
|
64
|
-
|
|
65
|
-
// ===== PLACEMENT CONTROLS =====
|
|
66
|
-
const controls = scene.addControls(target: Node, options?: {
|
|
67
|
-
minScale?: number, // pinch clamp; default 0.08
|
|
68
|
-
maxScale?: number, // default 2
|
|
69
|
-
pan?: boolean, // one finger: over the plane under the finger; default true
|
|
70
|
-
pinch?: boolean, // two-finger uniform scale; default true
|
|
71
|
-
twist?: boolean, // two-finger rotate around Y; default true
|
|
72
|
-
})
|
|
73
|
-
controls.remove() // detach the gesture listeners
|
|
74
|
-
|
|
75
|
-
// ===== ON THE WEB =====
|
|
76
|
-
// The camera under the scene, the camera turned by the phone's sensors, its position fixed: tracking
|
|
77
|
-
// is 'normal' at once, NO planes, NO images, hitTest resolves [], root never fires 'track' (it stays
|
|
78
|
-
// at the origin) — don't gate content on root's 'track' if the app must work in a browser.
|
|
79
|
-
|
|
80
|
-
// ===== AR RULES & MISTAKES =====
|
|
81
|
-
|
|
82
|
-
// The camera is the session's — READ it (position, getRay), NEVER place it.
|
|
83
|
-
// ❌ moving the camera to frame content — the session overwrites it every frame
|
|
84
|
-
arScene.camera.position = [0, 2, 4]
|
|
85
|
-
// ✅ move the WORLD: parent content under scene.root (or an anchor) and place THAT
|
|
86
|
-
scene.root.add(model); model.position = [0, 0, -1]
|
|
87
|
-
|
|
88
|
-
// ❌ forgetting the plugin: ARScene without "ar" in app.json "plugins" is unsupported everywhere
|
|
89
|
-
// ✅ app.json: { "plugins": { "ar": "*" } }
|
|
90
|
-
|
|
91
|
-
// ❌ treating open() like Scene.open()
|
|
92
|
-
arScene.open(); spawnStuff() // runs before the session started; rejection unhandled
|
|
93
|
-
// ✅ await it and handle failure
|
|
94
|
-
try { await arScene.open() } catch { toast('AR is not available on this device') }
|
|
95
|
-
|
|
96
|
-
// ❌ placing on a tap at a guessed distance
|
|
97
|
-
// ✅ hitTest the tap and anchor there
|
|
98
|
-
scene.addEventListener('click', async (ev) => {
|
|
99
|
-
const [hit] = await scene.hitTest(ev.clientX, ev.clientY, 'planes')
|
|
100
|
-
if (hit) scene.anchor(hit.pose).add(lamp.clone())
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
// Shadows on the real floor: a Mesh.plane({ normal: [0, 1, 0], material: Material.shadow() })
|
|
104
|
-
// under the object — invisible except where shadows fall.
|
|
105
|
-
|
|
106
|
-
## AR example
|
|
107
|
-
|
|
108
|
-
// Object-placement app: a GLB on the floor once it is found, drag/pinch/twist to position it,
|
|
109
|
-
// tap it to play its animation, shadow on the real floor, a HUD hint over the scene.
|
|
110
|
-
// app.json lists the plugin: { "plugins": { "ar": "*" } }
|
|
111
|
-
|
|
112
|
-
<file name="main.ts">
|
|
113
|
-
const scene = new ARScene()
|
|
114
|
-
|
|
115
|
-
const hint = UIText('Move your phone to find the floor').style({ color: 'white', fontSize: 16 })
|
|
116
|
-
scene.widgets(UIWidget(hint).style({ position: 'absolute', left: 0, right: 0, bottom: 48, alignItems: 'center' }))
|
|
117
|
-
|
|
118
|
-
async function main() {
|
|
119
|
-
const chair = await Model.load(asset('./chair.glb'))
|
|
120
|
-
chair.aspect(Shape, {}) // pick body → tappable
|
|
121
|
-
chair.addEventListener('click', () => chair.anim.play(0, { loop: false }))
|
|
122
|
-
|
|
123
|
-
// Shadow catcher on the real floor, parented with the model so it follows placement
|
|
124
|
-
const shadow = Mesh.plane({ normal: [0, 1, 0], material: Material.shadow(), scale: 2 })
|
|
125
|
-
chair.add(shadow)
|
|
126
|
-
|
|
127
|
-
// Content lives under the floor anchor (it joins the scene with it)
|
|
128
|
-
scene.root.add(chair)
|
|
129
|
-
scene.add(Light.sun({ shadowsQuality: 2 }))
|
|
130
|
-
scene.root.addEventListener('track', () => { hint.text = 'Drag to move · pinch to scale · tap to animate' })
|
|
131
|
-
|
|
132
|
-
scene.addControls(chair) // drag / pinch-scale / twist gestures
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
await scene.open() // camera permission → session
|
|
136
|
-
} catch {
|
|
137
|
-
toast('AR is not available on this device')
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
main()
|
|
142
|
-
</file>
|
package/prompts/canvas.md
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// ===== CANVAS — retained drawing baked to a texture (engine-independent) =====
|
|
2
|
-
// The way to get custom TEXT and vector graphics anywhere: charts, generated images, labels,
|
|
3
|
-
// paint surfaces. Mirrors the browser 2D context, but RECORDS commands and rasterizes on bake.
|
|
4
|
-
// A baked Canvas is accepted wherever an image/texture source is — the exact hookups are listed
|
|
5
|
-
// in the UI / engine sections of this prompt.
|
|
6
|
-
|
|
7
|
-
const c = new Canvas(width, height, { pixelRatio: device.pixelRatio })
|
|
8
|
-
// width/height are LOGICAL units — author all drawing logical; pixelRatio only multiplies baked
|
|
9
|
-
// resolution (crispness), never the on-screen size.
|
|
10
|
-
|
|
11
|
-
c.fillStyle = '#ff8800' // any CSS color string, as everywhere in the SDK;
|
|
12
|
-
c.lineCap = 'round' // also strokeStyle, lineWidth, lineJoin, miterLimit, globalAlpha, letterSpacing,
|
|
13
|
-
c.setLineDash([4, 2]) // dashes; a gradient: c.fillStyle = c.createLinearGradient(x0,y0,x1,y1).addColorStop(0,'#fff').addColorStop(1,'transparent')
|
|
14
|
-
c.font = 'bold 16px sans-serif' // textAlign ('left'|'center'|'right'), textBaseline
|
|
15
|
-
// custom fonts: draw only AFTER await registerFont(...)
|
|
16
|
-
c.fillRect(x, y, w, h) / c.strokeRect / c.clearRect
|
|
17
|
-
c.beginPath().moveTo(x,y).lineTo(x,y).arc(x,y,r,a0,a1).arcTo(x1,y1,x2,y2,r).ellipse(x,y,rx,ry,rot,a0,a1).rect(x,y,w,h).roundRect(x,y,w,h,r).closePath() // all chain
|
|
18
|
-
c.fill('evenodd'?) / c.stroke() / c.clip() / c.fillText(text, x, y) / c.strokeText(text, x, y)
|
|
19
|
-
c.measureText(text): { width, ascent, descent } // in the CURRENT font and letterSpacing, logical px
|
|
20
|
-
c.drawImage(bmp: Bitmap, dx, dy [, dw, dh]) // blits a Bitmap only (c.toBitmap(), await c.loadImage(SvgSource(svg) | fetchResponse))
|
|
21
|
-
|
|
22
|
-
c.update(): this // after redrawing, push new pixels to EVERY texture/image this canvas produced
|
|
23
|
-
c.reset(): this // discard the recorded drawing to start over
|
|
24
|
-
c.resize(w, h): this // new logical size (takes effect next bake; measure-then-resize works pre-bake)
|
|
25
|
-
c.toBitmap(): Bitmap // rasterized snapshot, usable with drawImage
|
|
26
|
-
// GOTCHA: reset() clears the recorded STATE too — font/fillStyle/textAlign fall back to defaults
|
|
27
|
-
// (the getters report the defaults again). Re-set font & friends after every reset().
|
|
28
|
-
// Ever-growing drawings: flatten — const snap = c.toBitmap(); c.reset(); c.drawImage(snap, 0, 0)
|