wgsl-play 0.0.35 → 0.0.36

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.
@@ -154,19 +154,25 @@ async function initWebGPU(canvas, alphaMode = "opaque") {
154
154
  /** Compile WESL fragment shader and create render pipeline. */
155
155
  async function createPipeline(state, fragmentSource, options) {
156
156
  state.device.pushErrorScope("validation");
157
- const pipeline = await linkAndCreatePipeline({
158
- device: state.device,
159
- fragmentSource,
160
- format: state.presentationFormat,
161
- layout: state.pipelineLayout,
162
- ...options
163
- });
164
- const gpuError = await state.device.popErrorScope();
165
- if (gpuError) {
157
+ let gpuError;
158
+ let jsError;
159
+ try {
160
+ state.pipeline = await linkAndCreatePipeline({
161
+ device: state.device,
162
+ fragmentSource,
163
+ format: state.presentationFormat,
164
+ layout: state.pipelineLayout,
165
+ ...options
166
+ });
167
+ } catch (e) {
168
+ jsError = e;
169
+ } finally {
170
+ gpuError = await state.device.popErrorScope();
171
+ }
172
+ if (jsError || gpuError) {
166
173
  state.pipeline = void 0;
167
- throw gpuError;
174
+ throw jsError ?? gpuError;
168
175
  }
169
- state.pipeline = pipeline;
170
176
  }
171
177
  /** Start the render loop. Returns a stop function. */
172
178
  function startRenderLoop(state, playback) {
@@ -38,6 +38,8 @@ declare class WgslPlay extends HTMLElement {
38
38
  private _sourceEl;
39
39
  private _sourceListener;
40
40
  private _fetchLibs;
41
+ private _dirty;
42
+ private _building;
41
43
  private _theme;
42
44
  private _mediaQuery;
43
45
  private _onFullscreenChange;
@@ -93,12 +95,12 @@ declare class WgslPlay extends HTMLElement {
93
95
  private loadInitialContent;
94
96
  /** Connect to a source provider element (e.g., wgsl-edit). */
95
97
  private connectToSource;
96
- /** Fetch shader from URL, auto-fetching any imported dependencies. */
98
+ /** Fetch shader from URL, then trigger a build. */
97
99
  private loadFromUrl;
98
- /** Rebuild GPU pipeline using stored state. For full projects with all sources. */
99
- private rebuildPipeline;
100
- /** Discover dependencies and rebuild. For HTTP/inline sources that may need fetching. */
101
- private discoverAndRebuild;
100
+ /** Mark build as needed. Coalesces rapid requests into a single build. */
101
+ private requestBuild;
102
+ /** Run builds until no longer dirty. Only one instance runs at a time. */
103
+ private runBuild;
102
104
  private handleCompileError;
103
105
  /** Extract source locations from a WESL parse error or GPU compilation error. */
104
106
  private extractLocations;
package/dist/WgslPlay.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as PlaybackControls, c as getConfig, i as startRenderLoop, l as resetConfig, n as createPipeline, o as ErrorOverlay, r as initWebGPU, s as defaults, t as WgslPlay_default } from "./WgslPlay-Ben4VdsC.js";
1
+ import { a as PlaybackControls, c as getConfig, i as startRenderLoop, l as resetConfig, n as createPipeline, o as ErrorOverlay, r as initWebGPU, s as defaults, t as WgslPlay_default } from "./WgslPlay-BRvURGA3.js";
2
2
  import { fetchDependencies, loadShaderFromUrl } from "wesl-fetch";
3
3
  import { WeslParseError, fileToModulePath } from "wesl";
4
4
 
@@ -37,6 +37,8 @@ var WgslPlay = class extends HTMLElement {
37
37
  _sourceEl = null;
38
38
  _sourceListener = null;
39
39
  _fetchLibs = true;
40
+ _dirty = false;
41
+ _building = false;
40
42
  _theme = "auto";
41
43
  _mediaQuery = null;
42
44
  _onFullscreenChange = () => this.controls.setFullscreen(!!document.fullscreenElement);
@@ -117,7 +119,7 @@ var WgslPlay = class extends HTMLElement {
117
119
  this._weslSrc = { [this._rootModuleName]: value };
118
120
  this._libs = void 0;
119
121
  this._fromFullProject = false;
120
- this.discoverAndRebuild();
122
+ this.requestBuild();
121
123
  }
122
124
  /** Conditions for conditional compilation (@if/@elif/@else). */
123
125
  get conditions() {
@@ -129,8 +131,7 @@ var WgslPlay = class extends HTMLElement {
129
131
  conditions: value
130
132
  };
131
133
  if (Object.keys(this._weslSrc).length === 0) return;
132
- if (this._fromFullProject) this.rebuildPipeline();
133
- else this.discoverAndRebuild();
134
+ this.requestBuild();
134
135
  }
135
136
  /** Set project configuration (mirrors wesl link() API). */
136
137
  set project(value) {
@@ -146,8 +147,7 @@ var WgslPlay = class extends HTMLElement {
146
147
  return;
147
148
  }
148
149
  if (Object.keys(this._weslSrc).length === 0) return;
149
- if (this._fromFullProject) this.rebuildPipeline();
150
- else this.discoverAndRebuild();
150
+ this.requestBuild();
151
151
  }
152
152
  /** Set sources from a full project with weslSrc. */
153
153
  setProjectSources(weslSrc, rootModuleName) {
@@ -156,7 +156,7 @@ var WgslPlay = class extends HTMLElement {
156
156
  this._weslSrc = toModulePaths(weslSrc, pkg);
157
157
  this._rootModuleName = fileToModulePath(root, pkg, false);
158
158
  this._fromFullProject = true;
159
- this.rebuildPipeline();
159
+ this.requestBuild();
160
160
  }
161
161
  /** Whether to auto-fetch missing library packages from npm (default: true). */
162
162
  get fetchLibs() {
@@ -248,7 +248,7 @@ var WgslPlay = class extends HTMLElement {
248
248
  try {
249
249
  const alphaMode = this.hasAttribute("transparent") ? "premultiplied" : "opaque";
250
250
  this.renderState = await initWebGPU(this.canvas, alphaMode);
251
- await this.loadInitialContent();
251
+ this.loadInitialContent();
252
252
  this.stopRenderLoop = startRenderLoop(this.renderState, this.playback);
253
253
  this.dispatchEvent(new CustomEvent("ready"));
254
254
  return true;
@@ -261,19 +261,25 @@ var WgslPlay = class extends HTMLElement {
261
261
  }
262
262
  }
263
263
  /** Load from source element, src URL, script child, or inline textContent. */
264
- async loadInitialContent() {
264
+ loadInitialContent() {
265
265
  const sourceId = this.getAttribute("source");
266
- if (sourceId) return this.connectToSource(sourceId);
266
+ if (sourceId) {
267
+ this.connectToSource(sourceId);
268
+ return;
269
+ }
267
270
  const src = this.getAttribute("src");
268
- if (src) return this.loadFromUrl(src);
271
+ if (src) {
272
+ this.loadFromUrl(src);
273
+ return;
274
+ }
269
275
  const inlineSource = this.querySelector("script[type=\"text/wgsl\"], script[type=\"text/wesl\"]")?.textContent?.trim() ?? this.textContent?.trim();
270
276
  if (!inlineSource) return;
271
277
  this._weslSrc = { [this._rootModuleName]: inlineSource };
272
278
  this._fromFullProject = false;
273
- await this.discoverAndRebuild();
279
+ this.requestBuild();
274
280
  }
275
281
  /** Connect to a source provider element (e.g., wgsl-edit). */
276
- async connectToSource(id) {
282
+ connectToSource(id) {
277
283
  const el = document.getElementById(id);
278
284
  if (!el) {
279
285
  console.error(`wgsl-play: source element "${id}" not found`);
@@ -296,8 +302,7 @@ var WgslPlay = class extends HTMLElement {
296
302
  };
297
303
  if (libs) this._libs = libs;
298
304
  this._fromFullProject = false;
299
- await this.discoverAndRebuild();
300
- this._fromFullProject = true;
305
+ this.requestBuild();
301
306
  this._sourceListener = (e) => {
302
307
  const detail = e.detail;
303
308
  const fallback = { [this._rootModuleName]: detail?.source ?? "" };
@@ -310,83 +315,61 @@ var WgslPlay = class extends HTMLElement {
310
315
  };
311
316
  el.addEventListener("change", this._sourceListener);
312
317
  }
313
- /** Fetch shader from URL, auto-fetching any imported dependencies. */
318
+ /** Fetch shader from URL, then trigger a build. */
314
319
  async loadFromUrl(url) {
315
- if (!this.renderState) return;
316
320
  try {
317
- this.errorOverlay.hide();
318
321
  const { weslSrc, libs, rootModuleName } = await loadShaderFromUrl(url, this.getConfigOverrides()?.shaderRoot);
319
322
  this._weslSrc = weslSrc;
320
323
  this._libs = libs;
321
324
  this._fromFullProject = false;
322
325
  if (rootModuleName) this._rootModuleName = rootModuleName;
323
- const mainSource = weslSrc[this._rootModuleName];
324
- if (!mainSource) {
325
- console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(weslSrc));
326
- return;
327
- }
328
- await createPipeline(this.renderState, mainSource, {
329
- ...this._linkOptions,
330
- weslSrc,
331
- libs,
332
- rootModuleName: this._rootModuleName
333
- });
334
- this.dispatchEvent(new CustomEvent("compile-success"));
335
- } catch (error) {
336
- this.handleCompileError(error);
337
- }
338
- }
339
- /** Rebuild GPU pipeline using stored state. For full projects with all sources. */
340
- async rebuildPipeline() {
341
- if (!await this.initialize()) return;
342
- const mainSource = this._weslSrc[this._rootModuleName];
343
- if (!mainSource) {
344
- console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(this._weslSrc));
345
- return;
346
- }
347
- try {
348
- this.errorOverlay.hide();
349
- await createPipeline(this.renderState, mainSource, {
350
- ...this._linkOptions,
351
- weslSrc: this._weslSrc,
352
- libs: this._libs,
353
- rootModuleName: this._rootModuleName
354
- });
355
- this.dispatchEvent(new CustomEvent("compile-success"));
326
+ this.requestBuild();
356
327
  } catch (error) {
357
328
  this.handleCompileError(error);
358
329
  }
359
330
  }
360
- /** Discover dependencies and rebuild. For HTTP/inline sources that may need fetching. */
361
- async discoverAndRebuild() {
362
- if (!await this.initialize()) return;
363
- const mainSource = this._weslSrc[this._rootModuleName];
364
- if (!mainSource) {
365
- console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(this._weslSrc));
366
- return;
367
- }
368
- try {
369
- this.errorOverlay.hide();
370
- const { weslSrc, libs } = await fetchDependencies(mainSource, {
371
- shaderRoot: this.getConfigOverrides()?.shaderRoot,
372
- existingSources: this._weslSrc,
373
- skipExternal: !this._fetchLibs
374
- });
375
- this._weslSrc = {
376
- ...this._weslSrc,
377
- ...weslSrc
378
- };
379
- this._libs = [...this._libs ?? [], ...libs];
380
- await createPipeline(this.renderState, mainSource, {
381
- ...this._linkOptions,
382
- weslSrc: this._weslSrc,
383
- libs: this._libs,
384
- rootModuleName: this._rootModuleName
385
- });
386
- this.dispatchEvent(new CustomEvent("compile-success"));
387
- } catch (error) {
388
- this.handleCompileError(error);
331
+ /** Mark build as needed. Coalesces rapid requests into a single build. */
332
+ requestBuild() {
333
+ this._dirty = true;
334
+ if (!this._building) this.runBuild();
335
+ }
336
+ /** Run builds until no longer dirty. Only one instance runs at a time. */
337
+ async runBuild() {
338
+ this._building = true;
339
+ while (this._dirty) {
340
+ this._dirty = false;
341
+ if (!await this.initialize()) break;
342
+ const mainSource = this._weslSrc[this._rootModuleName];
343
+ if (!mainSource) {
344
+ console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(this._weslSrc));
345
+ continue;
346
+ }
347
+ try {
348
+ this.errorOverlay.hide();
349
+ if (!this._fromFullProject) {
350
+ const { weslSrc, libs } = await fetchDependencies(mainSource, {
351
+ shaderRoot: this.getConfigOverrides()?.shaderRoot,
352
+ existingSources: this._weslSrc,
353
+ skipExternal: !this._fetchLibs
354
+ });
355
+ this._weslSrc = {
356
+ ...this._weslSrc,
357
+ ...weslSrc
358
+ };
359
+ this._libs = dedupLibs(this._libs, libs);
360
+ }
361
+ await createPipeline(this.renderState, mainSource, {
362
+ ...this._linkOptions,
363
+ weslSrc: this._weslSrc,
364
+ libs: this._libs,
365
+ rootModuleName: this._rootModuleName
366
+ });
367
+ if (!this._dirty) this.dispatchEvent(new CustomEvent("compile-success"));
368
+ } catch (error) {
369
+ if (!this._dirty) this.handleCompileError(error);
370
+ }
389
371
  }
372
+ this._building = false;
390
373
  }
391
374
  handleCompileError(error) {
392
375
  const message = error?.message ?? String(error);
@@ -443,6 +426,12 @@ function upgradeProperty(el, prop) {
443
426
  el[prop] = value;
444
427
  }
445
428
  }
429
+ /** Merge new libs, deduplicating by bundle name. */
430
+ function dedupLibs(existing, newLibs) {
431
+ if (!existing || newLibs.length === 0) return [...existing ?? [], ...newLibs];
432
+ const names = new Set(newLibs.map((b) => b.name));
433
+ return [...existing.filter((b) => !names.has(b.name)), ...newLibs];
434
+ }
446
435
  /** Normalize all keys in a weslSrc record to module paths. */
447
436
  function toModulePaths(weslSrc, pkg) {
448
437
  const result = {};
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c as getConfig, l as resetConfig, s as defaults } from "./WgslPlay-Ben4VdsC.js";
1
+ import { c as getConfig, l as resetConfig, s as defaults } from "./WgslPlay-BRvURGA3.js";
2
2
  import { WgslPlay } from "./WgslPlay.js";
3
3
 
4
4
  export * from "wesl-fetch"
package/dist/wgsl-play.js CHANGED
@@ -5641,19 +5641,25 @@ async function initWebGPU(canvas, alphaMode = "opaque") {
5641
5641
  /** Compile WESL fragment shader and create render pipeline. */
5642
5642
  async function createPipeline(state, fragmentSource, options) {
5643
5643
  state.device.pushErrorScope("validation");
5644
- const pipeline = await linkAndCreatePipeline({
5645
- device: state.device,
5646
- fragmentSource,
5647
- format: state.presentationFormat,
5648
- layout: state.pipelineLayout,
5649
- ...options
5650
- });
5651
- const gpuError = await state.device.popErrorScope();
5652
- if (gpuError) {
5644
+ let gpuError;
5645
+ let jsError;
5646
+ try {
5647
+ state.pipeline = await linkAndCreatePipeline({
5648
+ device: state.device,
5649
+ fragmentSource,
5650
+ format: state.presentationFormat,
5651
+ layout: state.pipelineLayout,
5652
+ ...options
5653
+ });
5654
+ } catch (e) {
5655
+ jsError = e;
5656
+ } finally {
5657
+ gpuError = await state.device.popErrorScope();
5658
+ }
5659
+ if (jsError || gpuError) {
5653
5660
  state.pipeline = void 0;
5654
- throw gpuError;
5661
+ throw jsError ?? gpuError;
5655
5662
  }
5656
- state.pipeline = pipeline;
5657
5663
  }
5658
5664
  /** Start the render loop. Returns a stop function. */
5659
5665
  function startRenderLoop(state, playback) {
@@ -5722,6 +5728,8 @@ var WgslPlay = class extends HTMLElement {
5722
5728
  _sourceEl = null;
5723
5729
  _sourceListener = null;
5724
5730
  _fetchLibs = true;
5731
+ _dirty = false;
5732
+ _building = false;
5725
5733
  _theme = "auto";
5726
5734
  _mediaQuery = null;
5727
5735
  _onFullscreenChange = () => this.controls.setFullscreen(!!document.fullscreenElement);
@@ -5802,7 +5810,7 @@ var WgslPlay = class extends HTMLElement {
5802
5810
  this._weslSrc = { [this._rootModuleName]: value };
5803
5811
  this._libs = void 0;
5804
5812
  this._fromFullProject = false;
5805
- this.discoverAndRebuild();
5813
+ this.requestBuild();
5806
5814
  }
5807
5815
  /** Conditions for conditional compilation (@if/@elif/@else). */
5808
5816
  get conditions() {
@@ -5814,8 +5822,7 @@ var WgslPlay = class extends HTMLElement {
5814
5822
  conditions: value
5815
5823
  };
5816
5824
  if (Object.keys(this._weslSrc).length === 0) return;
5817
- if (this._fromFullProject) this.rebuildPipeline();
5818
- else this.discoverAndRebuild();
5825
+ this.requestBuild();
5819
5826
  }
5820
5827
  /** Set project configuration (mirrors wesl link() API). */
5821
5828
  set project(value) {
@@ -5831,8 +5838,7 @@ var WgslPlay = class extends HTMLElement {
5831
5838
  return;
5832
5839
  }
5833
5840
  if (Object.keys(this._weslSrc).length === 0) return;
5834
- if (this._fromFullProject) this.rebuildPipeline();
5835
- else this.discoverAndRebuild();
5841
+ this.requestBuild();
5836
5842
  }
5837
5843
  /** Set sources from a full project with weslSrc. */
5838
5844
  setProjectSources(weslSrc, rootModuleName) {
@@ -5841,7 +5847,7 @@ var WgslPlay = class extends HTMLElement {
5841
5847
  this._weslSrc = toModulePaths(weslSrc, pkg);
5842
5848
  this._rootModuleName = fileToModulePath(root, pkg, false);
5843
5849
  this._fromFullProject = true;
5844
- this.rebuildPipeline();
5850
+ this.requestBuild();
5845
5851
  }
5846
5852
  /** Whether to auto-fetch missing library packages from npm (default: true). */
5847
5853
  get fetchLibs() {
@@ -5933,7 +5939,7 @@ var WgslPlay = class extends HTMLElement {
5933
5939
  try {
5934
5940
  const alphaMode = this.hasAttribute("transparent") ? "premultiplied" : "opaque";
5935
5941
  this.renderState = await initWebGPU(this.canvas, alphaMode);
5936
- await this.loadInitialContent();
5942
+ this.loadInitialContent();
5937
5943
  this.stopRenderLoop = startRenderLoop(this.renderState, this.playback);
5938
5944
  this.dispatchEvent(new CustomEvent("ready"));
5939
5945
  return true;
@@ -5946,19 +5952,25 @@ var WgslPlay = class extends HTMLElement {
5946
5952
  }
5947
5953
  }
5948
5954
  /** Load from source element, src URL, script child, or inline textContent. */
5949
- async loadInitialContent() {
5955
+ loadInitialContent() {
5950
5956
  const sourceId = this.getAttribute("source");
5951
- if (sourceId) return this.connectToSource(sourceId);
5957
+ if (sourceId) {
5958
+ this.connectToSource(sourceId);
5959
+ return;
5960
+ }
5952
5961
  const src = this.getAttribute("src");
5953
- if (src) return this.loadFromUrl(src);
5962
+ if (src) {
5963
+ this.loadFromUrl(src);
5964
+ return;
5965
+ }
5954
5966
  const inlineSource = this.querySelector("script[type=\"text/wgsl\"], script[type=\"text/wesl\"]")?.textContent?.trim() ?? this.textContent?.trim();
5955
5967
  if (!inlineSource) return;
5956
5968
  this._weslSrc = { [this._rootModuleName]: inlineSource };
5957
5969
  this._fromFullProject = false;
5958
- await this.discoverAndRebuild();
5970
+ this.requestBuild();
5959
5971
  }
5960
5972
  /** Connect to a source provider element (e.g., wgsl-edit). */
5961
- async connectToSource(id) {
5973
+ connectToSource(id) {
5962
5974
  const el = document.getElementById(id);
5963
5975
  if (!el) {
5964
5976
  console.error(`wgsl-play: source element "${id}" not found`);
@@ -5981,8 +5993,7 @@ var WgslPlay = class extends HTMLElement {
5981
5993
  };
5982
5994
  if (libs) this._libs = libs;
5983
5995
  this._fromFullProject = false;
5984
- await this.discoverAndRebuild();
5985
- this._fromFullProject = true;
5996
+ this.requestBuild();
5986
5997
  this._sourceListener = (e) => {
5987
5998
  const detail = e.detail;
5988
5999
  const fallback = { [this._rootModuleName]: detail?.source ?? "" };
@@ -5995,83 +6006,61 @@ var WgslPlay = class extends HTMLElement {
5995
6006
  };
5996
6007
  el.addEventListener("change", this._sourceListener);
5997
6008
  }
5998
- /** Fetch shader from URL, auto-fetching any imported dependencies. */
6009
+ /** Fetch shader from URL, then trigger a build. */
5999
6010
  async loadFromUrl(url) {
6000
- if (!this.renderState) return;
6001
6011
  try {
6002
- this.errorOverlay.hide();
6003
6012
  const { weslSrc, libs, rootModuleName } = await loadShaderFromUrl(url, this.getConfigOverrides()?.shaderRoot);
6004
6013
  this._weslSrc = weslSrc;
6005
6014
  this._libs = libs;
6006
6015
  this._fromFullProject = false;
6007
6016
  if (rootModuleName) this._rootModuleName = rootModuleName;
6008
- const mainSource = weslSrc[this._rootModuleName];
6009
- if (!mainSource) {
6010
- console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(weslSrc));
6011
- return;
6012
- }
6013
- await createPipeline(this.renderState, mainSource, {
6014
- ...this._linkOptions,
6015
- weslSrc,
6016
- libs,
6017
- rootModuleName: this._rootModuleName
6018
- });
6019
- this.dispatchEvent(new CustomEvent("compile-success"));
6017
+ this.requestBuild();
6020
6018
  } catch (error) {
6021
6019
  this.handleCompileError(error);
6022
6020
  }
6023
6021
  }
6024
- /** Rebuild GPU pipeline using stored state. For full projects with all sources. */
6025
- async rebuildPipeline() {
6026
- if (!await this.initialize()) return;
6027
- const mainSource = this._weslSrc[this._rootModuleName];
6028
- if (!mainSource) {
6029
- console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(this._weslSrc));
6030
- return;
6031
- }
6032
- try {
6033
- this.errorOverlay.hide();
6034
- await createPipeline(this.renderState, mainSource, {
6035
- ...this._linkOptions,
6036
- weslSrc: this._weslSrc,
6037
- libs: this._libs,
6038
- rootModuleName: this._rootModuleName
6039
- });
6040
- this.dispatchEvent(new CustomEvent("compile-success"));
6041
- } catch (error) {
6042
- this.handleCompileError(error);
6043
- }
6044
- }
6045
- /** Discover dependencies and rebuild. For HTTP/inline sources that may need fetching. */
6046
- async discoverAndRebuild() {
6047
- if (!await this.initialize()) return;
6048
- const mainSource = this._weslSrc[this._rootModuleName];
6049
- if (!mainSource) {
6050
- console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(this._weslSrc));
6051
- return;
6052
- }
6053
- try {
6054
- this.errorOverlay.hide();
6055
- const { weslSrc, libs } = await fetchDependencies(mainSource, {
6056
- shaderRoot: this.getConfigOverrides()?.shaderRoot,
6057
- existingSources: this._weslSrc,
6058
- skipExternal: !this._fetchLibs
6059
- });
6060
- this._weslSrc = {
6061
- ...this._weslSrc,
6062
- ...weslSrc
6063
- };
6064
- this._libs = [...this._libs ?? [], ...libs];
6065
- await createPipeline(this.renderState, mainSource, {
6066
- ...this._linkOptions,
6067
- weslSrc: this._weslSrc,
6068
- libs: this._libs,
6069
- rootModuleName: this._rootModuleName
6070
- });
6071
- this.dispatchEvent(new CustomEvent("compile-success"));
6072
- } catch (error) {
6073
- this.handleCompileError(error);
6022
+ /** Mark build as needed. Coalesces rapid requests into a single build. */
6023
+ requestBuild() {
6024
+ this._dirty = true;
6025
+ if (!this._building) this.runBuild();
6026
+ }
6027
+ /** Run builds until no longer dirty. Only one instance runs at a time. */
6028
+ async runBuild() {
6029
+ this._building = true;
6030
+ while (this._dirty) {
6031
+ this._dirty = false;
6032
+ if (!await this.initialize()) break;
6033
+ const mainSource = this._weslSrc[this._rootModuleName];
6034
+ if (!mainSource) {
6035
+ console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(this._weslSrc));
6036
+ continue;
6037
+ }
6038
+ try {
6039
+ this.errorOverlay.hide();
6040
+ if (!this._fromFullProject) {
6041
+ const { weslSrc, libs } = await fetchDependencies(mainSource, {
6042
+ shaderRoot: this.getConfigOverrides()?.shaderRoot,
6043
+ existingSources: this._weslSrc,
6044
+ skipExternal: !this._fetchLibs
6045
+ });
6046
+ this._weslSrc = {
6047
+ ...this._weslSrc,
6048
+ ...weslSrc
6049
+ };
6050
+ this._libs = dedupLibs(this._libs, libs);
6051
+ }
6052
+ await createPipeline(this.renderState, mainSource, {
6053
+ ...this._linkOptions,
6054
+ weslSrc: this._weslSrc,
6055
+ libs: this._libs,
6056
+ rootModuleName: this._rootModuleName
6057
+ });
6058
+ if (!this._dirty) this.dispatchEvent(new CustomEvent("compile-success"));
6059
+ } catch (error) {
6060
+ if (!this._dirty) this.handleCompileError(error);
6061
+ }
6074
6062
  }
6063
+ this._building = false;
6075
6064
  }
6076
6065
  handleCompileError(error) {
6077
6066
  const message = error?.message ?? String(error);
@@ -6128,6 +6117,12 @@ function upgradeProperty(el, prop) {
6128
6117
  el[prop] = value;
6129
6118
  }
6130
6119
  }
6120
+ /** Merge new libs, deduplicating by bundle name. */
6121
+ function dedupLibs(existing, newLibs) {
6122
+ if (!existing || newLibs.length === 0) return [...existing ?? [], ...newLibs];
6123
+ const names = new Set(newLibs.map((b) => b.name));
6124
+ return [...existing.filter((b) => !names.has(b.name)), ...newLibs];
6125
+ }
6131
6126
  /** Normalize all keys in a weslSrc record to module paths. */
6132
6127
  function toModulePaths(weslSrc, pkg) {
6133
6128
  const result = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wgsl-play",
3
- "version": "0.0.35",
3
+ "version": "0.0.36",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
package/src/Renderer.ts CHANGED
@@ -84,19 +84,25 @@ export async function createPipeline(
84
84
  options?: LinkOptions,
85
85
  ): Promise<void> {
86
86
  state.device.pushErrorScope("validation");
87
- const pipeline = await linkAndCreatePipeline({
88
- device: state.device,
89
- fragmentSource,
90
- format: state.presentationFormat,
91
- layout: state.pipelineLayout,
92
- ...options,
93
- });
94
- const gpuError = await state.device.popErrorScope();
95
- if (gpuError) {
87
+ let gpuError: unknown;
88
+ let jsError: unknown;
89
+ try {
90
+ state.pipeline = await linkAndCreatePipeline({
91
+ device: state.device,
92
+ fragmentSource,
93
+ format: state.presentationFormat,
94
+ layout: state.pipelineLayout,
95
+ ...options,
96
+ });
97
+ } catch (e) {
98
+ jsError = e;
99
+ } finally {
100
+ gpuError = await state.device.popErrorScope();
101
+ }
102
+ if (jsError || gpuError) {
96
103
  state.pipeline = undefined;
97
- throw gpuError;
104
+ throw jsError ?? gpuError;
98
105
  }
99
- state.pipeline = pipeline;
100
106
  }
101
107
 
102
108
  /** Start the render loop. Returns a stop function. */
package/src/WgslPlay.ts CHANGED
@@ -83,6 +83,8 @@ export class WgslPlay extends HTMLElement {
83
83
  private _sourceEl: HTMLElement | null = null;
84
84
  private _sourceListener: ((e: Event) => void) | null = null;
85
85
  private _fetchLibs = true;
86
+ private _dirty = false;
87
+ private _building = false;
86
88
  private _theme: "light" | "dark" | "auto" = "auto";
87
89
  private _mediaQuery: MediaQueryList | null = null;
88
90
  private _onFullscreenChange = () =>
@@ -193,7 +195,7 @@ export class WgslPlay extends HTMLElement {
193
195
  this._weslSrc = { [this._rootModuleName]: value };
194
196
  this._libs = undefined;
195
197
  this._fromFullProject = false;
196
- this.discoverAndRebuild();
198
+ this.requestBuild();
197
199
  }
198
200
 
199
201
  /** Conditions for conditional compilation (@if/@elif/@else). */
@@ -204,8 +206,7 @@ export class WgslPlay extends HTMLElement {
204
206
  set conditions(value: Conditions) {
205
207
  this._linkOptions = { ...this._linkOptions, conditions: value };
206
208
  if (Object.keys(this._weslSrc).length === 0) return;
207
- if (this._fromFullProject) this.rebuildPipeline();
208
- else this.discoverAndRebuild();
209
+ this.requestBuild();
209
210
  }
210
211
 
211
212
  /** Set project configuration (mirrors wesl link() API). */
@@ -230,10 +231,8 @@ export class WgslPlay extends HTMLElement {
230
231
  return;
231
232
  }
232
233
 
233
- // Partial update - may need to refetch if conditions changed
234
234
  if (Object.keys(this._weslSrc).length === 0) return;
235
- if (this._fromFullProject) this.rebuildPipeline();
236
- else this.discoverAndRebuild();
235
+ this.requestBuild();
237
236
  }
238
237
 
239
238
  /** Set sources from a full project with weslSrc. */
@@ -246,7 +245,7 @@ export class WgslPlay extends HTMLElement {
246
245
  this._weslSrc = toModulePaths(weslSrc, pkg);
247
246
  this._rootModuleName = fileToModulePath(root, pkg, false);
248
247
  this._fromFullProject = true;
249
- this.rebuildPipeline();
248
+ this.requestBuild();
250
249
  }
251
250
 
252
251
  /** Whether to auto-fetch missing library packages from npm (default: true). */
@@ -365,7 +364,7 @@ export class WgslPlay extends HTMLElement {
365
364
  ? "premultiplied"
366
365
  : "opaque";
367
366
  this.renderState = await initWebGPU(this.canvas, alphaMode);
368
- await this.loadInitialContent();
367
+ this.loadInitialContent();
369
368
  this.stopRenderLoop = startRenderLoop(this.renderState, this.playback);
370
369
  this.dispatchEvent(new CustomEvent("ready"));
371
370
  return true;
@@ -383,12 +382,18 @@ export class WgslPlay extends HTMLElement {
383
382
  }
384
383
 
385
384
  /** Load from source element, src URL, script child, or inline textContent. */
386
- private async loadInitialContent(): Promise<void> {
385
+ private loadInitialContent(): void {
387
386
  const sourceId = this.getAttribute("source");
388
- if (sourceId) return this.connectToSource(sourceId);
387
+ if (sourceId) {
388
+ this.connectToSource(sourceId);
389
+ return;
390
+ }
389
391
 
390
392
  const src = this.getAttribute("src");
391
- if (src) return this.loadFromUrl(src);
393
+ if (src) {
394
+ this.loadFromUrl(src);
395
+ return;
396
+ }
392
397
 
393
398
  // Prefer <script type="text/wgsl"> or <script type="text/wesl"> (no HTML escaping needed)
394
399
  const script = this.querySelector(
@@ -400,11 +405,11 @@ export class WgslPlay extends HTMLElement {
400
405
 
401
406
  this._weslSrc = { [this._rootModuleName]: inlineSource };
402
407
  this._fromFullProject = false;
403
- await this.discoverAndRebuild();
408
+ this.requestBuild();
404
409
  }
405
410
 
406
411
  /** Connect to a source provider element (e.g., wgsl-edit). */
407
- private async connectToSource(id: string): Promise<void> {
412
+ private connectToSource(id: string): void {
408
413
  const el = document.getElementById(id);
409
414
  if (!el) {
410
415
  console.error(`wgsl-play: source element "${id}" not found`);
@@ -421,7 +426,6 @@ export class WgslPlay extends HTMLElement {
421
426
  };
422
427
 
423
428
  // Load initial sources, conditions, and libs from source element.
424
- // Use discoverAndRebuild (not project setter) so external deps are fetched.
425
429
  const { conditions, rootModuleName, libs } = el as any;
426
430
  const root = rootModuleName ?? "main";
427
431
  this._weslSrc = getSources();
@@ -429,8 +433,7 @@ export class WgslPlay extends HTMLElement {
429
433
  if (conditions) this._linkOptions = { ...this._linkOptions, conditions };
430
434
  if (libs) this._libs = libs;
431
435
  this._fromFullProject = false;
432
- await this.discoverAndRebuild();
433
- this._fromFullProject = true; // fast rebuilds on subsequent edits
436
+ this.requestBuild();
434
437
 
435
438
  // Listen for changes - rebuild with libs from source element
436
439
  this._sourceListener = (e: Event) => {
@@ -446,12 +449,9 @@ export class WgslPlay extends HTMLElement {
446
449
  el.addEventListener("change", this._sourceListener);
447
450
  }
448
451
 
449
- /** Fetch shader from URL, auto-fetching any imported dependencies. */
452
+ /** Fetch shader from URL, then trigger a build. */
450
453
  private async loadFromUrl(url: string): Promise<void> {
451
- if (!this.renderState) return;
452
-
453
454
  try {
454
- this.errorOverlay.hide();
455
455
  const { weslSrc, libs, rootModuleName } = await loadShaderFromUrl(
456
456
  url,
457
457
  this.getConfigOverrides()?.shaderRoot,
@@ -460,88 +460,58 @@ export class WgslPlay extends HTMLElement {
460
460
  this._libs = libs;
461
461
  this._fromFullProject = false;
462
462
  if (rootModuleName) this._rootModuleName = rootModuleName;
463
-
464
- const mainSource = weslSrc[this._rootModuleName];
465
- if (!mainSource) {
466
- console.warn(
467
- `wgsl-play: root module "${this._rootModuleName}" not found in sources:`,
468
- Object.keys(weslSrc),
469
- );
470
- return;
471
- }
472
-
473
- await createPipeline(this.renderState, mainSource, {
474
- ...this._linkOptions,
475
- weslSrc,
476
- libs,
477
- rootModuleName: this._rootModuleName,
478
- });
479
- this.dispatchEvent(new CustomEvent("compile-success"));
463
+ this.requestBuild();
480
464
  } catch (error) {
481
465
  this.handleCompileError(error);
482
466
  }
483
467
  }
484
468
 
485
- /** Rebuild GPU pipeline using stored state. For full projects with all sources. */
486
- private async rebuildPipeline(): Promise<void> {
487
- if (!(await this.initialize())) return;
488
-
489
- const mainSource = this._weslSrc[this._rootModuleName];
490
- if (!mainSource) {
491
- console.warn(
492
- `wgsl-play: root module "${this._rootModuleName}" not found in sources:`,
493
- Object.keys(this._weslSrc),
494
- );
495
- return;
496
- }
497
-
498
- try {
499
- this.errorOverlay.hide();
500
- await createPipeline(this.renderState!, mainSource, {
501
- ...this._linkOptions,
502
- weslSrc: this._weslSrc,
503
- libs: this._libs,
504
- rootModuleName: this._rootModuleName,
505
- });
506
- this.dispatchEvent(new CustomEvent("compile-success"));
507
- } catch (error) {
508
- this.handleCompileError(error);
509
- }
469
+ /** Mark build as needed. Coalesces rapid requests into a single build. */
470
+ private requestBuild(): void {
471
+ this._dirty = true;
472
+ if (!this._building) this.runBuild();
510
473
  }
511
474
 
512
- /** Discover dependencies and rebuild. For HTTP/inline sources that may need fetching. */
513
- private async discoverAndRebuild(): Promise<void> {
514
- if (!(await this.initialize())) return;
475
+ /** Run builds until no longer dirty. Only one instance runs at a time. */
476
+ private async runBuild(): Promise<void> {
477
+ this._building = true;
478
+ while (this._dirty) {
479
+ this._dirty = false;
480
+ if (!(await this.initialize())) break;
515
481
 
516
- const mainSource = this._weslSrc[this._rootModuleName];
517
- if (!mainSource) {
518
- console.warn(
519
- `wgsl-play: root module "${this._rootModuleName}" not found in sources:`,
520
- Object.keys(this._weslSrc),
521
- );
522
- return;
523
- }
482
+ const mainSource = this._weslSrc[this._rootModuleName];
483
+ if (!mainSource) {
484
+ console.warn(
485
+ `wgsl-play: root module "${this._rootModuleName}" not found in sources:`,
486
+ Object.keys(this._weslSrc),
487
+ );
488
+ continue;
489
+ }
524
490
 
525
- try {
526
- this.errorOverlay.hide();
527
- const { weslSrc, libs } = await fetchDependencies(mainSource, {
528
- shaderRoot: this.getConfigOverrides()?.shaderRoot,
529
- existingSources: this._weslSrc,
530
- skipExternal: !this._fetchLibs,
531
- });
532
- this._weslSrc = { ...this._weslSrc, ...weslSrc };
533
- this._libs = [...(this._libs ?? []), ...libs];
534
-
535
- await createPipeline(this.renderState!, mainSource, {
536
- ...this._linkOptions,
537
- weslSrc: this._weslSrc,
538
- libs: this._libs,
539
- rootModuleName: this._rootModuleName,
540
- });
541
- this.dispatchEvent(new CustomEvent("compile-success"));
542
- } catch (error) {
543
- this.handleCompileError(error);
491
+ try {
492
+ this.errorOverlay.hide();
493
+ if (!this._fromFullProject) {
494
+ const { weslSrc, libs } = await fetchDependencies(mainSource, {
495
+ shaderRoot: this.getConfigOverrides()?.shaderRoot,
496
+ existingSources: this._weslSrc,
497
+ skipExternal: !this._fetchLibs,
498
+ });
499
+ this._weslSrc = { ...this._weslSrc, ...weslSrc };
500
+ this._libs = dedupLibs(this._libs, libs);
501
+ }
502
+ await createPipeline(this.renderState!, mainSource, {
503
+ ...this._linkOptions,
504
+ weslSrc: this._weslSrc,
505
+ libs: this._libs,
506
+ rootModuleName: this._rootModuleName,
507
+ });
508
+ if (!this._dirty)
509
+ this.dispatchEvent(new CustomEvent("compile-success"));
510
+ } catch (error) {
511
+ if (!this._dirty) this.handleCompileError(error);
512
+ }
544
513
  }
514
+ this._building = false;
545
515
  }
546
516
 
547
517
  private handleCompileError(error: unknown): void {
@@ -617,6 +587,17 @@ function upgradeProperty(el: HTMLElement, prop: string): void {
617
587
  }
618
588
  }
619
589
 
590
+ /** Merge new libs, deduplicating by bundle name. */
591
+ function dedupLibs(
592
+ existing: WeslBundle[] | undefined,
593
+ newLibs: WeslBundle[],
594
+ ): WeslBundle[] {
595
+ if (!existing || newLibs.length === 0)
596
+ return [...(existing ?? []), ...newLibs];
597
+ const names = new Set(newLibs.map(b => b.name));
598
+ return [...existing.filter(b => !names.has(b.name)), ...newLibs];
599
+ }
600
+
620
601
  /** Normalize all keys in a weslSrc record to module paths. */
621
602
  function toModulePaths(
622
603
  weslSrc: Record<string, string>,