lecodes-sdk 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/global.d.ts +18 -4
- package/dist/inject.js +260 -361
- package/dist/types/animate/tween/Animation.d.ts +69 -0
- package/dist/types/animate/tween/Timeline.d.ts +55 -0
- package/dist/types/animate/tween/animateValue.d.ts +27 -0
- package/dist/types/animate/tween/easing.d.ts +29 -0
- package/dist/types/animate/tween/spec.d.ts +178 -0
- package/dist/types/g2/Node2D.d.ts +16 -0
- package/dist/types/g2/Sprite.d.ts +11 -1
- package/dist/types/gl/Camera.d.ts +15 -1
- package/dist/types/gl/Foliage.d.ts +47 -0
- package/dist/types/gl/Geometry.d.ts +24 -0
- package/dist/types/gl/Light.d.ts +28 -7
- package/dist/types/gl/Lightmap.d.ts +90 -60
- package/dist/types/gl/Material.d.ts +28 -20
- package/dist/types/gl/Model.d.ts +7 -5
- package/dist/types/gl/Node.d.ts +18 -0
- package/dist/types/gl/Particles.d.ts +55 -1
- package/dist/types/gl/Scene.d.ts +20 -0
- package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
- package/dist/types/gl/animation/Animator.d.ts +27 -0
- package/dist/types/gl/animation/DynamicBone.d.ts +19 -8
- package/dist/types/gl/animation/IK.d.ts +86 -30
- package/dist/types/gl/animation/Locomotion.d.ts +52 -3
- package/dist/types/gl/animation/Warp.d.ts +2 -1
- package/dist/types/gl/animation/core.d.ts +35 -4
- package/dist/types/gl/physics/Ragdoll.d.ts +87 -12
- package/dist/types/gl/terrain/Terrain.d.ts +12 -2
- package/dist/types/inject.d.ts +8 -2
- package/dist/types/runtime/input.d.ts +7 -0
- package/dist/types/scene/defineScene.d.ts +44 -32
- package/dist/types/ui/UIButton.d.ts +3 -1
- package/dist/types/ui/UIInput.d.ts +5 -1
- package/dist/types/ui/UINode.d.ts +24 -24
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/prompts/core-design.md +27 -4
- package/prompts/core.md +35 -6
- package/prompts/dist/2d-game.md +197 -408
- package/prompts/dist/3d-app.md +166 -491
- package/prompts/dist/ar-app.md +163 -373
- package/prompts/dist/design.md +87 -83
- package/prompts/dist/ui-app.md +136 -325
- package/prompts/select.ts +19 -4
- package/src/animate/tween/Animation.ts +378 -0
- package/src/animate/tween/Timeline.ts +175 -0
- package/src/animate/tween/animateValue.ts +100 -0
- package/src/animate/tween/easing.ts +172 -0
- package/src/animate/tween/spec.ts +479 -0
- package/src/audio/audio.ts +161 -161
- package/src/bridges.d.ts +235 -65
- package/src/compile/__tests__/assetMacro.test.ts +26 -0
- package/src/compile/__tests__/detectEntry.test.ts +19 -0
- package/src/compile/__tests__/serverSplit.test.ts +27 -0
- package/src/compile/bundler.ts +34 -4
- package/src/compile/compileProject.ts +31 -1
- package/src/compile/detectEntry.ts +8 -3
- package/src/compile/index.ts +2 -0
- package/src/compile/serverSplit.ts +9 -3
- package/src/g2/Node2D.ts +38 -0
- package/src/g2/Sprite.ts +20 -1
- package/src/gl/Camera.ts +34 -1
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Foliage.ts +102 -0
- package/src/gl/Geometry.ts +393 -348
- package/src/gl/Light.ts +49 -16
- package/src/gl/Lightmap.ts +439 -275
- package/src/gl/Material.ts +59 -47
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Model.ts +23 -12
- package/src/gl/Node.ts +39 -0
- package/src/gl/Particles.ts +80 -2
- package/src/gl/Scene.ts +34 -1
- package/src/gl/animation/AnimationClip.ts +52 -0
- package/src/gl/animation/Animator.ts +42 -2
- package/src/gl/animation/DynamicBone.ts +35 -12
- package/src/gl/animation/IK.ts +173 -152
- package/src/gl/animation/Locomotion.ts +72 -8
- package/src/gl/animation/Playback.ts +5 -4
- package/src/gl/animation/Warp.ts +5 -2
- package/src/gl/animation/core.ts +65 -4
- package/src/gl/physics/Ragdoll.ts +451 -272
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/terrain/Terrain.ts +33 -2
- package/src/inject.ts +236 -226
- package/src/runtime/input.ts +11 -0
- package/src/scene/defineScene.ts +72 -62
- package/src/scene/gizmos.ts +148 -148
- package/src/ui/UIButton.ts +2 -2
- package/src/ui/UIInput.ts +3 -3
- package/src/ui/UINode.ts +61 -36
- package/dist/types/animate/animate.d.ts +0 -20
- package/src/animate/animate.ts +0 -238
package/src/bridges.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ declare global {
|
|
|
22
22
|
removeEntityFromScene(sceneId: number, entityId: number): void
|
|
23
23
|
getSceneMaterial(sceneId: number): number
|
|
24
24
|
warmRender(sceneId: number, onComplete: () => void): void
|
|
25
|
+
/** Compile every shader variant the scene's materials can need — sun / shadows / fog / skinning
|
|
26
|
+
* and the dynamic-light key — on the backend's compiler threads, then call back. Materials
|
|
27
|
+
* loaded later are queued in the background as they are created. Optional: a host without it
|
|
28
|
+
* resolves at once (the SDK feature-detects). */
|
|
29
|
+
precompileShaders?(sceneId: number, onComplete: () => void): void
|
|
25
30
|
|
|
26
31
|
createEntity(): number
|
|
27
32
|
cloneEntity(entityId: number): number
|
|
@@ -53,11 +58,19 @@ declare global {
|
|
|
53
58
|
/** Punctual light. `intensity` is luminous POWER in lumens; `falloff` is the metres of
|
|
54
59
|
* influence (filament's own default is 1 m, i.e. invisible, so it is always passed). */
|
|
55
60
|
createPointLight?(entityId: number, intensity: number, color: number, falloff: number, castShadows: boolean): void
|
|
61
|
+
/** The lightmap bake takes this point light as a RECTANGLE of `width` x `height` metres (an area light: a ceiling
|
|
62
|
+
* panel) in the light's local XZ plane, emitting along its local -Y with the same lumens; 0, 0 = a point again. */
|
|
63
|
+
setLightBakeArea?(entityId: number, width: number, height: number): void
|
|
56
64
|
/** Live intensity for any light (sun: lux, point: lumens) — a flash animates instead of rebuilding. */
|
|
57
65
|
setLightIntensity?(entityId: number, intensity: number): void
|
|
58
66
|
setLightColor?(entityId: number, color: number): void
|
|
59
67
|
/** `iblFetchId` (optional, -1 = none) names the scene's own probe; without it the host looks for a project-wide `ibl.ktx`. */
|
|
60
68
|
setDefaultIbl(sceneId: number, intensity: number, iblFetchId?: number): void
|
|
69
|
+
/** The environment's intensity in lux, live (`scene.environmentIntensity`) — `setDefaultIbl` is
|
|
70
|
+
* the only other way to change it and it rebuilds the cubemap and the IndirectLight from the
|
|
71
|
+
* ktx every call, so it can never carry a slider. No-op until the scene has an IBL. Optional:
|
|
72
|
+
* hosts that predate it keep whatever `setDefaultIbl` set at open. */
|
|
73
|
+
setEnvironmentIntensity?(sceneId: number, intensity: number): void
|
|
61
74
|
setBloomOptions(sceneId: number, enabled: boolean, strength: number, quality: number): void
|
|
62
75
|
setToneMapping?(sceneId: number, mode: number): void
|
|
63
76
|
/** Exposure of the scene's camera — filament's physical model: `aperture` f-stops,
|
|
@@ -299,15 +312,19 @@ declare global {
|
|
|
299
312
|
animatorSetBlend(animatorId: number, layer: number, dims: 1 | 2, slots: Uint16Array, positions: Float32Array, fade: number, speed: number, phases: Float32Array): void
|
|
300
313
|
animatorSetBlendValue(animatorId: number, layer: number, x: number, y: number): void
|
|
301
314
|
// Play a slot as the layer's one-shot, transitioned in over fadeIn (0 = cut) from whatever the layer
|
|
302
|
-
// showed (playing a blend MEMBER brings the whole blend back instead). A non-looping one-shot
|
|
303
|
-
// layer back to the loop
|
|
304
|
-
//
|
|
315
|
+
// showed (playing a blend MEMBER brings the whole blend back instead). A non-looping one-shot plays to its
|
|
316
|
+
// end and hands the layer back to the loop THERE, the return transition taking fadeOut from its last pose
|
|
317
|
+
// (0 = cut back; on a loopless layer hold the last frame).
|
|
305
318
|
// restart = rewind even if already playing (a restart is a new source: it transitions from the pose shown).
|
|
306
319
|
animatorPlay(animatorId: number, slot: number, loop: boolean, speed: number, fadeIn: number, fadeOut: number, restart: boolean): void
|
|
307
320
|
// TURN WARP for the slot's current play (right after animatorPlay): the node's turn from the clip's baked heading is
|
|
308
321
|
// scaled to `radians` total; the pose keeps its own turn, the extra pivots about the planted foot. enabled false =
|
|
309
322
|
// the clip's own turn. Optional: older hosts lack it.
|
|
310
323
|
animatorSlotSetTurn?(animatorId: number, slot: number, radians: number, enabled: boolean): void
|
|
324
|
+
// WINDOW of the slot's current play (right after animatorPlay): the one-shot runs [start, end] clip seconds — enters
|
|
325
|
+
// at start (at end when backwards) if the play rewound, completes + hands over at end, clip events outside never fire.
|
|
326
|
+
// end < 0 = the clip's end. Loops ignore it. Optional: older hosts lack it (the whole clip plays).
|
|
327
|
+
animatorSlotSetWindow?(animatorId: number, slot: number, start: number, end: number): void
|
|
311
328
|
// Release over `fade` (a transition toward what is left — the loop, or the rest pose): one slot; a
|
|
312
329
|
// layer's one-shot + loop (slot -1); everything (layer -1).
|
|
313
330
|
animatorStop(animatorId: number, layer: number, slot: number, fade: number): void
|
|
@@ -440,6 +457,36 @@ declare global {
|
|
|
440
457
|
// anchor xyz, target xyz] (world) — a debug overlay's beam under the foot. False = no such foot.
|
|
441
458
|
animatorFootState?(animatorId: number, side: number, out: Float32Array): boolean
|
|
442
459
|
|
|
460
|
+
// ---- IK chains + sockets (creator-anim anchors.cpp: the gun-master rig's hands) ----
|
|
461
|
+
// The chains are solved in the engine's LATE pass — after the app's update(dt) phase, before the
|
|
462
|
+
// dynamic bones and the skin flush — on the joints as the app left them: a world-target chain reaches
|
|
463
|
+
// the point / rotation given (or its target NODES, read by the engine then), an ANCHORED chain rides
|
|
464
|
+
// a live socket, keeping the animated end bone's pose relative to the same socket on the take's
|
|
465
|
+
// donor and following the playing clips' anchor spans between sockets. Nothing per frame from the
|
|
466
|
+
// SDK. Optional: a host without them has inert IK (the SDK warns once).
|
|
467
|
+
// kind 0 two-bone (the chain = the end entity, its parent, its grandparent) / 1 look-at. 0 = the
|
|
468
|
+
// entity is no joint of the animator.
|
|
469
|
+
animatorIkCreate?(animatorId: number, kind: 0 | 1, endEntityId: number): number
|
|
470
|
+
animatorIkDestroy?(animatorId: number, ik: number): void
|
|
471
|
+
// CANIM_IK_* order: [target xyz, pole xyz, has pole, weight, rotation xyzw, rotation weight, look-at
|
|
472
|
+
// axis xyz, look-at limit°, enabled, anchor time s].
|
|
473
|
+
animatorIkSet?(animatorId: number, ik: number, params: Float32Array): void
|
|
474
|
+
// Nodes the engine reads every late pass (0 = none): the target's world position, the pole's, the
|
|
475
|
+
// rotation node's world rotation — in place of the fixed floats above.
|
|
476
|
+
animatorIkNodes?(animatorId: number, ik: number, targetEntity: number, poleEntity: number, rotationEntity: number): void
|
|
477
|
+
// Anchor the chain to the live socket ('' = a world-target chain).
|
|
478
|
+
animatorIkAnchor?(animatorId: number, ik: number, socket: string): void
|
|
479
|
+
// out ← [reach error m, anchor delta m, anchor delta rad, applied weight]. False = no such chain.
|
|
480
|
+
animatorIkState?(animatorId: number, ik: number, out: Float32Array): boolean
|
|
481
|
+
// A socket: set '' = live, else a donor's name; on joint `jointEntityId` (a bone of the animator). A
|
|
482
|
+
// NODE socket (nodeEntityId != 0) is that entity, read every late pass (its world → the joint's
|
|
483
|
+
// space); else trs = [t.xyz, r.xyzw] fixed in the joint's space. No node and null trs = remove.
|
|
484
|
+
animatorSetSocket?(animatorId: number, set: string, name: string, jointEntityId: number, nodeEntityId: number, trs: Float32Array | null): void
|
|
485
|
+
// A clip's anchor spans for one bone: the donor set, one '\n'-joined socket name per span ('' = the
|
|
486
|
+
// joint itself), spans = (from, to, rotation 0..1) per span in NORMALIZED time. Empty spans + donor
|
|
487
|
+
// '' = remove; a clip without data takes no part in the anchor blend.
|
|
488
|
+
setClipAnchors?(clipSetId: number, clip: number, bone: string, donor: string, sockets: string, spans: Float32Array): void
|
|
489
|
+
|
|
443
490
|
// ---- locomotion: the movement model + the clip selector, both engine-side ----
|
|
444
491
|
// The intent (`locoSetInput`) becomes a desired velocity; the simulated velocity springs toward it
|
|
445
492
|
// one fixed substep at a time (`locoStep`, given the capsule's place), and `locoUpdate` — called
|
|
@@ -496,45 +543,107 @@ declare global {
|
|
|
496
543
|
|
|
497
544
|
// JoltPhysics. motionType: 0 static / 1 kinematic / 2 dynamic.
|
|
498
545
|
physicsHasSupport(): boolean
|
|
499
|
-
/** Lightmap bake (
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
|
|
546
|
+
/** Lightmap bake (packages/creator-bake) — a dev-time tool compiled into the desktop host only; `lightmapBake` exists
|
|
547
|
+
* only where `lightmapHasSupport()` is true. Describes the given static instances (GLB roots / Mesh entities;
|
|
548
|
+
* `keysJoined` = one key per id, '
|
|
549
|
+
'-joined), the scene's brightest sun, its point lights (spherical emitters of
|
|
550
|
+
* `lightRadius` metres) and its IBL to the bake, which traces them on the NVIDIA card — direct light plus
|
|
551
|
+
* `bounceRays` Lambertian paths of `bounces` vertices per texel (0 / 0 = direct only) with `patchSamples` next-event
|
|
552
|
+
* samples over the bright patches of a first direct pass — and writes
|
|
553
|
+
* `<outDir>/<stem>.bake` + `<stem>-light[_n].ktx2` (irradiance in lux / lightScale, BC6H) + `<stem>-aux[_n].ktx2`
|
|
554
|
+
* (sun / sky visibility + light direction); `split` adds `<stem>-direct[_n]` / `-indirect[_n]` page sets for the
|
|
555
|
+
* SDK's two views; `denoise` runs OptiX's AI denoiser over 0 nothing, 1 the indirect part, 2 + the sky and the lamps, 3 + the sun; `filter` = the texel's reconstruction filter radius in texels (1 = ±1 texel tent, 0.5 = within the texel, 0 = the centre alone);
|
|
556
|
+
* `probeSpacing` > 0 places REFLECTION PROBES on a grid of that many metres and writes `<stem>-probes.ktx2` (a BC6H atlas of octahedral
|
|
557
|
+
* maps: one column of roughness-level tiles per probe, captured with `probeSize`² cube faces and `probeRays` paths per texel; the scene's
|
|
558
|
+
* IBL cubemap is what an escaping ray sees; `probeIndoor` keeps only the probes under a roof (an open share of the sphere of at most 15 %) — the
|
|
559
|
+
* outdoors reflects the sky through the texel's baked sky visibility instead; `volumeSpacing` (metres, 0 = none) bakes THE LIGHT GRID for movers — `<name>.lgrid`, named in the .bake's `volume`: per cell
|
|
560
|
+
* in free space an ambient cube without the direct sun, links between neighbours a ray connects (what `setAmbientCube`
|
|
561
|
+
* is fed from); `minSize` (metres, 0 = off) leaves every instance smaller than that along every axis out of the atlas — an occluder
|
|
562
|
+
* only, `"small": true` + its `light` (an ambient cube baked where it stands) in the .bake, applied by lightmapObjectApply; `probeLayout` 0 = ROOMS (indoor points slide to their
|
|
563
|
+
* room's middle and merge: one probe per room), 1 = GRID; `probeBox` = 6 floats, min xyz + max xyz, keeps the grid inside that box — null = the
|
|
564
|
+
* receivers' bounds) and the grid into the .bake; `debugDir` (optional) gets the float layers per page. Synchronous; progress on stdout as
|
|
565
|
+
* "[lightmap] …" lines, "[lightmap] done" on success. */
|
|
507
566
|
lightmapHasSupport?(): boolean
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
567
|
+
lightmapBake?(outDir: string, stem: string, entityIds: Uint32Array, keysJoined: string, size: number, texel: number, pages: number,
|
|
568
|
+
sunRays: number, skyRays: number, lightRays: number, lightRadius: number, sunAngleDeg: number, bias: number, seed: number,
|
|
569
|
+
bounceRays: number, bounces: number, patchSamples: number, split: boolean, denoise: number, filter: number,
|
|
570
|
+
probeSpacing: number, probeSize: number, probeRays: number, probeIndoor: boolean, probeLayout: number, probeBox: Float32Array | null, minSize: number, volumeSpacing: number, emissiveNits: number, detailTexels: number, debugDir?: string,
|
|
571
|
+
transmit?: string): boolean
|
|
572
|
+
/** Lightmap consumption: the next createGlb takes lightmap.filamat (the material behind that instance id) for its
|
|
573
|
+
* OPAQUE materials and the masked twin for its MASK materials instead of the ubershader, and keeps TEXCOORD_1;
|
|
574
|
+
* BLEND (glass) materials always keep the ubershader. 0xFFFFFFFF clears. The same call arms the foliage tier. */
|
|
512
575
|
setNextGlbLightmapped?(materialInstanceId: number, maskedMaterialInstanceId?: number): void
|
|
513
|
-
/** The level-wide
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
576
|
+
/** The level-wide scale every lightmap-material instance shares (present and future): lux per encoded 1.0 of the
|
|
577
|
+
* light atlas — the .bake's `lightScale` times whatever multiplier a debug knob wants. `emissiveNits` = the bake's
|
|
578
|
+
* radiance of emission 1.0 (the .bake's `emissiveNits`): over 0 the statics' glow is drawn scene-referred with it -
|
|
579
|
+
* a panel looks as bright as the light it gives; 0 = screen-referred. */
|
|
580
|
+
lightmapSetOptions?(lightScale: number, emissiveNits?: number): void
|
|
581
|
+
/** A MOVER'S SUN SHADOW ON THE BAKED STATICS: (dx, dy, dz) = TO the sun, (r, g, b) = its illuminance in lux, `on` =
|
|
582
|
+
* the level's aux pages end in the bake's SUN MAP (the .bake's `sunMap`; never true without it). The lightmap
|
|
583
|
+
* material takes the sun's part out of the baked light where the real-time shadow map says "in shadow". */
|
|
584
|
+
lightmapSunSet?(dx: number, dy: number, dz: number, r: number, g: number, b: number, on: boolean, levels?: number): void
|
|
585
|
+
/** The lightmap material's DATA views, level-wide: 0 = the picture, 1 lighting only (a white diffuse material under the camera's exposure), 2 albedo, 3 geometric normal,
|
|
586
|
+
* 4 shading normal, 5 sun visibility, 6 sky visibility, 7 the atlas texel checker (tinted per instance), 8 light
|
|
587
|
+
* direction, 9 false-colour log10 lux over [p1, p2], 10 relief (E on the shading normal / E), 13 what the specular
|
|
588
|
+
* reflects (the probe along the reflection, or the sky + baked light without one), 14 the probe map (grey = the probes' share of the reflection, tinted by the texel's pair), 15 the sky's path of the reflection alone, 16 the probes' path alone. */
|
|
589
|
+
lightmapDebug?(mode: number, p0?: number, p1?: number, p2?: number, p3?: number): void
|
|
590
|
+
/** The level's reflection probes, level-wide (every lightmap-material instance, present and future): the octahedral
|
|
591
|
+
* atlas as a loaded texture (`<stem>-probes.ktx2`, `Texture.load` like a page), its layout (`oct` = level 0's tile edge,
|
|
592
|
+
* `levels`, `perRow`), the probe count and the range the probe maps' records decode with (the .bake's `probes.encode`:
|
|
593
|
+
* lo xyz, size xyz). WHICH probes a pixel reflects is baked into the pages: every aux page carries its probe map
|
|
594
|
+
* under the page (per 4 x 4 block of texels the two probes those texels SEE — rays at bake time — and their weight).
|
|
595
|
+
* Returns the probe count; an invalid texture id clears. */
|
|
596
|
+
lightmapProbesSet?(atlasTextureId: number, oct: number, levels: number, perRow: number, count: number,
|
|
597
|
+
lox: number, loy: number, loz: number, sx: number, sy: number, sz: number): number
|
|
598
|
+
/** Bind the light page + the aux page + this instance's rect (uv1 * [sx, sy] + [ox, oy]) on every lightmap-material
|
|
599
|
+
* instance of the entity (a GLB instance's renderables, or a Mesh entity's own) and move those renderables to light
|
|
600
|
+
* channel 1 alone — a baked static takes nothing from the real-time sun and lamps (channel 0). Returns how many
|
|
522
601
|
* material instances took it (0 = not loaded lightmapped). */
|
|
523
|
-
lightmapApply?(entityId: number,
|
|
602
|
+
lightmapApply?(entityId: number, lightTextureId: number, auxTextureId: number, sx: number, sy: number, ox: number, oy: number, group?: number): number
|
|
603
|
+
/** A SMALL STATIC the atlas left out (the bake's `minSize`: `small` + `light` in the .bake): no rect — the light
|
|
604
|
+
* baked where it stands goes into its material parameters. `data` = 27 floats: the ambient cube — per side of its
|
|
605
|
+
* bounds (+x -x +y -y +z -z) the irradiance rgb in lux and the sky visibility — then probe a, probe b, a's weight
|
|
606
|
+
* (65535 = no probe, 65534 = the sky). The textures = any page of the level (the probes' records sit under every
|
|
607
|
+
* aux page). Returns the material instances that took it (0 = not loaded lightmapped). */
|
|
608
|
+
lightmapObjectApply?(entityId: number, lightTextureId: number, auxTextureId: number, data: Float32Array, group?: number): number
|
|
609
|
+
/** BAKED AMBIENT LIGHT for a mover, or anything on a standard or custom LIT shader: every renderable of the GLB
|
|
610
|
+
* instance (or the Mesh entity) takes its diffuse indirect light from `data`'s ambient cube instead of the scene's
|
|
611
|
+
* IBL, and the IBL's reflections through the sky visibility. `data` = 25 or 26 floats: six sides (+x -x +y -y +z -z) of
|
|
612
|
+
* irradiance rgb in lux + one unused float each, the sky visibility (0 a room .. 1 the open sky), then optionally the
|
|
613
|
+
* baked SUN visibility (1 when absent): the real-time directional light on it is multiplied by it — the baked
|
|
614
|
+
* statics' shadow, they cast none in real time while a light grid is loaded. Lamps stay as they are. Cheap per frame. `null` = back to the IBL. Returns the renderables touched. */
|
|
615
|
+
setAmbientCube?(entityId: number, data: Float32Array | null): number
|
|
616
|
+
/** THE LIGHT GRID for movers (`lecodes lightmap bake` writes `<name>.lgrid`, the .bake's `volume` names it):
|
|
617
|
+
* `lightVolumeLoad(fetchId)` hands the engine the fetched file (false = not a grid); `lightVolumeTrack(entityId, on)`
|
|
618
|
+
* marks a GLB instance root or a Mesh entity as a MOVER — every frame the engine gives it the ambient cube of the
|
|
619
|
+
* place it is at (`setAmbientCube`), blended from the reachable cells around it and eased over ~0.06 s;
|
|
620
|
+
* `fixed` = a STATIC the atlas left out (glass, a material the lightmap shader declines): sampled once a grid, and
|
|
621
|
+
* ignored when every primitive of it is baked;
|
|
622
|
+
* `lightVolumeSample(x, y, z)` = that cube at a point (26 floats, null = no grid or no valid cell near), for tools;
|
|
623
|
+
* `lightVolumeClear()` drops the grid and every mover's cube. */
|
|
624
|
+
lightVolumeLoad?(fetchId: number): boolean
|
|
625
|
+
lightVolumeTrack?(entityId: number, on: boolean, fixed?: boolean): void
|
|
626
|
+
lightVolumeClear?(): void
|
|
627
|
+
lightVolumeSample?(x: number, y: number, z: number): Float32Array | null
|
|
628
|
+
/** A light's channels (Filament's, 0..7). Every light is on channel 0 at creation; one that must reach BAKED statics
|
|
629
|
+
* too (an unbaked lamp, a muzzle flash) is added to channel 1. */
|
|
630
|
+
setLightChannel?(entityId: number, channel: number, enable: boolean): void
|
|
524
631
|
/** Shadow flags on every renderable of a GLB instance (Mesh has setCastShadows/setReceiveShadows). */
|
|
525
632
|
setGlbShadows?(entityId: number, cast: boolean, receive: boolean): void
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
*
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
*
|
|
536
|
-
|
|
537
|
-
|
|
633
|
+
/** Foliage — the vegetation tier (`foliage.filamat` / `foliage-masked.filamat`, armed per model
|
|
634
|
+
* through setNextGlbLightmapped; the engine knows the tier by its `benders` parameter). Level-wide wind: direction on
|
|
635
|
+
* the ground (normalised by the host), strength = metres of sway at bendHeight, speed rad/s, gust 0..1. Applies to
|
|
636
|
+
* every foliage instance, present and future. */
|
|
637
|
+
foliageSetWind?(dirX: number, dirZ: number, strength: number, speed: number, gust?: number): void
|
|
638
|
+
/** Level-wide sway options: bendHeight = metres above the instance origin where the full sway / touch applies,
|
|
639
|
+
* touchStrength = metres a bender pushes, variation 0..1 = per-copy tint, sunWrap 0..1 = wrapped Lambert. */
|
|
640
|
+
foliageSetOptions?(bendHeight: number, touchStrength: number, variation: number, sunWrap: number): void
|
|
641
|
+
/** An entity whose live world position bends the vegetation within `radius` metres; <= 0 removes it. The host
|
|
642
|
+
* writes the 8 benders nearest the camera to the shader each frame. */
|
|
643
|
+
foliageSetBender?(entityId: number, radius: number): void
|
|
644
|
+
/** Distance fade for every instance of the entity's ASSET (present and future): the cards thin out between `start`
|
|
645
|
+
* and `end` metres from the camera and past `end` the LOD pass takes the instance out of the scene. end 0 = none. */
|
|
646
|
+
foliageSetFade?(entityId: number, start: number, end: number): void
|
|
538
647
|
physicsConfigure(gx: number, gy: number, gz: number, maxBodies: number): void
|
|
539
648
|
setInterpolation(enabled: boolean): void
|
|
540
649
|
// Shape/Physics/Trigger aspects: build a shape once, create bodies from it.
|
|
@@ -699,25 +808,43 @@ declare global {
|
|
|
699
808
|
vehicleBodyId(vehicleId: number): number
|
|
700
809
|
// Ragdoll (Jolt Ragdoll): a Model's skeleton handed to physics — one dynamic body per listed bone
|
|
701
810
|
// (a capsule from the bone's origin to the next joint) joined to its parent part by a swing-twist
|
|
702
|
-
// constraint. Built ONCE from the pose the bones are in (that
|
|
703
|
-
// limits) and kept out of the world until ragdollActivate,
|
|
704
|
-
//
|
|
705
|
-
//
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
//
|
|
811
|
+
// constraint that carries a swing + twist MOTOR. Built ONCE from the pose the bones are in (that
|
|
812
|
+
// pose is the joints' neutral for the limits) and kept out of the world until ragdollActivate,
|
|
813
|
+
// which takes the bones' CURRENT pose and adds the bodies moving as the engine measured each bone
|
|
814
|
+
// over the last two frames (a swinging arm keeps swinging) plus a launch; from then on the engine
|
|
815
|
+
// writes the bodies' poses onto the bone entities every frame AFTER the animator, until
|
|
816
|
+
// ragdollDeactivate. POWERED (ragdollSetDrive): the motors pull every joint toward the relative
|
|
817
|
+
// rotation the ANIMATED pose shows (the engine hands the animator's pose to physics every frame,
|
|
818
|
+
// right after the animator wrote it and before the bodies overwrite it), and the root can be
|
|
819
|
+
// ANCHORED — the hips body kinematic on the animated hips — so a standing character stays in its
|
|
820
|
+
// animation while a hit jolts a limb and the motors bring it back; strength 0 with no anchor =
|
|
821
|
+
// limp. The skin joints under the hips that carry no part (the spine bones between two parts, the
|
|
822
|
+
// neck, the clavicles, fingers, toes) follow the animation while driven and HOLD their pose as the
|
|
823
|
+
// strength goes to 0 — a limp body never breathes with the loop playing underneath. Deactivating
|
|
824
|
+
// with a blend seeds the model's animator with the fallen pose, so whatever plays next
|
|
825
|
+
// transitions out of it (canimAnimatorSeedPose). Every body carries its bone entity as user
|
|
826
|
+
// data, so physicsRaycast / contact events name the bone. Optional: a host without it has no
|
|
827
|
+
// ragdolls.
|
|
709
828
|
// bones[partCount * 2]: bone entity, `to` entity (0 = a leaf: `length` along the parent's line;
|
|
710
829
|
// a leaf ROOT points along the model's up)
|
|
711
|
-
// settings — header[
|
|
830
|
+
// settings — header[16]: version(3), partCount, stride(12), friction, linearDamping,
|
|
712
831
|
// angularDamping (0 = Jolt's own), collide (0 = everything, 1 = the STATIC world and
|
|
713
832
|
// other such ragdolls only: dynamic bodies, character controllers and vehicles pass
|
|
714
|
-
// through — Jolt's DEBRIS object/broadphase layer), freeze (1 = once every part
|
|
715
|
-
// asleep the bodies turn STATIC where they lie: the last pose keeps being
|
|
716
|
-
// onto the bones, raycasts still hit, nothing wakes them, the solver skips
|
|
717
|
-
// ragdollActive reads false, ragdollDeactivate / ragdollActivate still work —
|
|
718
|
-
// activate makes the parts dynamic again), freezeAfter (seconds
|
|
719
|
-
// regardless; 0 = no cap),
|
|
720
|
-
//
|
|
833
|
+
// through — Jolt's DEBRIS object/broadphase layer), freeze (1 = once every part of a
|
|
834
|
+
// LIMP body is asleep the bodies turn STATIC where they lie: the last pose keeps being
|
|
835
|
+
// written onto the bones, raycasts still hit, nothing wakes them, the solver skips
|
|
836
|
+
// them; ragdollActive reads false, ragdollDeactivate / ragdollActivate still work —
|
|
837
|
+
// activate makes the parts dynamic again), freezeAfter (seconds LIMP after which the
|
|
838
|
+
// freeze happens regardless; 0 = no cap), driveFrequency (Hz: the motors' position
|
|
839
|
+
// spring), driveDamping (ratio, 1 = critical), driveTorque (N·m per kg of the part:
|
|
840
|
+
// the motors' limit at strength 1), jointFriction (N·m per kg of the part: a torque
|
|
841
|
+
// that resists any joint motion, motor or not), limits (0 = the part cones below;
|
|
842
|
+
// 1 = LEARNED: at the first activation that finds clips bound to the model's
|
|
843
|
+
// animator the engine measures, per joint, how far the clips swing and twist it in
|
|
844
|
+
// its constraint frame and rebuilds the joints to those ranges — the motors never
|
|
845
|
+
// target a pose the limits forbid; no clips yet = the cones, measured at a later
|
|
846
|
+
// activation), limitsMargin (degrees added on each side of a learned range),
|
|
847
|
+
// reserved x1
|
|
721
848
|
// + partCount * 12: parentIndex (-1 = the root; always < the part's own index), radius, length
|
|
722
849
|
// (0 = up to the `to` bone), mass (kg; 0 = from the volume), swingDeg (cone
|
|
723
850
|
// half-angle), twistDeg (half-angle), hinge (0/1), hingeAxisX/Y/Z (in the MODEL
|
|
@@ -725,15 +852,23 @@ declare global {
|
|
|
725
852
|
// flat (2°) across the axis and [min, max] about it (a knee, an elbow)
|
|
726
853
|
ragdollCreate?(rootEntityId: number, bones: Uint32Array, settings: Float32Array): number
|
|
727
854
|
ragdollDestroy?(ragdollId: number): void
|
|
728
|
-
/** Pose the bodies from the bones' current transforms
|
|
729
|
-
*
|
|
855
|
+
/** Pose the bodies from the bones' current transforms and add them to the world, each moving as
|
|
856
|
+
* its bone was measured over the last two frames, plus the launch (vx, vy, vz) on every body.
|
|
857
|
+
* Already active = re-wake + the launch. false = unknown id / no world. */
|
|
730
858
|
ragdollActivate?(ragdollId: number, vx: number, vy: number, vz: number): boolean
|
|
731
|
-
/** Take the bodies out of the world
|
|
732
|
-
|
|
859
|
+
/** Take the bodies out of the world. The bones get the bodies' last pose written against the
|
|
860
|
+
* parents' CURRENT worlds (move the model node under the hips first), and with `blend` > 0 the
|
|
861
|
+
* model's animator is seeded with it: whatever plays next transitions from the fallen pose over
|
|
862
|
+
* `blend` seconds. */
|
|
863
|
+
ragdollDeactivate?(ragdollId: number, blend: number): void
|
|
733
864
|
/** In the world AND at least one body still awake (a settled ragdoll reads false). */
|
|
734
865
|
ragdollActive?(ragdollId: number): boolean
|
|
735
866
|
/** The rigid body of part `index` (for physicsApplyImpulseAt / velocities). 0 if unknown. */
|
|
736
867
|
ragdollBodyId?(ragdollId: number, index: number): number
|
|
868
|
+
/** The joint motors' strength (0..1 = the share of driveTorque; 0 = off = limp) and the root
|
|
869
|
+
* anchor (the hips body kinematic on the animated hips). Kept across activations; a driven or
|
|
870
|
+
* anchored body never freezes. */
|
|
871
|
+
ragdollSetDrive?(ragdollId: number, strength: number, anchor: boolean): void
|
|
737
872
|
// Dynamic bones (the SDK's DynamicBone; creator-anim canimDyn* behind creator-gl): secondary motion
|
|
738
873
|
// for tails, ears, hair and cloaks — a bone tree under one root simulated as Verlet particle chains
|
|
739
874
|
// (gravity, wind, drag, stiffness toward the animated shape, an angle cone, capsule / sphere
|
|
@@ -786,8 +921,9 @@ declare global {
|
|
|
786
921
|
dynamicBoneTargets?(id: number, rows: Float32Array | null): void
|
|
787
922
|
/** A capsule a → b in the entity's local space (a == b = a sphere) that rides the entity — a bone
|
|
788
923
|
* of the body the chains must not pass through. Returns a collider id (0 on failure). */
|
|
789
|
-
|
|
790
|
-
|
|
924
|
+
/** `sided` 1 = one-sided: while the capsule moves away from the chain's `side` it puts a bone it has run into over to that side instead of carrying it (an arm under a cape); 0 = pushes to the nearest surface. */
|
|
925
|
+
dynamicBoneColliderCreate?(entityId: number, ax: number, ay: number, az: number, bx: number, by: number, bz: number, radius: number, sided: number): number
|
|
926
|
+
dynamicBoneColliderSet?(colliderId: number, ax: number, ay: number, az: number, bx: number, by: number, bz: number, radius: number, sided: number): void
|
|
791
927
|
dynamicBoneColliderDestroy?(colliderId: number): void
|
|
792
928
|
// Legacy coupled shape+body (still used by the worker RigidBody).
|
|
793
929
|
physicsCreateBox(entityId: number, hx: number, hy: number, hz: number, motionType: number, mass: number): number
|
|
@@ -1266,10 +1402,40 @@ declare global {
|
|
|
1266
1402
|
removeNode(nodeId: number, childNode: any): void
|
|
1267
1403
|
setContent(nodeId: number, oldChildren: any[], children: any[]): void
|
|
1268
1404
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1405
|
+
// Legacy one-shot style tweens, implemented by the hosts that animate JS-side (web, Apple) and
|
|
1406
|
+
// ABSENT on creator-pkg hosts since 2026-09-14 (they have the tween core below; the SDK's fallback
|
|
1407
|
+
// only calls these where tweenCreate is missing): from the current values to `style` (animateTo)
|
|
1408
|
+
// / from `style` to the current values (animateFrom), meta keys duration / delay / loop /
|
|
1409
|
+
// loopMode / commit / layer in the bag.
|
|
1410
|
+
animateTo?(nodeId: number, style: any): void
|
|
1411
|
+
animateFrom?(nodeId: number, style: any): void
|
|
1271
1412
|
toast(msg: string): void
|
|
1272
1413
|
|
|
1414
|
+
// ---- Keyframe animation core (docs/timeline-plan.md; creator-tween) -------------------------
|
|
1415
|
+
// ONE evaluator for UI style props, 3D node transforms / lights / cameras and 2D sprites: the SDK
|
|
1416
|
+
// hands over a flat blob (layout in sdk/src/animate/tween/spec.ts — header, time-callbacks,
|
|
1417
|
+
// easing tables, tracks of keyframes; string-valued lanes index `strings`, a track's target lane
|
|
1418
|
+
// indexes `targets`: the element's `_id` handle for the UI domain, an entity / scene id for the
|
|
1419
|
+
// 3D and 2D domains) and gets an id.
|
|
1420
|
+
// Created = paused at t = 0, nothing applied. Ops: 0 play (from t = 0, a running one restarts),
|
|
1421
|
+
// 1 pause, 2 resume, 3 cancel (drop, values stay where they are, no commit), 4 finish (jump to
|
|
1422
|
+
// the end, commit, report finish). The host reports through the registered callback:
|
|
1423
|
+
// kind 0 = call (index = the blob's call slot, fired when playback crosses its time, in time
|
|
1424
|
+
// order, before finish), kind 1 = finish (the end was reached, one-shot only), kind 2 = a VALUE
|
|
1425
|
+
// track's frame (index = the track's target slot — unique per world, then the lanes as plain number arguments —
|
|
1426
|
+
// delivered synchronously after the evaluator's pass, never from inside it). Seek is in TOTAL
|
|
1427
|
+
// ms (iterations included); tweenGetTime reads the position inside the current iteration,
|
|
1428
|
+
// tweenGetElapsed the total. Clock 0 = host wall time (ui), 1 = game time (Time.scale, pause).
|
|
1429
|
+
// Optional: the SDK feature-detects tweenCreate and falls back to animateTo / animateFrom
|
|
1430
|
+
// (UI tracks, single values, timer clock) on hosts without it.
|
|
1431
|
+
tweenCreate?(data: Float32Array, strings: string[], targets: any[]): number
|
|
1432
|
+
tweenControl?(id: number, op: number): void
|
|
1433
|
+
tweenSeek?(id: number, ms: number): void
|
|
1434
|
+
tweenSetRate?(id: number, rate: number): void
|
|
1435
|
+
tweenGetTime?(id: number): number
|
|
1436
|
+
tweenGetElapsed?(id: number): number
|
|
1437
|
+
registerTweenEvent?(callback: (id: number, kind: number, index: number) => void): void
|
|
1438
|
+
|
|
1273
1439
|
// Router over destinations. On hosts that implement openView (below), `screen`/`view` is any
|
|
1274
1440
|
// view descriptor (not just a screen object) and the extra `transition` args apply; legacy
|
|
1275
1441
|
// hosts receive screen objects only and ignore the extras.
|
|
@@ -1423,6 +1589,15 @@ declare global {
|
|
|
1423
1589
|
* setLoop tick (creator-pkg drains its dispatch queue after the frame's timers, so a poll can
|
|
1424
1590
|
* see the new state one tick before the event; web/headless deliver synchronously). */
|
|
1425
1591
|
registerInputEvent?(callback: (kind: number, code: string, gamepad: number, repeat: number) => void): void
|
|
1592
|
+
/** Dual-motor rumble on pad `gamepad` (0..3) — the W3C "dual-rumble" effect. `strong` = the
|
|
1593
|
+
* low-frequency (left, heavy) motor, `weak` = the high-frequency (right, light) one, both 0..1
|
|
1594
|
+
* (hosts clamp); `durationMs` > 0, hosts cap it at 5000. A new call REPLACES the running effect
|
|
1595
|
+
* on that pad; `strong = weak = 0` (any duration) stops it. The HOST owns the clock: the motors
|
|
1596
|
+
* stop by themselves after `durationMs`, on focus loss, when the pad disconnects and when the
|
|
1597
|
+
* world that started the effect is torn down — an app never has to clean up. Returns whether
|
|
1598
|
+
* the pad took the command (false: no such pad, or it has no motors). Optional: a host without
|
|
1599
|
+
* it has no rumble. */
|
|
1600
|
+
inputRumble?(gamepad: number, strong: number, weak: number, durationMs: number): boolean
|
|
1426
1601
|
// ---- World control (run/restart/quit — the platform's `location` analog) --------------------
|
|
1427
1602
|
// Worlds carry two host-tracked bits: an IDENTITY (project uuid) and a TRUST flag. The host's
|
|
1428
1603
|
// own boot world (the bundled launcher) is trusted; a world created by `run` is trusted only
|
|
@@ -1444,11 +1619,6 @@ declare global {
|
|
|
1444
1619
|
* exit programmatically, iOS/desktop, register nothing and the call is a no-op). */
|
|
1445
1620
|
quit?(): void
|
|
1446
1621
|
|
|
1447
|
-
animate(callback: (val: number) => void, duration: number): number
|
|
1448
|
-
stopAnimation(id: number): void
|
|
1449
|
-
pauseAnimation(id: number): void
|
|
1450
|
-
resumeAnimation(id: number): void
|
|
1451
|
-
|
|
1452
1622
|
localStorageGetValue(key: string): string | null
|
|
1453
1623
|
localStorageSetValue(key: string, value: string): void
|
|
1454
1624
|
localStorageRemoveValue(key: string): void
|
|
@@ -34,6 +34,32 @@ test("the same asset present in the entries compiles to its URL", async () => {
|
|
|
34
34
|
expect(js).not.toContain("./assets/lightmap.bake")
|
|
35
35
|
})
|
|
36
36
|
|
|
37
|
+
// Only the modules the bundle reaches are checked: a project keeps one entry per level, and a
|
|
38
|
+
// level whose art is not on this machine must not fail an entry that never imports it.
|
|
39
|
+
test("a missing asset in a module the entry never imports does not fail the compile", async () => {
|
|
40
|
+
const js = await compileProject({
|
|
41
|
+
name: "Unreached",
|
|
42
|
+
publicUrl: "https://le.codes",
|
|
43
|
+
entries: [
|
|
44
|
+
{ path: "/main.ts", type: "text", text: "const scene = new Scene()\nconsole.log(scene)\n" },
|
|
45
|
+
{ path: "/levels/lost.ts", type: "text", text: "export const lost = asset('./lost.glb')\n" },
|
|
46
|
+
],
|
|
47
|
+
})
|
|
48
|
+
expect(js).not.toContain("lost.glb")
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("the same missing asset fails once the entry imports its module", async () => {
|
|
52
|
+
const compile = compileProject({
|
|
53
|
+
name: "Reached",
|
|
54
|
+
publicUrl: "https://le.codes",
|
|
55
|
+
entries: [
|
|
56
|
+
{ path: "/main.ts", type: "text", text: "import { lost } from './levels/lost'\nconsole.log(lost)\n" },
|
|
57
|
+
{ path: "/levels/lost.ts", type: "text", text: "export const lost = asset('./lost.glb')\n" },
|
|
58
|
+
],
|
|
59
|
+
})
|
|
60
|
+
await expect(compile).rejects.toThrow("asset not found: ./lost.glb (levels/lost.ts:1)")
|
|
61
|
+
})
|
|
62
|
+
|
|
37
63
|
test("a commented-out asset() is not an asset at all", () => {
|
|
38
64
|
const src = [
|
|
39
65
|
"/** The atlas image — write `asset('./TileSet.png')`. */",
|
|
@@ -130,3 +130,22 @@ test("compileProject auto-detects the importer as entry and runs the registratio
|
|
|
130
130
|
expect(js).toContain("app-side-effect")
|
|
131
131
|
expect(js).toContain("register-side-effect")
|
|
132
132
|
})
|
|
133
|
+
|
|
134
|
+
test("design/ screens are never the entry — a design-only project has none, app code next to a design wins", () => {
|
|
135
|
+
// A Figma import: screens on the board, shared modules, no app yet. The largest root used to win.
|
|
136
|
+
const designOnly = detectEntry([
|
|
137
|
+
text("/design/screens/a4-40.ts", "import { at } from '../shared/figma'\nexport default UIScreen([])\n" + "// big\n".repeat(50)),
|
|
138
|
+
text("/design/shared/figma.ts", "export const at = () => ({})\n"),
|
|
139
|
+
text("/app.server.ts", "export function ping() { return 1 }\n"),
|
|
140
|
+
])
|
|
141
|
+
expect(designOnly.entry).toBe("")
|
|
142
|
+
expect(designOnly.roots).toEqual([])
|
|
143
|
+
|
|
144
|
+
// The same project once the app exists: main.ts is the entry however small it is.
|
|
145
|
+
const withApp = detectEntry([
|
|
146
|
+
text("/design/screens/a4-40.ts", "export default UIScreen([])\n" + "// big\n".repeat(50)),
|
|
147
|
+
text("/main.ts", "Router.init(home)\n"),
|
|
148
|
+
])
|
|
149
|
+
expect(withApp.entry).toBe("/main.ts")
|
|
150
|
+
expect(withApp.roots).toEqual(["/main.ts"])
|
|
151
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { test, expect } from "bun:test"
|
|
2
|
+
import { compileServerBundle } from "../serverSplit"
|
|
3
|
+
|
|
4
|
+
// The server bundle's file map is the whole project (a server module may import shared code), and
|
|
5
|
+
// the bundler checks every `asset()` in that map. Seen live 2026-09-17: a Figma-exported design next
|
|
6
|
+
// to app.server.ts failed the SERVER build with "asset not found: ../assets/figma/…" because the
|
|
7
|
+
// map had the screens but not the binaries. Binaries are stubbed, design/ stays out.
|
|
8
|
+
|
|
9
|
+
const text = (path: string, t: string) => ({ path, type: "text" as const, text: t })
|
|
10
|
+
const res = (path: string) => ({ path, type: "resource" as const, fileSrc: "/uploads/x.bin" })
|
|
11
|
+
|
|
12
|
+
test("a client screen's asset() does not break the server bundle", async () => {
|
|
13
|
+
const out = await compileServerBundle({
|
|
14
|
+
validate: false,
|
|
15
|
+
entries: [
|
|
16
|
+
text("/app.server.ts", "export const ping = async () => 1\n"),
|
|
17
|
+
text("/main.ts", "import { ping } from './app.server'\nconst hero = asset('./hero.png')\nping()\n"),
|
|
18
|
+
res("/hero.png"),
|
|
19
|
+
text("/design/screens/a4-40.ts", "export default UIScreen([ UIImage(asset('../assets/figma/a4-40/rectangle.png')), UIImage(asset('../assets/figma/a4-40/logo.svg')) ])\n"),
|
|
20
|
+
res("/design/assets/figma/a4-40/rectangle.png"),
|
|
21
|
+
text("/design/assets/figma/a4-40/logo.svg", "<svg/>"),
|
|
22
|
+
],
|
|
23
|
+
})
|
|
24
|
+
expect(out).not.toBeNull()
|
|
25
|
+
expect(out!.code).toContain("ping")
|
|
26
|
+
expect(Object.keys(out!.manifest.modules)).toEqual(["app.server.ts"])
|
|
27
|
+
})
|
package/src/compile/bundler.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { createRequire } from "node:module"
|
|
|
4
4
|
import { dirname, join, relative } from "node:path"
|
|
5
5
|
import { fileURLToPath } from "node:url"
|
|
6
6
|
import { collectAssetMacros, desugarAssetMacros, hasAssetMacro, type AssetMacroRef } from "./assetMacro"
|
|
7
|
-
import type
|
|
7
|
+
import { scanModulesFallback, type ScanModule } from "./detectEntry"
|
|
8
8
|
import { hasAspectExtends, injectAspectNames } from "./aspectMacro"
|
|
9
9
|
import { hasImportStatement, rewriteLibraryImports } from "./libraryImports"
|
|
10
10
|
|
|
@@ -132,8 +132,30 @@ export const scanModulesChisel = (files: MemoryFiles): Record<string, ScanModule
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
135
|
+
* The modules the bundle will actually contain: everything reachable from `entry` over static
|
|
136
|
+
* imports (chisel's scan, else the regex scanner — same graph detectEntry roots on). `sources` are
|
|
137
|
+
* the post-library-rewrite, pre-desugar texts, so a bare `from "acme-ui"` already points into
|
|
138
|
+
* `/__lib/<slug>/…` and follows through. A dynamic `import()` is not an edge for either scanner.
|
|
139
|
+
*/
|
|
140
|
+
const reachableFrom = (entry: string, sources: MemoryFiles): Set<string> => {
|
|
141
|
+
const graph = scanModulesChisel(sources) ?? scanModulesFallback(sources)
|
|
142
|
+
const seen = new Set<string>([ entry ])
|
|
143
|
+
const queue = [ entry ]
|
|
144
|
+
while (queue.length > 0) {
|
|
145
|
+
const file = queue.pop()!
|
|
146
|
+
for (const target of graph[file]?.imports ?? []) {
|
|
147
|
+
if (!seen.has(target)) { seen.add(target); queue.push(target) }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return seen
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Every `asset('…')` in a module the bundle REACHES must name a file that's actually in the
|
|
155
|
+
* compile input — a missing one is a COMPILE ERROR, never a fallback. Modules the entry never
|
|
156
|
+
* imports are not checked: a project keeps one entry per level, and a level whose GLBs are not on
|
|
157
|
+
* this machine (gitignored art) must not fail the compile of an entry that never loads it — it
|
|
158
|
+
* fails its own compile, with the same message (fps-demo demo-logo.ts vs chernobyl, 2026-09-17).
|
|
137
159
|
*
|
|
138
160
|
* chisel used to turn an asset import with no backing module into `const x = "<the specifier>"`:
|
|
139
161
|
* a relative source path nothing resolves at runtime, so the app got a silently broken reference
|
|
@@ -173,11 +195,15 @@ const bundleProjectChisel = async (
|
|
|
173
195
|
// - inject `static aspect = '<name>'` from `class … extends Aspect<'<name>', …>`
|
|
174
196
|
const projFiles: MemoryFiles = {}
|
|
175
197
|
const assetRefs: { file: string, ref: AssetMacroRef }[] = []
|
|
198
|
+
// What the module scanner sees: import-rewritten, NOT yet asset-desugared (a desugared asset is a
|
|
199
|
+
// default import of a non-module — the fallback scanner drops it, chisel's scan would not).
|
|
200
|
+
const scanSources: MemoryFiles = {}
|
|
176
201
|
for (const [ k, v ] of Object.entries(files)) {
|
|
177
202
|
const path = normalize(k)
|
|
178
203
|
let src = v
|
|
179
204
|
if (/\.[tj]sx?$/.test(path)) {
|
|
180
205
|
if (hasImportStatement(src)) src = rewriteLibraryImports(src, slugs)
|
|
206
|
+
scanSources[path] = src
|
|
181
207
|
if (hasAssetMacro(src)) {
|
|
182
208
|
const found = collectAssetMacros(src, path)
|
|
183
209
|
for (const ref of found) assetRefs.push({ file: path, ref })
|
|
@@ -198,6 +224,7 @@ const bundleProjectChisel = async (
|
|
|
198
224
|
const path = normalize(`/__lib/${slug}/${k}`)
|
|
199
225
|
let src = v
|
|
200
226
|
if (/\.[tj]sx?$/.test(path)) {
|
|
227
|
+
scanSources[path] = src
|
|
201
228
|
if (hasAssetMacro(src)) {
|
|
202
229
|
const found = collectAssetMacros(src, path)
|
|
203
230
|
for (const ref of found) assetRefs.push({ file: path, ref })
|
|
@@ -209,7 +236,10 @@ const bundleProjectChisel = async (
|
|
|
209
236
|
}
|
|
210
237
|
}
|
|
211
238
|
|
|
212
|
-
|
|
239
|
+
if (assetRefs.length > 0) {
|
|
240
|
+
const reachable = reachableFrom(entry, scanSources)
|
|
241
|
+
assertAssetsExist(assetRefs.filter(({ file }) => reachable.has(file)), projFiles)
|
|
242
|
+
}
|
|
213
243
|
|
|
214
244
|
const sdkFiles = collectSdkFiles(resolveSdkSrc())
|
|
215
245
|
// Shipped bundles minify by default; edit-mode bundles pass `false` — mangling erases class
|