onejs-sl 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ops.ts ADDED
@@ -0,0 +1,346 @@
1
+ /**
2
+ * The shader language opcode table.
3
+ *
4
+ * Phase 1 of `Specs/SHADER_LANG.md`. This is the contract three things will
5
+ * share: the encoder that packs a program into a texture, the VM shader that
6
+ * evaluates it, and the HLSL emitter that prints it. Two of those do not exist
7
+ * yet, which is exactly why the numbering is fixed now rather than later.
8
+ *
9
+ * Numbered in families with gaps, the convention `fx/ops.ts` set, so a reader
10
+ * can tell what an opcode is from its value:
11
+ *
12
+ * 0..15 construct and swizzle
13
+ * 16..47 arithmetic
14
+ * 48..79 maths
15
+ * 80..95 geometry
16
+ * 96..111 interpolate
17
+ * 112..127 colour
18
+ * 128..143 procedural
19
+ * 144..159 sampling
20
+ *
21
+ * Leave the gaps. A later opcode landing in the right family is worth more than
22
+ * a dense table.
23
+ */
24
+
25
+ /**
26
+ * The newest VM encoding this encoder writes. The VM accepts 1..its own and
27
+ * refuses newer, where there is a VM: a WebGL player draws compiled and never
28
+ * reads the buffer.
29
+ *
30
+ * A payload records the LOWEST version that can run it (`Encoded.wire`), not
31
+ * this: a program that uses nothing new stays 1, so a new bundle still runs on
32
+ * an older Play container, and only a program that needs a newer instruction
33
+ * is refused by an older one rather than drawn wrong.
34
+ */
35
+ export const SL_WIRE_VERSION = 2
36
+
37
+ /**
38
+ * What the VM shader actually declares, and therefore what a program may use.
39
+ *
40
+ * `FxProgram.shader` has `float4 _Uniforms[16]` and four `sampler2D`s, and
41
+ * `SLProgramBridge.cs` fills exactly those. Past either ceiling the VM does not
42
+ * fail, it clamps: uniform 17 reads slot 15 and texture slot 4 samples `_Tex3`,
43
+ * while the generated HLSL would declare both correctly. That is a silent
44
+ * disagreement between the backends, so the text form reports it at the
45
+ * declaration with a file and a line. See `Specs/SL_TEXT.md` 3.7.
46
+ */
47
+ export const VM_UNIFORMS = 16
48
+ export const VM_TEXTURES = 4
49
+
50
+ /** Input ids, fixed here because the shader switches on them. */
51
+ export const INPUT_ID: Record<string, number> = {
52
+ uv: 0, fragCoord: 1, resolution: 2, time: 3, aspect: 4,
53
+ }
54
+
55
+ export const SLOP = {
56
+ // Construct and swizzle
57
+ CONST: 0,
58
+ INPUT: 1,
59
+ UNIFORM: 2,
60
+ COMPOSE: 3, // build a wider value from narrower parts
61
+ SWIZZLE: 4,
62
+
63
+ // Arithmetic
64
+ ADD: 16,
65
+ SUB: 17,
66
+ MUL: 18,
67
+ DIV: 19,
68
+ MOD: 20,
69
+ POW: 21,
70
+ NEG: 22,
71
+ RECIP: 23,
72
+
73
+ // Maths
74
+ SIN: 48,
75
+ COS: 49,
76
+ TAN: 50,
77
+ ASIN: 51,
78
+ ACOS: 52,
79
+ ATAN2: 53,
80
+ EXP: 54,
81
+ LOG: 55,
82
+ SQRT: 56,
83
+ ABS: 57,
84
+ SIGN: 58,
85
+ FLOOR: 59,
86
+ CEIL: 60,
87
+ ROUND: 61,
88
+ FRACT: 62,
89
+ MIN: 63,
90
+ MAX: 64,
91
+ CLAMP: 65,
92
+ SATURATE: 66,
93
+
94
+ // Geometry
95
+ LENGTH: 80,
96
+ DISTANCE: 81,
97
+ DOT: 82,
98
+ CROSS: 83,
99
+ NORMALIZE: 84,
100
+ REFLECT: 85,
101
+
102
+ // Interpolate
103
+ MIX: 96,
104
+ STEP: 97,
105
+ SMOOTHSTEP: 98,
106
+ SELECT: 99,
107
+ REMAP: 100,
108
+
109
+ // Colour
110
+ RAMP: 112,
111
+ HSV2RGB: 113,
112
+ RGB2HSV: 114,
113
+ LUMINANCE: 115,
114
+ // A colour as written (sRGB, like CSS) to the working space the target
115
+ // holds. Gamma-aware in both backends; alpha is left alone.
116
+ TO_LINEAR: 116,
117
+
118
+ // Procedural. These are SUPERINSTRUCTIONS: one opcode expanding to a
119
+ // substantial block in the shader, the way SOURCE_NOISE and SOURCE_SDF
120
+ // already do in FxSources. Making an author build fbm out of thirty
121
+ // primitives would be slower to run and worse to write, and we already own
122
+ // the HLSL.
123
+ NOISE: 128,
124
+ SIMPLEX: 129,
125
+ FBM: 130,
126
+ SDF: 131,
127
+ VORONOI: 132,
128
+ // Same field family as fx noise: |simplex| octaves summed, and the crease
129
+ // made bright. imm.x carries the octave count, as FBM does.
130
+ TURBULENCE: 133,
131
+ RIDGED: 134,
132
+ // VM only: an SDF given a fifth or sixth parameter, which one instruction's
133
+ // four immediates and id cannot hold. The encoder makes it (`forVm`); the
134
+ // IR and every compiled backend only ever see SDF.
135
+ SDF_WIDE: 135,
136
+
137
+ // Sampling
138
+ SAMPLE: 144,
139
+ SAMPLE_LOD: 145,
140
+ } as const
141
+
142
+ export type SLOpCode = (typeof SLOP)[keyof typeof SLOP]
143
+
144
+ /**
145
+ * How many arguments each op takes, for validation and for the encoder.
146
+ *
147
+ * -1 means variadic: COMPOSE takes however many parts add up to its width, RAMP
148
+ * takes a value plus its stops. Everything else is fixed, and a mismatch is a
149
+ * bug in the builder rather than something an author can cause.
150
+ */
151
+ export const SL_ARITY: Record<number, number> = {
152
+ [SLOP.CONST]: 0, [SLOP.INPUT]: 0, [SLOP.UNIFORM]: 0,
153
+ [SLOP.COMPOSE]: -1, [SLOP.SWIZZLE]: 1,
154
+
155
+ [SLOP.ADD]: 2, [SLOP.SUB]: 2, [SLOP.MUL]: 2, [SLOP.DIV]: 2,
156
+ [SLOP.MOD]: 2, [SLOP.POW]: 2, [SLOP.NEG]: 1, [SLOP.RECIP]: 1,
157
+
158
+ [SLOP.SIN]: 1, [SLOP.COS]: 1, [SLOP.TAN]: 1, [SLOP.ASIN]: 1, [SLOP.ACOS]: 1,
159
+ [SLOP.ATAN2]: 2, [SLOP.EXP]: 1, [SLOP.LOG]: 1, [SLOP.SQRT]: 1, [SLOP.ABS]: 1,
160
+ [SLOP.SIGN]: 1, [SLOP.FLOOR]: 1, [SLOP.CEIL]: 1, [SLOP.ROUND]: 1,
161
+ [SLOP.FRACT]: 1, [SLOP.MIN]: 2, [SLOP.MAX]: 2, [SLOP.CLAMP]: 3,
162
+ [SLOP.SATURATE]: 1,
163
+
164
+ [SLOP.LENGTH]: 1, [SLOP.DISTANCE]: 2, [SLOP.DOT]: 2, [SLOP.CROSS]: 2,
165
+ [SLOP.NORMALIZE]: 1, [SLOP.REFLECT]: 2,
166
+
167
+ [SLOP.MIX]: 3, [SLOP.STEP]: 2, [SLOP.SMOOTHSTEP]: 3, [SLOP.SELECT]: 3,
168
+ [SLOP.REMAP]: 5,
169
+
170
+ [SLOP.RAMP]: -1, [SLOP.HSV2RGB]: 1, [SLOP.RGB2HSV]: 1, [SLOP.LUMINANCE]: 1, [SLOP.TO_LINEAR]: 1,
171
+
172
+ [SLOP.NOISE]: 1, [SLOP.SIMPLEX]: 1, [SLOP.FBM]: 1, [SLOP.SDF]: 1,
173
+ [SLOP.VORONOI]: 1, [SLOP.TURBULENCE]: 1, [SLOP.RIDGED]: 1, [SLOP.SDF_WIDE]: 2,
174
+
175
+ [SLOP.SAMPLE]: 1, [SLOP.SAMPLE_LOD]: 2,
176
+ }
177
+
178
+ /** Name per opcode, for error messages and for the HLSL emitter's comments. */
179
+ export const SL_NAME: Record<number, string> = Object.fromEntries(
180
+ Object.entries(SLOP).map(([name, code]) => [code, name.toLowerCase()]),
181
+ )
182
+
183
+ export const FIRST_ARITHMETIC = 16
184
+ export const FIRST_MATHS = 48
185
+ export const FIRST_GEOMETRY = 80
186
+ export const FIRST_INTERPOLATE = 96
187
+ export const FIRST_COLOUR = 112
188
+ export const FIRST_PROCEDURAL = 128
189
+ export const FIRST_SAMPLING = 144
190
+
191
+ /**
192
+ * Ops that read a texture. Sampler slots are the one resource ceiling neither
193
+ * backend can widen (16 in a fragment shader on WebGL2), so these are counted
194
+ * at record time.
195
+ */
196
+ export function isSampling(op: number): boolean {
197
+ return op >= FIRST_SAMPLING
198
+ }
199
+
200
+ /**
201
+ * How each opcode is written in the text form, `Specs/SL_TEXT.md` 3.4.
202
+ *
203
+ * ONE COLUMN, READ BY BOTH SURFACES. The EDSL's names and the parser's names
204
+ * are the same set by construction rather than by two lists that have to be
205
+ * kept level; `lang/builtins.ts` attaches a lowering to each `call` entry here
206
+ * and a contract test refuses an opcode that has neither a spelling nor a
207
+ * reason to lack one.
208
+ *
209
+ * `call` is a name an author writes as a function. `syntax` is everything the
210
+ * language produces from a form instead: an operator, a constructor, a swizzle,
211
+ * the conditional. An opcode has exactly one of the two.
212
+ */
213
+ export interface SLSurface {
214
+ call?: string
215
+ syntax?: string
216
+ /**
217
+ * Set when the surface form does not emit this opcode at all: it expands
218
+ * into ops both backends already have. `ramp` and `remap` are macros for
219
+ * the same reason `sl.ramp` is, and the opcode number stays reserved.
220
+ */
221
+ macro?: true
222
+ }
223
+
224
+ export const SL_HLSL: Record<number, SLSurface> = {
225
+ [SLOP.CONST]: { syntax: "a number literal" },
226
+ [SLOP.INPUT]: { syntax: "uv, fragCoord, resolution, time, aspect" },
227
+ [SLOP.UNIFORM]: { syntax: "uniform <type> name = <default>;" },
228
+ [SLOP.COMPOSE]: { syntax: "float2(), float3(), float4()" },
229
+ [SLOP.SWIZZLE]: { syntax: ".xyzw / .rgba" },
230
+
231
+ [SLOP.ADD]: { syntax: "+" },
232
+ [SLOP.SUB]: { syntax: "-" },
233
+ [SLOP.MUL]: { syntax: "*" },
234
+ [SLOP.DIV]: { syntax: "/" },
235
+ [SLOP.MOD]: { syntax: "%" },
236
+ [SLOP.POW]: { call: "pow" },
237
+ [SLOP.NEG]: { syntax: "unary -" },
238
+ [SLOP.RECIP]: { call: "rcp" },
239
+
240
+ [SLOP.SIN]: { call: "sin" },
241
+ [SLOP.COS]: { call: "cos" },
242
+ [SLOP.TAN]: { call: "tan" },
243
+ [SLOP.ASIN]: { call: "asin" },
244
+ [SLOP.ACOS]: { call: "acos" },
245
+ [SLOP.ATAN2]: { call: "atan2" },
246
+ [SLOP.EXP]: { call: "exp" },
247
+ [SLOP.LOG]: { call: "log" },
248
+ [SLOP.SQRT]: { call: "sqrt" },
249
+ [SLOP.ABS]: { call: "abs" },
250
+ [SLOP.SIGN]: { call: "sign" },
251
+ [SLOP.FLOOR]: { call: "floor" },
252
+ [SLOP.CEIL]: { call: "ceil" },
253
+ [SLOP.ROUND]: { call: "round" },
254
+ [SLOP.FRACT]: { call: "frac" },
255
+ [SLOP.MIN]: { call: "min" },
256
+ [SLOP.MAX]: { call: "max" },
257
+ [SLOP.CLAMP]: { call: "clamp" },
258
+ [SLOP.SATURATE]: { call: "saturate" },
259
+
260
+ [SLOP.LENGTH]: { call: "length" },
261
+ [SLOP.DISTANCE]: { call: "distance" },
262
+ [SLOP.DOT]: { call: "dot" },
263
+ [SLOP.CROSS]: { call: "cross" },
264
+ [SLOP.NORMALIZE]: { call: "normalize" },
265
+ [SLOP.REFLECT]: { call: "reflect" },
266
+
267
+ [SLOP.MIX]: { call: "lerp" },
268
+ [SLOP.STEP]: { call: "step" },
269
+ [SLOP.SMOOTHSTEP]: { call: "smoothstep" },
270
+ // `select` exists as a name too, because `sl.select` does and a contract
271
+ // test holds the two surfaces level, but `?:` is how anyone writes it.
272
+ [SLOP.SELECT]: { syntax: "?:" },
273
+ [SLOP.REMAP]: { call: "remap", macro: true },
274
+
275
+ [SLOP.RAMP]: { call: "ramp", macro: true },
276
+ [SLOP.HSV2RGB]: { call: "hsv2rgb" },
277
+ [SLOP.RGB2HSV]: { call: "rgb2hsv" },
278
+ [SLOP.LUMINANCE]: { call: "luminance" },
279
+ [SLOP.TO_LINEAR]: { call: "toLinear" },
280
+
281
+ [SLOP.NOISE]: { call: "noise" },
282
+ [SLOP.SIMPLEX]: { call: "simplex" },
283
+ [SLOP.FBM]: { call: "fbm" },
284
+ [SLOP.SDF]: { call: "sdf" },
285
+ [SLOP.VORONOI]: { call: "voronoi" },
286
+ [SLOP.TURBULENCE]: { call: "turbulence" },
287
+ [SLOP.RIDGED]: { call: "ridged" },
288
+ [SLOP.SDF_WIDE]: { syntax: "sdf.<shape>(p, ...) given a fifth or sixth parameter" },
289
+
290
+ [SLOP.SAMPLE]: { call: "tex2D" },
291
+ [SLOP.SAMPLE_LOD]: { call: "tex2Dlod" },
292
+ }
293
+
294
+ /**
295
+ * Opcodes no backend implements.
296
+ *
297
+ * Numbered so the families stay in order, and refused at the surface until
298
+ * somebody writes both halves. A program reaching one would render as whatever
299
+ * the VM's dispatch falls through to and fail outright in the HLSL emitter,
300
+ * which is the silent-disagreement failure this design exists to prevent, so
301
+ * the parser names them rather than treating them as unknown identifiers.
302
+ *
303
+ * `REMAP` is here because the opcode is unimplemented; the SURFACE `remap` is a
304
+ * macro over arithmetic and works everywhere.
305
+ */
306
+ export const SL_UNIMPLEMENTED: Record<number, string> = {
307
+ [SLOP.RGB2HSV]: "neither the VM shader nor the HLSL emitter has a case for it",
308
+ [SLOP.SAMPLE_LOD]: "the VM samples without an explicit LOD; use tex2D",
309
+ [SLOP.REMAP]: "the surface form is a macro over arithmetic, so the opcode is unused",
310
+ }
311
+
312
+ /**
313
+ * GLSL spellings, recognised only so they can be refused by their HLSL name.
314
+ *
315
+ * Accepting both would be two ways to write everything and a table that has to
316
+ * stay complete in two columns forever (`Specs/SL_TEXT.md` 9.1). Recognising
317
+ * them costs one entry each and turns "unknown identifier mix" into the answer.
318
+ */
319
+ export const SL_GLSL_HINT: Record<string, string> = {
320
+ mix: "lerp",
321
+ fract: "frac",
322
+ mod: "%",
323
+ texture: "tex2D",
324
+ textureLod: "tex2Dlod",
325
+ vec2: "float2",
326
+ vec3: "float3",
327
+ vec4: "float4",
328
+ ivec2: "float2",
329
+ ivec3: "float3",
330
+ ivec4: "float4",
331
+ inversesqrt: "1 / sqrt(x); there is no rsqrt opcode",
332
+ atan: "atan2",
333
+ sampler2D: "texture2D",
334
+ gl_FragCoord: "fragCoord",
335
+ gl_FragColor: "the value main returns",
336
+ iTime: "time",
337
+ iResolution: "resolution",
338
+ iMouse: "a uniform you declare",
339
+ }
340
+
341
+ /** Every opcode that an author can write as a call, by the name they write. */
342
+ export const SL_CALL_NAMES: Record<string, number> = Object.fromEntries(
343
+ Object.entries(SL_HLSL)
344
+ .filter(([, s]) => s.call !== undefined)
345
+ .map(([code, s]) => [s.call!, Number(code)]),
346
+ )
package/src/serial.ts ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * The IR as JSON, for a host that stores or ships a program rather than its
3
+ * source (`Specs/SL_PACKAGE.md` section 2).
4
+ *
5
+ * `toJSON` writes the version beside the graph. `fromJSON` is the only way
6
+ * back in, and it checks everything an emitter relies on, because an emitter
7
+ * handed a malformed graph does not fail: it prints a shader that computes
8
+ * something else. A newer version is refused with a message naming both; an
9
+ * older one is migrated to this one and rehashed under it.
10
+ */
11
+ import {
12
+ INPUTS, SLError, SL_IR_VERSION, TYPE, hashProgram,
13
+ type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
14
+ } from "./ir"
15
+ import { SL_ARITY, SL_NAME } from "./ops"
16
+
17
+ export interface ProgramJSON {
18
+ /** `SL_IR_VERSION` of the writer. Absent means 1. */
19
+ v: number
20
+ nodes: SLNode[]
21
+ result: NodeRef
22
+ uniforms: UniformDecl[]
23
+ textures: TextureDecl[]
24
+ hash: string
25
+ }
26
+
27
+ export function toJSON(p: Program): ProgramJSON {
28
+ return { v: p.version, nodes: p.nodes, result: p.result, uniforms: p.uniforms, textures: p.textures, hash: p.hash }
29
+ }
30
+
31
+ export function fromJSON(json: unknown): Program {
32
+ if (typeof json !== "object" || json === null) fail("a program must be an object")
33
+ const j = json as Partial<ProgramJSON>
34
+ const v = j.v ?? 1
35
+ if (!Number.isInteger(v) || v < 1) fail(`its version must be a whole number from 1, got ${String(j.v)}`)
36
+ if (v > SL_IR_VERSION) {
37
+ fail(`it is IR version ${v}, and this compiler reads up to ${SL_IR_VERSION}. ` +
38
+ `Update onejs-sl to read it, or rebuild it from its .sl source.`)
39
+ }
40
+ if (!Array.isArray(j.nodes) || j.nodes.length === 0) fail("it has no nodes")
41
+ const nodes = j.nodes.map((n, i) => node(n, i))
42
+ const result = j.result
43
+ if (!Number.isInteger(result) || result! < 0 || result! >= nodes.length) fail(`its result ${String(result)} is not a node`)
44
+ if (nodes[result!]!.type !== TYPE.VEC4) fail("its result is not a float4")
45
+ const uniforms = (j.uniforms ?? []).map(uniform)
46
+ const textures = (j.textures ?? []).map(texture)
47
+ for (const n of nodes) {
48
+ if (n.k === "uniform" && n.slot >= uniforms.length) fail(`a node reads uniform slot ${n.slot}, which is not declared`)
49
+ }
50
+ const hash = hashProgram(nodes, result!, uniforms, textures)
51
+ // Same version, so the same maths and the same hash; a different one means
52
+ // the file was edited or damaged, and its cached shader belongs to
53
+ // something else. An older version is rehashed under this one on purpose.
54
+ if (v === SL_IR_VERSION && j.hash !== undefined && j.hash !== hash) {
55
+ fail(`its hash ${j.hash} does not match its nodes (${hash}); it was changed after it was written`)
56
+ }
57
+ return { version: SL_IR_VERSION, nodes, result: result!, uniforms, textures, hash, loops: [] }
58
+ }
59
+
60
+ function node(n: unknown, i: number): SLNode {
61
+ if (typeof n !== "object" || n === null) fail(`node ${i} is not an object`)
62
+ const x = n as Record<string, unknown>
63
+ const type = x.type
64
+ if (type !== 1 && type !== 2 && type !== 3 && type !== 4) fail(`node ${i} has no width 1 to 4`)
65
+ const t = type as SLType
66
+ const ref = (r: unknown, what: string): NodeRef => {
67
+ if (!Number.isInteger(r) || (r as number) < 0 || (r as number) >= i) {
68
+ fail(`node ${i}'s ${what} ${String(r)} does not refer to an earlier node`)
69
+ }
70
+ return r as number
71
+ }
72
+ const nums = (a: unknown, what: string): number[] => {
73
+ if (!Array.isArray(a) || !a.every((e) => typeof e === "number" && Number.isFinite(e))) {
74
+ fail(`node ${i}'s ${what} must be finite numbers`)
75
+ }
76
+ return a as number[]
77
+ }
78
+ switch (x.k) {
79
+ case "const": {
80
+ const v = nums(x.v, "value")
81
+ if (v.length !== t) fail(`node ${i} is a float${t === 1 ? "" : t} constant with ${v.length} values`)
82
+ return { k: "const", type: t, v }
83
+ }
84
+ case "input": {
85
+ const name = x.name as keyof typeof INPUTS
86
+ if (!(name in INPUTS) || INPUTS[name] !== t) fail(`node ${i} reads an input "${String(x.name)}" that does not exist at that width`)
87
+ return { k: "input", type: t, name }
88
+ }
89
+ case "uniform": {
90
+ if (!Number.isInteger(x.slot) || (x.slot as number) < 0) fail(`node ${i} has no uniform slot`)
91
+ return { k: "uniform", type: t, slot: x.slot as number }
92
+ }
93
+ case "swizzle": {
94
+ const chans = nums(x.chans, "channels")
95
+ if (chans.length !== t || !chans.every((c) => Number.isInteger(c) && c >= 0 && c <= 3)) {
96
+ fail(`node ${i}'s swizzle does not pick ${t} channels`)
97
+ }
98
+ return { k: "swizzle", type: t, src: ref(x.src, "source"), chans }
99
+ }
100
+ case "call": {
101
+ const op = x.op as number
102
+ if (SL_NAME[op] === undefined) {
103
+ fail(`node ${i} calls opcode ${String(x.op)}, which this compiler does not know. ` +
104
+ `A newer compiler wrote it; its version says otherwise, so the file is damaged.`)
105
+ }
106
+ if (!Array.isArray(x.args)) fail(`node ${i} has no arguments list`)
107
+ const args = (x.args as unknown[]).map((a) => ref(a, "argument"))
108
+ const arity = SL_ARITY[op]
109
+ if (arity !== undefined && arity >= 0 && args.length !== arity) {
110
+ fail(`node ${i} calls ${SL_NAME[op]} with ${args.length} arguments; it takes ${arity}`)
111
+ }
112
+ const out: SLNode = { k: "call", type: t, op: op as never, args }
113
+ if (x.imm !== undefined) out.imm = nums(x.imm, "immediates")
114
+ return out
115
+ }
116
+ default:
117
+ fail(`node ${i} is a "${String(x.k)}", which is not a kind of node`)
118
+ }
119
+ }
120
+
121
+ function uniform(u: unknown, i: number): UniformDecl {
122
+ const x = (u ?? {}) as Partial<UniformDecl>
123
+ if (typeof x.name !== "string" || ![1, 2, 3, 4].includes(x.type as number) || !Array.isArray(x.value)) {
124
+ fail(`uniform ${i} needs a name, a width and a value`)
125
+ }
126
+ return { name: x.name!, type: x.type as SLType, value: x.value!.slice() }
127
+ }
128
+
129
+ function texture(t: unknown, i: number): TextureDecl {
130
+ const x = (t ?? {}) as Partial<TextureDecl>
131
+ if (typeof x.name !== "string" || x.slot !== i) fail(`texture ${i} needs a name and slot ${i}`)
132
+ return { name: x.name!, slot: i }
133
+ }
134
+
135
+ function fail(why: string): never {
136
+ throw new SLError(`this program's IR cannot be read: ${why}.`)
137
+ }
package/src/shapes.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Shape names and ids for `sl.sdf`.
3
+ *
4
+ * A SECOND COPY of `fx/sdf.ts`'s table, deliberately, and guarded by a test
5
+ * that compares the two.
6
+ *
7
+ * `fx` lives in onejs-unity and this package has no Unity in it, so the table
8
+ * cannot be imported from there. It was a copy before the move too: OneJS's
9
+ * Play eject scaffold vendors each module into its own folder and does not
10
+ * rewrite relative paths across modules. onejs-unity's `sl/shapes.test.ts`
11
+ * compares the two.
12
+ *
13
+ * Duplicating and pinning is the trade this repository already makes for the
14
+ * painter and particle opcode tables, and for the SDF dispatcher that exists in
15
+ * both FxSources and SLCommon. The ids are a contract with SDF2D.cginc either
16
+ * way; what matters is that a test fails when they drift, not that there is
17
+ * only one copy.
18
+ */
19
+ export const SL_SDF_SHAPES = {
20
+ circle: 0, roundedBox: 1, box: 2, orientedBox: 3, segment: 4, rhombus: 5,
21
+ trapezoid: 6, parallelogram: 7, equilateralTriangle: 8, triangleIsosceles: 9,
22
+ triangle: 10, unevenCapsule: 11, pentagon: 12, hexagon: 13, octagon: 14,
23
+ hexagram: 15, star5: 16, star: 17, pie: 18, cutDisk: 19, arc: 20, ring: 21,
24
+ horseshoe: 22, vesica: 23, orientedVesica: 24, moon: 25, roundedCross: 26,
25
+ egg: 27, heart: 28, cross: 29, roundedX: 30, ellipse: 31, parabola: 32,
26
+ parabolaSegment: 33, bezier: 34, blobbyCross: 35, tunnel: 36, stairs: 37,
27
+ quadraticCircle: 38, hyperbola: 39, coolS: 40, circleWave: 41,
28
+ } as const
29
+
30
+ export type SlSdfKind = keyof typeof SL_SDF_SHAPES
31
+
32
+ /**
33
+ * How many parameters each shape reads, in the order SDF2D.cginc takes them
34
+ * (its `a` then `b`). Six shapes take more than four: roundedBox's last two
35
+ * corner radii, orientedBox's thickness, triangle's third vertex, horseshoe's
36
+ * second width, orientedVesica's width and bezier's end point. `sl.sdf` takes
37
+ * up to this many, and a test derives the same counts from the web emitters'
38
+ * call table, so the two cannot drift.
39
+ */
40
+ export const SL_SDF_PARAMS: Record<SlSdfKind, number> = {
41
+ circle: 1, roundedBox: 6, box: 2, orientedBox: 5, segment: 4, rhombus: 2,
42
+ trapezoid: 3, parallelogram: 3, equilateralTriangle: 1, triangleIsosceles: 2,
43
+ triangle: 6, unevenCapsule: 3, pentagon: 1, hexagon: 1, octagon: 1,
44
+ hexagram: 1, star5: 2, star: 3, pie: 3, cutDisk: 2, arc: 4, ring: 4,
45
+ horseshoe: 5, vesica: 2, orientedVesica: 5, moon: 3, roundedCross: 1,
46
+ egg: 4, heart: 0, cross: 3, roundedX: 2, ellipse: 2, parabola: 1,
47
+ parabolaSegment: 2, bezier: 6, blobbyCross: 1, tunnel: 2, stairs: 3,
48
+ quadraticCircle: 0, hyperbola: 2, coolS: 0, circleWave: 2,
49
+ }