@motion-core/motion-gpu 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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -0
  3. package/dist/FragCanvas.svelte +511 -0
  4. package/dist/FragCanvas.svelte.d.ts +26 -0
  5. package/dist/MotionGPUErrorOverlay.svelte +394 -0
  6. package/dist/MotionGPUErrorOverlay.svelte.d.ts +7 -0
  7. package/dist/Portal.svelte +46 -0
  8. package/dist/Portal.svelte.d.ts +8 -0
  9. package/dist/advanced-scheduler.d.ts +44 -0
  10. package/dist/advanced-scheduler.js +58 -0
  11. package/dist/advanced.d.ts +14 -0
  12. package/dist/advanced.js +9 -0
  13. package/dist/core/error-diagnostics.d.ts +40 -0
  14. package/dist/core/error-diagnostics.js +111 -0
  15. package/dist/core/error-report.d.ts +67 -0
  16. package/dist/core/error-report.js +190 -0
  17. package/dist/core/material-preprocess.d.ts +63 -0
  18. package/dist/core/material-preprocess.js +166 -0
  19. package/dist/core/material.d.ts +157 -0
  20. package/dist/core/material.js +358 -0
  21. package/dist/core/recompile-policy.d.ts +27 -0
  22. package/dist/core/recompile-policy.js +15 -0
  23. package/dist/core/render-graph.d.ts +55 -0
  24. package/dist/core/render-graph.js +73 -0
  25. package/dist/core/render-targets.d.ts +39 -0
  26. package/dist/core/render-targets.js +63 -0
  27. package/dist/core/renderer.d.ts +9 -0
  28. package/dist/core/renderer.js +1097 -0
  29. package/dist/core/shader.d.ts +42 -0
  30. package/dist/core/shader.js +196 -0
  31. package/dist/core/texture-loader.d.ts +129 -0
  32. package/dist/core/texture-loader.js +295 -0
  33. package/dist/core/textures.d.ts +114 -0
  34. package/dist/core/textures.js +136 -0
  35. package/dist/core/types.d.ts +523 -0
  36. package/dist/core/types.js +4 -0
  37. package/dist/core/uniforms.d.ts +48 -0
  38. package/dist/core/uniforms.js +222 -0
  39. package/dist/current-writable.d.ts +31 -0
  40. package/dist/current-writable.js +27 -0
  41. package/dist/frame-context.d.ts +287 -0
  42. package/dist/frame-context.js +731 -0
  43. package/dist/index.d.ts +17 -0
  44. package/dist/index.js +11 -0
  45. package/dist/motiongpu-context.d.ts +77 -0
  46. package/dist/motiongpu-context.js +26 -0
  47. package/dist/passes/BlitPass.d.ts +32 -0
  48. package/dist/passes/BlitPass.js +158 -0
  49. package/dist/passes/CopyPass.d.ts +25 -0
  50. package/dist/passes/CopyPass.js +53 -0
  51. package/dist/passes/ShaderPass.d.ts +40 -0
  52. package/dist/passes/ShaderPass.js +182 -0
  53. package/dist/passes/index.d.ts +3 -0
  54. package/dist/passes/index.js +3 -0
  55. package/dist/use-motiongpu-user-context.d.ts +35 -0
  56. package/dist/use-motiongpu-user-context.js +74 -0
  57. package/dist/use-texture.d.ts +35 -0
  58. package/dist/use-texture.js +147 -0
  59. package/package.json +94 -0
@@ -0,0 +1,111 @@
1
+ function isMaterialSourceMetadata(value) {
2
+ if (value === null || typeof value !== 'object') {
3
+ return false;
4
+ }
5
+ const record = value;
6
+ if (record.component !== undefined && typeof record.component !== 'string') {
7
+ return false;
8
+ }
9
+ if (record.file !== undefined && typeof record.file !== 'string') {
10
+ return false;
11
+ }
12
+ if (record.functionName !== undefined && typeof record.functionName !== 'string') {
13
+ return false;
14
+ }
15
+ if (record.line !== undefined && typeof record.line !== 'number') {
16
+ return false;
17
+ }
18
+ if (record.column !== undefined && typeof record.column !== 'number') {
19
+ return false;
20
+ }
21
+ return true;
22
+ }
23
+ function isMaterialSourceLocation(value) {
24
+ if (value === null) {
25
+ return true;
26
+ }
27
+ if (typeof value !== 'object') {
28
+ return false;
29
+ }
30
+ const record = value;
31
+ const kind = record.kind;
32
+ if (kind !== 'fragment' && kind !== 'include' && kind !== 'define') {
33
+ return false;
34
+ }
35
+ return typeof record.line === 'number';
36
+ }
37
+ function isShaderCompilationDiagnostic(value) {
38
+ if (value === null || typeof value !== 'object') {
39
+ return false;
40
+ }
41
+ const record = value;
42
+ if (typeof record.generatedLine !== 'number') {
43
+ return false;
44
+ }
45
+ if (typeof record.message !== 'string') {
46
+ return false;
47
+ }
48
+ if (record.linePos !== undefined && typeof record.linePos !== 'number') {
49
+ return false;
50
+ }
51
+ if (record.lineLength !== undefined && typeof record.lineLength !== 'number') {
52
+ return false;
53
+ }
54
+ if (!isMaterialSourceLocation(record.sourceLocation)) {
55
+ return false;
56
+ }
57
+ return true;
58
+ }
59
+ /**
60
+ * Attaches structured diagnostics payload to an Error.
61
+ */
62
+ export function attachShaderCompilationDiagnostics(error, payload) {
63
+ error.motiongpuDiagnostics = payload;
64
+ return error;
65
+ }
66
+ /**
67
+ * Extracts structured diagnostics payload from unknown error value.
68
+ */
69
+ export function getShaderCompilationDiagnostics(error) {
70
+ if (!(error instanceof Error)) {
71
+ return null;
72
+ }
73
+ const payload = error.motiongpuDiagnostics;
74
+ if (payload === null || typeof payload !== 'object') {
75
+ return null;
76
+ }
77
+ const record = payload;
78
+ if (record.kind !== 'shader-compilation') {
79
+ return null;
80
+ }
81
+ if (!Array.isArray(record.diagnostics) ||
82
+ !record.diagnostics.every(isShaderCompilationDiagnostic)) {
83
+ return null;
84
+ }
85
+ if (typeof record.fragmentSource !== 'string') {
86
+ return null;
87
+ }
88
+ if (record.defineBlockSource !== undefined && typeof record.defineBlockSource !== 'string') {
89
+ return null;
90
+ }
91
+ if (record.includeSources === null || typeof record.includeSources !== 'object') {
92
+ return null;
93
+ }
94
+ const includeSources = record.includeSources;
95
+ if (Object.values(includeSources).some((value) => typeof value !== 'string')) {
96
+ return null;
97
+ }
98
+ if (record.materialSource !== null && !isMaterialSourceMetadata(record.materialSource)) {
99
+ return null;
100
+ }
101
+ return {
102
+ kind: 'shader-compilation',
103
+ diagnostics: record.diagnostics,
104
+ fragmentSource: record.fragmentSource,
105
+ includeSources: includeSources,
106
+ ...(record.defineBlockSource !== undefined
107
+ ? { defineBlockSource: record.defineBlockSource }
108
+ : {}),
109
+ materialSource: (record.materialSource ?? null)
110
+ };
111
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Runtime phase in which an error occurred.
3
+ */
4
+ export type MotionGPUErrorPhase = 'initialization' | 'render';
5
+ /**
6
+ * One source-code line displayed in diagnostics snippet.
7
+ */
8
+ export interface MotionGPUErrorSourceLine {
9
+ number: number;
10
+ code: string;
11
+ highlight: boolean;
12
+ }
13
+ /**
14
+ * Structured source context displayed for shader compilation errors.
15
+ */
16
+ export interface MotionGPUErrorSource {
17
+ component: string;
18
+ location: string;
19
+ line: number;
20
+ column?: number;
21
+ snippet: MotionGPUErrorSourceLine[];
22
+ }
23
+ /**
24
+ * Structured error payload used by UI diagnostics.
25
+ */
26
+ export interface MotionGPUErrorReport {
27
+ /**
28
+ * Short category title.
29
+ */
30
+ title: string;
31
+ /**
32
+ * Primary human-readable message.
33
+ */
34
+ message: string;
35
+ /**
36
+ * Suggested remediation hint.
37
+ */
38
+ hint: string;
39
+ /**
40
+ * Additional parsed details (for example WGSL line errors).
41
+ */
42
+ details: string[];
43
+ /**
44
+ * Stack trace lines when available.
45
+ */
46
+ stack: string[];
47
+ /**
48
+ * Original unmodified message.
49
+ */
50
+ rawMessage: string;
51
+ /**
52
+ * Runtime phase where the error occurred.
53
+ */
54
+ phase: MotionGPUErrorPhase;
55
+ /**
56
+ * Optional source context for shader-related diagnostics.
57
+ */
58
+ source: MotionGPUErrorSource | null;
59
+ }
60
+ /**
61
+ * Converts unknown errors to a consistent, display-ready error report.
62
+ *
63
+ * @param error - Unknown thrown value.
64
+ * @param phase - Phase during which error occurred.
65
+ * @returns Normalized error report.
66
+ */
67
+ export declare function toMotionGPUErrorReport(error: unknown, phase: MotionGPUErrorPhase): MotionGPUErrorReport;
@@ -0,0 +1,190 @@
1
+ import { getShaderCompilationDiagnostics } from './error-diagnostics';
2
+ import { formatShaderSourceLocation } from './shader';
3
+ /**
4
+ * Splits multi-line values into trimmed non-empty lines.
5
+ */
6
+ function splitLines(value) {
7
+ return value
8
+ .split('\n')
9
+ .map((line) => line.trim())
10
+ .filter((line) => line.length > 0);
11
+ }
12
+ function toDisplayName(path) {
13
+ const normalized = path.split(/[?#]/)[0] ?? path;
14
+ const chunks = normalized.split(/[\\/]/);
15
+ const last = chunks[chunks.length - 1];
16
+ return last && last.length > 0 ? last : path;
17
+ }
18
+ function toSnippet(source, line, radius = 3) {
19
+ const lines = source.replace(/\r\n?/g, '\n').split('\n');
20
+ if (lines.length === 0) {
21
+ return [];
22
+ }
23
+ const targetLine = Math.min(Math.max(1, line), lines.length);
24
+ const start = Math.max(1, targetLine - radius);
25
+ const end = Math.min(lines.length, targetLine + radius);
26
+ const snippet = [];
27
+ for (let index = start; index <= end; index += 1) {
28
+ snippet.push({
29
+ number: index,
30
+ code: lines[index - 1] ?? '',
31
+ highlight: index === targetLine
32
+ });
33
+ }
34
+ return snippet;
35
+ }
36
+ function buildSourceFromDiagnostics(error) {
37
+ const diagnostics = getShaderCompilationDiagnostics(error);
38
+ if (!diagnostics || diagnostics.diagnostics.length === 0) {
39
+ return null;
40
+ }
41
+ const primary = diagnostics.diagnostics.find((entry) => entry.sourceLocation !== null);
42
+ if (!primary?.sourceLocation) {
43
+ return null;
44
+ }
45
+ const location = primary.sourceLocation;
46
+ const column = primary.linePos && primary.linePos > 0 ? primary.linePos : undefined;
47
+ if (location.kind === 'fragment') {
48
+ const component = diagnostics.materialSource?.component ??
49
+ (diagnostics.materialSource?.file
50
+ ? toDisplayName(diagnostics.materialSource.file)
51
+ : 'User shader fragment');
52
+ const locationLabel = formatShaderSourceLocation(location) ?? `fragment line ${location.line}`;
53
+ return {
54
+ component,
55
+ location: `${component} (${locationLabel})`,
56
+ line: location.line,
57
+ ...(column !== undefined ? { column } : {}),
58
+ snippet: toSnippet(diagnostics.fragmentSource, location.line)
59
+ };
60
+ }
61
+ if (location.kind === 'include') {
62
+ const includeName = location.include ?? 'unknown';
63
+ const includeSource = diagnostics.includeSources[includeName] ?? '';
64
+ const component = `#include <${includeName}>`;
65
+ const locationLabel = formatShaderSourceLocation(location) ?? `include <${includeName}>`;
66
+ return {
67
+ component,
68
+ location: `${component} (${locationLabel})`,
69
+ line: location.line,
70
+ ...(column !== undefined ? { column } : {}),
71
+ snippet: toSnippet(includeSource, location.line)
72
+ };
73
+ }
74
+ const defineName = location.define ?? 'unknown';
75
+ const defineLine = Math.max(1, location.line);
76
+ const component = `#define ${defineName}`;
77
+ const locationLabel = formatShaderSourceLocation(location) ?? `define "${defineName}" line ${defineLine}`;
78
+ return {
79
+ component,
80
+ location: `${component} (${locationLabel})`,
81
+ line: defineLine,
82
+ ...(column !== undefined ? { column } : {}),
83
+ snippet: toSnippet(diagnostics.defineBlockSource ?? '', defineLine, 2)
84
+ };
85
+ }
86
+ function formatDiagnosticMessage(entry) {
87
+ const sourceLabel = formatShaderSourceLocation(entry.sourceLocation);
88
+ const generatedLineLabel = entry.generatedLine > 0 ? `generated WGSL line ${entry.generatedLine}` : null;
89
+ const labels = [sourceLabel, generatedLineLabel].filter((value) => Boolean(value));
90
+ if (labels.length === 0) {
91
+ return entry.message;
92
+ }
93
+ return `[${labels.join(' | ')}] ${entry.message}`;
94
+ }
95
+ /**
96
+ * Maps known WebGPU/WGSL error patterns to a user-facing title and hint.
97
+ */
98
+ function classifyErrorMessage(message) {
99
+ if (message.includes('WebGPU is not available in this browser')) {
100
+ return {
101
+ title: 'WebGPU unavailable',
102
+ hint: 'Use a browser with WebGPU enabled (latest Chrome/Edge/Safari TP) and secure context.'
103
+ };
104
+ }
105
+ if (message.includes('Unable to acquire WebGPU adapter')) {
106
+ return {
107
+ title: 'WebGPU adapter unavailable',
108
+ hint: 'GPU adapter request failed. Check browser permissions, flags and device support.'
109
+ };
110
+ }
111
+ if (message.includes('Canvas does not support webgpu context')) {
112
+ return {
113
+ title: 'Canvas cannot create WebGPU context',
114
+ hint: 'Make sure this canvas is attached to DOM and not using an unsupported context option.'
115
+ };
116
+ }
117
+ if (message.includes('WGSL compilation failed')) {
118
+ return {
119
+ title: 'WGSL compilation failed',
120
+ hint: 'Check WGSL line numbers below and verify struct/binding/function signatures.'
121
+ };
122
+ }
123
+ if (message.includes('WebGPU device lost') || message.includes('Device Lost')) {
124
+ return {
125
+ title: 'WebGPU device lost',
126
+ hint: 'GPU device/context was lost. Recreate the renderer and check OS/GPU stability.'
127
+ };
128
+ }
129
+ if (message.includes('WebGPU uncaptured error')) {
130
+ return {
131
+ title: 'WebGPU uncaptured error',
132
+ hint: 'A GPU command failed asynchronously. Review details and validate resource/state usage.'
133
+ };
134
+ }
135
+ if (message.includes('CreateBindGroup') || message.includes('bind group layout')) {
136
+ return {
137
+ title: 'Bind group mismatch',
138
+ hint: 'Bindings in shader and runtime resources are out of sync. Verify uniforms/textures layout.'
139
+ };
140
+ }
141
+ if (message.includes('Destination texture needs to have CopyDst')) {
142
+ return {
143
+ title: 'Invalid texture usage flags',
144
+ hint: 'Texture used as upload destination must include CopyDst (and often RenderAttachment).'
145
+ };
146
+ }
147
+ return {
148
+ title: 'MotionGPU render error',
149
+ hint: 'Review technical details below. If issue persists, isolate shader/uniform/texture changes.'
150
+ };
151
+ }
152
+ /**
153
+ * Converts unknown errors to a consistent, display-ready error report.
154
+ *
155
+ * @param error - Unknown thrown value.
156
+ * @param phase - Phase during which error occurred.
157
+ * @returns Normalized error report.
158
+ */
159
+ export function toMotionGPUErrorReport(error, phase) {
160
+ const shaderDiagnostics = getShaderCompilationDiagnostics(error);
161
+ const rawMessage = error instanceof Error
162
+ ? error.message
163
+ : typeof error === 'string'
164
+ ? error
165
+ : 'Unknown FragCanvas error';
166
+ const rawLines = splitLines(rawMessage);
167
+ const defaultMessage = rawLines[0] ?? rawMessage;
168
+ const defaultDetails = rawLines.slice(1);
169
+ const source = buildSourceFromDiagnostics(error);
170
+ const message = shaderDiagnostics && shaderDiagnostics.diagnostics[0]
171
+ ? formatDiagnosticMessage(shaderDiagnostics.diagnostics[0])
172
+ : defaultMessage;
173
+ const details = shaderDiagnostics
174
+ ? shaderDiagnostics.diagnostics.slice(1).map((entry) => formatDiagnosticMessage(entry))
175
+ : defaultDetails;
176
+ const stack = error instanceof Error && error.stack
177
+ ? splitLines(error.stack).filter((line) => line !== message)
178
+ : [];
179
+ const classification = classifyErrorMessage(rawMessage);
180
+ return {
181
+ title: classification.title,
182
+ message,
183
+ hint: classification.hint,
184
+ details,
185
+ stack,
186
+ rawMessage,
187
+ phase,
188
+ source
189
+ };
190
+ }
@@ -0,0 +1,63 @@
1
+ import type { MaterialDefineValue, MaterialDefines, MaterialIncludes } from './material';
2
+ /**
3
+ * Source location metadata for one generated fragment line.
4
+ */
5
+ export interface MaterialSourceLocation {
6
+ /**
7
+ * Origin category for this generated line.
8
+ */
9
+ kind: 'fragment' | 'include' | 'define';
10
+ /**
11
+ * 1-based line in the origin source.
12
+ */
13
+ line: number;
14
+ /**
15
+ * Include chunk identifier when `kind === "include"`.
16
+ */
17
+ include?: string;
18
+ /**
19
+ * Define identifier when `kind === "define"`.
20
+ */
21
+ define?: string;
22
+ }
23
+ /**
24
+ * 1-based line map from generated fragment WGSL to user source locations.
25
+ */
26
+ export type MaterialLineMap = Array<MaterialSourceLocation | null>;
27
+ /**
28
+ * Preprocess output used by material resolution and diagnostics mapping.
29
+ */
30
+ export interface PreprocessedMaterialFragment {
31
+ /**
32
+ * Final fragment source after defines/include expansion.
33
+ */
34
+ fragment: string;
35
+ /**
36
+ * 1-based generated-line source map.
37
+ */
38
+ lineMap: MaterialLineMap;
39
+ /**
40
+ * Deterministic WGSL define block used to build the final fragment source.
41
+ */
42
+ defineBlockSource: string;
43
+ }
44
+ /**
45
+ * Validates and normalizes define entries.
46
+ */
47
+ export declare function normalizeDefines(defines: MaterialDefines | undefined): MaterialDefines;
48
+ /**
49
+ * Validates include map identifiers and source chunks.
50
+ */
51
+ export declare function normalizeIncludes(includes: MaterialIncludes | undefined): MaterialIncludes;
52
+ /**
53
+ * Converts one define declaration to WGSL `const`.
54
+ */
55
+ export declare function toDefineLine(key: string, value: MaterialDefineValue): string;
56
+ /**
57
+ * Preprocesses material fragment with deterministic define/include expansion and line mapping.
58
+ */
59
+ export declare function preprocessMaterialFragment(input: {
60
+ fragment: string;
61
+ defines?: MaterialDefines;
62
+ includes?: MaterialIncludes;
63
+ }): PreprocessedMaterialFragment;
@@ -0,0 +1,166 @@
1
+ import { assertUniformName } from './uniforms';
2
+ const INCLUDE_DIRECTIVE_PATTERN = /^\s*#include\s+<([A-Za-z_][A-Za-z0-9_]*)>\s*$/;
3
+ function normalizeTypedDefine(name, define) {
4
+ const value = define.value;
5
+ if (define.type === 'bool') {
6
+ if (typeof value !== 'boolean') {
7
+ throw new Error(`Invalid define value for "${name}". bool define requires boolean value.`);
8
+ }
9
+ return {
10
+ type: 'bool',
11
+ value
12
+ };
13
+ }
14
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
15
+ throw new Error(`Invalid define value for "${name}". Numeric define must be finite.`);
16
+ }
17
+ if ((define.type === 'i32' || define.type === 'u32') && !Number.isInteger(value)) {
18
+ throw new Error(`Invalid define value for "${name}". ${define.type} define requires integer.`);
19
+ }
20
+ if (define.type === 'u32' && value < 0) {
21
+ throw new Error(`Invalid define value for "${name}". u32 define must be >= 0.`);
22
+ }
23
+ return {
24
+ type: define.type,
25
+ value
26
+ };
27
+ }
28
+ /**
29
+ * Validates and normalizes define entries.
30
+ */
31
+ export function normalizeDefines(defines) {
32
+ const resolved = {};
33
+ for (const [name, value] of Object.entries(defines ?? {})) {
34
+ assertUniformName(name);
35
+ if (typeof value === 'boolean') {
36
+ resolved[name] = value;
37
+ continue;
38
+ }
39
+ if (typeof value === 'number') {
40
+ if (!Number.isFinite(value)) {
41
+ throw new Error(`Invalid define value for "${name}". Define numbers must be finite.`);
42
+ }
43
+ resolved[name] = value;
44
+ continue;
45
+ }
46
+ const normalized = normalizeTypedDefine(name, value);
47
+ resolved[name] = Object.freeze({
48
+ type: normalized.type,
49
+ value: normalized.value
50
+ });
51
+ }
52
+ return resolved;
53
+ }
54
+ /**
55
+ * Validates include map identifiers and source chunks.
56
+ */
57
+ export function normalizeIncludes(includes) {
58
+ const resolved = {};
59
+ for (const [name, source] of Object.entries(includes ?? {})) {
60
+ assertUniformName(name);
61
+ if (typeof source !== 'string' || source.trim().length === 0) {
62
+ throw new Error(`Invalid include "${name}". Include source must be a non-empty WGSL string.`);
63
+ }
64
+ resolved[name] = source;
65
+ }
66
+ return resolved;
67
+ }
68
+ /**
69
+ * Converts one define declaration to WGSL `const`.
70
+ */
71
+ export function toDefineLine(key, value) {
72
+ if (typeof value === 'boolean') {
73
+ return `const ${key}: bool = ${value ? 'true' : 'false'};`;
74
+ }
75
+ if (typeof value === 'number') {
76
+ const valueLiteral = Number.isInteger(value) ? `${value}.0` : `${value}`;
77
+ return `const ${key}: f32 = ${valueLiteral};`;
78
+ }
79
+ if (value.type === 'bool') {
80
+ return `const ${key}: bool = ${value.value ? 'true' : 'false'};`;
81
+ }
82
+ if (value.type === 'f32') {
83
+ const numberValue = value.value;
84
+ const valueLiteral = Number.isInteger(numberValue) ? `${numberValue}.0` : `${numberValue}`;
85
+ return `const ${key}: f32 = ${valueLiteral};`;
86
+ }
87
+ if (value.type === 'i32') {
88
+ return `const ${key}: i32 = ${value.value};`;
89
+ }
90
+ return `const ${key}: u32 = ${value.value}u;`;
91
+ }
92
+ function expandChunk(source, kind, includeName, includes, stack) {
93
+ const sourceLines = source.split('\n');
94
+ const lines = [];
95
+ const mapEntries = [];
96
+ for (let index = 0; index < sourceLines.length; index += 1) {
97
+ const sourceLine = sourceLines[index];
98
+ if (sourceLine === undefined) {
99
+ continue;
100
+ }
101
+ const includeMatch = sourceLine.match(INCLUDE_DIRECTIVE_PATTERN);
102
+ if (!includeMatch) {
103
+ lines.push(sourceLine);
104
+ mapEntries.push({
105
+ kind,
106
+ line: index + 1,
107
+ ...(kind === 'include' && includeName ? { include: includeName } : {})
108
+ });
109
+ continue;
110
+ }
111
+ const includeKey = includeMatch[1];
112
+ if (!includeKey) {
113
+ throw new Error('Invalid include directive in fragment shader.');
114
+ }
115
+ assertUniformName(includeKey);
116
+ const includeSource = includes[includeKey];
117
+ if (!includeSource) {
118
+ throw new Error(`Unknown include "${includeKey}" referenced in fragment shader.`);
119
+ }
120
+ if (stack.includes(includeKey)) {
121
+ throw new Error(`Circular include detected for "${includeKey}". Include stack: ${[...stack, includeKey].join(' -> ')}.`);
122
+ }
123
+ const nested = expandChunk(includeSource, 'include', includeKey, includes, [
124
+ ...stack,
125
+ includeKey
126
+ ]);
127
+ lines.push(...nested.lines);
128
+ mapEntries.push(...nested.mapEntries);
129
+ }
130
+ return { lines, mapEntries };
131
+ }
132
+ /**
133
+ * Preprocesses material fragment with deterministic define/include expansion and line mapping.
134
+ */
135
+ export function preprocessMaterialFragment(input) {
136
+ const normalizedDefines = normalizeDefines(input.defines);
137
+ const normalizedIncludes = normalizeIncludes(input.includes);
138
+ const fragmentExpanded = expandChunk(input.fragment, 'fragment', undefined, normalizedIncludes, []);
139
+ const defineEntries = Object.entries(normalizedDefines).sort(([a], [b]) => a.localeCompare(b));
140
+ const lines = [];
141
+ const defineLines = [];
142
+ const mapEntries = [];
143
+ for (let index = 0; index < defineEntries.length; index += 1) {
144
+ const entry = defineEntries[index];
145
+ if (!entry) {
146
+ continue;
147
+ }
148
+ const [name, value] = entry;
149
+ const defineLine = toDefineLine(name, value);
150
+ lines.push(defineLine);
151
+ defineLines.push(defineLine);
152
+ mapEntries.push({ kind: 'define', line: index + 1, define: name });
153
+ }
154
+ if (defineEntries.length > 0) {
155
+ lines.push('');
156
+ mapEntries.push(null);
157
+ }
158
+ lines.push(...fragmentExpanded.lines);
159
+ mapEntries.push(...fragmentExpanded.mapEntries);
160
+ const lineMap = [null, ...mapEntries];
161
+ return {
162
+ fragment: lines.join('\n'),
163
+ lineMap,
164
+ defineBlockSource: defineLines.join('\n')
165
+ };
166
+ }