@flighthq/shading 0.2.1-next.410.a23f189

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/createAnimatedNormalModifier.d.ts +10 -0
  2. package/dist/createAnimatedNormalModifier.d.ts.map +1 -0
  3. package/dist/createAnimatedNormalModifier.js +22 -0
  4. package/dist/createAnimatedNormalModifier.js.map +1 -0
  5. package/dist/createDissolveModifier.d.ts +10 -0
  6. package/dist/createDissolveModifier.d.ts.map +1 -0
  7. package/dist/createDissolveModifier.js +21 -0
  8. package/dist/createDissolveModifier.js.map +1 -0
  9. package/dist/createEmissiveModifier.d.ts +11 -0
  10. package/dist/createEmissiveModifier.d.ts.map +1 -0
  11. package/dist/createEmissiveModifier.js +21 -0
  12. package/dist/createEmissiveModifier.js.map +1 -0
  13. package/dist/createEnvReflectModifier.d.ts +9 -0
  14. package/dist/createEnvReflectModifier.d.ts.map +1 -0
  15. package/dist/createEnvReflectModifier.js +18 -0
  16. package/dist/createEnvReflectModifier.js.map +1 -0
  17. package/dist/createFogModifier.d.ts +11 -0
  18. package/dist/createFogModifier.d.ts.map +1 -0
  19. package/dist/createFogModifier.js +18 -0
  20. package/dist/createFogModifier.js.map +1 -0
  21. package/dist/createRimModifier.d.ts +9 -0
  22. package/dist/createRimModifier.d.ts.map +1 -0
  23. package/dist/createRimModifier.js +17 -0
  24. package/dist/createRimModifier.js.map +1 -0
  25. package/dist/createShadedMaterial.d.ts +13 -0
  26. package/dist/createShadedMaterial.d.ts.map +1 -0
  27. package/dist/createShadedMaterial.js +28 -0
  28. package/dist/createShadedMaterial.js.map +1 -0
  29. package/dist/createToonModifier.d.ts +7 -0
  30. package/dist/createToonModifier.d.ts.map +1 -0
  31. package/dist/createToonModifier.js +15 -0
  32. package/dist/createToonModifier.js.map +1 -0
  33. package/dist/createVertexDisplaceModifier.d.ts +13 -0
  34. package/dist/createVertexDisplaceModifier.d.ts.map +1 -0
  35. package/dist/createVertexDisplaceModifier.js +24 -0
  36. package/dist/createVertexDisplaceModifier.js.map +1 -0
  37. package/dist/getModifierDefineKey.d.ts +4 -0
  38. package/dist/getModifierDefineKey.d.ts.map +1 -0
  39. package/dist/getModifierDefineKey.js +36 -0
  40. package/dist/getModifierDefineKey.js.map +1 -0
  41. package/dist/getUnregisteredModifierKinds.d.ts +4 -0
  42. package/dist/getUnregisteredModifierKinds.d.ts.map +1 -0
  43. package/dist/getUnregisteredModifierKinds.js +17 -0
  44. package/dist/getUnregisteredModifierKinds.js.map +1 -0
  45. package/dist/index.d.ts +17 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +17 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/isBuiltInModifierSlot.d.ts +2 -0
  50. package/dist/isBuiltInModifierSlot.d.ts.map +1 -0
  51. package/dist/isBuiltInModifierSlot.js +18 -0
  52. package/dist/isBuiltInModifierSlot.js.map +1 -0
  53. package/dist/isModifierStackValid.d.ts +4 -0
  54. package/dist/isModifierStackValid.d.ts.map +1 -0
  55. package/dist/isModifierStackValid.js +8 -0
  56. package/dist/isModifierStackValid.js.map +1 -0
  57. package/dist/modifierRegistry.d.ts +13 -0
  58. package/dist/modifierRegistry.d.ts.map +1 -0
  59. package/dist/modifierRegistry.js +18 -0
  60. package/dist/modifierRegistry.js.map +1 -0
  61. package/dist/orderModifierStack.d.ts +3 -0
  62. package/dist/orderModifierStack.d.ts.map +1 -0
  63. package/dist/orderModifierStack.js +36 -0
  64. package/dist/orderModifierStack.js.map +1 -0
  65. package/dist/registerBuiltInModifiers.d.ts +11 -0
  66. package/dist/registerBuiltInModifiers.d.ts.map +1 -0
  67. package/dist/registerBuiltInModifiers.js +108 -0
  68. package/dist/registerBuiltInModifiers.js.map +1 -0
  69. package/package.json +43 -0
  70. package/src/createAnimatedNormalModifier.test.ts +49 -0
  71. package/src/createDissolveModifier.test.ts +30 -0
  72. package/src/createEmissiveModifier.test.ts +44 -0
  73. package/src/createEnvReflectModifier.test.ts +28 -0
  74. package/src/createFogModifier.test.ts +31 -0
  75. package/src/createRimModifier.test.ts +31 -0
  76. package/src/createShadedMaterial.test.ts +45 -0
  77. package/src/createToonModifier.test.ts +23 -0
  78. package/src/createVertexDisplaceModifier.test.ts +36 -0
  79. package/src/getModifierDefineKey.test.ts +74 -0
  80. package/src/getUnregisteredModifierKinds.test.ts +40 -0
  81. package/src/isBuiltInModifierSlot.test.ts +25 -0
  82. package/src/isModifierStackValid.test.ts +27 -0
  83. package/src/modifierRegistry.test.ts +59 -0
  84. package/src/orderModifierStack.test.ts +48 -0
  85. package/src/registerBuiltInModifiers.test.ts +118 -0
@@ -0,0 +1,18 @@
1
+ // Allocates an empty modifier registry. The only allocating registry function; register and resolve
2
+ // read or mutate an existing registry in place.
3
+ export function createModifierRegistry() {
4
+ return { definitions: new Map() };
5
+ }
6
+ // Registers (or replaces) the definition for `definition.kind`. Last-write-wins: a later
7
+ // registration of the same kind overrides the earlier one, so a user can shadow a built-in with a
8
+ // vendor-prefixed override. Kind collisions are avoided by the vendor-prefix convention (bare names
9
+ // reserved for built-ins), not by a guard here.
10
+ export function registerModifier(registry, definition) {
11
+ registry.definitions.set(definition.kind, definition);
12
+ }
13
+ // Returns the registered definition for `kind`, or `null` when no definition is registered — the
14
+ // expected-miss sentinel a caller checks before composing an unknown kind.
15
+ export function resolveModifier(registry, kind) {
16
+ return registry.definitions.get(kind) ?? null;
17
+ }
18
+ //# sourceMappingURL=modifierRegistry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modifierRegistry.js","sourceRoot":"","sources":["../src/modifierRegistry.ts"],"names":[],"mappings":"AA4BA,oGAAoG;AACpG,gDAAgD;AAChD,MAAM,UAAU,sBAAsB;IACpC,OAAO,EAAE,WAAW,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC;AACpC,CAAC;AAED,yFAAyF;AACzF,kGAAkG;AAClG,oGAAoG;AACpG,gDAAgD;AAChD,MAAM,UAAU,gBAAgB,CAAC,QAAoC,EAAE,UAAwC;IAC7G,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;AACxD,CAAC;AAED,iGAAiG;AACjG,2EAA2E;AAC3E,MAAM,UAAU,eAAe,CAAC,QAAoC,EAAE,IAAkB;IACtF,OAAO,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;AAChD,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Modifier } from '@flighthq/types';
2
+ export declare function orderModifierStack(stack: readonly Modifier[]): readonly Modifier[];
3
+ //# sourceMappingURL=orderModifierStack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orderModifierStack.d.ts","sourceRoot":"","sources":["../src/orderModifierStack.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAehD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,SAAS,QAAQ,EAAE,CAOlF"}
@@ -0,0 +1,36 @@
1
+ import { ModifierSlot } from '@flighthq/types';
2
+ // Orders a modifier stack into the deterministic feature-set the compile path assembles: modifiers
3
+ // are grouped by the canonical slot pipeline order (Vertex deforms the geometry first, then Normal
4
+ // perturbs the normal before lighting, then Diffuse/Specular contribute at shading, then Emissive
5
+ // adds, then Effect runs last on the shaded output), and within one slot the caller's authoring order
6
+ // is preserved. Because the result depends
7
+ // only on the SET of modifiers and their per-slot order — never on cross-slot authoring order — two
8
+ // stacks with the same features produce the same feature-set and therefore the same define-key, so
9
+ // they batch together. Reserved and vendor-prefixed slots outside the built-in taxonomy sort after
10
+ // the known slots, in stable authoring order.
11
+ //
12
+ // Allocates a new array (a compile-time composition step, not a hot-loop write); the input stack is
13
+ // not mutated.
14
+ export function orderModifierStack(stack) {
15
+ const indexed = stack.map((modifier, index) => ({ index, modifier }));
16
+ indexed.sort((a, b) => {
17
+ const rankDelta = getModifierSlotRank(a.modifier.slot) - getModifierSlotRank(b.modifier.slot);
18
+ return rankDelta !== 0 ? rankDelta : a.index - b.index;
19
+ });
20
+ return indexed.map((entry) => entry.modifier);
21
+ }
22
+ // The canonical slot pipeline rank. Unknown slots (reserved Ambient/Shadow, vendor-prefixed) sort
23
+ // after every built-in slot while staying stable relative to one another via the index tie-break.
24
+ function getModifierSlotRank(slot) {
25
+ const rank = SLOT_RANK.get(slot);
26
+ return rank !== undefined ? rank : SLOT_RANK.size;
27
+ }
28
+ const SLOT_RANK = new Map([
29
+ [ModifierSlot.Vertex, 0],
30
+ [ModifierSlot.Normal, 1],
31
+ [ModifierSlot.Diffuse, 2],
32
+ [ModifierSlot.Specular, 3],
33
+ [ModifierSlot.Emissive, 4],
34
+ [ModifierSlot.Effect, 5],
35
+ ]);
36
+ //# sourceMappingURL=orderModifierStack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orderModifierStack.js","sourceRoot":"","sources":["../src/orderModifierStack.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C,mGAAmG;AACnG,mGAAmG;AACnG,kGAAkG;AAClG,sGAAsG;AACtG,2CAA2C;AAC3C,oGAAoG;AACpG,mGAAmG;AACnG,mGAAmG;AACnG,8CAA8C;AAC9C,EAAE;AACF,oGAAoG;AACpG,eAAe;AACf,MAAM,UAAU,kBAAkB,CAAC,KAA0B;IAC3D,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;IACtE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACpB,MAAM,SAAS,GAAG,mBAAmB,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC9F,OAAO,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;IACzD,CAAC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;AAChD,CAAC;AAED,kGAAkG;AAClG,kGAAkG;AAClG,SAAS,mBAAmB,CAAC,IAAkB;IAC7C,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC;AACpD,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,GAAG,CAAuB;IAC9C,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC;IACxB,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC;IACxB,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC,CAAC;IACzB,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC1B,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC1B,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC;CACzB,CAAC,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { ModifierDefinition, ModifierRegistry } from './modifierRegistry';
2
+ export declare const animatedNormalModifierDefinition: ModifierDefinition;
3
+ export declare const dissolveModifierDefinition: ModifierDefinition;
4
+ export declare const emissiveModifierDefinition: ModifierDefinition;
5
+ export declare const envReflectModifierDefinition: ModifierDefinition;
6
+ export declare const fogModifierDefinition: ModifierDefinition;
7
+ export declare function registerBuiltInModifiers(registry: Readonly<ModifierRegistry>): void;
8
+ export declare const rimModifierDefinition: ModifierDefinition;
9
+ export declare const toonModifierDefinition: ModifierDefinition;
10
+ export declare const vertexDisplaceModifierDefinition: ModifierDefinition;
11
+ //# sourceMappingURL=registerBuiltInModifiers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registerBuiltInModifiers.d.ts","sourceRoot":"","sources":["../src/registerBuiltInModifiers.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAQ/E,eAAO,MAAM,gCAAgC,EAAE,kBAQ9C,CAAC;AAKF,eAAO,MAAM,0BAA0B,EAAE,kBAMxC,CAAC;AAMF,eAAO,MAAM,0BAA0B,EAAE,kBAUxC,CAAC;AAKF,eAAO,MAAM,4BAA4B,EAAE,kBAG1C,CAAC;AAKF,eAAO,MAAM,qBAAqB,EAAE,kBASnC,CAAC;AAMF,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GAAG,IAAI,CASnF;AAID,eAAO,MAAM,qBAAqB,EAAE,kBAGnC,CAAC;AAIF,eAAO,MAAM,sBAAsB,EAAE,kBAGpC,CAAC;AAMF,eAAO,MAAM,gCAAgC,EAAE,kBAS9C,CAAC"}
@@ -0,0 +1,108 @@
1
+ import { AnimatedNormalModifierKind, DissolveModifierKind, EmissiveModifierFacing, EmissiveModifierKind, EnvReflectModifierKind, FogModifierKind, FogModifierMode, ModifierSlot, RimModifierKind, ToonModifierKind, VertexDisplaceModifierKind, VertexDisplaceModifierSource, } from '@flighthq/types';
2
+ import { registerModifier } from './modifierRegistry';
3
+ // The substrate-agnostic AnimatedNormal built-in definition (slot: Normal). Its signature keys the
4
+ // layer count from map structure — `0` disabled (no map), `1` single-layer, `2` dual-layer
5
+ // (secondaryMap present) — the only compile-time-affecting choices; scroll/strength are uniforms.
6
+ // A backend snippet reuses this definition (kind/slot/signature) rather than re-deriving it, so the
7
+ // framework-computed batch key and the compiled variant can never drift.
8
+ export const animatedNormalModifierDefinition = {
9
+ kind: AnimatedNormalModifierKind,
10
+ slot: ModifierSlot.Normal,
11
+ getDefineSignature(modifier) {
12
+ const animated = modifier;
13
+ if (animated.map === null)
14
+ return '0';
15
+ return animated.secondaryMap !== undefined ? '2' : '1';
16
+ },
17
+ };
18
+ // The substrate-agnostic Dissolve built-in definition (slot: Effect). Its signature is `m` when a
19
+ // mask/noise texture is supplied (a sampled-noise program) and `''` for the procedural-noise variant
20
+ // — the one compile-time-affecting choice; threshold/edge width/scale are uniforms.
21
+ export const dissolveModifierDefinition = {
22
+ kind: DissolveModifierKind,
23
+ slot: ModifierSlot.Effect,
24
+ getDefineSignature(modifier) {
25
+ return modifier.map !== undefined ? 'm' : '';
26
+ },
27
+ };
28
+ // The substrate-agnostic Emissive built-in definition (slot: Emissive). Its signature encodes the
29
+ // two compile-time-affecting options — `m` when masked, `g` when facing-gated (facing set and not
30
+ // Ignore), combined `mg` — never the uniform-fed color/strength. Shared by every backend snippet so
31
+ // the batch key and the compiled program variant stay single-sourced.
32
+ export const emissiveModifierDefinition = {
33
+ kind: EmissiveModifierKind,
34
+ slot: ModifierSlot.Emissive,
35
+ getDefineSignature(modifier) {
36
+ const emissive = modifier;
37
+ let signature = '';
38
+ if (emissive.mask !== undefined)
39
+ signature += 'm';
40
+ if (emissive.facing !== undefined && emissive.facing !== EmissiveModifierFacing.Ignore)
41
+ signature += 'g';
42
+ return signature;
43
+ },
44
+ };
45
+ // The substrate-agnostic EnvReflect built-in definition (slot: Effect). One program shape, so no
46
+ // signature — tint/intensity/fresnelBias/roughness are all uniforms and never split a variant. It
47
+ // samples the shared baked environment cubemap the lit block already binds.
48
+ export const envReflectModifierDefinition = {
49
+ kind: EnvReflectModifierKind,
50
+ slot: ModifierSlot.Effect,
51
+ };
52
+ // The substrate-agnostic Fog built-in definition (slot: Effect). Its signature is the distance-curve
53
+ // mode — `l` Linear, `e` Exponential, `x` Exponential2 — the only compile-time-affecting choice; the
54
+ // near/far/density scalars and color are uniforms.
55
+ export const fogModifierDefinition = {
56
+ kind: FogModifierKind,
57
+ slot: ModifierSlot.Effect,
58
+ getDefineSignature(modifier) {
59
+ const fog = modifier;
60
+ if (fog.mode === FogModifierMode.Exponential)
61
+ return 'e';
62
+ if (fog.mode === FogModifierMode.Exponential2)
63
+ return 'x';
64
+ return 'l';
65
+ },
66
+ };
67
+ // Registers the eight built-in modifiers into `registry` with their slots and define-key signatures
68
+ // (the eight exported built-in definitions). Not called at module load (packages are import
69
+ // side-effect-free); a caller opts in once, alongside any vendor-prefixed kinds, before composing.
70
+ // Last-write-wins, so calling it after a custom override re-installs the built-ins.
71
+ export function registerBuiltInModifiers(registry) {
72
+ registerModifier(registry, animatedNormalModifierDefinition);
73
+ registerModifier(registry, dissolveModifierDefinition);
74
+ registerModifier(registry, emissiveModifierDefinition);
75
+ registerModifier(registry, envReflectModifierDefinition);
76
+ registerModifier(registry, fogModifierDefinition);
77
+ registerModifier(registry, rimModifierDefinition);
78
+ registerModifier(registry, toonModifierDefinition);
79
+ registerModifier(registry, vertexDisplaceModifierDefinition);
80
+ }
81
+ // The substrate-agnostic Rim built-in definition (slot: Effect). One program shape, so no signature —
82
+ // power/intensity/bias are all uniforms and never split a variant.
83
+ export const rimModifierDefinition = {
84
+ kind: RimModifierKind,
85
+ slot: ModifierSlot.Effect,
86
+ };
87
+ // The substrate-agnostic Toon built-in definition (slot: Effect). One program shape, so no signature —
88
+ // steps/smoothness are uniforms and never split a variant.
89
+ export const toonModifierDefinition = {
90
+ kind: ToonModifierKind,
91
+ slot: ModifierSlot.Effect,
92
+ };
93
+ // The substrate-agnostic VertexDisplace built-in definition (slot: Vertex — the one vertex-stage
94
+ // built-in). Its signature keys the two compile-time-affecting choices: the noise `source` (`s` Sine,
95
+ // `h` HeightMap) and whether a fixed `axis` replaces the surface-normal push (`+a`); amplitude/
96
+ // frequency/speed/direction are uniforms.
97
+ export const vertexDisplaceModifierDefinition = {
98
+ kind: VertexDisplaceModifierKind,
99
+ slot: ModifierSlot.Vertex,
100
+ getDefineSignature(modifier) {
101
+ const displace = modifier;
102
+ let signature = displace.source === VertexDisplaceModifierSource.HeightMap ? 'h' : 's';
103
+ if (displace.axis !== undefined)
104
+ signature += 'a';
105
+ return signature;
106
+ },
107
+ };
108
+ //# sourceMappingURL=registerBuiltInModifiers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registerBuiltInModifiers.js","sourceRoot":"","sources":["../src/registerBuiltInModifiers.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@flighthq/shading",
3
+ "version": "0.2.1-next.410.a23f189",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/flighthq/flight.git",
7
+ "directory": "packages/shading"
8
+ },
9
+ "type": "module",
10
+ "main": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src/**/*.test.ts",
21
+ "!dist/**/*.test.js",
22
+ "!dist/**/*.test.d.ts",
23
+ "!dist/**/*.test.js.map",
24
+ "!dist/**/*.test.d.ts.map"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc -b",
28
+ "clean": "tsc -b --clean",
29
+ "test": "vitest run --config vitest.config.ts",
30
+ "test:watch": "vitest --watch --config vitest.config.ts",
31
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
32
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
33
+ },
34
+ "dependencies": {
35
+ "@flighthq/entity": "0.2.1-next.410.a23f189",
36
+ "@flighthq/types": "0.2.1-next.410.a23f189"
37
+ },
38
+ "devDependencies": {
39
+ "typescript": "^5.3.0"
40
+ },
41
+ "description": "Material Feature / Modifier tier — compiled shader augmentations that inject into a ShadedMaterial's shading and produce batchable variants. A composable base lit material carrying an ordered Modifier stack, an open registry keyed by feature kind over the diffuse/specular/normal/emissive/effect slot taxonomy, the composition/ordering contract that turns a stack into a deterministic ordered feature-set plus a stable define-key, seed emissive/rim/animated-normal modifier constructors, and sentinel-returning validation; substrate-agnostic and tree-shakable, the tier between complete materials and post-output adjustments",
42
+ "sideEffects": false
43
+ }
@@ -0,0 +1,49 @@
1
+ import type { Texture, Vector2Like } from '@flighthq/types';
2
+ import { AnimatedNormalModifierKind, ModifierSlot } from '@flighthq/types';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { createAnimatedNormalModifier } from './createAnimatedNormalModifier';
6
+
7
+ describe('createAnimatedNormalModifier', () => {
8
+ const scroll: Vector2Like = { x: 0.1, y: 0 };
9
+
10
+ it('sets the kind and Normal slot', () => {
11
+ const modifier = createAnimatedNormalModifier({ map: null, scroll });
12
+ expect(modifier.kind).toBe(AnimatedNormalModifierKind);
13
+ expect(modifier.slot).toBe(ModifierSlot.Normal);
14
+ });
15
+
16
+ it('accepts a null map (the disabled variant)', () => {
17
+ const modifier = createAnimatedNormalModifier({ map: null, scroll });
18
+ expect(modifier.map).toBeNull();
19
+ });
20
+
21
+ it('keeps the map and scroll by reference and defaults strength to 1', () => {
22
+ const map = {} as Texture;
23
+ const modifier = createAnimatedNormalModifier({ map, scroll });
24
+ expect(modifier.map).toBe(map);
25
+ expect(modifier.scroll).toBe(scroll);
26
+ expect(modifier.strength).toBe(1);
27
+ });
28
+
29
+ it('leaves the second layer absent when not provided', () => {
30
+ const modifier = createAnimatedNormalModifier({ map: {} as Texture, scroll });
31
+ expect(modifier.secondaryMap).toBeUndefined();
32
+ expect(modifier.secondaryScroll).toBeUndefined();
33
+ });
34
+
35
+ it('carries the second dual-scroll layer when provided', () => {
36
+ const secondaryMap = {} as Texture;
37
+ const secondaryScroll: Vector2Like = { x: -0.05, y: 0.02 };
38
+ const modifier = createAnimatedNormalModifier({
39
+ map: {} as Texture,
40
+ scroll,
41
+ strength: 0.5,
42
+ secondaryMap,
43
+ secondaryScroll,
44
+ });
45
+ expect(modifier.strength).toBe(0.5);
46
+ expect(modifier.secondaryMap).toBe(secondaryMap);
47
+ expect(modifier.secondaryScroll).toBe(secondaryScroll);
48
+ });
49
+ });
@@ -0,0 +1,30 @@
1
+ import type { Texture } from '@flighthq/types';
2
+ import { DissolveModifierKind, ModifierSlot } from '@flighthq/types';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { createDissolveModifier } from './createDissolveModifier';
6
+
7
+ describe('createDissolveModifier', () => {
8
+ it('sets the kind and Effect slot', () => {
9
+ const modifier = createDissolveModifier({ threshold: 0.5 });
10
+ expect(modifier.kind).toBe(DissolveModifierKind);
11
+ expect(modifier.slot).toBe(ModifierSlot.Effect);
12
+ });
13
+
14
+ it('carries the threshold through and defaults the edge and noise scale', () => {
15
+ const modifier = createDissolveModifier({ threshold: 0.3 });
16
+ expect(modifier.threshold).toBe(0.3);
17
+ expect(modifier.edgeColor).toBe(0xff6600ff);
18
+ expect(modifier.edgeWidth).toBe(0.05);
19
+ expect(modifier.scale).toBe(8);
20
+ expect(modifier.map).toBeUndefined();
21
+ });
22
+
23
+ it('keeps a noise map by reference when provided', () => {
24
+ const map = {} as Texture;
25
+ const modifier = createDissolveModifier({ threshold: 0.5, map, edgeColor: 0x00ffddff, edgeWidth: 0.1 });
26
+ expect(modifier.map).toBe(map);
27
+ expect(modifier.edgeColor).toBe(0x00ffddff);
28
+ expect(modifier.edgeWidth).toBe(0.1);
29
+ });
30
+ });
@@ -0,0 +1,44 @@
1
+ import type { Texture } from '@flighthq/types';
2
+ import { EmissiveModifierFacing, EmissiveModifierKind, ModifierSlot } from '@flighthq/types';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { createEmissiveModifier } from './createEmissiveModifier';
6
+
7
+ describe('createEmissiveModifier', () => {
8
+ it('sets the kind and Emissive slot', () => {
9
+ const modifier = createEmissiveModifier({ color: 0xffaa00ff });
10
+ expect(modifier.kind).toBe(EmissiveModifierKind);
11
+ expect(modifier.slot).toBe(ModifierSlot.Emissive);
12
+ });
13
+
14
+ it('carries the packed RGBA color through', () => {
15
+ const modifier = createEmissiveModifier({ color: 0x1234abff });
16
+ expect(modifier.color).toBe(0x1234abff);
17
+ });
18
+
19
+ it('defaults strength to 1, facing to Ignore, softness to 0, and leaves the mask absent', () => {
20
+ const modifier = createEmissiveModifier({ color: 0xffffffff });
21
+ expect(modifier.strength).toBe(1);
22
+ expect(modifier.facing).toBe(EmissiveModifierFacing.Ignore);
23
+ expect(modifier.facingSoftness).toBe(0);
24
+ expect(modifier.mask).toBeUndefined();
25
+ });
26
+
27
+ it('applies provided scalars and a facing gate', () => {
28
+ const modifier = createEmissiveModifier({
29
+ color: 0xffffffff,
30
+ strength: 4,
31
+ facing: EmissiveModifierFacing.AwayFromLight,
32
+ facingSoftness: 0.25,
33
+ });
34
+ expect(modifier.strength).toBe(4);
35
+ expect(modifier.facing).toBe(EmissiveModifierFacing.AwayFromLight);
36
+ expect(modifier.facingSoftness).toBe(0.25);
37
+ });
38
+
39
+ it('keeps the mask by reference when provided', () => {
40
+ const mask = {} as Texture;
41
+ const modifier = createEmissiveModifier({ color: 0xffffffff, mask });
42
+ expect(modifier.mask).toBe(mask);
43
+ });
44
+ });
@@ -0,0 +1,28 @@
1
+ import { EnvReflectModifierKind, ModifierSlot } from '@flighthq/types';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { createEnvReflectModifier } from './createEnvReflectModifier';
5
+
6
+ describe('createEnvReflectModifier', () => {
7
+ it('sets the kind and Effect slot', () => {
8
+ const modifier = createEnvReflectModifier();
9
+ expect(modifier.kind).toBe(EnvReflectModifierKind);
10
+ expect(modifier.slot).toBe(ModifierSlot.Effect);
11
+ });
12
+
13
+ it('defaults to a mirror-sharp opaque-white dielectric reflection', () => {
14
+ const modifier = createEnvReflectModifier();
15
+ expect(modifier.tint).toBe(0xffffffff);
16
+ expect(modifier.intensity).toBe(1);
17
+ expect(modifier.fresnelBias).toBe(0.04);
18
+ expect(modifier.roughness).toBe(0);
19
+ });
20
+
21
+ it('applies provided scalars', () => {
22
+ const modifier = createEnvReflectModifier({ tint: 0x88ccffff, intensity: 0.5, fresnelBias: 0.1, roughness: 0.6 });
23
+ expect(modifier.tint).toBe(0x88ccffff);
24
+ expect(modifier.intensity).toBe(0.5);
25
+ expect(modifier.fresnelBias).toBe(0.1);
26
+ expect(modifier.roughness).toBe(0.6);
27
+ });
28
+ });
@@ -0,0 +1,31 @@
1
+ import { FogModifierKind, FogModifierMode, ModifierSlot } from '@flighthq/types';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { createFogModifier } from './createFogModifier';
5
+
6
+ describe('createFogModifier', () => {
7
+ it('sets the kind and Effect slot', () => {
8
+ const modifier = createFogModifier({ color: 0xaabbccff });
9
+ expect(modifier.kind).toBe(FogModifierKind);
10
+ expect(modifier.slot).toBe(ModifierSlot.Effect);
11
+ });
12
+
13
+ it('carries the packed RGBA color through', () => {
14
+ const modifier = createFogModifier({ color: 0x1122ddff });
15
+ expect(modifier.color).toBe(0x1122ddff);
16
+ });
17
+
18
+ it('defaults to a linear ramp from 0 to 1 with density 1', () => {
19
+ const modifier = createFogModifier({ color: 0xffffffff });
20
+ expect(modifier.mode).toBe(FogModifierMode.Linear);
21
+ expect(modifier.near).toBe(0);
22
+ expect(modifier.far).toBe(1);
23
+ expect(modifier.density).toBe(1);
24
+ });
25
+
26
+ it('applies provided mode and scalars', () => {
27
+ const modifier = createFogModifier({ color: 0xffffffff, mode: FogModifierMode.Exponential2, density: 0.05 });
28
+ expect(modifier.mode).toBe(FogModifierMode.Exponential2);
29
+ expect(modifier.density).toBe(0.05);
30
+ });
31
+ });
@@ -0,0 +1,31 @@
1
+ import { ModifierSlot, RimModifierKind } from '@flighthq/types';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { createRimModifier } from './createRimModifier';
5
+
6
+ describe('createRimModifier', () => {
7
+ it('sets the kind and Effect slot', () => {
8
+ const modifier = createRimModifier({ color: 0x88ccffff });
9
+ expect(modifier.kind).toBe(RimModifierKind);
10
+ expect(modifier.slot).toBe(ModifierSlot.Effect);
11
+ });
12
+
13
+ it('carries the packed RGBA color through', () => {
14
+ const modifier = createRimModifier({ color: 0x010203ff });
15
+ expect(modifier.color).toBe(0x010203ff);
16
+ });
17
+
18
+ it('defaults power to 3, intensity to 1, bias to 0', () => {
19
+ const modifier = createRimModifier({ color: 0xffffffff });
20
+ expect(modifier.power).toBe(3);
21
+ expect(modifier.intensity).toBe(1);
22
+ expect(modifier.bias).toBe(0);
23
+ });
24
+
25
+ it('applies provided falloff scalars', () => {
26
+ const modifier = createRimModifier({ color: 0xffffffff, power: 5, intensity: 2, bias: 0.1 });
27
+ expect(modifier.power).toBe(5);
28
+ expect(modifier.intensity).toBe(2);
29
+ expect(modifier.bias).toBe(0.1);
30
+ });
31
+ });
@@ -0,0 +1,45 @@
1
+ import { EntityRuntimeKey, ShadedMaterialKind } from '@flighthq/types';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { createEmissiveModifier } from './createEmissiveModifier';
5
+ import { createShadedMaterial } from './createShadedMaterial';
6
+
7
+ describe('createShadedMaterial', () => {
8
+ it('builds an entity with the ShadedMaterial kind', () => {
9
+ const material = createShadedMaterial();
10
+ expect(material.kind).toBe(ShadedMaterialKind);
11
+ expect(EntityRuntimeKey in material).toBe(true);
12
+ });
13
+
14
+ it('defaults the lit base to white diffuse/specular, shininess 32, unit normal scale, no maps', () => {
15
+ const material = createShadedMaterial();
16
+ expect(material.diffuse).toBe(0xffffffff);
17
+ expect(material.specular).toBe(0xffffffff);
18
+ expect(material.shininess).toBe(32);
19
+ expect(material.normalScale).toBe(1);
20
+ expect(material.diffuseMap).toBeNull();
21
+ expect(material.specularMap).toBeNull();
22
+ expect(material.normalMap).toBeNull();
23
+ });
24
+
25
+ it('defaults the SurfaceMaterial trailer to opaque single-sided straight-alpha', () => {
26
+ const material = createShadedMaterial();
27
+ expect(material.alphaMode).toBe('opaque');
28
+ expect(material.alphaType).toBe('straight');
29
+ expect(material.alphaCutoff).toBe(0.5);
30
+ expect(material.doubleSided).toBe(false);
31
+ });
32
+
33
+ it('defaults to an empty modifier stack', () => {
34
+ const material = createShadedMaterial();
35
+ expect(material.modifiers).toEqual([]);
36
+ });
37
+
38
+ it('applies provided base values and modifier stack', () => {
39
+ const modifiers = [createEmissiveModifier({ color: 0xffaa00ff })];
40
+ const material = createShadedMaterial({ diffuse: 0x102030ff, shininess: 8, modifiers });
41
+ expect(material.diffuse).toBe(0x102030ff);
42
+ expect(material.shininess).toBe(8);
43
+ expect(material.modifiers).toBe(modifiers);
44
+ });
45
+ });
@@ -0,0 +1,23 @@
1
+ import { ModifierSlot, ToonModifierKind } from '@flighthq/types';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { createToonModifier } from './createToonModifier';
5
+
6
+ describe('createToonModifier', () => {
7
+ it('sets the kind and Effect slot', () => {
8
+ const modifier = createToonModifier({ steps: 3 });
9
+ expect(modifier.kind).toBe(ToonModifierKind);
10
+ expect(modifier.slot).toBe(ModifierSlot.Effect);
11
+ });
12
+
13
+ it('carries the step count through and defaults smoothness to 0', () => {
14
+ const modifier = createToonModifier({ steps: 4 });
15
+ expect(modifier.steps).toBe(4);
16
+ expect(modifier.smoothness).toBe(0);
17
+ });
18
+
19
+ it('applies a provided smoothness', () => {
20
+ const modifier = createToonModifier({ steps: 2, smoothness: 0.2 });
21
+ expect(modifier.smoothness).toBe(0.2);
22
+ });
23
+ });
@@ -0,0 +1,36 @@
1
+ import type { Texture, Vector3Like } from '@flighthq/types';
2
+ import { ModifierSlot, VertexDisplaceModifierKind, VertexDisplaceModifierSource } from '@flighthq/types';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { createVertexDisplaceModifier } from './createVertexDisplaceModifier';
6
+
7
+ describe('createVertexDisplaceModifier', () => {
8
+ it('sets the kind and the Vertex slot', () => {
9
+ const modifier = createVertexDisplaceModifier({ source: VertexDisplaceModifierSource.Sine, amplitude: 0.1 });
10
+ expect(modifier.kind).toBe(VertexDisplaceModifierKind);
11
+ expect(modifier.slot).toBe(ModifierSlot.Vertex);
12
+ });
13
+
14
+ it('defaults the Sine wave params and leaves axis/map absent', () => {
15
+ const modifier = createVertexDisplaceModifier({ source: VertexDisplaceModifierSource.Sine, amplitude: 0.2 });
16
+ expect(modifier.frequency).toBe(1);
17
+ expect(modifier.speed).toBe(1);
18
+ expect(modifier.direction).toEqual({ x: 1, y: 0, z: 0 });
19
+ expect(modifier.axis).toBeUndefined();
20
+ expect(modifier.map).toBeUndefined();
21
+ });
22
+
23
+ it('keeps a fixed axis and a height map by reference when provided', () => {
24
+ const axis: Vector3Like = { x: 0, y: 1, z: 0 };
25
+ const map = {} as Texture;
26
+ const modifier = createVertexDisplaceModifier({
27
+ source: VertexDisplaceModifierSource.HeightMap,
28
+ amplitude: 1,
29
+ axis,
30
+ map,
31
+ });
32
+ expect(modifier.source).toBe(VertexDisplaceModifierSource.HeightMap);
33
+ expect(modifier.axis).toBe(axis);
34
+ expect(modifier.map).toBe(map);
35
+ });
36
+ });
@@ -0,0 +1,74 @@
1
+ import type { Texture } from '@flighthq/types';
2
+ import { EmissiveModifierFacing } from '@flighthq/types';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { createAnimatedNormalModifier } from './createAnimatedNormalModifier';
6
+ import { createEmissiveModifier } from './createEmissiveModifier';
7
+ import { createRimModifier } from './createRimModifier';
8
+ import { getModifierDefineKey } from './getModifierDefineKey';
9
+ import { createModifierRegistry } from './modifierRegistry';
10
+ import { registerBuiltInModifiers } from './registerBuiltInModifiers';
11
+
12
+ describe('getModifierDefineKey', () => {
13
+ const registry = createModifierRegistry();
14
+ registerBuiltInModifiers(registry);
15
+
16
+ it('returns the empty string for an empty stack', () => {
17
+ expect(getModifierDefineKey([], registry)).toBe('');
18
+ });
19
+
20
+ it('joins slot-ordered kinds with the plus separator', () => {
21
+ const normal = createAnimatedNormalModifier({ map: {} as Texture, scroll: { x: 0, y: 0 } });
22
+ const emissive = createEmissiveModifier({ color: 0xffffffff });
23
+ const rim = createRimModifier({ color: 0xffffffff });
24
+ const key = getModifierDefineKey([rim, emissive, normal], registry);
25
+ expect(key).toBe('AnimatedNormalModifier:1+EmissiveModifier+RimModifier');
26
+ });
27
+
28
+ it('is stable regardless of cross-slot authoring order', () => {
29
+ const normal = createAnimatedNormalModifier({ map: {} as Texture, scroll: { x: 0, y: 0 } });
30
+ const rim = createRimModifier({ color: 0xffffffff });
31
+ expect(getModifierDefineKey([normal, rim], registry)).toBe(getModifierDefineKey([rim, normal], registry));
32
+ });
33
+
34
+ it('captures compile-time variants but not uniform-fed scalars', () => {
35
+ const plain = createEmissiveModifier({ color: 0xffffffff });
36
+ const strongerButSameProgram = createEmissiveModifier({ color: 0x00ff00ff, strength: 9 });
37
+ const masked = createEmissiveModifier({ color: 0xffffffff, mask: {} as Texture });
38
+ const gated = createEmissiveModifier({ color: 0xffffffff, facing: EmissiveModifierFacing.AwayFromLight });
39
+ const maskedAndGated = createEmissiveModifier({
40
+ color: 0xffffffff,
41
+ mask: {} as Texture,
42
+ facing: EmissiveModifierFacing.TowardLight,
43
+ });
44
+ expect(getModifierDefineKey([plain], registry)).toBe('EmissiveModifier');
45
+ expect(getModifierDefineKey([strongerButSameProgram], registry)).toBe('EmissiveModifier');
46
+ expect(getModifierDefineKey([masked], registry)).toBe('EmissiveModifier:m');
47
+ expect(getModifierDefineKey([gated], registry)).toBe('EmissiveModifier:g');
48
+ expect(getModifierDefineKey([maskedAndGated], registry)).toBe('EmissiveModifier:mg');
49
+ });
50
+
51
+ it('distinguishes single, dual, and disabled animated-normal variants', () => {
52
+ const disabled = createAnimatedNormalModifier({ map: null, scroll: { x: 0, y: 0 } });
53
+ const single = createAnimatedNormalModifier({ map: {} as Texture, scroll: { x: 0, y: 0 } });
54
+ const dual = createAnimatedNormalModifier({
55
+ map: {} as Texture,
56
+ scroll: { x: 0, y: 0 },
57
+ secondaryMap: {} as Texture,
58
+ });
59
+ expect(getModifierDefineKey([disabled], registry)).toBe('AnimatedNormalModifier:0');
60
+ expect(getModifierDefineKey([single], registry)).toBe('AnimatedNormalModifier:1');
61
+ expect(getModifierDefineKey([dual], registry)).toBe('AnimatedNormalModifier:2');
62
+ });
63
+
64
+ it('falls back to bare kinds without a registry', () => {
65
+ const masked = createEmissiveModifier({ color: 0xffffffff, mask: {} as Texture });
66
+ expect(getModifierDefineKey([masked])).toBe('EmissiveModifier');
67
+ });
68
+
69
+ it('reflects per-slot count and order in the key', () => {
70
+ const rimA = createRimModifier({ color: 0x111111ff });
71
+ const rimB = createRimModifier({ color: 0x222222ff });
72
+ expect(getModifierDefineKey([rimA, rimB], registry)).toBe('RimModifier+RimModifier');
73
+ });
74
+ });