Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/tools/example-showcase/src/main.rs
6596 views
1
//! Tool to run all examples or generate a showcase page for the Bevy website.
2
3
#![expect(clippy::print_stdout, reason = "Allowed in tools.")]
4
5
use core::{
6
fmt::Display,
7
hash::{Hash, Hasher},
8
time::Duration,
9
};
10
use std::{
11
collections::{hash_map::DefaultHasher, HashMap},
12
fs::{self, File},
13
io::Write,
14
path::{Path, PathBuf},
15
process::exit,
16
thread,
17
time::Instant,
18
};
19
20
use clap::{error::ErrorKind, CommandFactory, Parser, ValueEnum};
21
use pbr::ProgressBar;
22
use regex::Regex;
23
use toml_edit::{DocumentMut, Item};
24
use xshell::{cmd, Shell};
25
26
#[derive(Parser, Debug)]
27
struct Args {
28
#[arg(long, default_value = "release")]
29
/// Compilation profile to use
30
profile: String,
31
32
#[command(subcommand)]
33
action: Action,
34
35
#[arg(long)]
36
/// Pagination control - page number. To use with --per-page
37
page: Option<usize>,
38
39
#[arg(long)]
40
/// Pagination control - number of examples per page. To use with --page
41
per_page: Option<usize>,
42
}
43
44
#[derive(clap::Subcommand, Debug)]
45
enum Action {
46
/// Run all the examples
47
Run {
48
#[arg(long)]
49
/// WGPU backend to use
50
wgpu_backend: Option<String>,
51
52
#[arg(long, default_value = "250")]
53
/// Which frame to automatically stop the example at.
54
///
55
/// This defaults to frame 250. Set it to 0 to not stop the example automatically.
56
stop_frame: u32,
57
58
#[arg(long, default_value = "false")]
59
/// Automatically ends after taking a screenshot
60
///
61
/// Only works if `screenshot-frame` is set to non-0, and overrides `stop-frame`.
62
auto_stop_frame: bool,
63
64
#[arg(long)]
65
/// Which frame to take a screenshot at. Set to 0 for no screenshot.
66
screenshot_frame: u32,
67
68
#[arg(long, default_value = "0.05")]
69
/// Fixed duration of a frame, in seconds. Only used when taking a screenshot, default to 0.05
70
fixed_frame_time: f32,
71
72
#[arg(long)]
73
/// Running in CI (some adaptation to the code)
74
in_ci: bool,
75
76
#[arg(long)]
77
/// Do not run stress test examples
78
ignore_stress_tests: bool,
79
80
#[arg(long)]
81
/// Report execution details in files
82
report_details: bool,
83
84
#[arg(long)]
85
/// Show the logs during execution
86
show_logs: bool,
87
88
#[arg(long)]
89
/// File containing the list of examples to run, incompatible with pagination
90
example_list: Option<String>,
91
92
#[arg(long)]
93
/// Only run examples that don't need extra features
94
only_default_features: bool,
95
},
96
/// Build the markdown files for the website
97
BuildWebsiteList {
98
#[arg(long)]
99
/// Path to the folder where the content should be created
100
content_folder: String,
101
102
#[arg(value_enum, long, default_value_t = WebApi::Webgpu)]
103
/// Which API to use for rendering
104
api: WebApi,
105
},
106
/// Build the examples in wasm
107
BuildWasmExamples {
108
#[arg(long)]
109
/// Path to the folder where the content should be created
110
content_folder: String,
111
112
#[arg(long)]
113
/// Enable hacks for Bevy website integration
114
website_hacks: bool,
115
116
#[arg(long)]
117
/// Optimize the wasm file for size with wasm-opt
118
optimize_size: bool,
119
120
#[arg(value_enum, long)]
121
/// Which API to use for rendering
122
api: WebApi,
123
},
124
}
125
126
#[derive(Debug, Copy, Clone, ValueEnum)]
127
enum WebApi {
128
Webgl2,
129
Webgpu,
130
}
131
132
impl Display for WebApi {
133
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
134
match self {
135
WebApi::Webgl2 => write!(f, "webgl2"),
136
WebApi::Webgpu => write!(f, "webgpu"),
137
}
138
}
139
}
140
141
fn main() {
142
let cli = Args::parse();
143
144
if cli.page.is_none() != cli.per_page.is_none() {
145
let mut cmd = Args::command();
146
cmd.error(
147
ErrorKind::MissingRequiredArgument,
148
"page and per-page must be used together",
149
)
150
.exit();
151
}
152
153
let profile = cli.profile;
154
155
match cli.action {
156
Action::Run {
157
wgpu_backend,
158
stop_frame,
159
auto_stop_frame,
160
screenshot_frame,
161
fixed_frame_time,
162
in_ci,
163
ignore_stress_tests,
164
report_details,
165
show_logs,
166
example_list,
167
only_default_features,
168
} => {
169
if example_list.is_some() && cli.page.is_some() {
170
let mut cmd = Args::command();
171
cmd.error(
172
ErrorKind::ArgumentConflict,
173
"example-list can't be used with pagination",
174
)
175
.exit();
176
}
177
let example_filter = example_list
178
.as_ref()
179
.map(|path| {
180
let file = fs::read_to_string(path).unwrap();
181
file.lines().map(ToString::to_string).collect::<Vec<_>>()
182
})
183
.unwrap_or_default();
184
185
let mut examples_to_run = parse_examples();
186
187
let mut failed_examples = vec![];
188
let mut successful_examples = vec![];
189
let mut no_screenshot_examples = vec![];
190
191
let mut extra_parameters = vec![];
192
193
match (stop_frame, screenshot_frame, auto_stop_frame) {
194
// When the example does not automatically stop nor take a screenshot.
195
(0, 0, _) => (),
196
// When the example automatically stops at an automatic frame.
197
(0, _, true) => {
198
let mut file = File::create("example_showcase_config.ron").unwrap();
199
file.write_all(
200
format!("(setup: (fixed_frame_time: Some({fixed_frame_time})), events: [({screenshot_frame}, ScreenshotAndExit)])").as_bytes(),
201
)
202
.unwrap();
203
extra_parameters.push("--features");
204
extra_parameters.push("bevy_ci_testing");
205
}
206
// When the example does not automatically stop.
207
(0, _, false) => {
208
let mut file = File::create("example_showcase_config.ron").unwrap();
209
file.write_all(
210
format!("(setup: (fixed_frame_time: Some({fixed_frame_time})), events: [({screenshot_frame}, Screenshot)])").as_bytes(),
211
)
212
.unwrap();
213
extra_parameters.push("--features");
214
extra_parameters.push("bevy_ci_testing");
215
}
216
// When the example does not take a screenshot.
217
(_, 0, _) => {
218
let mut file = File::create("example_showcase_config.ron").unwrap();
219
file.write_all(format!("(events: [({stop_frame}, AppExit)])").as_bytes())
220
.unwrap();
221
extra_parameters.push("--features");
222
extra_parameters.push("bevy_ci_testing");
223
}
224
// When the example both automatically stops at an automatic frame and takes a screenshot.
225
(_, _, true) => {
226
let mut file = File::create("example_showcase_config.ron").unwrap();
227
file.write_all(
228
format!("(setup: (fixed_frame_time: Some({fixed_frame_time})), events: [({screenshot_frame}, ScreenshotAndExit)])").as_bytes(),
229
)
230
.unwrap();
231
extra_parameters.push("--features");
232
extra_parameters.push("bevy_ci_testing");
233
}
234
// When the example both automatically stops and takes a screenshot.
235
(_, _, false) => {
236
let mut file = File::create("example_showcase_config.ron").unwrap();
237
file.write_all(
238
format!("(setup: (fixed_frame_time: Some({fixed_frame_time})), events: [({screenshot_frame}, Screenshot), ({stop_frame}, AppExit)])").as_bytes(),
239
)
240
.unwrap();
241
extra_parameters.push("--features");
242
extra_parameters.push("bevy_ci_testing");
243
}
244
}
245
246
if in_ci {
247
// Removing desktop mode as is slows down too much in CI
248
let sh = Shell::new().unwrap();
249
cmd!(
250
sh,
251
"git apply --ignore-whitespace tools/example-showcase/remove-desktop-app-mode.patch"
252
)
253
.run()
254
.unwrap();
255
256
// Don't use automatic position as it's "random" on Windows and breaks screenshot comparison
257
// using the cursor position
258
let sh = Shell::new().unwrap();
259
cmd!(
260
sh,
261
"git apply --ignore-whitespace tools/example-showcase/fixed-window-position.patch"
262
)
263
.run()
264
.unwrap();
265
266
// Setting lights ClusterConfig to have less clusters by default
267
// This is needed as the default config is too much for the CI runner
268
cmd!(
269
sh,
270
"git apply --ignore-whitespace tools/example-showcase/reduce-light-cluster-config.patch"
271
)
272
.run()
273
.unwrap();
274
275
// Sending extra WindowResize events. They are not sent on CI with xvfb x11 server
276
// This is needed for example split_screen that uses the window size to set the panels
277
cmd!(
278
sh,
279
"git apply --ignore-whitespace tools/example-showcase/extra-window-resized-events.patch"
280
)
281
.run()
282
.unwrap();
283
284
// Don't try to get an audio output stream in CI as there isn't one
285
// On macOS m1 runner in GitHub Actions, getting one timeouts after 15 minutes
286
cmd!(
287
sh,
288
"git apply --ignore-whitespace tools/example-showcase/disable-audio.patch"
289
)
290
.run()
291
.unwrap();
292
293
// Sort the examples so that they are not run by category
294
examples_to_run.sort_by_key(|example| {
295
let mut hasher = DefaultHasher::new();
296
example.hash(&mut hasher);
297
hasher.finish()
298
});
299
}
300
301
let work_to_do = || {
302
examples_to_run
303
.iter()
304
.filter(|example| example.category != "Stress Tests" || !ignore_stress_tests)
305
.filter(|example| example.example_type == ExampleType::Bin)
306
.filter(|example| {
307
example_list.is_none() || example_filter.contains(&example.technical_name)
308
})
309
.filter(|example| {
310
!only_default_features || example.required_features.is_empty()
311
})
312
.skip(cli.page.unwrap_or(0) * cli.per_page.unwrap_or(0))
313
.take(cli.per_page.unwrap_or(usize::MAX))
314
};
315
316
let mut pb = ProgressBar::new(work_to_do().count() as u64);
317
318
let reports_path = "example-showcase-reports";
319
if report_details {
320
fs::create_dir(reports_path)
321
.expect("Failed to create example-showcase-reports directory");
322
}
323
324
for to_run in work_to_do() {
325
let sh = Shell::new().unwrap();
326
let example = &to_run.technical_name;
327
let required_features = if to_run.required_features.is_empty() {
328
vec![]
329
} else {
330
vec!["--features".to_string(), to_run.required_features.join(",")]
331
};
332
let local_extra_parameters = extra_parameters
333
.iter()
334
.map(ToString::to_string)
335
.chain(required_features.iter().cloned())
336
.collect::<Vec<_>>();
337
338
let _ = cmd!(
339
sh,
340
"cargo build --profile {profile} --example {example} {local_extra_parameters...}"
341
).run();
342
let local_extra_parameters = extra_parameters
343
.iter()
344
.map(ToString::to_string)
345
.chain(required_features.iter().cloned())
346
.collect::<Vec<_>>();
347
let mut cmd = cmd!(
348
sh,
349
"cargo run --profile {profile} --example {example} {local_extra_parameters...}"
350
);
351
352
if let Some(backend) = wgpu_backend.as_ref() {
353
cmd = cmd.env("WGPU_BACKEND", backend);
354
}
355
356
if stop_frame > 0 || screenshot_frame > 0 {
357
cmd = cmd.env("CI_TESTING_CONFIG", "example_showcase_config.ron");
358
}
359
360
let before = Instant::now();
361
if report_details || show_logs {
362
cmd = cmd.ignore_status();
363
}
364
let result = cmd.output();
365
366
let duration = before.elapsed();
367
368
if (!report_details && result.is_ok())
369
|| (report_details && result.as_ref().unwrap().status.success())
370
{
371
if screenshot_frame > 0 {
372
let _ = fs::create_dir_all(Path::new("screenshots").join(&to_run.category));
373
let renamed_screenshot = fs::rename(
374
format!("screenshot-{screenshot_frame}.png"),
375
Path::new("screenshots")
376
.join(&to_run.category)
377
.join(format!("{}.png", to_run.technical_name)),
378
);
379
if let Err(err) = renamed_screenshot {
380
println!("Failed to rename screenshot: {err}");
381
no_screenshot_examples.push((to_run, duration));
382
} else {
383
successful_examples.push((to_run, duration));
384
}
385
} else {
386
successful_examples.push((to_run, duration));
387
}
388
} else {
389
failed_examples.push((to_run, duration));
390
}
391
392
if report_details || show_logs {
393
let result = result.unwrap();
394
let stdout = String::from_utf8_lossy(&result.stdout);
395
let stderr = String::from_utf8_lossy(&result.stderr);
396
if show_logs {
397
println!("{stdout}");
398
println!("{stderr}");
399
}
400
if report_details {
401
let mut file =
402
File::create(format!("{reports_path}/{example}.log")).unwrap();
403
file.write_all(b"==== stdout ====\n").unwrap();
404
file.write_all(stdout.as_bytes()).unwrap();
405
file.write_all(b"\n==== stderr ====\n").unwrap();
406
file.write_all(stderr.as_bytes()).unwrap();
407
}
408
}
409
410
thread::sleep(Duration::from_secs(1));
411
pb.inc();
412
}
413
pb.finish_print("done");
414
415
if report_details {
416
let _ = fs::write(
417
format!("{reports_path}/successes"),
418
successful_examples
419
.iter()
420
.map(|(example, duration)| {
421
format!(
422
"{}/{} - {}",
423
example.category,
424
example.technical_name,
425
duration.as_secs_f32()
426
)
427
})
428
.collect::<Vec<_>>()
429
.join("\n"),
430
);
431
let _ = fs::write(
432
format!("{reports_path}/failures"),
433
failed_examples
434
.iter()
435
.map(|(example, duration)| {
436
format!(
437
"{}/{} - {}",
438
example.category,
439
example.technical_name,
440
duration.as_secs_f32()
441
)
442
})
443
.collect::<Vec<_>>()
444
.join("\n"),
445
);
446
if screenshot_frame > 0 {
447
let _ = fs::write(
448
format!("{reports_path}/no_screenshots"),
449
no_screenshot_examples
450
.iter()
451
.map(|(example, duration)| {
452
format!(
453
"{}/{} - {}",
454
example.category,
455
example.technical_name,
456
duration.as_secs_f32()
457
)
458
})
459
.collect::<Vec<_>>()
460
.join("\n"),
461
);
462
}
463
}
464
465
println!(
466
"total: {} / passed: {}, failed: {}, no screenshot: {}",
467
work_to_do().count(),
468
successful_examples.len(),
469
failed_examples.len(),
470
no_screenshot_examples.len()
471
);
472
if failed_examples.is_empty() {
473
println!("All examples passed!");
474
} else {
475
println!("Failed examples:");
476
for (example, _) in failed_examples {
477
println!(
478
" {} / {} ({})",
479
example.category, example.name, example.technical_name
480
);
481
}
482
exit(1);
483
}
484
}
485
Action::BuildWebsiteList {
486
content_folder,
487
api,
488
} => {
489
let mut examples_to_run = parse_examples();
490
examples_to_run.sort_by_key(|e| format!("{}-{}", e.category, e.name));
491
492
let root_path = Path::new(&content_folder);
493
494
let _ = fs::create_dir_all(root_path);
495
496
let mut index = File::create(root_path.join("_index.md")).unwrap();
497
if matches!(api, WebApi::Webgpu) {
498
index
499
.write_all(
500
"+++
501
title = \"Bevy Examples in WebGPU\"
502
template = \"examples-webgpu.html\"
503
sort_by = \"weight\"
504
505
[extra]
506
header_message = \"Examples (WebGPU)\"
507
+++"
508
.as_bytes(),
509
)
510
.unwrap();
511
} else {
512
index
513
.write_all(
514
"+++
515
title = \"Bevy Examples in WebGL2\"
516
template = \"examples.html\"
517
sort_by = \"weight\"
518
519
[extra]
520
header_message = \"Examples (WebGL2)\"
521
+++"
522
.as_bytes(),
523
)
524
.unwrap();
525
}
526
527
let mut categories = HashMap::new();
528
for to_show in examples_to_run {
529
if to_show.example_type != ExampleType::Bin {
530
continue;
531
}
532
533
if !to_show.wasm {
534
continue;
535
}
536
537
// This beautifies the category name
538
// to make it a good looking URL
539
// rather than having weird whitespace
540
// and other characters that don't
541
// work well in a URL path.
542
let beautified_category = to_show
543
.category
544
.replace(['(', ')'], "")
545
.replace(' ', "-")
546
.to_lowercase();
547
548
let category_path = root_path.join(&beautified_category);
549
550
if !categories.contains_key(&to_show.category) {
551
let _ = fs::create_dir_all(&category_path);
552
let mut category_index = File::create(category_path.join("_index.md")).unwrap();
553
category_index
554
.write_all(
555
format!(
556
"+++
557
title = \"{}\"
558
sort_by = \"weight\"
559
weight = {}
560
+++",
561
to_show.category,
562
categories.len()
563
)
564
.as_bytes(),
565
)
566
.unwrap();
567
categories.insert(to_show.category.clone(), 0);
568
}
569
let example_path = category_path.join(to_show.technical_name.replace('_', "-"));
570
let _ = fs::create_dir_all(&example_path);
571
572
let code_path = example_path.join(Path::new(&to_show.path).file_name().unwrap());
573
let code = fs::read_to_string(&to_show.path).unwrap();
574
let (docblock, code) = split_docblock_and_code(&code);
575
let _ = fs::write(&code_path, code);
576
577
let mut example_index = File::create(example_path.join("index.md")).unwrap();
578
example_index
579
.write_all(
580
format!(
581
"+++
582
title = \"{}\"
583
template = \"example{}.html\"
584
weight = {}
585
description = \"{}\"
586
# This creates redirection pages
587
# for the old URLs which used
588
# uppercase letters and whitespace.
589
aliases = [\"/examples{}/{}/{}\"]
590
591
[extra]
592
technical_name = \"{}\"
593
link = \"/examples{}/{}/{}/\"
594
image = \"../static/screenshots/{}/{}.png\"
595
code_path = \"content/examples{}/{}\"
596
shader_code_paths = {:?}
597
github_code_path = \"{}\"
598
header_message = \"Examples ({})\"
599
+++
600
601
{}
602
",
603
to_show.name,
604
match api {
605
WebApi::Webgpu => "-webgpu",
606
WebApi::Webgl2 => "",
607
},
608
categories.get(&to_show.category).unwrap(),
609
to_show.description.replace('"', "'"),
610
match api {
611
WebApi::Webgpu => "-webgpu",
612
WebApi::Webgl2 => "",
613
},
614
to_show.category,
615
&to_show.technical_name.replace('_', "-"),
616
&to_show.technical_name.replace('_', "-"),
617
match api {
618
WebApi::Webgpu => "-webgpu",
619
WebApi::Webgl2 => "",
620
},
621
&beautified_category,
622
&to_show.technical_name.replace('_', "-"),
623
&to_show.category,
624
&to_show.technical_name,
625
match api {
626
WebApi::Webgpu => "-webgpu",
627
WebApi::Webgl2 => "",
628
},
629
code_path
630
.components()
631
.skip(1)
632
.collect::<PathBuf>()
633
.display(),
634
to_show.shader_paths,
635
&to_show.path,
636
match api {
637
WebApi::Webgpu => "WebGPU",
638
WebApi::Webgl2 => "WebGL2",
639
},
640
docblock,
641
)
642
.as_bytes(),
643
)
644
.unwrap();
645
}
646
}
647
Action::BuildWasmExamples {
648
content_folder,
649
website_hacks,
650
optimize_size,
651
api,
652
} => {
653
let api = format!("{api}");
654
let examples_to_build = parse_examples();
655
656
let root_path = Path::new(&content_folder);
657
658
let _ = fs::create_dir_all(root_path);
659
660
if website_hacks {
661
// setting up the headers file for cloudflare for the correct Content-Type
662
let mut headers = File::create(root_path.join("_headers")).unwrap();
663
headers
664
.write_all(
665
"/*/wasm_example_bg.wasm
666
Content-Type: application/wasm
667
"
668
.as_bytes(),
669
)
670
.unwrap();
671
672
let sh = Shell::new().unwrap();
673
674
// setting a canvas by default to help with integration
675
cmd!(
676
sh,
677
"git apply --ignore-whitespace tools/example-showcase/window-settings-wasm.patch"
678
)
679
.run()
680
.unwrap();
681
682
// setting the asset folder root to the root url of this domain
683
cmd!(
684
sh,
685
"git apply --ignore-whitespace tools/example-showcase/asset-source-website.patch"
686
)
687
.run()
688
.unwrap();
689
}
690
691
let work_to_do = || {
692
examples_to_build
693
.iter()
694
.filter(|to_build| to_build.wasm)
695
.filter(|to_build| to_build.example_type == ExampleType::Bin)
696
.skip(cli.page.unwrap_or(0) * cli.per_page.unwrap_or(0))
697
.take(cli.per_page.unwrap_or(usize::MAX))
698
};
699
700
let mut pb = ProgressBar::new(work_to_do().count() as u64);
701
for to_build in work_to_do() {
702
let sh = Shell::new().unwrap();
703
let example = &to_build.technical_name;
704
let required_features = if to_build.required_features.is_empty() {
705
vec![]
706
} else {
707
vec![
708
"--features".to_string(),
709
to_build.required_features.join(","),
710
]
711
};
712
713
if optimize_size {
714
cmd!(
715
sh,
716
"cargo run -p build-wasm-example -- --api {api} {example} --optimize-size {required_features...}"
717
)
718
.run()
719
.unwrap();
720
} else {
721
cmd!(
722
sh,
723
"cargo run -p build-wasm-example -- --api {api} {example} {required_features...}"
724
)
725
.run()
726
.unwrap();
727
}
728
729
let category_path = root_path.join(&to_build.category);
730
let _ = fs::create_dir_all(&category_path);
731
732
let example_path = category_path.join(to_build.technical_name.replace('_', "-"));
733
let _ = fs::create_dir_all(&example_path);
734
735
if website_hacks {
736
// set up the loader bar for asset loading
737
cmd!(sh, "sed -i.bak -e 's/getObject(arg0).fetch(/window.bevyLoadingBarFetch(/' -e 's/input = fetch(/input = window.bevyLoadingBarFetch(/' examples/wasm/target/wasm_example.js").run().unwrap();
738
}
739
740
let _ = fs::rename(
741
Path::new("examples/wasm/target/wasm_example.js"),
742
example_path.join("wasm_example.js"),
743
);
744
if optimize_size {
745
let _ = fs::rename(
746
Path::new("examples/wasm/target/wasm_example_bg.wasm.optimized"),
747
example_path.join("wasm_example_bg.wasm"),
748
);
749
} else {
750
let _ = fs::rename(
751
Path::new("examples/wasm/target/wasm_example_bg.wasm"),
752
example_path.join("wasm_example_bg.wasm"),
753
);
754
}
755
pb.inc();
756
}
757
pb.finish_print("done");
758
}
759
}
760
}
761
762
fn split_docblock_and_code(code: &str) -> (String, &str) {
763
let mut docblock_lines = Vec::new();
764
let mut code_byte_start = 0;
765
766
for line in code.lines() {
767
if line.starts_with("//!") {
768
docblock_lines.push(line.trim_start_matches("//!").trim());
769
} else if !line.trim().is_empty() {
770
break;
771
}
772
773
code_byte_start += line.len() + 1;
774
}
775
776
(docblock_lines.join("\n"), &code[code_byte_start..])
777
}
778
779
fn parse_examples() -> Vec<Example> {
780
let manifest_file = fs::read_to_string("Cargo.toml").unwrap();
781
let manifest = manifest_file.parse::<DocumentMut>().unwrap();
782
let metadatas = manifest
783
.get("package")
784
.unwrap()
785
.get("metadata")
786
.as_ref()
787
.unwrap()["example"]
788
.clone();
789
790
manifest["example"]
791
.as_array_of_tables()
792
.unwrap()
793
.iter()
794
.flat_map(|val| {
795
let technical_name = val.get("name").unwrap().as_str().unwrap().to_string();
796
797
let source_code = fs::read_to_string(val["path"].as_str().unwrap()).unwrap();
798
let shader_regex = Regex::new(r"shaders\/\w+\.(wgsl|frag|vert|wesl)").unwrap();
799
800
// Find all instances of references to shader files, and keep them in an ordered and deduped vec.
801
let mut shader_paths = vec![];
802
for path in shader_regex
803
.find_iter(&source_code)
804
.map(|matches| matches.as_str().to_owned())
805
{
806
if !shader_paths.contains(&path) {
807
shader_paths.push(path);
808
}
809
}
810
811
if metadatas
812
.get(&technical_name)
813
.and_then(|metadata| metadata.get("hidden"))
814
.and_then(Item::as_bool)
815
.and_then(|hidden| hidden.then_some(()))
816
.is_some()
817
{
818
return None;
819
}
820
821
metadatas.get(&technical_name).map(|metadata| Example {
822
technical_name,
823
path: val["path"].as_str().unwrap().to_string(),
824
shader_paths,
825
name: metadata["name"].as_str().unwrap().to_string(),
826
description: metadata["description"].as_str().unwrap().to_string(),
827
category: metadata["category"].as_str().unwrap().to_string(),
828
wasm: metadata["wasm"].as_bool().unwrap(),
829
required_features: val
830
.get("required-features")
831
.map(|rf| {
832
rf.as_array()
833
.unwrap()
834
.into_iter()
835
.map(|v| v.as_str().unwrap().to_string())
836
.collect()
837
})
838
.unwrap_or_default(),
839
example_type: match val.get("crate-type") {
840
Some(crate_type) => {
841
match crate_type
842
.as_array()
843
.unwrap()
844
.get(0)
845
.unwrap()
846
.as_str()
847
.unwrap()
848
{
849
"lib" => ExampleType::Lib,
850
_ => ExampleType::Bin,
851
}
852
}
853
None => ExampleType::Bin,
854
},
855
})
856
})
857
.collect()
858
}
859
860
/// Data for this struct comes from both the entry for an example in the Cargo.toml file, and its associated metadata.
861
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
862
struct Example {
863
// From the example entry
864
/// Name of the example, used to start it from the cargo CLI with `--example`
865
technical_name: String,
866
/// Path to the example file
867
path: String,
868
/// Path to the associated wgsl file if it exists
869
shader_paths: Vec<String>,
870
/// List of non default required features
871
required_features: Vec<String>,
872
// From the example metadata
873
/// Pretty name, used for display
874
name: String,
875
/// Description of the example, for discoverability
876
description: String,
877
/// Pretty category name, matching the folder containing the example
878
category: String,
879
/// Does this example work in Wasm?
880
// TODO: be able to differentiate between WebGL2, WebGPU, both, or neither (for examples that could run on Wasm without a renderer)
881
wasm: bool,
882
/// Type of example
883
example_type: ExampleType,
884
}
885
886
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
887
enum ExampleType {
888
Lib,
889
Bin,
890
}
891
892