@motion-core/motion-gpu 0.5.0 → 0.7.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 (71) hide show
  1. package/README.md +35 -2
  2. package/dist/core/compute-bindgroup-cache.d.ts +13 -0
  3. package/dist/core/compute-bindgroup-cache.d.ts.map +1 -0
  4. package/dist/core/compute-bindgroup-cache.js +45 -0
  5. package/dist/core/compute-bindgroup-cache.js.map +1 -0
  6. package/dist/core/compute-shader.d.ts +48 -0
  7. package/dist/core/compute-shader.d.ts.map +1 -1
  8. package/dist/core/compute-shader.js +34 -1
  9. package/dist/core/compute-shader.js.map +1 -1
  10. package/dist/core/error-diagnostics.d.ts +8 -1
  11. package/dist/core/error-diagnostics.d.ts.map +1 -1
  12. package/dist/core/error-diagnostics.js +7 -3
  13. package/dist/core/error-diagnostics.js.map +1 -1
  14. package/dist/core/error-report.d.ts.map +1 -1
  15. package/dist/core/error-report.js +19 -1
  16. package/dist/core/error-report.js.map +1 -1
  17. package/dist/core/material.d.ts.map +1 -1
  18. package/dist/core/material.js +2 -1
  19. package/dist/core/material.js.map +1 -1
  20. package/dist/core/pointer.d.ts +96 -0
  21. package/dist/core/pointer.d.ts.map +1 -0
  22. package/dist/core/pointer.js +71 -0
  23. package/dist/core/pointer.js.map +1 -0
  24. package/dist/core/renderer.d.ts.map +1 -1
  25. package/dist/core/renderer.js +150 -85
  26. package/dist/core/renderer.js.map +1 -1
  27. package/dist/core/runtime-loop.d.ts.map +1 -1
  28. package/dist/core/runtime-loop.js +26 -14
  29. package/dist/core/runtime-loop.js.map +1 -1
  30. package/dist/core/shader.d.ts +7 -2
  31. package/dist/core/shader.d.ts.map +1 -1
  32. package/dist/core/shader.js +1 -0
  33. package/dist/core/shader.js.map +1 -1
  34. package/dist/core/textures.d.ts +4 -0
  35. package/dist/core/textures.d.ts.map +1 -1
  36. package/dist/core/textures.js +2 -1
  37. package/dist/core/textures.js.map +1 -1
  38. package/dist/core/types.d.ts +1 -1
  39. package/dist/core/types.d.ts.map +1 -1
  40. package/dist/react/advanced.js +2 -1
  41. package/dist/react/index.d.ts +2 -0
  42. package/dist/react/index.d.ts.map +1 -1
  43. package/dist/react/index.js +2 -1
  44. package/dist/react/use-pointer.d.ts +94 -0
  45. package/dist/react/use-pointer.d.ts.map +1 -0
  46. package/dist/react/use-pointer.js +285 -0
  47. package/dist/react/use-pointer.js.map +1 -0
  48. package/dist/svelte/advanced.js +2 -1
  49. package/dist/svelte/index.d.ts +2 -0
  50. package/dist/svelte/index.d.ts.map +1 -1
  51. package/dist/svelte/index.js +2 -1
  52. package/dist/svelte/use-pointer.d.ts +94 -0
  53. package/dist/svelte/use-pointer.d.ts.map +1 -0
  54. package/dist/svelte/use-pointer.js +292 -0
  55. package/dist/svelte/use-pointer.js.map +1 -0
  56. package/package.json +1 -1
  57. package/src/lib/core/compute-bindgroup-cache.ts +73 -0
  58. package/src/lib/core/compute-shader.ts +86 -0
  59. package/src/lib/core/error-diagnostics.ts +29 -4
  60. package/src/lib/core/error-report.ts +26 -1
  61. package/src/lib/core/material.ts +2 -1
  62. package/src/lib/core/pointer.ts +177 -0
  63. package/src/lib/core/renderer.ts +198 -92
  64. package/src/lib/core/runtime-loop.ts +37 -16
  65. package/src/lib/core/shader.ts +13 -2
  66. package/src/lib/core/textures.ts +6 -1
  67. package/src/lib/core/types.ts +1 -1
  68. package/src/lib/react/index.ts +10 -0
  69. package/src/lib/react/use-pointer.ts +515 -0
  70. package/src/lib/svelte/index.ts +10 -0
  71. package/src/lib/svelte/use-pointer.ts +507 -0
@@ -0,0 +1,292 @@
1
+ import { createCurrentWritable } from "../core/current-value.js";
2
+ import { createInitialPointerState, getPointerCoordinates, getPointerNowSeconds, normalizePointerKind, resolvePointerFrameRequestMode } from "../core/pointer.js";
3
+ import { useMotionGPU } from "./motiongpu-context.js";
4
+ import { onMount } from "svelte";
5
+ //#region src/lib/svelte/use-pointer.ts
6
+ /**
7
+ * Normalizes click button configuration with a primary-button fallback.
8
+ */
9
+ function normalizeClickButtons(buttons) {
10
+ const source = buttons && buttons.length > 0 ? buttons : [0];
11
+ return new Set(source);
12
+ }
13
+ /**
14
+ * Resolves a valid click duration threshold in milliseconds.
15
+ */
16
+ function resolveClickMaxDurationMs(value) {
17
+ if (typeof value !== "number" || Number.isNaN(value) || value <= 0) return 350;
18
+ return value;
19
+ }
20
+ /**
21
+ * Resolves a valid click travel threshold in pixels.
22
+ */
23
+ function resolveClickMaxMovePx(value) {
24
+ if (typeof value !== "number" || Number.isNaN(value) || value < 0) return 8;
25
+ return value;
26
+ }
27
+ /**
28
+ * Tracks normalized pointer coordinates and click/tap snapshots for the active `FragCanvas`.
29
+ */
30
+ function usePointer(options = {}) {
31
+ const motiongpu = useMotionGPU();
32
+ const pointerState = createCurrentWritable(createInitialPointerState());
33
+ const lastClick = createCurrentWritable(null);
34
+ const enabled = options.enabled ?? true;
35
+ const requestFrameMode = options.requestFrame ?? "auto";
36
+ const capturePointer = options.capturePointer ?? true;
37
+ const trackOutside = options.trackWhilePressedOutsideCanvas ?? true;
38
+ const clickEnabled = options.clickEnabled ?? true;
39
+ const clickMaxDurationMs = resolveClickMaxDurationMs(options.clickMaxDurationMs);
40
+ const clickMaxMovePx = resolveClickMaxMovePx(options.clickMaxMovePx);
41
+ const clickButtons = normalizeClickButtons(options.clickButtons);
42
+ let activePointerId = null;
43
+ let downSnapshot = null;
44
+ let clickCounter = 0;
45
+ let previousPx = null;
46
+ let previousUv = null;
47
+ let previousTimeSeconds = 0;
48
+ const requestFrame = () => {
49
+ const mode = resolvePointerFrameRequestMode(requestFrameMode, motiongpu.renderMode.current);
50
+ if (mode === "invalidate") {
51
+ motiongpu.invalidate();
52
+ return;
53
+ }
54
+ if (mode === "advance") motiongpu.advance();
55
+ };
56
+ /**
57
+ * Commits a full pointer state snapshot with computed delta and velocity vectors.
58
+ */
59
+ const updatePointerState = (input) => {
60
+ const nowSeconds = getPointerNowSeconds();
61
+ const dt = previousTimeSeconds > 0 ? Math.max(nowSeconds - previousTimeSeconds, 1e-6) : 0;
62
+ const deltaPx = input.resetDelta || !previousPx ? [0, 0] : [input.point.px[0] - previousPx[0], input.point.px[1] - previousPx[1]];
63
+ const deltaUv = input.resetDelta || !previousUv ? [0, 0] : [input.point.uv[0] - previousUv[0], input.point.uv[1] - previousUv[1]];
64
+ const velocityPx = dt > 0 ? [deltaPx[0] / dt, deltaPx[1] / dt] : [0, 0];
65
+ const velocityUv = dt > 0 ? [deltaUv[0] / dt, deltaUv[1] / dt] : [0, 0];
66
+ const nextState = {
67
+ px: input.point.px,
68
+ uv: input.point.uv,
69
+ ndc: input.point.ndc,
70
+ inside: input.inside,
71
+ pressed: input.pressed,
72
+ dragging: input.dragging,
73
+ pointerType: input.pointerType,
74
+ pointerId: input.pointerId,
75
+ button: input.button,
76
+ buttons: input.buttons,
77
+ time: nowSeconds,
78
+ downPx: input.downPx,
79
+ downUv: input.downUv,
80
+ deltaPx,
81
+ deltaUv,
82
+ velocityPx,
83
+ velocityUv
84
+ };
85
+ pointerState.set(nextState);
86
+ previousPx = input.point.px;
87
+ previousUv = input.point.uv;
88
+ previousTimeSeconds = nowSeconds;
89
+ requestFrame();
90
+ return nextState;
91
+ };
92
+ /**
93
+ * Updates only the `inside` flag while keeping the latest pointer coordinates.
94
+ */
95
+ const updateInsideState = (inside) => {
96
+ const nextState = {
97
+ ...pointerState.current,
98
+ inside,
99
+ time: getPointerNowSeconds(),
100
+ deltaPx: [0, 0],
101
+ deltaUv: [0, 0],
102
+ velocityPx: [0, 0],
103
+ velocityUv: [0, 0]
104
+ };
105
+ pointerState.set(nextState);
106
+ requestFrame();
107
+ return nextState;
108
+ };
109
+ /**
110
+ * Checks whether an event belongs to the active tracked pointer.
111
+ */
112
+ const isTrackedPointer = (event) => activePointerId === null || event.pointerId === activePointerId;
113
+ onMount(() => {
114
+ if (!enabled) return;
115
+ const canvas = motiongpu.canvas;
116
+ if (!canvas) return;
117
+ const handlePointerDown = (event) => {
118
+ const point = getPointerCoordinates(event.clientX, event.clientY, canvas.getBoundingClientRect());
119
+ const pointerType = normalizePointerKind(event.pointerType);
120
+ activePointerId = event.pointerId;
121
+ downSnapshot = {
122
+ pointerId: event.pointerId,
123
+ pointerType,
124
+ button: event.button,
125
+ timeMs: getPointerNowSeconds() * 1e3,
126
+ px: point.px,
127
+ uv: point.uv,
128
+ inside: point.inside
129
+ };
130
+ if (capturePointer) try {
131
+ canvas.setPointerCapture(event.pointerId);
132
+ } catch {}
133
+ const nextState = updatePointerState({
134
+ point,
135
+ inside: point.inside,
136
+ pressed: true,
137
+ dragging: false,
138
+ pointerType,
139
+ pointerId: event.pointerId,
140
+ button: event.button,
141
+ buttons: event.buttons,
142
+ downPx: point.px,
143
+ downUv: point.uv,
144
+ resetDelta: true
145
+ });
146
+ options.onDown?.(nextState, event);
147
+ };
148
+ const handleMove = (event) => {
149
+ if (!isTrackedPointer(event)) return;
150
+ const point = getPointerCoordinates(event.clientX, event.clientY, canvas.getBoundingClientRect());
151
+ const pressed = activePointerId !== null && event.pointerId === activePointerId;
152
+ const downPx = pressed ? downSnapshot?.px ?? point.px : null;
153
+ const downUv = pressed ? downSnapshot?.uv ?? point.uv : null;
154
+ let dragging = false;
155
+ if (pressed && downPx) {
156
+ const dx = point.px[0] - downPx[0];
157
+ const dy = point.px[1] - downPx[1];
158
+ dragging = Math.hypot(dx, dy) > 0;
159
+ }
160
+ const nextState = updatePointerState({
161
+ point,
162
+ inside: point.inside,
163
+ pressed,
164
+ dragging,
165
+ pointerType: normalizePointerKind(event.pointerType),
166
+ pointerId: event.pointerId,
167
+ button: pressed ? downSnapshot?.button ?? event.button : null,
168
+ buttons: event.buttons,
169
+ downPx,
170
+ downUv
171
+ });
172
+ options.onMove?.(nextState, event);
173
+ };
174
+ const handleWindowMove = (event) => {
175
+ if (!trackOutside || activePointerId === null || event.pointerId !== activePointerId) return;
176
+ const point = getPointerCoordinates(event.clientX, event.clientY, canvas.getBoundingClientRect());
177
+ if (point.inside) return;
178
+ const downPx = downSnapshot?.px ?? point.px;
179
+ const downUv = downSnapshot?.uv ?? point.uv;
180
+ const dx = point.px[0] - downPx[0];
181
+ const dy = point.px[1] - downPx[1];
182
+ const nextState = updatePointerState({
183
+ point,
184
+ inside: false,
185
+ pressed: true,
186
+ dragging: Math.hypot(dx, dy) > 0,
187
+ pointerType: downSnapshot?.pointerType ?? normalizePointerKind(event.pointerType),
188
+ pointerId: event.pointerId,
189
+ button: downSnapshot?.button ?? event.button,
190
+ buttons: event.buttons,
191
+ downPx,
192
+ downUv
193
+ });
194
+ options.onMove?.(nextState, event);
195
+ };
196
+ const releasePointer = (event, emitClick) => {
197
+ if (activePointerId === null || event.pointerId !== activePointerId) return;
198
+ const point = getPointerCoordinates(event.clientX, event.clientY, canvas.getBoundingClientRect());
199
+ const pointerType = downSnapshot?.pointerType ?? normalizePointerKind(event.pointerType);
200
+ const previous = downSnapshot;
201
+ const nextState = updatePointerState({
202
+ point,
203
+ inside: point.inside,
204
+ pressed: false,
205
+ dragging: false,
206
+ pointerType,
207
+ pointerId: null,
208
+ button: null,
209
+ buttons: event.buttons,
210
+ downPx: null,
211
+ downUv: null
212
+ });
213
+ options.onUp?.(nextState, event);
214
+ if (capturePointer && canvas.hasPointerCapture(event.pointerId)) try {
215
+ canvas.releasePointerCapture(event.pointerId);
216
+ } catch {}
217
+ if (emitClick && clickEnabled && previous && clickButtons.has(previous.button)) {
218
+ const durationMs = getPointerNowSeconds() * 1e3 - previous.timeMs;
219
+ const dx = point.px[0] - previous.px[0];
220
+ const dy = point.px[1] - previous.px[1];
221
+ const moveDistance = Math.hypot(dx, dy);
222
+ if (previous.inside && point.inside && durationMs <= clickMaxDurationMs && moveDistance <= clickMaxMovePx) {
223
+ clickCounter += 1;
224
+ const click = {
225
+ id: clickCounter,
226
+ time: getPointerNowSeconds(),
227
+ pointerType,
228
+ pointerId: event.pointerId,
229
+ button: previous.button,
230
+ modifiers: {
231
+ alt: event.altKey,
232
+ ctrl: event.ctrlKey,
233
+ shift: event.shiftKey,
234
+ meta: event.metaKey
235
+ },
236
+ px: point.px,
237
+ uv: point.uv,
238
+ ndc: point.ndc
239
+ };
240
+ lastClick.set(click);
241
+ options.onClick?.(click, nextState, event);
242
+ requestFrame();
243
+ }
244
+ }
245
+ activePointerId = null;
246
+ downSnapshot = null;
247
+ };
248
+ const handlePointerUp = (event) => {
249
+ releasePointer(event, true);
250
+ };
251
+ const handlePointerCancel = (event) => {
252
+ releasePointer(event, false);
253
+ };
254
+ const handlePointerLeave = () => {
255
+ if (activePointerId !== null) return;
256
+ updateInsideState(false);
257
+ };
258
+ canvas.addEventListener("pointerdown", handlePointerDown);
259
+ canvas.addEventListener("pointermove", handleMove);
260
+ canvas.addEventListener("pointerup", handlePointerUp);
261
+ canvas.addEventListener("pointercancel", handlePointerCancel);
262
+ canvas.addEventListener("pointerleave", handlePointerLeave);
263
+ if (trackOutside) {
264
+ window.addEventListener("pointermove", handleWindowMove);
265
+ window.addEventListener("pointerup", handlePointerUp);
266
+ window.addEventListener("pointercancel", handlePointerCancel);
267
+ }
268
+ return () => {
269
+ canvas.removeEventListener("pointerdown", handlePointerDown);
270
+ canvas.removeEventListener("pointermove", handleMove);
271
+ canvas.removeEventListener("pointerup", handlePointerUp);
272
+ canvas.removeEventListener("pointercancel", handlePointerCancel);
273
+ canvas.removeEventListener("pointerleave", handlePointerLeave);
274
+ if (trackOutside) {
275
+ window.removeEventListener("pointermove", handleWindowMove);
276
+ window.removeEventListener("pointerup", handlePointerUp);
277
+ window.removeEventListener("pointercancel", handlePointerCancel);
278
+ }
279
+ };
280
+ });
281
+ return {
282
+ state: pointerState,
283
+ lastClick,
284
+ resetClick: () => {
285
+ lastClick.set(null);
286
+ }
287
+ };
288
+ }
289
+ //#endregion
290
+ export { usePointer };
291
+
292
+ //# sourceMappingURL=use-pointer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-pointer.js","names":[],"sources":["../../src/lib/svelte/use-pointer.ts"],"sourcesContent":["import { onMount } from 'svelte';\nimport {\n\tcreateCurrentWritable as currentWritable,\n\ttype CurrentReadable\n} from '../core/current-value.js';\nimport {\n\tcreateInitialPointerState,\n\tgetPointerCoordinates,\n\tgetPointerNowSeconds,\n\tnormalizePointerKind,\n\tresolvePointerFrameRequestMode,\n\ttype PointerClick,\n\ttype PointerFrameRequestMode,\n\ttype PointerState,\n\ttype PointerVec2\n} from '../core/pointer.js';\nimport { useMotionGPU } from './motiongpu-context.js';\n\nexport type {\n\tPointerClick,\n\tPointerFrameRequestMode,\n\tPointerKind,\n\tPointerPoint,\n\tPointerState\n} from '../core/pointer.js';\n\n/**\n * Configuration for pointer input handling in `usePointer`.\n */\nexport interface UsePointerOptions {\n\t/**\n\t * Enables pointer listeners.\n\t *\n\t * @default true\n\t */\n\tenabled?: boolean;\n\t/**\n\t * Frame wake-up strategy for pointer-driven state changes.\n\t *\n\t * @default 'auto'\n\t */\n\trequestFrame?: PointerFrameRequestMode;\n\t/**\n\t * Requests pointer capture on pointer down.\n\t *\n\t * @default true\n\t */\n\tcapturePointer?: boolean;\n\t/**\n\t * Tracks pointer move/up outside canvas while pointer is pressed.\n\t *\n\t * @default true\n\t */\n\ttrackWhilePressedOutsideCanvas?: boolean;\n\t/**\n\t * Enables click/tap synthesis on pointer up.\n\t *\n\t * @default true\n\t */\n\tclickEnabled?: boolean;\n\t/**\n\t * Maximum press duration to consider pointer up a click (milliseconds).\n\t *\n\t * @default 350\n\t */\n\tclickMaxDurationMs?: number;\n\t/**\n\t * Maximum pointer travel from down to up to consider pointer up a click (pixels).\n\t *\n\t * @default 8\n\t */\n\tclickMaxMovePx?: number;\n\t/**\n\t * Allowed pointer buttons for click synthesis.\n\t *\n\t * @default [0]\n\t */\n\tclickButtons?: number[];\n\t/**\n\t * Called after pointer move state update.\n\t */\n\tonMove?: (state: PointerState, event: PointerEvent) => void;\n\t/**\n\t * Called after pointer down state update.\n\t */\n\tonDown?: (state: PointerState, event: PointerEvent) => void;\n\t/**\n\t * Called after pointer up/cancel state update.\n\t */\n\tonUp?: (state: PointerState, event: PointerEvent) => void;\n\t/**\n\t * Called when click/tap is synthesized.\n\t */\n\tonClick?: (click: PointerClick, state: PointerState, event: PointerEvent) => void;\n}\n\n/**\n * Reactive state returned by `usePointer`.\n */\nexport interface UsePointerResult {\n\t/**\n\t * Current pointer state.\n\t */\n\tstate: CurrentReadable<PointerState>;\n\t/**\n\t * Last synthesized click/tap event.\n\t */\n\tlastClick: CurrentReadable<PointerClick | null>;\n\t/**\n\t * Clears last click snapshot.\n\t */\n\tresetClick: () => void;\n}\n\ninterface PointerDownSnapshot {\n\tbutton: number;\n\tinside: boolean;\n\tpointerId: number;\n\tpointerType: 'mouse' | 'pen' | 'touch';\n\tpx: PointerVec2;\n\ttimeMs: number;\n\tuv: PointerVec2;\n}\n\n/**\n * Normalizes click button configuration with a primary-button fallback.\n */\nfunction normalizeClickButtons(buttons: number[] | undefined): Set<number> {\n\tconst source = buttons && buttons.length > 0 ? buttons : [0];\n\treturn new Set(source);\n}\n\n/**\n * Resolves a valid click duration threshold in milliseconds.\n */\nfunction resolveClickMaxDurationMs(value: number | undefined): number {\n\tif (typeof value !== 'number' || Number.isNaN(value) || value <= 0) {\n\t\treturn 350;\n\t}\n\n\treturn value;\n}\n\n/**\n * Resolves a valid click travel threshold in pixels.\n */\nfunction resolveClickMaxMovePx(value: number | undefined): number {\n\tif (typeof value !== 'number' || Number.isNaN(value) || value < 0) {\n\t\treturn 8;\n\t}\n\n\treturn value;\n}\n\n/**\n * Tracks normalized pointer coordinates and click/tap snapshots for the active `FragCanvas`.\n */\nexport function usePointer(options: UsePointerOptions = {}): UsePointerResult {\n\tconst motiongpu = useMotionGPU();\n\tconst pointerState = currentWritable<PointerState>(createInitialPointerState());\n\tconst lastClick = currentWritable<PointerClick | null>(null);\n\tconst enabled = options.enabled ?? true;\n\tconst requestFrameMode = options.requestFrame ?? 'auto';\n\tconst capturePointer = options.capturePointer ?? true;\n\tconst trackOutside = options.trackWhilePressedOutsideCanvas ?? true;\n\tconst clickEnabled = options.clickEnabled ?? true;\n\tconst clickMaxDurationMs = resolveClickMaxDurationMs(options.clickMaxDurationMs);\n\tconst clickMaxMovePx = resolveClickMaxMovePx(options.clickMaxMovePx);\n\tconst clickButtons = normalizeClickButtons(options.clickButtons);\n\tlet activePointerId: number | null = null;\n\tlet downSnapshot: PointerDownSnapshot | null = null;\n\tlet clickCounter = 0;\n\tlet previousPx: PointerVec2 | null = null;\n\tlet previousUv: PointerVec2 | null = null;\n\tlet previousTimeSeconds = 0;\n\n\tconst requestFrame = (): void => {\n\t\tconst mode = resolvePointerFrameRequestMode(requestFrameMode, motiongpu.renderMode.current);\n\t\tif (mode === 'invalidate') {\n\t\t\tmotiongpu.invalidate();\n\t\t\treturn;\n\t\t}\n\t\tif (mode === 'advance') {\n\t\t\tmotiongpu.advance();\n\t\t}\n\t};\n\n\t/**\n\t * Commits a full pointer state snapshot with computed delta and velocity vectors.\n\t */\n\tconst updatePointerState = (input: {\n\t\tbutton: number | null;\n\t\tbuttons: number;\n\t\tdownPx: PointerVec2 | null;\n\t\tdownUv: PointerVec2 | null;\n\t\tdragging: boolean;\n\t\tinside: boolean;\n\t\tpointerId: number | null;\n\t\tpointerType: 'mouse' | 'pen' | 'touch' | null;\n\t\tpressed: boolean;\n\t\tpoint: {\n\t\t\tndc: PointerVec2;\n\t\t\tpx: PointerVec2;\n\t\t\tuv: PointerVec2;\n\t\t};\n\t\tresetDelta?: boolean;\n\t}): PointerState => {\n\t\tconst nowSeconds = getPointerNowSeconds();\n\t\tconst dt = previousTimeSeconds > 0 ? Math.max(nowSeconds - previousTimeSeconds, 1e-6) : 0;\n\t\tconst deltaPx: PointerVec2 =\n\t\t\tinput.resetDelta || !previousPx\n\t\t\t\t? [0, 0]\n\t\t\t\t: [input.point.px[0] - previousPx[0], input.point.px[1] - previousPx[1]];\n\t\tconst deltaUv: PointerVec2 =\n\t\t\tinput.resetDelta || !previousUv\n\t\t\t\t? [0, 0]\n\t\t\t\t: [input.point.uv[0] - previousUv[0], input.point.uv[1] - previousUv[1]];\n\t\tconst velocityPx: PointerVec2 = dt > 0 ? [deltaPx[0] / dt, deltaPx[1] / dt] : [0, 0];\n\t\tconst velocityUv: PointerVec2 = dt > 0 ? [deltaUv[0] / dt, deltaUv[1] / dt] : [0, 0];\n\t\tconst nextState: PointerState = {\n\t\t\tpx: input.point.px,\n\t\t\tuv: input.point.uv,\n\t\t\tndc: input.point.ndc,\n\t\t\tinside: input.inside,\n\t\t\tpressed: input.pressed,\n\t\t\tdragging: input.dragging,\n\t\t\tpointerType: input.pointerType,\n\t\t\tpointerId: input.pointerId,\n\t\t\tbutton: input.button,\n\t\t\tbuttons: input.buttons,\n\t\t\ttime: nowSeconds,\n\t\t\tdownPx: input.downPx,\n\t\t\tdownUv: input.downUv,\n\t\t\tdeltaPx,\n\t\t\tdeltaUv,\n\t\t\tvelocityPx,\n\t\t\tvelocityUv\n\t\t};\n\t\tpointerState.set(nextState);\n\t\tpreviousPx = input.point.px;\n\t\tpreviousUv = input.point.uv;\n\t\tpreviousTimeSeconds = nowSeconds;\n\t\trequestFrame();\n\t\treturn nextState;\n\t};\n\n\t/**\n\t * Updates only the `inside` flag while keeping the latest pointer coordinates.\n\t */\n\tconst updateInsideState = (inside: boolean): PointerState => {\n\t\tconst current = pointerState.current;\n\t\tconst nextState: PointerState = {\n\t\t\t...current,\n\t\t\tinside,\n\t\t\ttime: getPointerNowSeconds(),\n\t\t\tdeltaPx: [0, 0],\n\t\t\tdeltaUv: [0, 0],\n\t\t\tvelocityPx: [0, 0],\n\t\t\tvelocityUv: [0, 0]\n\t\t};\n\t\tpointerState.set(nextState);\n\t\trequestFrame();\n\t\treturn nextState;\n\t};\n\n\t/**\n\t * Checks whether an event belongs to the active tracked pointer.\n\t */\n\tconst isTrackedPointer = (event: PointerEvent): boolean =>\n\t\tactivePointerId === null || event.pointerId === activePointerId;\n\n\tonMount(() => {\n\t\tif (!enabled) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst canvas = motiongpu.canvas;\n\t\tif (!canvas) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst handlePointerDown = (event: PointerEvent): void => {\n\t\t\tconst point = getPointerCoordinates(\n\t\t\t\tevent.clientX,\n\t\t\t\tevent.clientY,\n\t\t\t\tcanvas.getBoundingClientRect()\n\t\t\t);\n\t\t\tconst pointerType = normalizePointerKind(event.pointerType);\n\t\t\tactivePointerId = event.pointerId;\n\t\t\tdownSnapshot = {\n\t\t\t\tpointerId: event.pointerId,\n\t\t\t\tpointerType,\n\t\t\t\tbutton: event.button,\n\t\t\t\ttimeMs: getPointerNowSeconds() * 1000,\n\t\t\t\tpx: point.px,\n\t\t\t\tuv: point.uv,\n\t\t\t\tinside: point.inside\n\t\t\t};\n\t\t\tif (capturePointer) {\n\t\t\t\ttry {\n\t\t\t\t\tcanvas.setPointerCapture(event.pointerId);\n\t\t\t\t} catch {\n\t\t\t\t\t// Browser rejected capture (e.g. unsupported pointer state).\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst nextState = updatePointerState({\n\t\t\t\tpoint,\n\t\t\t\tinside: point.inside,\n\t\t\t\tpressed: true,\n\t\t\t\tdragging: false,\n\t\t\t\tpointerType,\n\t\t\t\tpointerId: event.pointerId,\n\t\t\t\tbutton: event.button,\n\t\t\t\tbuttons: event.buttons,\n\t\t\t\tdownPx: point.px,\n\t\t\t\tdownUv: point.uv,\n\t\t\t\tresetDelta: true\n\t\t\t});\n\t\t\toptions.onDown?.(nextState, event);\n\t\t};\n\n\t\tconst handleMove = (event: PointerEvent): void => {\n\t\t\tif (!isTrackedPointer(event)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst point = getPointerCoordinates(\n\t\t\t\tevent.clientX,\n\t\t\t\tevent.clientY,\n\t\t\t\tcanvas.getBoundingClientRect()\n\t\t\t);\n\t\t\tconst pressed = activePointerId !== null && event.pointerId === activePointerId;\n\t\t\tconst downPx = pressed ? (downSnapshot?.px ?? point.px) : null;\n\t\t\tconst downUv = pressed ? (downSnapshot?.uv ?? point.uv) : null;\n\t\t\tlet dragging = false;\n\t\t\tif (pressed && downPx) {\n\t\t\t\tconst dx = point.px[0] - downPx[0];\n\t\t\t\tconst dy = point.px[1] - downPx[1];\n\t\t\t\tdragging = Math.hypot(dx, dy) > 0;\n\t\t\t}\n\t\t\tconst nextState = updatePointerState({\n\t\t\t\tpoint,\n\t\t\t\tinside: point.inside,\n\t\t\t\tpressed,\n\t\t\t\tdragging,\n\t\t\t\tpointerType: normalizePointerKind(event.pointerType),\n\t\t\t\tpointerId: event.pointerId,\n\t\t\t\tbutton: pressed ? (downSnapshot?.button ?? event.button) : null,\n\t\t\t\tbuttons: event.buttons,\n\t\t\t\tdownPx,\n\t\t\t\tdownUv\n\t\t\t});\n\t\t\toptions.onMove?.(nextState, event);\n\t\t};\n\n\t\tconst handleWindowMove = (event: PointerEvent): void => {\n\t\t\tif (!trackOutside || activePointerId === null || event.pointerId !== activePointerId) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst point = getPointerCoordinates(\n\t\t\t\tevent.clientX,\n\t\t\t\tevent.clientY,\n\t\t\t\tcanvas.getBoundingClientRect()\n\t\t\t);\n\t\t\tif (point.inside) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst downPx = downSnapshot?.px ?? point.px;\n\t\t\tconst downUv = downSnapshot?.uv ?? point.uv;\n\t\t\tconst dx = point.px[0] - downPx[0];\n\t\t\tconst dy = point.px[1] - downPx[1];\n\t\t\tconst nextState = updatePointerState({\n\t\t\t\tpoint,\n\t\t\t\tinside: false,\n\t\t\t\tpressed: true,\n\t\t\t\tdragging: Math.hypot(dx, dy) > 0,\n\t\t\t\tpointerType: downSnapshot?.pointerType ?? normalizePointerKind(event.pointerType),\n\t\t\t\tpointerId: event.pointerId,\n\t\t\t\tbutton: downSnapshot?.button ?? event.button,\n\t\t\t\tbuttons: event.buttons,\n\t\t\t\tdownPx,\n\t\t\t\tdownUv\n\t\t\t});\n\t\t\toptions.onMove?.(nextState, event);\n\t\t};\n\n\t\tconst releasePointer = (event: PointerEvent, emitClick: boolean): void => {\n\t\t\tif (activePointerId === null || event.pointerId !== activePointerId) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst point = getPointerCoordinates(\n\t\t\t\tevent.clientX,\n\t\t\t\tevent.clientY,\n\t\t\t\tcanvas.getBoundingClientRect()\n\t\t\t);\n\t\t\tconst pointerType = downSnapshot?.pointerType ?? normalizePointerKind(event.pointerType);\n\t\t\tconst previous = downSnapshot;\n\t\t\tconst nextState = updatePointerState({\n\t\t\t\tpoint,\n\t\t\t\tinside: point.inside,\n\t\t\t\tpressed: false,\n\t\t\t\tdragging: false,\n\t\t\t\tpointerType,\n\t\t\t\tpointerId: null,\n\t\t\t\tbutton: null,\n\t\t\t\tbuttons: event.buttons,\n\t\t\t\tdownPx: null,\n\t\t\t\tdownUv: null\n\t\t\t});\n\t\t\toptions.onUp?.(nextState, event);\n\n\t\t\tif (capturePointer && canvas.hasPointerCapture(event.pointerId)) {\n\t\t\t\ttry {\n\t\t\t\t\tcanvas.releasePointerCapture(event.pointerId);\n\t\t\t\t} catch {\n\t\t\t\t\t// Browser rejected release for this pointer id.\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (emitClick && clickEnabled && previous && clickButtons.has(previous.button)) {\n\t\t\t\tconst durationMs = getPointerNowSeconds() * 1000 - previous.timeMs;\n\t\t\t\tconst dx = point.px[0] - previous.px[0];\n\t\t\t\tconst dy = point.px[1] - previous.px[1];\n\t\t\t\tconst moveDistance = Math.hypot(dx, dy);\n\t\t\t\tif (\n\t\t\t\t\tprevious.inside &&\n\t\t\t\t\tpoint.inside &&\n\t\t\t\t\tdurationMs <= clickMaxDurationMs &&\n\t\t\t\t\tmoveDistance <= clickMaxMovePx\n\t\t\t\t) {\n\t\t\t\t\tclickCounter += 1;\n\t\t\t\t\tconst click: PointerClick = {\n\t\t\t\t\t\tid: clickCounter,\n\t\t\t\t\t\ttime: getPointerNowSeconds(),\n\t\t\t\t\t\tpointerType,\n\t\t\t\t\t\tpointerId: event.pointerId,\n\t\t\t\t\t\tbutton: previous.button,\n\t\t\t\t\t\tmodifiers: {\n\t\t\t\t\t\t\talt: event.altKey,\n\t\t\t\t\t\t\tctrl: event.ctrlKey,\n\t\t\t\t\t\t\tshift: event.shiftKey,\n\t\t\t\t\t\t\tmeta: event.metaKey\n\t\t\t\t\t\t},\n\t\t\t\t\t\tpx: point.px,\n\t\t\t\t\t\tuv: point.uv,\n\t\t\t\t\t\tndc: point.ndc\n\t\t\t\t\t};\n\t\t\t\t\tlastClick.set(click);\n\t\t\t\t\toptions.onClick?.(click, nextState, event);\n\t\t\t\t\trequestFrame();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tactivePointerId = null;\n\t\t\tdownSnapshot = null;\n\t\t};\n\n\t\tconst handlePointerUp = (event: PointerEvent): void => {\n\t\t\treleasePointer(event, true);\n\t\t};\n\n\t\tconst handlePointerCancel = (event: PointerEvent): void => {\n\t\t\treleasePointer(event, false);\n\t\t};\n\n\t\tconst handlePointerLeave = (): void => {\n\t\t\tif (activePointerId !== null) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tupdateInsideState(false);\n\t\t};\n\n\t\tcanvas.addEventListener('pointerdown', handlePointerDown);\n\t\tcanvas.addEventListener('pointermove', handleMove);\n\t\tcanvas.addEventListener('pointerup', handlePointerUp);\n\t\tcanvas.addEventListener('pointercancel', handlePointerCancel);\n\t\tcanvas.addEventListener('pointerleave', handlePointerLeave);\n\t\tif (trackOutside) {\n\t\t\twindow.addEventListener('pointermove', handleWindowMove);\n\t\t\twindow.addEventListener('pointerup', handlePointerUp);\n\t\t\twindow.addEventListener('pointercancel', handlePointerCancel);\n\t\t}\n\n\t\treturn () => {\n\t\t\tcanvas.removeEventListener('pointerdown', handlePointerDown);\n\t\t\tcanvas.removeEventListener('pointermove', handleMove);\n\t\t\tcanvas.removeEventListener('pointerup', handlePointerUp);\n\t\t\tcanvas.removeEventListener('pointercancel', handlePointerCancel);\n\t\t\tcanvas.removeEventListener('pointerleave', handlePointerLeave);\n\t\t\tif (trackOutside) {\n\t\t\t\twindow.removeEventListener('pointermove', handleWindowMove);\n\t\t\t\twindow.removeEventListener('pointerup', handlePointerUp);\n\t\t\t\twindow.removeEventListener('pointercancel', handlePointerCancel);\n\t\t\t}\n\t\t};\n\t});\n\n\treturn {\n\t\tstate: pointerState,\n\t\tlastClick,\n\t\tresetClick: () => {\n\t\t\tlastClick.set(null);\n\t\t}\n\t};\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-core/motion-gpu",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Framework-agnostic WebGPU runtime for fullscreen WGSL shaders with explicit Svelte and React adapter entrypoints.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -0,0 +1,73 @@
1
+ export interface ComputeStorageBindGroupCacheRequest {
2
+ topologyKey: string;
3
+ layoutEntries: GPUBindGroupLayoutEntry[];
4
+ bindGroupEntries: GPUBindGroupEntry[];
5
+ resourceRefs: readonly unknown[];
6
+ }
7
+
8
+ export interface ComputeStorageBindGroupCache {
9
+ getOrCreate: (request: ComputeStorageBindGroupCacheRequest) => GPUBindGroup | null;
10
+ reset: () => void;
11
+ }
12
+
13
+ function equalResourceRefs(previous: readonly unknown[], next: readonly unknown[]): boolean {
14
+ if (previous.length !== next.length) {
15
+ return false;
16
+ }
17
+
18
+ for (let index = 0; index < previous.length; index += 1) {
19
+ if (!Object.is(previous[index], next[index])) {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ return true;
25
+ }
26
+
27
+ export function createComputeStorageBindGroupCache(
28
+ device: GPUDevice
29
+ ): ComputeStorageBindGroupCache {
30
+ let cachedTopologyKey: string | null = null;
31
+ let cachedLayout: GPUBindGroupLayout | null = null;
32
+ let cachedBindGroup: GPUBindGroup | null = null;
33
+ let cachedResourceRefs: readonly unknown[] = [];
34
+
35
+ const reset = (): void => {
36
+ cachedTopologyKey = null;
37
+ cachedLayout = null;
38
+ cachedBindGroup = null;
39
+ cachedResourceRefs = [];
40
+ };
41
+
42
+ return {
43
+ getOrCreate(request) {
44
+ if (request.layoutEntries.length === 0) {
45
+ reset();
46
+ return null;
47
+ }
48
+
49
+ if (cachedTopologyKey !== request.topologyKey) {
50
+ cachedTopologyKey = request.topologyKey;
51
+ cachedLayout = device.createBindGroupLayout({ entries: request.layoutEntries });
52
+ cachedBindGroup = null;
53
+ cachedResourceRefs = [];
54
+ }
55
+
56
+ if (!cachedLayout) {
57
+ throw new Error('Compute storage bind group cache is missing a layout.');
58
+ }
59
+
60
+ if (cachedBindGroup && equalResourceRefs(cachedResourceRefs, request.resourceRefs)) {
61
+ return cachedBindGroup;
62
+ }
63
+
64
+ cachedBindGroup = device.createBindGroup({
65
+ layout: cachedLayout,
66
+ entries: request.bindGroupEntries
67
+ });
68
+ cachedResourceRefs = [...request.resourceRefs];
69
+ return cachedBindGroup;
70
+ },
71
+ reset
72
+ };
73
+ }
@@ -277,6 +277,27 @@ ${options.compute}
277
277
  `;
278
278
  }
279
279
 
280
+ /**
281
+ * Source location for generated compute shader lines.
282
+ */
283
+ export interface ComputeShaderSourceLocation {
284
+ kind: 'compute';
285
+ line: number;
286
+ }
287
+
288
+ /**
289
+ * 1-based line map from generated compute WGSL to user compute source.
290
+ */
291
+ export type ComputeShaderLineMap = Array<ComputeShaderSourceLocation | null>;
292
+
293
+ /**
294
+ * Result of compute shader source generation with line mapping metadata.
295
+ */
296
+ export interface BuiltComputeShaderSource {
297
+ code: string;
298
+ lineMap: ComputeShaderLineMap;
299
+ }
300
+
280
301
  /**
281
302
  * Assembles full compute shader WGSL with preamble.
282
303
  *
@@ -324,3 +345,68 @@ ${storageTextureBindings ? '\n' + storageTextureBindings : ''}
324
345
  ${options.compute}
325
346
  `;
326
347
  }
348
+
349
+ function buildComputeLineMap(
350
+ generatedCode: string,
351
+ userComputeSource: string
352
+ ): ComputeShaderLineMap {
353
+ const lineCount = generatedCode.split('\n').length;
354
+ const lineMap: ComputeShaderLineMap = new Array(lineCount + 1).fill(null);
355
+ const computeStartIndex = generatedCode.indexOf(userComputeSource);
356
+ if (computeStartIndex === -1) {
357
+ return lineMap;
358
+ }
359
+
360
+ const computeStartLine = generatedCode.slice(0, computeStartIndex).split('\n').length;
361
+ const computeLineCount = userComputeSource.split('\n').length;
362
+ for (let line = 0; line < computeLineCount; line += 1) {
363
+ lineMap[computeStartLine + line] = {
364
+ kind: 'compute',
365
+ line: line + 1
366
+ };
367
+ }
368
+
369
+ return lineMap;
370
+ }
371
+
372
+ /**
373
+ * Assembles full compute shader WGSL with source line mapping metadata.
374
+ */
375
+ export function buildComputeShaderSourceWithMap(options: {
376
+ compute: string;
377
+ uniformLayout: UniformLayout;
378
+ storageBufferKeys: string[];
379
+ storageBufferDefinitions: Record<
380
+ string,
381
+ { type: StorageBufferType; access: StorageBufferAccess }
382
+ >;
383
+ storageTextureKeys: string[];
384
+ storageTextureDefinitions: Record<string, { format: GPUTextureFormat }>;
385
+ }): BuiltComputeShaderSource {
386
+ const code = buildComputeShaderSource(options);
387
+ return {
388
+ code,
389
+ lineMap: buildComputeLineMap(code, options.compute)
390
+ };
391
+ }
392
+
393
+ /**
394
+ * Assembles ping-pong compute shader WGSL with source line mapping metadata.
395
+ */
396
+ export function buildPingPongComputeShaderSourceWithMap(options: {
397
+ compute: string;
398
+ uniformLayout: UniformLayout;
399
+ storageBufferKeys: string[];
400
+ storageBufferDefinitions: Record<
401
+ string,
402
+ { type: StorageBufferType; access: StorageBufferAccess }
403
+ >;
404
+ target: string;
405
+ targetFormat: GPUTextureFormat;
406
+ }): BuiltComputeShaderSource {
407
+ const code = buildPingPongComputeShaderSource(options);
408
+ return {
409
+ code,
410
+ lineMap: buildComputeLineMap(code, options.compute)
411
+ };
412
+ }
@@ -11,6 +11,13 @@ export interface MaterialSourceMetadata {
11
11
  functionName?: string;
12
12
  }
13
13
 
14
+ export interface ComputeSourceLocation {
15
+ kind: 'compute';
16
+ line: number;
17
+ }
18
+
19
+ export type ShaderSourceLocation = MaterialSourceLocation | ComputeSourceLocation;
20
+
14
21
  /**
15
22
  * One WGSL compiler diagnostic enriched with source-location metadata.
16
23
  */
@@ -19,7 +26,7 @@ export interface ShaderCompilationDiagnostic {
19
26
  message: string;
20
27
  linePos?: number;
21
28
  lineLength?: number;
22
- sourceLocation: MaterialSourceLocation | null;
29
+ sourceLocation: ShaderSourceLocation | null;
23
30
  }
24
31
 
25
32
  /**
@@ -41,8 +48,10 @@ export interface ShaderCompilationRuntimeContext {
41
48
  */
42
49
  export interface ShaderCompilationDiagnosticsPayload {
43
50
  kind: 'shader-compilation';
51
+ shaderStage?: 'fragment' | 'compute';
44
52
  diagnostics: ShaderCompilationDiagnostic[];
45
53
  fragmentSource: string;
54
+ computeSource?: string;
46
55
  includeSources: Record<string, string>;
47
56
  defineBlockSource?: string;
48
57
  materialSource: MaterialSourceMetadata | null;
@@ -78,7 +87,7 @@ function isMaterialSourceMetadata(value: unknown): value is MaterialSourceMetada
78
87
  return true;
79
88
  }
80
89
 
81
- function isMaterialSourceLocation(value: unknown): value is MaterialSourceLocation | null {
90
+ function isShaderSourceLocation(value: unknown): value is ShaderSourceLocation | null {
82
91
  if (value === null) {
83
92
  return true;
84
93
  }
@@ -89,7 +98,7 @@ function isMaterialSourceLocation(value: unknown): value is MaterialSourceLocati
89
98
 
90
99
  const record = value as Record<string, unknown>;
91
100
  const kind = record.kind;
92
- if (kind !== 'fragment' && kind !== 'include' && kind !== 'define') {
101
+ if (kind !== 'fragment' && kind !== 'include' && kind !== 'define' && kind !== 'compute') {
93
102
  return false;
94
103
  }
95
104
 
@@ -114,7 +123,7 @@ function isShaderCompilationDiagnostic(value: unknown): value is ShaderCompilati
114
123
  if (record.lineLength !== undefined && typeof record.lineLength !== 'number') {
115
124
  return false;
116
125
  }
117
- if (!isMaterialSourceLocation(record.sourceLocation)) {
126
+ if (!isShaderSourceLocation(record.sourceLocation)) {
118
127
  return false;
119
128
  }
120
129
 
@@ -191,6 +200,13 @@ export function getShaderCompilationDiagnostics(
191
200
  if (record.kind !== 'shader-compilation') {
192
201
  return null;
193
202
  }
203
+ if (
204
+ record.shaderStage !== undefined &&
205
+ record.shaderStage !== 'fragment' &&
206
+ record.shaderStage !== 'compute'
207
+ ) {
208
+ return null;
209
+ }
194
210
  if (
195
211
  !Array.isArray(record.diagnostics) ||
196
212
  !record.diagnostics.every(isShaderCompilationDiagnostic)
@@ -200,6 +216,9 @@ export function getShaderCompilationDiagnostics(
200
216
  if (typeof record.fragmentSource !== 'string') {
201
217
  return null;
202
218
  }
219
+ if (record.computeSource !== undefined && typeof record.computeSource !== 'string') {
220
+ return null;
221
+ }
203
222
  if (record.defineBlockSource !== undefined && typeof record.defineBlockSource !== 'string') {
204
223
  return null;
205
224
  }
@@ -222,8 +241,14 @@ export function getShaderCompilationDiagnostics(
222
241
 
223
242
  return {
224
243
  kind: 'shader-compilation',
244
+ ...(record.shaderStage !== undefined
245
+ ? { shaderStage: record.shaderStage as 'fragment' | 'compute' }
246
+ : {}),
225
247
  diagnostics: record.diagnostics as ShaderCompilationDiagnostic[],
226
248
  fragmentSource: record.fragmentSource,
249
+ ...(record.computeSource !== undefined
250
+ ? { computeSource: record.computeSource as string }
251
+ : {}),
227
252
  includeSources: includeSources as Record<string, string>,
228
253
  ...(record.defineBlockSource !== undefined
229
254
  ? { defineBlockSource: record.defineBlockSource as string }
@@ -211,6 +211,19 @@ function buildSourceFromDiagnostics(error: unknown): MotionGPUErrorSource | null
211
211
  };
212
212
  }
213
213
 
214
+ if (location.kind === 'compute') {
215
+ const computeSource = diagnostics.computeSource ?? diagnostics.fragmentSource;
216
+ const component = 'Compute shader';
217
+ const locationLabel = formatShaderSourceLocation(location) ?? `compute line ${location.line}`;
218
+ return {
219
+ component,
220
+ location: `${component} (${locationLabel})`,
221
+ line: location.line,
222
+ ...(column !== undefined ? { column } : {}),
223
+ snippet: toSnippet(computeSource, location.line)
224
+ };
225
+ }
226
+
214
227
  const defineName = location.define ?? 'unknown';
215
228
  const defineLine = Math.max(1, location.line);
216
229
  const component = `#define ${defineName}`;
@@ -516,7 +529,19 @@ export function toMotionGPUErrorReport(
516
529
  error instanceof Error && error.stack
517
530
  ? splitLines(error.stack).filter((line) => line !== message)
518
531
  : [];
519
- const classification = classifyErrorMessage(rawMessage);
532
+ let classification = classifyErrorMessage(rawMessage);
533
+ if (
534
+ shaderDiagnostics?.shaderStage === 'compute' &&
535
+ classification.code === 'WGSL_COMPILATION_FAILED'
536
+ ) {
537
+ classification = {
538
+ code: 'COMPUTE_COMPILATION_FAILED',
539
+ severity: 'error',
540
+ recoverable: true,
541
+ title: 'Compute shader compilation failed',
542
+ hint: 'Check WGSL compute shader sources below and verify storage bindings.'
543
+ };
544
+ }
520
545
 
521
546
  return {
522
547
  code: classification.code,
@@ -542,7 +542,8 @@ function buildTextureConfigSignature<TTextureKey extends string>(
542
542
  normalized.anisotropy,
543
543
  normalized.filter,
544
544
  normalized.addressModeU,
545
- normalized.addressModeV
545
+ normalized.addressModeV,
546
+ normalized.fragmentVisible ? '1' : '0'
546
547
  ].join(':');
547
548
  }
548
549