@solidrt/flux-types 0.0.58 → 0.0.60

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/gui/audio.d.ts CHANGED
@@ -144,9 +144,10 @@ declare module "flux:audio" {
144
144
  /**
145
145
  * Open a clip for streaming: it is decoded on demand instead of loaded fully
146
146
  * into memory, so a large track needs little RAM. Takes a `file()` from
147
- * `flux:fs` (not a path), so the source rides the `file()` proxy override - a
148
- * dev-server-proxied file streams from the server. Play the result as a single
149
- * playback; do not overlap a stream with itself. Call `unload()` when done.
147
+ * `flux:fs` (not a path), read from local storage like any `file()` (the
148
+ * app's assets in a packed app); there is no network streaming. Play the
149
+ * result as a single playback; do not overlap a stream with itself. Call
150
+ * `unload()` when done.
150
151
  */
151
152
  export function stream(source: ReturnType<typeof import("flux:fs").file>): Clip
152
153
  /**
package/gui/gpu.d.ts CHANGED
@@ -422,14 +422,14 @@ declare module "flux:gpu" {
422
422
  * Pair a linked program with draw state, returning a render pipeline id
423
423
  * (its own id space, like programs and buffers - not a texture id): the
424
424
  * pipeline state object of every modern GPU API. The pipeline owns HOW its
425
- * targets draw - `attributes` (the interleaved vertex layout; omit for
426
- * attributeless rendering via gl_VertexID), `instanceAttributes` (the
427
- * per-instance layout, fetched from each entry's `instanceBuffer` - see
428
- * the option's doc), `topology`, `blend`, `cull`, `depth`, `depthWrite`
425
+ * targets draw - `buffers` (the layouts of the buffers its vertex stage
426
+ * reads, see {@link VertexBufferLayout}; omit for attributeless rendering
427
+ * via gl_VertexID), `topology`, `blend`, `cull`, `depth`, `depthWrite`
429
428
  * (`false` requires `depth: true`) - while each target brings its own
430
- * size, buffers, uniforms, and clear. Both layouts share one attribute
431
- * namespace (each name is one `in` of the vertex stage), so a name in
432
- * both lists throws. Creating a pipeline compiles nothing, and many
429
+ * size, buffers, uniforms, and clear. Every attribute name across the
430
+ * layouts is one `in` of the vertex stage, so a name twice throws, and
431
+ * every `in` the program reads must appear in some layout with a format
432
+ * of its component count. Creating a pipeline compiles nothing, and many
433
433
  * pipelines may share one program. The vocabulary is validated here, so a
434
434
  * bad word throws at this call site. Free with
435
435
  * {@link destroyRenderPipeline}; the program is yours and outlives it.
@@ -437,26 +437,7 @@ declare module "flux:gpu" {
437
437
  export function createRenderPipeline(
438
438
  program: ProgramId,
439
439
  opts?: {
440
- attributes?: VertexAttribute[]
441
- /**
442
- * One interleaved record per INSTANCE (WebGPU's `stepMode:
443
- * "instance"`): these attributes read from the entry's
444
- * `instanceBuffer` and advance per instance instead of per vertex, so
445
- * every vertex of instance N sees record N - real per-instance state
446
- * (offsets, colors, a packed transform) with no `gl_InstanceID`
447
- * arithmetic. Declaring any makes `instanceBuffer` required on every
448
- * entry drawn with this pipeline. A mat4 per instance is its four
449
- * vec4 columns, reassembled in the shader (attributes have no matrix
450
- * formats, as in WebGPU). Instance N always reads record N of the
451
- * entry's buffer, from record 0 (ES 3.0 has no base instance), so
452
- * several independently culled groups cannot share one buffer as
453
- * sub-ranges: give each group its own `instanceBuffer` and entry, and
454
- * cull it by `instanceCount`. Each attribute's `slot` (default 0)
455
- * picks a buffer of the entry's `instanceBuffers` list (see
456
- * {@link InstanceAttribute}); a pipeline using only slot 0 binds via
457
- * the plain `instanceBuffer` key.
458
- */
459
- instanceAttributes?: InstanceAttribute[]
440
+ buffers?: VertexBufferLayout[]
460
441
  topology?: Topology
461
442
  blend?: BlendMode
462
443
  cull?: CullMode
@@ -477,19 +458,18 @@ declare module "flux:gpu" {
477
458
  * a texture id exactly like the fused creates do (drive uniforms
478
459
  * via the `params` prop or {@link setTargetParams}, resize with
479
460
  * {@link setTargetSize}, destroy with {@link destroyTexture}). Many targets
480
- * may share one pipeline, and creating a target compiles nothing. `buffer`
481
- * supplies the concrete vertex buffer the pipeline's attribute layout
482
- * describes (required when the pipeline declares attributes), and
483
- * `instanceBuffer` the per-instance records its `instanceAttributes`
484
- * describe (required exactly when it declares any); the
485
- * {@link DrawRange} keys pick what is drawn from them - `vertexCount`
486
- * defaults to the rest of the buffer from `firstVertex` on,
487
- * `instanceCount` to one instance per instance-buffer record (1 without
488
- * one) - and a fetch past either buffer's end throws here. A fullscreen
461
+ * may share one pipeline, and creating a target compiles nothing.
462
+ * `buffers` supplies the concrete buffers the pipeline's layouts
463
+ * describe, one id per layout in declaration order (required exactly for
464
+ * the declared layouts); the {@link DrawRange} keys pick what is drawn
465
+ * from them - `vertexCount` defaults to the rest of the tightest
466
+ * vertex-step buffer from `firstVertex` on, `instanceCount` to one
467
+ * instance per record of the tightest instance-step buffer (1 without
468
+ * one) - and a fetch past any buffer's end throws here. A fullscreen
489
469
  * pass over an attributeless pipeline is `vertexCount: 3` with a
490
- * covering-triangle vertex stage. Draw-state keys
491
- * (`attributes`, `topology`, `blend`, `depth`, `depthWrite`) belong to the
492
- * pipeline and throw here. `params` and `textures` are validated against
470
+ * covering-triangle vertex stage. Draw-state keys (`topology`, `blend`,
471
+ * `depth`, `depthWrite`) belong to the pipeline and throw here, and so
472
+ * does a layout object where a buffer id belongs. `params` and `textures` are validated against
493
473
  * the pipeline's program (see {@link ShaderParams}).
494
474
  *
495
475
  * `render: "manual"` opts the target out of runtime-driven rendering (see
@@ -528,11 +508,8 @@ declare module "flux:gpu" {
528
508
  params?: ShaderParams | null,
529
509
  opts?: {
530
510
  textures?: TextureBindings
531
- buffer?: BufferId
532
- instanceBuffer?: BufferId
533
- /** One buffer per instance slot of the pipeline (index = the
534
- * attributes' `slot`); pass this OR `instanceBuffer`, not both. */
535
- instanceBuffers?: BufferId[]
511
+ /** One buffer id per layout of the pipeline, in declaration order. */
512
+ buffers?: BufferId[]
536
513
  /** Draw the instance records in key order (see {@link InstanceOrder}). */
537
514
  instanceOrder?: InstanceOrder
538
515
  clearColor?: [number, number, number, number]
@@ -551,13 +528,15 @@ declare module "flux:gpu" {
551
528
  */
552
529
  export function destroyProgram(id: ProgramId): void
553
530
  /**
554
- * The vertex attributes a linked program actually reads (name and format),
555
- * as the compiler left them: an `in` the vertex stage never uses is not
556
- * listed, and a type no layout can feed (a matrix, an integer vector) is
557
- * rejected at linkProgram. This is the list a pipeline over the program
558
- * must cover between `attributes` and `instanceAttributes` - an uncovered
559
- * or mis-formatted one throws at createRenderPipeline. Answered locally,
560
- * no GPU round trip.
531
+ * The vertex attributes a linked program actually reads, as the compiler
532
+ * left them: an `in` the vertex stage never uses is not listed, and a
533
+ * type no layout can feed (a matrix) is rejected at linkProgram. The
534
+ * format is the 32-bit form of the family the shader declares
535
+ * (`float32`..`float32x4` for `float`/`vec*`, `uint32`.. for `uint`/
536
+ * `uvec*`, `sint32`.. for `int`/`ivec*`); a layout feeds it with any
537
+ * format of the same kind and component count. This is the list a
538
+ * pipeline over the program must cover across its buffer layouts.
539
+ * Answered locally, no GPU round trip.
561
540
  */
562
541
  export function programAttributes(program: ProgramId): VertexAttribute[]
563
542
  export type Topology = "points" | "lines" | "line-strip" | "triangles" | "triangle-strip"
@@ -605,39 +584,73 @@ declare module "flux:gpu" {
605
584
  */
606
585
  export type CullMode = "none" | "back" | "front"
607
586
  /**
608
- * One float attribute of an interleaved record - a vertex of `attributes`
609
- * or an instance record of `instanceAttributes`. The list's order defines
610
- * the byte layout; locations are resolved by name against the vertex
587
+ * The byte format of one vertex attribute: WebGPU's spelling of the
588
+ * (component type, count, normalized) triple. Every format is a multiple
589
+ * of 4 bytes, so offsets and strides are 4-aligned by construction. A
590
+ * format feeds the shader `in` of its kind and component count, WebGPU's
591
+ * rule: the float and normalized formats feed a float `in` (`vec4`; the
592
+ * fetch converts, mapping `unorm`/`snorm` integers onto 0..1 / -1..1),
593
+ * the `uint*` formats an unsigned integer `in` (`uvec4`) and the `sint*`
594
+ * formats a signed one (`ivec4`), each exact at every width. A pipeline
595
+ * whose layout crosses kinds is refused at creation.
596
+ */
597
+ export type VertexFormat =
598
+ | "float32" | "float32x2" | "float32x3" | "float32x4"
599
+ | "float16x2" | "float16x4"
600
+ | "unorm8x4" | "snorm8x4"
601
+ | "unorm16x2" | "unorm16x4" | "snorm16x2" | "snorm16x4"
602
+ | "uint8x4" | "uint16x2" | "uint16x4"
603
+ | "uint32" | "uint32x2" | "uint32x3" | "uint32x4"
604
+ | "sint8x4" | "sint16x2" | "sint16x4"
605
+ | "sint32" | "sint32x2" | "sint32x3" | "sint32x4"
606
+ /**
607
+ * One attribute of a buffer layout: the vertex-stage `in` it feeds (by
608
+ * name), its byte format, and its byte offset within the record
609
+ * (`offset`, default: right after the previous attribute in list order,
610
+ * 0 for the first). Locations are resolved by name against the vertex
611
611
  * shader's `in` declarations.
612
612
  */
613
- export type VertexAttribute = { name: string; format: "f32" | "vec2" | "vec3" | "vec4" }
613
+ export type VertexAttribute = { name: string; format: VertexFormat; offset?: number }
614
614
  /**
615
- * One float attribute of a per-instance record. `slot` (default 0) picks
616
- * which buffer of the entry's `instanceBuffers` list the attribute
617
- * fetches from: attributes sharing a slot interleave into one record in
618
- * list order, distinct slots are distinct buffers with their own strides
619
- * - which is what lets two writers own instance data independently (a
620
- * core-written pose buffer beside an app-written style buffer). Slots
621
- * must be dense from 0, at most 4; a single-slot pipeline (every `slot`
622
- * omitted) binds via the plain `instanceBuffer` key.
615
+ * The layout of one buffer a pipeline reads, WebGPU's
616
+ * `GPUVertexBufferLayout`: a record of `arrayStride` bytes (default: the
617
+ * attributes' byte sum, so a plain list is tightly interleaved in list
618
+ * order) fetched once per vertex (`stepMode: "vertex"`, the default) or
619
+ * once per INSTANCE (`"instance"`: every vertex of instance N reads
620
+ * record N - real per-instance state, offsets, colors, a packed
621
+ * transform, with no `gl_InstanceID` arithmetic; a mat4 per instance is
622
+ * its four vec4 columns, reassembled in the shader). An explicit
623
+ * `arrayStride` wider than the attributes lets a layout name only some
624
+ * fields of a record - a depth pass reading positions alone out of a
625
+ * full vertex record - and attributes the shader does not read are
626
+ * skipped over the stride. Strides and offsets are multiples of 4, an
627
+ * attribute stays inside its record, a pipeline declares at most 8
628
+ * layouts, and the entry binds exactly one buffer per layout, in the
629
+ * same order. Instance N always reads record N (ES 3.0 has no base
630
+ * instance), so several independently culled groups cannot share one
631
+ * instance buffer as sub-ranges: give each group its own buffer and
632
+ * entry, and cull it by `instanceCount`. Two instance-step buffers on
633
+ * one entry are how two writers own instance data independently (a
634
+ * core-written pose buffer beside an app-written style buffer).
623
635
  */
624
- export type InstanceAttribute = VertexAttribute & { slot?: number }
636
+ export type VertexBufferLayout = { stepMode?: "vertex" | "instance"; arrayStride?: number; attributes: VertexAttribute[] }
625
637
  /**
626
638
  * A pipeline target's draw as data, WebGPU-style: `firstVertex` +
627
639
  * `vertexCount` pick the vertex range `[firstVertex, firstVertex +
628
640
  * vertexCount)` of the buffer, `instanceCount` draws that range as N
629
641
  * instances (`glDrawArraysInstanced`) told apart by `gl_InstanceID` (and
630
- * by their `instanceAttributes` records, when the pipeline declares any).
642
+ * by their instance-step records, when the pipeline declares any).
631
643
  * All keys optional: at create, `firstVertex` defaults to 0, `vertexCount`
632
- * to the rest of the buffer, and `instanceCount` to one instance per
633
- * record of the entry's `instanceBuffer` - 1 without one, the plain draw;
644
+ * to the rest of the tightest vertex-step buffer, and `instanceCount` to
645
+ * one instance per record of the tightest instance-step buffer - 1
646
+ * without one, the plain draw;
634
647
  * in {@link setDraw}, absent keys keep their current value.
635
648
  * `instanceCount: 0` draws nothing - a cheap off switch. With an instance
636
649
  * buffer bound, `instanceCount` is bounds-checked against it like every
637
650
  * fetch (instances 0..N-1 each read one record). Two GL facts worth
638
651
  * knowing: `gl_VertexID` includes `firstVertex` (as in WebGPU), and
639
652
  * `gl_InstanceID` always counts from 0 - ES 3.0 has no base instance, so
640
- * instance N reads record N of the entry's `instanceBuffer` and a group
653
+ * instance N reads record N of every instance-step buffer and a group
641
654
  * that is culled independently needs its own buffer and entry.
642
655
  */
643
656
  export type DrawRange = { firstVertex?: number; vertexCount?: number; instanceCount?: number }
@@ -672,27 +685,21 @@ declare module "flux:gpu" {
672
685
  */
673
686
  export type IndexRange = { firstIndex?: number; indexCount?: number; instanceCount?: number }
674
687
  /**
675
- * A draw entry's buffer swap: each key present re-points that role of the
676
- * entry at another {@link createBuffer} buffer, absent keys keep their
677
- * current buffer. Replace-only - the roles an entry fills are pipeline
678
- * layout state (`attributes` needs a `buffer`, `instanceAttributes` an
679
- * `instanceBuffer`) and indexing is its draw vocabulary, so naming a role
680
- * the entry does not fill throws; `indexBuffer` travels with
681
- * `indexFormat` as at create. The entry's current range is kept and
682
- * rechecked against the new buffers' sizes: a swap to a buffer too small
683
- * for the live range throws (shrink the range first); a larger buffer
684
- * never does. This is the growth primitive: a population outgrowing its
685
- * instance buffer creates a larger one, writes it, swaps, and destroys
686
- * the old (the entry holds the old buffer alive until the swap lands, so
687
- * either order is safe). `instanceBuffer` swaps slot 0;
688
- * `instanceBuffers` swaps every slot at once and must fill exactly the
689
- * slots the entry fills (pass one spelling or the other).
688
+ * A draw entry's buffer swap: `buffers` re-points every declared layout
689
+ * at another {@link createBuffer} buffer (one id per layout, the full
690
+ * list, as at create), an `indexBuffer` + `indexFormat` pair re-points
691
+ * the index binding; absent keys keep their current buffers.
692
+ * Replace-only - which buffers an entry binds is pipeline layout state
693
+ * and indexing is its draw vocabulary, so a list that adds or drops a
694
+ * layout throws, as does an index pair on an unindexed entry. The
695
+ * entry's current range is kept and rechecked against the new buffers'
696
+ * sizes: a swap to a buffer too small for the live range throws (shrink
697
+ * the range first); a larger buffer never does. This is the growth
698
+ * primitive: a population outgrowing its instance buffer creates a
699
+ * larger one, writes it, swaps, and destroys the old (the entry holds
700
+ * the old buffer alive until the swap lands, so either order is safe).
690
701
  */
691
- export type BufferUpdate = {
692
- buffer?: BufferId
693
- instanceBuffer?: BufferId
694
- instanceBuffers?: BufferId[]
695
- } & ({} | IndexBinding)
702
+ export type BufferUpdate = { buffers?: BufferId[] } & ({} | IndexBinding)
696
703
  /**
697
704
  * A draw entry's instance ORDER, declared at creation via the
698
705
  * `instanceOrder` option: the entry draws the records of its instance
@@ -728,33 +735,36 @@ declare module "flux:gpu" {
728
735
  * `instanceCount` below the published record count draws the first N in
729
736
  * key order.
730
737
  *
731
- * An entry with SEVERAL instance buffers orders them all under ONE
732
- * permutation: the key reads from slot 0's records, and every other
733
- * slot's publishes gather to match, so split records (a core-written
738
+ * An entry with SEVERAL instance-step buffers orders them all under ONE
739
+ * permutation: the key reads from the key buffer's records (`buffer`,
740
+ * default the first instance-step layout), and every other instance-step
741
+ * buffer's publishes gather to match, so split records (a core-written
734
742
  * pose buffer plus an app-written style buffer) always describe the same
735
- * draw order. This retains a copy of each slot's records: when the key
743
+ * draw order. This retains a copy of each buffer's records: when the key
736
744
  * order changes - including through spatial record sinks, with no
737
745
  * publish from the app anywhere - the sibling buffers republish
738
- * themselves in the same frame. The entry's instance buffers must be
739
- * pairwise distinct, each ordered by no other entry; a buffer swap
740
- * ({@link BufferUpdate}) carries the order to the new buffers, every
741
- * swapped slot at once - republish app-written slots after a swap, the
742
- * new buffer starts empty.
746
+ * themselves in the same frame. An ordered buffer must be bound once on
747
+ * the entry (not as another layout's buffer nor as its index buffer) and
748
+ * ordered by no other entry; a buffer swap ({@link BufferUpdate})
749
+ * carries the order to the new buffers, every swapped instance-step
750
+ * buffer at once - republish app-written buffers after a swap, the new
751
+ * buffer starts empty.
743
752
  */
744
753
  export type InstanceOrder = ({ field: number } | { position: number; direction: [number, number, number] }) & {
745
754
  descending?: boolean
746
755
  /**
747
- * Which instance slot's records hold the key (default 0). On a
748
- * multi-buffer entry the key may live in any one slot - a core-written
749
- * pose slot keyed by position, or an app-written style slot keyed by
750
- * an explicit sort field - and every other slot gathers under its
751
- * permutation.
756
+ * The pipeline buffer index (into its `buffers` layouts) whose
757
+ * records hold the key; default the first instance-step layout. With
758
+ * several instance-step buffers the key may live in any one of them -
759
+ * a core-written pose buffer keyed by position, or an app-written
760
+ * style buffer keyed by an explicit sort field - and every other one
761
+ * gathers under its permutation.
752
762
  */
753
- slot?: number
763
+ buffer?: number
754
764
  /**
755
765
  * The retained-copy strategy, for write-once populations (a splat
756
766
  * cloud: records written once, only the ORDER changes per camera
757
- * move). Core keeps a CPU copy of each slot's published records, and
767
+ * move). Core keeps a CPU copy of each buffer's published records, and
758
768
  * an `orderDirection` update re-sorts that copy and republishes the
759
769
  * entry's buffers core-side, in the same call, with no publish from
760
770
  * the app; when the re-sort leaves the order unchanged nothing
@@ -785,16 +795,16 @@ declare module "flux:gpu" {
785
795
  * pipeline's own; app-driven uniforms are the source's own declarations).
786
796
  * Clip space is y-down: `gl_Position` y = -1 is the top
787
797
  * row of the target and +1 the bottom, so camera-up geometry must negate y
788
- * (or fold the flip into its projection) to display up. `attributes`
789
- * describes one interleaved vertex in `buffer` (a {@link createBuffer} id);
790
- * omit both for attributeless rendering via gl_VertexID.
791
- * `instanceAttributes` describes one per-instance record in
792
- * `instanceBuffer` (see {@link createRenderPipeline}; declare both or
793
- * neither). The {@link DrawRange} keys pick what is drawn: `vertexCount`
794
- * defaults to the rest of the buffer from `firstVertex` on,
795
- * `instanceCount` draws the range as N instances told apart by
796
- * `gl_InstanceID` and defaults to one per instance-buffer record; a fetch
797
- * past either buffer's end throws. With
798
+ * (or fold the flip into its projection) to display up. `buffers` lists
799
+ * the layouts the vertex stage reads (see {@link VertexBufferLayout}),
800
+ * each naming the {@link createBuffer} id it reads as `buffer` - the
801
+ * fused create's spelling of what the split API declares on the pipeline
802
+ * and binds on the entry; omit it for attributeless rendering via
803
+ * gl_VertexID. The {@link DrawRange} keys pick what is drawn:
804
+ * `vertexCount` defaults to the rest of the tightest vertex-step buffer
805
+ * from `firstVertex` on, `instanceCount` draws the range as N instances
806
+ * told apart by `gl_InstanceID` and defaults to one per record of the
807
+ * tightest instance-step buffer; a fetch past any buffer's end throws. With
798
808
  * `depth: true` the pipeline gets a private depth buffer, cleared and tested
799
809
  * on every render; `depthWrite: false` (requires `depth: true`) keeps the
800
810
  * test but stops the draw from writing depth. `blend` sets the draw's own blending (see
@@ -817,14 +827,8 @@ declare module "flux:gpu" {
817
827
  params?: ShaderParams | null,
818
828
  opts?: {
819
829
  textures?: TextureBindings
820
- attributes?: VertexAttribute[]
821
- buffer?: BufferId
822
- /** See {@link createRenderPipeline}'s `instanceAttributes`. */
823
- instanceAttributes?: InstanceAttribute[]
824
- instanceBuffer?: BufferId
825
- /** One buffer per instance slot (index = the attributes' `slot`);
826
- * pass this OR `instanceBuffer`, not both. */
827
- instanceBuffers?: BufferId[]
830
+ /** The pipeline's layouts, each with the buffer it reads. */
831
+ buffers?: (VertexBufferLayout & { buffer: BufferId })[]
828
832
  /** Draw the instance records in key order (see {@link InstanceOrder}). */
829
833
  instanceOrder?: InstanceOrder
830
834
  topology?: Topology
@@ -1076,9 +1080,10 @@ declare module "flux:gpu" {
1076
1080
  * {@link setDrawOrder}. An {@link IndexBinding} makes the entry draw
1077
1081
  * indexed - real meshes share most vertices, and indexing stores and
1078
1082
  * shades each one once - with the range in {@link IndexRange} spelling.
1079
- * `instanceBuffer` supplies the per-instance records the pipeline's
1080
- * `instanceAttributes` describe (required exactly when it declares any);
1081
- * `instanceCount` then defaults to one instance per record.
1083
+ * `buffers` binds one buffer per layout the pipeline declares, in
1084
+ * declaration order (required exactly for the declared layouts); with an
1085
+ * instance-step layout bound, `instanceCount` defaults to one instance
1086
+ * per record.
1082
1087
  *
1083
1088
  * Seed every uniform the entry's program declares - here, via the
1084
1089
  * target's shared params, or with a later write. GL uniform state lives
@@ -1094,11 +1099,8 @@ declare module "flux:gpu" {
1094
1099
  params?: ShaderParams | null,
1095
1100
  opts?: {
1096
1101
  textures?: TextureBindings
1097
- buffer?: BufferId
1098
- instanceBuffer?: BufferId
1099
- /** One buffer per instance slot of the pipeline (index = the
1100
- * attributes' `slot`); pass this OR `instanceBuffer`, not both. */
1101
- instanceBuffers?: BufferId[]
1102
+ /** One buffer id per layout of the pipeline, in declaration order. */
1103
+ buffers?: BufferId[]
1102
1104
  /** Draw the instance records in key order (see {@link InstanceOrder}). */
1103
1105
  instanceOrder?: InstanceOrder
1104
1106
  before?: DrawId
package/gui/spatial.d.ts CHANGED
@@ -18,9 +18,43 @@ declare module "flux:spatial" {
18
18
 
19
19
  /** A new root node. `visible: false` hides the node's whole subtree. */
20
20
  export function createNode(transform: Float32Array, visible: boolean): NodeId
21
- /** Free a node; its children become roots. A bound sink is dropped
22
- * without a write (removing the entry is the caller's job). */
21
+ /** Free a node NOW, exit or no exit; its children become roots, except
22
+ * leaving ones (exitNode), which go with it. A bound sink is dropped
23
+ * without a write (removing the entry is the caller's job). On a leaving
24
+ * node this is the cancel: it frees where it stands and still reports
25
+ * through "spatialNodeFreed". */
23
26
  export function destroyNode(node: NodeId): void
27
+ /**
28
+ * Let go of a node the way its declaration says (the removal every
29
+ * consumer's destroy verb rides on): each component with an `exit`
30
+ * animates from where it is now to that value on the exit's own motion,
31
+ * and the node is LEAVING meanwhile - still drawn and flushed, invisible
32
+ * to raycast/overlap/sweep/moveAndSlide and every pick built on them,
33
+ * refused by setParent on either side - until its exit tracks settle and
34
+ * no leaving child remains under it, when it frees and one
35
+ * "spatialNodeFreed" engine event (payload `{ node }`) reports it.
36
+ * Children let go of first are those leaving children, so a subtree
37
+ * torn down children-first frees root-last with each corpse in its
38
+ * parent's frame to the end. Returns whether the node is now leaving:
39
+ * with nothing to animate (no exit declared, every exit value already
40
+ * held, nothing leaving below) it frees at once and returns false, so
41
+ * the caller cleans up synchronously and no event follows. No
42
+ * "spatialTransitionEnd" fires for an exit track; a second exitNode on
43
+ * a leaving node changes nothing.
44
+ */
45
+ export function exitNode(node: NodeId): boolean
46
+ /**
47
+ * The node's lifecycle state, for probing: whether it is leaving, its
48
+ * effective visibility as of the last flush, and the motion in force
49
+ * per component - one entry per running track or held write, `to` the
50
+ * target lanes, `heldUntil` the animation-clock ms a held write applies
51
+ * at (null for a running track).
52
+ */
53
+ export function describeNode(node: NodeId): {
54
+ leaving: boolean
55
+ shown: boolean
56
+ motion: { component: NodeComponent; to: number[]; heldUntil: number | null }[]
57
+ }
24
58
  /** Re-parent (null = make a root). Throws on a cycle. */
25
59
  export function setParent(node: NodeId, parent: NodeId | null): void
26
60
  /** Replace the local transform (compare before calling; an unchanged
@@ -29,25 +63,81 @@ declare module "flux:spatial" {
29
63
  * (last write wins - the producer rule). */
30
64
  export function setTransform(node: NodeId, transform: Float32Array): void
31
65
  /**
32
- * One node-transition spec, the element `transition` vocabulary minus
33
- * the lifecycle conveniences: `{ duration }` / `{ duration, bounce }`
34
- * is a spring (the default kind; retargets keep position and velocity,
35
- * rotation springs keep angular velocity along the geodesic),
36
- * `{ duration, curve }` a tween (rotation tweens slerp the geodesic;
37
- * retargets restart from the current value), or the shorthand string
38
- * `"<duration>ms [curve]"`. Durations in ms; no delay, from or exit.
66
+ * One node-transition spec, the element `transition` vocabulary whole
67
+ * minus stagger: `{ duration }` / `{ duration, bounce }` is a spring
68
+ * (the default kind; retargets keep position and velocity, rotation
69
+ * springs keep angular velocity along the geodesic), `{ duration, curve }`
70
+ * a tween (rotation tweens slerp the geodesic; retargets restart from
71
+ * the current value), or the shorthand string
72
+ * `"<duration>ms [curve] [<delay>ms]"`. Durations and delays in ms; a
73
+ * `delay` holds every write to the component that long on the animation
74
+ * clock before it applies, and a held write that starts late (a hitch)
75
+ * runs as if started on time.
76
+ *
77
+ * `from` and `exit` on a component entry (not on `all`) are its
78
+ * lifecycle endpoints, in the lanes of the component - position and
79
+ * scale `[x, y, z]`, rotation a quaternion `[x, y, z, w]`, weights one
80
+ * number per morph target - each a bare lane array (the entry's motion)
81
+ * or the endpoint object
82
+ * `{ value, duration?, curve?, bounce?, delay? }` owning its direction's
83
+ * motion (a field left out is the entry's; naming a curve or a bounce
84
+ * decides the kind outright). `from` is the enter value: the node starts
85
+ * there and animates to the transform it holds at its first frame - its
86
+ * created transform, or the target of a write made in the creating
87
+ * tick; an enter plays once per node, at creation, so the declaration
88
+ * must be set in the creating tick (a later one animates writes only).
89
+ * `exit` is where the component animates to when exitNode lets go of
90
+ * the node.
39
91
  */
92
+ export type NodeEndpoint = {
93
+ value: number[]
94
+ duration?: number
95
+ curve?: "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | [number, number, number, number]
96
+ bounce?: number
97
+ delay?: number
98
+ }
40
99
  export type NodeTransitionSpec =
41
- | { duration: number; bounce?: number }
42
- | { duration: number; curve: "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | [number, number, number, number] }
100
+ | { duration: number; bounce?: number; delay?: number; from?: number[] | NodeEndpoint; exit?: number[] | NodeEndpoint }
101
+ | {
102
+ duration: number
103
+ curve: "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | [number, number, number, number]
104
+ delay?: number
105
+ from?: number[] | NodeEndpoint
106
+ exit?: number[] | NodeEndpoint
107
+ }
108
+ | string
109
+ /** A motion alone - a spec without lifecycle endpoints: what `all` and
110
+ * a one-off motion on a write (writeWeights) take. */
111
+ export type NodeMotionSpec =
112
+ | { duration: number; bounce?: number; delay?: number }
113
+ | {
114
+ duration: number
115
+ curve: "linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | [number, number, number, number]
116
+ delay?: number
117
+ }
43
118
  | string
44
- /** The declaration setTransition takes: a spec per transform component
45
- * plus `all` as a catch-all (per-component entries win). */
119
+ /** The registers a node transition animates: the local TRS components
120
+ * (writeTransform) and the weights register (writeWeights). */
121
+ export type NodeComponent = "position" | "rotation" | "scale" | "weights"
122
+ /** The declaration setTransition takes: a spec per node component
123
+ * (the TRS components and `weights`, the morph target weights
124
+ * register) plus `all` as a catch-all (per-component entries win). `stagger`
125
+ * (ms) makes the node a stagger group: every descendant enter (`from`)
126
+ * or exit that begins in the same frame under it gets `index * stagger`
127
+ * of extra delay, indexed in occurrence order - creation order for
128
+ * enters, TREE order (children order, depth first) for exits whatever
129
+ * order the exits were let go of in, so a cascade out reads like the
130
+ * cascade in; enters and exits count separately. The nearest declaring
131
+ * ancestor wins, nested groups never compound, and it orchestrates
132
+ * descendants only: the node's own lifecycle is staggered by ITS
133
+ * ancestors, and ordinary writes never stagger. */
46
134
  export interface NodeTransition {
47
135
  position?: NodeTransitionSpec
48
136
  rotation?: NodeTransitionSpec
49
137
  scale?: NodeTransitionSpec
50
- all?: NodeTransitionSpec
138
+ weights?: NodeTransitionSpec
139
+ all?: NodeMotionSpec
140
+ stagger?: number
51
141
  }
52
142
  /**
53
143
  * Declare (or with null clear) the node's transitions: with a config
@@ -63,15 +153,18 @@ declare module "flux:spatial" {
63
153
  * target), an undeclared one snaps. Without a declaration this is
64
154
  * setTransform. A component matching its running track's target is
65
155
  * left alone, so rewriting the whole array to move one component never
66
- * restarts the others. Each settled component fires one
156
+ * restarts the others (a held write's target counts, so re-sending it
157
+ * does not restart its delay). Each settled component fires one
67
158
  * "spatialTransitionEnd" engine event (srt:events), payload
68
- * `{ node, component: "position" | "rotation" | "scale" }`.
159
+ * `{ node, component: NodeComponent }` - on a live node; a leaving
160
+ * node's settles feed its free instead (exitNode).
69
161
  */
70
162
  export function writeTransform(node: NodeId, transform: Float32Array): void
71
163
  export function setVisible(node: NodeId, visible: boolean): void
72
164
  /**
73
165
  * Route the node's world matrix to one draw entry's `uModel` (and
74
- * `uNormal`, the inverse-transpose, when `normal`). Validated like
166
+ * `uNormal`, the inverse-transpose, when `normal`; and with `fade` the
167
+ * LOD cross-fade band into `uLodFade`, see setLod). Validated like
75
168
  * setDrawParams: the entry must exist and declare those uniforms. The
76
169
  * entry is assumed switched off (instanceCount 0); the next flush turns it
77
170
  * on with `count` when the node is shown, and off again when hidden.
@@ -79,7 +172,7 @@ declare module "flux:spatial" {
79
172
  * into replaces that sink, binding on another target adds one - a mesh
80
173
  * drawn by a scene and by each of its views is one node with one flush.
81
174
  */
82
- export function bindDraw(node: NodeId, target: TextureId, draw: DrawId, normal: boolean, count: number): void
175
+ export function bindDraw(node: NodeId, target: TextureId, draw: DrawId, normal: boolean, count: number, fade: boolean): void
83
176
  /** Remove the node's draw sink on `target`, or every draw sink without
84
177
  * one. Issues no write: the entries are the caller's to remove. */
85
178
  export function unbindDraw(node: NodeId, target?: TextureId): void
@@ -113,8 +206,9 @@ declare module "flux:spatial" {
113
206
  uv?: [number, number]
114
207
  }
115
208
 
116
- /** Set (null clears) the node's LOCAL tight box [minX, minY, minZ, maxX,
117
- * maxY, maxZ]. With one the node is in the picking index: its world box
209
+ /** Set (null clears) the LOCAL tight box [minX, minY, minZ, maxX, maxY,
210
+ * maxZ] of a node without a shape (a shaped node's box is its shape's:
211
+ * throws). With one the node is in the picking index: its world box
118
212
  * follows the flush; hidden nodes stay in and are skipped at query time. */
119
213
  export function setBounds(node: NodeId, bounds: Float32Array | null): void
120
214
  /**
@@ -137,16 +231,82 @@ declare module "flux:spatial" {
137
231
  * own. Members without a box contribute nothing; with none at all the
138
232
  * node is not culled. */
139
233
  export function setCullGroup(node: NodeId, members: NodeId[]): void
234
+ /** One level of a LOD group (setLod): the node drawn at this level (a
235
+ * direct child of the group), absent for a population level (the
236
+ * instance nodes under the group carry one record buffer per level,
237
+ * bindMatrixRecord's list), and `size` - the projected size (the group's
238
+ * bounding sphere's diameter as a fraction of the viewport height) below
239
+ * which the level hands over to the next. */
240
+ export type LodLevel = { node?: NodeId; size: number }
241
+ /**
242
+ * Make the node a LOD group (an empty list makes it plain again):
243
+ * `levels` nearest first with strictly descending sizes, all naming a
244
+ * child or none at all (never mixed); the last level's size is the cull
245
+ * threshold, 0 for never culled. After the walk each flush measures the
246
+ * group on every target with a LOD view (setLodView) - the sphere
247
+ * around its own box, else around its levels' world boxes - and gates
248
+ * the draw sinks under each level to the level that target picked, so
249
+ * a thousand groups cost no JS per frame. `fade` widens each threshold
250
+ * `s` into a cross-fade band `[s, s * (1 + fade))` in which both levels
251
+ * draw with complementary screen-hash dithers (entries bound with
252
+ * bindDraw's `fade`; others switch hard at the band's midpoint); 0 is a
253
+ * hard switch with a one-sided hysteresis band, so a boundary never
254
+ * flickers. `reference` is the target population levels measure by
255
+ * (records are target-agnostic); node levels measure per target. A
256
+ * nested group chains: a node draws only where every group above it
257
+ * picked its level. A target with no LOD view draws the first level.
258
+ */
259
+ export function setLod(node: NodeId, levels: LodLevel[], fade: number, reference?: TextureId): void
260
+ /**
261
+ * What `target` measures projected size with: a Float32Array of 6 - the
262
+ * eye position xyz, the projection's vertical focal factor (the
263
+ * magnitude of `proj[5]`: `1 / tan(fov / 2)` for a perspective
264
+ * projection, `2 / (top - bottom)` for an orthographic one - positive,
265
+ * whatever clip flip the projection bakes in), 1 for orthographic else
266
+ * 0, and a bias every
267
+ * measured size is multiplied by (below 1 switches sooner) - or null to
268
+ * lift it. Read at flush, like setFrustum; set it beside the frustum on
269
+ * every camera move.
270
+ */
271
+ export function setLodView(target: TextureId, view: Float32Array | null): void
272
+ /**
273
+ * A geometry's positions as the core keeps them, one copy shared by
274
+ * every node that references it: the source of those nodes' local
275
+ * boxes, and the picking narrowphase's triangles when `indices` (a
276
+ * Uint16Array/Uint32Array triangle list) is given - without it the
277
+ * shape only gives its box. Positions are read from an interleaved
278
+ * vertex array (`stride` floats per vertex, xyz at `posOffset`, uv at
279
+ * `uvOffset`, -1 for none). Throws on out-of-range indices.
280
+ */
281
+ export function createShape(vertices: Float32Array, stride: number, posOffset: number, uvOffset: number, indices?: Uint16Array | Uint32Array): ShapeId
282
+ /**
283
+ * Rewrite the shape's vertices from `first` on with those in
284
+ * `vertices`, laid out as for createShape (uvs exactly when the shape
285
+ * has them). The box follows at the next flush on every node carrying
286
+ * the shape; the triangle index is rebuilt by the next query. Throws
287
+ * when the range runs past the shape's vertices.
288
+ */
289
+ export function updateShape(shape: ShapeId, vertices: Float32Array, stride: number, posOffset: number, uvOffset: number, first: number): void
290
+ /** One channel of an interleaved float vertex array: `stride` floats
291
+ * per vertex, the channel's floats at `offset` within each. */
292
+ export type VertexChannel = { data: Float32Array; stride: number; offset: number }
140
293
  /**
141
- * Triangle data for the picking narrowphase, one copy shared by every
142
- * node that references it: positions read from an interleaved vertex
143
- * array (`stride` floats per vertex, xyz at `posOffset`, uv at
144
- * `uvOffset`, -1 for none) and a Uint16Array/Uint32Array triangle list.
145
- * Throws on out-of-range indices.
294
+ * Recompute vertex normals in place from a triangle list (a
295
+ * Uint16Array/Uint32Array): positions are read from the `positions`
296
+ * channel and unit normals written into the `normals` channel (its
297
+ * `data` may be the same array when the layout interleaves them). Each
298
+ * vertex sums the normals of the faces that name it, weighted by the
299
+ * corner angle, so the result does not depend on how a surface was
300
+ * triangulated; a vertex no face names gets zero. Throws on an index
301
+ * past the last vertex or an offset outside its stride.
146
302
  */
147
- export function createShape(vertices: Float32Array, stride: number, posOffset: number, uvOffset: number, indices: Uint16Array | Uint32Array): ShapeId
148
- /** Free a shape; nodes still referencing it fall back to their box. */
303
+ export function computeNormals(positions: VertexChannel, normals: VertexChannel, indices: Uint16Array | Uint32Array): void
304
+ /** Free a shape; nodes still referencing it keep their last box and
305
+ * fall back to it. */
149
306
  export function destroyShape(shape: ShapeId): void
307
+ /** Give the node a shape (null takes it away): its box is the shape's
308
+ * from here on, and setBounds is refused. A shape with no vertices
309
+ * leaves the node without a box. */
150
310
  export function setShape(node: NodeId, shape: ShapeId | null): void
151
311
  /** The node's layer membership bitmask (default 1), what a query's
152
312
  * `layers` mask is tested against. Query-only: needs no flush. */
@@ -156,12 +316,15 @@ declare module "flux:spatial" {
156
316
  * optional: `root` admits only the nodes under it (the caller's own
157
317
  * subtree in the arena every scene and layer shares), `layers` only
158
318
  * nodes whose mask intersects it (a node's mask of 0 then never
159
- * reports), `nodes` only the listed ones. A dead root throws.
319
+ * reports), `nodes` only the listed ones, `target` only the LOD levels
320
+ * that draw target picks (setLod; without it every level reports). A
321
+ * dead root throws.
160
322
  */
161
323
  export type QueryFilter = {
162
324
  root?: NodeId
163
325
  layers?: number
164
326
  nodes?: NodeId[]
327
+ target?: TextureId
165
328
  }
166
329
  /** Every shown node with bounds the ray strikes, nearest first. The
167
330
  * direction need not be normalized; distances are world units. Reads
@@ -291,6 +454,46 @@ declare module "flux:spatial" {
291
454
  /** Remove the node's texture slot on `texture`, or every texture slot
292
455
  * without one (abandoned rows keep their last value). */
293
456
  export function unbindTextureSlot(node: NodeId, texture?: TextureId): void
457
+ /**
458
+ * Bind the node's WEIGHTS register (setWeights - a mesh's morph target
459
+ * weights) to row `row` of `texture`: four weights per texel across the
460
+ * texture's width, the register padded with zeros to the row (or cut
461
+ * to it), one whole-texture upload per texture per flush like a
462
+ * palette. Weights rows stage from the register, not the transform
463
+ * walk, so a weights write costs no matrix recompute. A texture holds
464
+ * one row kind - matrix rows (bindTextureSlot) or weights rows - never
465
+ * both. Validated at bind time: an uploadable rgba32f texture with
466
+ * `row` inside it. Rebinding the same texture replaces the node's slot
467
+ * there; another texture adds one.
468
+ */
469
+ export function bindWeightsSlot(node: NodeId, texture: TextureId, row: number): void
470
+ /** Remove the node's weights slot on `texture`, or every weights slot
471
+ * without one (abandoned rows keep their last value). */
472
+ export function unbindWeightsSlot(node: NodeId, texture?: TextureId): void
473
+ /** Write the node's weights register, any length; an equal write is
474
+ * free. Published through its weights slots at the next flush. Never
475
+ * consults or cancels a weights track (setTransform's rule): a running
476
+ * track overwrites it at the next frame. */
477
+ export function setWeights(node: NodeId, weights: Float32Array): void
478
+ /**
479
+ * Write the weights register THROUGH a motion, the writeTransform of
480
+ * the weights lane: with `transition` (a motion spec, the `all`
481
+ * vocabulary: `{ duration, bounce? | curve, delay? }` or a shorthand
482
+ * string) the write animates on it whatever the node declares - a
483
+ * one-off motion for this write; without one the declaration's
484
+ * `weights` entry (or `all`) decides, and with neither the write snaps
485
+ * like setWeights. One lane per target: a write shorter or longer than
486
+ * the register widens the track to the longer of the two, the missing
487
+ * lanes at zero. A write matching the running track's target is left
488
+ * alone (a tween never restarts on a re-send); each settle fires one
489
+ * "spatialTransitionEnd" with component "weights". The clip players
490
+ * write the same register (a weights channel, path code 3): both are
491
+ * producers, last write wins at each frame.
492
+ */
493
+ export function writeWeights(node: NodeId, weights: Float32Array, transition?: NodeMotionSpec | null): void
494
+ /** Fill `out` from the node's weights register as the arena holds it
495
+ * (zeros past the register's length) and return the register's length. */
496
+ export function readWeights(node: NodeId, out: Float32Array): number
294
497
 
295
498
  /**
296
499
  * Route the node's world pose to record slot `index` of vertex buffer
@@ -319,9 +522,13 @@ declare module "flux:spatial" {
319
522
  * rebinding as bindPoseRecord; a hidden, unbound or destroyed node's
320
523
  * slot becomes a zero-scale matrix (w stays 1), so the instance
321
524
  * collapses to a point and draws nothing. Every sink on one buffer
322
- * shares one projection.
525
+ * shares one projection. `buffers` names one buffer per LOD level of
526
+ * the anchor's group (setLod with population levels), slot `index` in
527
+ * each: the flush stages the record into the level the node's own
528
+ * projected size picks on the group's reference target and the hidden
529
+ * record into the others; one buffer is the plain population.
323
530
  */
324
- export function bindMatrixRecord(node: NodeId, buffer: BufferId, index: number, anchor?: NodeId): void
531
+ export function bindMatrixRecord(node: NodeId, buffers: BufferId[], index: number, anchor?: NodeId): void
325
532
  /** Remove the node's record sink (its slot hides at the next flush). */
326
533
  export function unbindRecord(node: NodeId): void
327
534
  /**
@@ -342,13 +549,17 @@ declare module "flux:spatial" {
342
549
  export type PlayerId = number
343
550
 
344
551
  /**
345
- * Register a baked animation clip: `duration` in seconds, `meta` four
552
+ * Register a baked animation clip: `duration` in seconds, `meta` five
346
553
  * words per channel - [targetSlot, path (0 position, 1 rotation,
347
- * 2 scale), interpolation (0 step, 1 linear, 2 cubic), keyCount] - and
348
- * `times`/`values` every channel's key arrays concatenated in meta
349
- * order (3 floats per key, 4 for rotation; cubic stores three elements
350
- * per key: in-tangent, value, out-tangent, tangents per second - the
351
- * glTF CUBICSPLINE layout). A clip is shared data: `targetSlot`
554
+ * 2 scale, 3 weights), interpolation (0 step, 1 linear, 2 cubic),
555
+ * keyCount, elements] - and `times`/`values` every channel's key arrays
556
+ * concatenated in meta order (`elements` floats per key: 3 for position
557
+ * and scale, 4 for rotation, one per morph target for weights - the
558
+ * TRS counts are checked; cubic stores three elements per key:
559
+ * in-tangent, value, out-tangent, tangents per second - the glTF
560
+ * CUBICSPLINE layout). A weights channel writes the target node's
561
+ * weights register (setWeights), blended across players like a TRS
562
+ * path. A clip is shared data: `targetSlot`
352
563
  * indexes the target table each PLAYER supplies, so one clip drives
353
564
  * any number of instances, and retargeting is a different table.
354
565
  */
package/gui/video.d.ts CHANGED
@@ -1,48 +1,154 @@
1
1
  // Video playback (gui-enabled runtime only). The imperative primitive;
2
2
  // @solidrt/core wraps it with SolidJS reactivity. There is no video element:
3
- // the player's `texture` id is displayed with <texture>/<d-texture>, and a
4
- // richer Video component composes in a higher layer.
3
+ // a texture player's `texture` id is displayed with <texture>/<d-texture>,
4
+ // and a richer Video component composes in a higher layer. A plane player
5
+ // (Android) has no texture at all: the platform composites the decoded
6
+ // picture fullscreen beneath the UI, off the frame loop entirely, and it
7
+ // streams: its source may be a URL, read as it plays.
5
8
 
6
9
  declare module "flux:video" {
10
+ import type { FluxFile } from "flux:fs"
7
11
  import type { TextureId } from "flux:gpu"
8
12
 
9
- /** An opened video: the frame texture plus controls bound to it. */
10
- type VideoPlayer = {
11
- /**
12
- * GPU texture id decoded frames are uploaded into (use as a texture
13
- * source). Holds the current frame; black until playback starts.
14
- */
15
- texture: TextureId
16
- /** Frame width in pixels. */
13
+ /**
14
+ * What went wrong with a stream, for the app to key on:
15
+ * - "network": the source could not be opened or stopped delivering (an
16
+ * unreachable or failing server, an unreadable file, a truncated body);
17
+ * - "decode": the bytes are not a stream the decoders can follow;
18
+ * - "unsupported": a stream, codec or feature this player does not play;
19
+ * - "no-plane": no video plane (not on this platform, or one is open).
20
+ * An app falling back from a plane to a texture keys on "no-plane" only:
21
+ * a network error would fail the texture the same way.
22
+ */
23
+ export type VideoErrorKind = "network" | "decode" | "unsupported" | "no-plane"
24
+
25
+ /** The Error `open` rejects with and `error()` returns. */
26
+ export interface VideoError extends Error {
27
+ kind: VideoErrorKind
28
+ }
29
+
30
+ /** Controls and facts shared by both kinds of player. */
31
+ export type VideoTransport = {
32
+ /** Frame width in pixels, at open. */
17
33
  width: number
18
- /** Frame height in pixels. */
34
+ /** Frame height in pixels, at open. */
19
35
  height: number
20
- /** Duration in seconds. */
21
- duration: number
36
+ /**
37
+ * Duration in seconds; undefined when the source has no known end (a
38
+ * live stream, a container written to a pipe).
39
+ */
40
+ duration: number | undefined
22
41
  /** Whether the file has a playable audio track. */
23
42
  hasAudio: boolean
43
+ /**
44
+ * Whether `seek` does anything. False for a texture player, and for a
45
+ * source that cannot be read at an offset (an HTTP response without
46
+ * Range support); `seek` is then a no-op.
47
+ */
48
+ seekable: boolean
24
49
  /** Start or resume playback. */
25
50
  play(): void
26
51
  /** Pause playback (the current frame stays displayed). */
27
52
  pause(): void
53
+ /**
54
+ * Seek to a time in seconds. Lands on the frame at that time (decoding
55
+ * from the preceding keyframe); while paused the target frame is shown.
56
+ * A no-op when `seekable` is false.
57
+ */
58
+ seek(seconds: number): void
28
59
  /** Whether playback is running. */
29
60
  playing(): boolean
30
61
  /** Presentation time of the displayed frame, in seconds. */
31
62
  currentTime(): number
32
- /** Whether the last frame has been displayed. */
63
+ /**
64
+ * Whether the last frame has been displayed. A failure does not set
65
+ * it: see `error`.
66
+ */
33
67
  finished(): boolean
34
- /** Stop playback and release the decoder, texture, and audio sink. */
68
+ /**
69
+ * Whether playback is held for the source to catch up: playing, with
70
+ * the picture and the sound paused until enough is read ahead. Never
71
+ * true for a texture player.
72
+ */
73
+ buffering(): boolean
74
+ /**
75
+ * The failure that stopped playback, if one did: the reader could not
76
+ * read on, or the decoder gave up. Playback stops where it was
77
+ * (`playing()` goes false, the last frame stays up); a `seek` retries
78
+ * the source. The same object `failed` resolves with.
79
+ */
80
+ error(): VideoError | undefined
81
+ /**
82
+ * Resolves with the failure that stops playback, once, or with
83
+ * undefined when the player closes without one. What a reactive layer
84
+ * watches instead of polling `error()`.
85
+ */
86
+ failed: Promise<VideoError | undefined>
87
+ /** Stop playback and release everything the player holds. */
35
88
  close(): void
36
89
  }
37
90
 
91
+ /** A texture player: frames land in a GPU texture on the UI's clock. */
92
+ export type VideoPlayer = VideoTransport & {
93
+ /**
94
+ * GPU texture id decoded frames are uploaded into (use as a texture
95
+ * source). Holds the current frame; black until playback starts.
96
+ */
97
+ texture: TextureId
98
+ }
99
+
100
+ /**
101
+ * A plane player: the decoder renders into the platform's own video
102
+ * surface, fullscreen beneath the UI, on the video's clock. Exists from
103
+ * open until close; one at a time. The UI draws over it wherever it
104
+ * paints, and the window's uncovered pixels show the video. Its audio
105
+ * track plays on the default output, in sync with the picture.
106
+ */
107
+ export type VideoPlane = VideoTransport
108
+
109
+ /**
110
+ * A video source: a path resolved like file() paths (through the app's
111
+ * assets in a packed app), an http: or https: URL, or a file() from
112
+ * flux:fs (a packed asset is read out of the exe). Any other scheme
113
+ * throws. A URL plays on a plane only, and is read by the runtime
114
+ * directly: no disk cache, no dev-server proxying, so the device must
115
+ * reach the host itself.
116
+ */
117
+ export type VideoSource = string | FluxFile
118
+
119
+ export type VideoOpenOptions = {
120
+ /** Where the picture goes: a texture (default) or the platform's video plane. */
121
+ present?: "texture"
122
+ /**
123
+ * Aborting it abandons the open: the promise rejects with the signal's
124
+ * reason and the source is closed. (A texture player opens at once;
125
+ * the signal matters for a plane's streamed open.)
126
+ */
127
+ signal?: AbortSignal
128
+ }
129
+
130
+ export type VideoPlaneOptions = {
131
+ present: "plane"
132
+ /** How the picture fits the window: letterboxed (default) or cropped to fill. */
133
+ fit?: "contain" | "cover"
134
+ /** Aborting it abandons the open (see VideoOpenOptions). */
135
+ signal?: AbortSignal
136
+ }
137
+
38
138
  /**
39
- * Open a video file (MP4 with H.264 video; AAC audio plays, other audio
40
- * tracks are ignored). The path resolves like file() paths (through the
41
- * app's assets in a packed app). Playback starts paused; call `play()`.
42
- * Rejects when the file is unreadable or its codec unsupported.
139
+ * Open a video (WebM with 8-bit 4:2:0 VP9 video, i.e. profile 0; Opus
140
+ * audio plays, other audio tracks are ignored). Playback starts paused;
141
+ * call `play()`. Rejects with a VideoError: "network" when the source
142
+ * cannot be opened (or does not answer within the open timeout),
143
+ * "unsupported" when its codec is not played or a URL is given to a
144
+ * texture player, "no-plane" for `present: "plane"` on a platform without
145
+ * a video plane (Android only) or while another plane is open or opening.
146
+ * A plane open reads the header off the JS thread and resolves once the
147
+ * plane exists; a texture player opens at once.
43
148
  *
44
- * The built-in software decoder does not decode B-frames; encode dev
45
- * content with `-bf 0` until the platform hardware decoders land.
149
+ * Encode with `ffmpeg -c:v libvpx-vp9 -c:a libopus out.webm`; VP9 and
150
+ * Opus are royalty-free, so both decoders ship on every platform.
46
151
  */
47
- export function open(path: string): Promise<VideoPlayer>
152
+ export function open(source: VideoSource, options?: VideoOpenOptions): Promise<VideoPlayer>
153
+ export function open(source: VideoSource, options: VideoPlaneOptions): Promise<VideoPlane>
48
154
  }
package/modules/fs.d.ts CHANGED
@@ -99,4 +99,14 @@ declare module "flux:fs" {
99
99
  * @param path Path to a file or directory.
100
100
  */
101
101
  export function realpath(path: string): Promise<string>
102
+ /**
103
+ * Move a file or directory from `from` to `to`. The OS rename: an existing
104
+ * target is replaced, a missing source rejects (nothing moved), and a move
105
+ * across filesystems rejects rather than falling back to a copy. Parent
106
+ * directories of `to` are not created.
107
+ *
108
+ * @param from Path of the file or directory to move.
109
+ * @param to Path it should have afterwards.
110
+ */
111
+ export function rename(from: string, to: string): Promise<void>
102
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/flux-types",
3
- "version": "0.0.58",
3
+ "version": "0.0.60",
4
4
  "license": "MIT",
5
5
  "funding": "https://github.com/sponsors/wellawaretech",
6
6
  "author": "Antoine van Wel",