generate_icons.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import { join, extname } from "path";
  2. import * as fs from "fs";
  3. import util from "node:util";
  4. import * as child_process from "node:child_process";
  5. const _exec = util.promisify(child_process.exec);
  6. const dark_colors = {
  7. "#fc7f7f": "#fc9c9c",
  8. "#8da5f3": "#a5b7f3",
  9. "#e0e0e0": "#e0e0e0",
  10. "#c38ef1": "#cea4f1",
  11. "#8eef97": "#a5efac",
  12. };
  13. const light_colors = {
  14. "#fc7f7f": "#ff5f5f",
  15. "#8da5f3": "#6d90ff",
  16. "#e0e0e0": "#4f4f4f",
  17. "#c38ef1": "#bb6dff",
  18. "#8eef97": "#29d739",
  19. };
  20. function replace_colors(colors: object, data: string) {
  21. for (const [from, to] of Object.entries(colors)) {
  22. data = data.replace(from, to);
  23. }
  24. return data;
  25. }
  26. const iconsPath = "editor/icons";
  27. const modulesPath = "modules";
  28. const outputPath = "resources/godot_icons";
  29. const godotPath = process.argv[2];
  30. async function exec(command) {
  31. const { stdout, stderr } = await _exec(command);
  32. return stdout;
  33. }
  34. const git = {
  35. diff: "git diff HEAD",
  36. check_branch: "git rev-parse --abbrev-ref HEAD",
  37. reset: "git reset --hard",
  38. stash_push: "git stash push",
  39. stash_pop: "git stash pop",
  40. checkout: "git checkout ",
  41. checkout_4: "git checkout master",
  42. checkout_3: "git checkout 3.x",
  43. };
  44. function to_title_case(str) {
  45. return str.replace(
  46. /\w\S*/g,
  47. function (txt) {
  48. return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
  49. }
  50. );
  51. }
  52. function get_class_list(modules) {
  53. const classes: string[] = [
  54. "ArrowDown.svg",
  55. "ArrowLeft.svg",
  56. "ArrowRight.svg",
  57. "ArrowUp.svg",
  58. "GuiVisibilityHidden.svg",
  59. "GuiVisibilityVisible.svg",
  60. "GuiVisibilityXray.svg",
  61. "Edit.svg",
  62. "Help.svg",
  63. "HelpSearch.svg",
  64. "ImportCheck.svg",
  65. "ImportFail.svg",
  66. "Info.svg",
  67. "Play.svg",
  68. "PlayBackwards.svg",
  69. "PlayCustom.svg",
  70. "PlayRemote.svg",
  71. "PlayScene.svg",
  72. "PlayStart.svg",
  73. "Progress1.svg",
  74. "Progress2.svg",
  75. "Progress3.svg",
  76. "Progress4.svg",
  77. "Progress5.svg",
  78. "Progress6.svg",
  79. "Progress7.svg",
  80. "Progress8.svg",
  81. "Progress9.svg",
  82. "Reload.svg",
  83. "ReloadSmall.svg",
  84. "Script.svg",
  85. "ScriptCreate.svg",
  86. "ScriptRemove.svg",
  87. "Search.svg",
  88. "Signals.svg",
  89. "SignalsAndGroups.svg",
  90. "Slot.svg",
  91. "Stop.svg",
  92. "Lock.svg",
  93. "Unlock.svg",
  94. "Zoom.svg",
  95. "ZoomLess.svg",
  96. "ZoomMore.svg",
  97. "ZoomReset.svg",
  98. ];
  99. const files = ["scene/register_scene_types.cpp"];
  100. modules.forEach(mod => {
  101. files.push(join(mod, "register_types.cpp"));
  102. });
  103. const patterns = [
  104. /GDREGISTER_CLASS\((\w*)\)/,
  105. /register_class<(\w*)>/,
  106. ];
  107. files.forEach(fileName => {
  108. const file = fs.readFileSync(fileName, "utf8");
  109. file.split("\n").forEach(line => {
  110. patterns.forEach(pattern => {
  111. const match = line.match(pattern);
  112. if (match) {
  113. classes.push(match[1] + ".svg");
  114. }
  115. });
  116. });
  117. });
  118. return classes;
  119. }
  120. function discover_modules() {
  121. const modules: string[] = [];
  122. // a valid module is a subdir of modulesPath, and contains a subdir 'icons'
  123. fs.readdirSync(modulesPath, { withFileTypes: true }).forEach(mod => {
  124. if (mod.isDirectory()) {
  125. fs.readdirSync(join(modulesPath, mod.name), { withFileTypes: true }).forEach(child => {
  126. if (child.isDirectory() && child.name == "icons") {
  127. modules.push(join(modulesPath, mod.name));
  128. }
  129. });
  130. }
  131. });
  132. return modules;
  133. }
  134. interface IconData {
  135. name: string;
  136. contents: string;
  137. }
  138. function get_icons(): IconData[] {
  139. const modules = discover_modules();
  140. const classes = get_class_list(modules);
  141. const searchPaths = [iconsPath];
  142. modules.forEach(mod => {
  143. searchPaths.push(join(mod, "icons"));
  144. });
  145. const icons: IconData[] = [];
  146. searchPaths.forEach(searchPath => {
  147. fs.readdirSync(searchPath).forEach(file => {
  148. if (extname(file) === ".svg") {
  149. let name = file;
  150. if (name.startsWith("icon_")) {
  151. name = name.replace("icon_", "");
  152. let parts = name.split("_");
  153. parts = parts.map(to_title_case);
  154. name = parts.join("");
  155. }
  156. if (!classes.includes(name)) {
  157. return;
  158. }
  159. const f = {
  160. name: name,
  161. contents: fs.readFileSync(join(searchPath, file), "utf8")
  162. };
  163. icons.push(f);
  164. }
  165. });
  166. });
  167. return icons;
  168. }
  169. function ensure_paths() {
  170. const paths = [
  171. outputPath,
  172. join(outputPath, "light"),
  173. join(outputPath, "dark"),
  174. ];
  175. paths.forEach(path => {
  176. if (!fs.existsSync(path)) {
  177. fs.mkdirSync(path);
  178. }
  179. });
  180. }
  181. async function run() {
  182. if (godotPath == undefined) {
  183. console.log("Please provide the absolute path to your godot repo");
  184. return;
  185. }
  186. const original_cwd = process.cwd();
  187. process.chdir(godotPath);
  188. const diff = (await exec(git.diff)).trim();
  189. if (diff) {
  190. console.log("There appear to be uncommitted changes in your godot repo");
  191. console.log("Revert or stash these changes and try again");
  192. return;
  193. }
  194. const branch = (await exec(git.check_branch)).trim();
  195. console.log("Gathering Godot 3 icons...");
  196. await exec(git.checkout_3);
  197. const g3 = get_icons();
  198. console.log("Gathering Godot 4 icons...");
  199. await exec(git.checkout_4);
  200. const g4 = get_icons();
  201. await exec(git.checkout + branch);
  202. process.chdir(original_cwd);
  203. console.log(`Found ${g3.length + g4.length} icons...`);
  204. const light_icons: Map<string, string> = new Map();
  205. const dark_icons: Map<string, string> = new Map();
  206. console.log("Generating themed icons...");
  207. g3.forEach(file => {
  208. light_icons[file.name] = replace_colors(light_colors, file.contents);
  209. });
  210. g4.forEach(file => {
  211. light_icons[file.name] = replace_colors(light_colors, file.contents);
  212. });
  213. g3.forEach(file => {
  214. dark_icons[file.name] = replace_colors(dark_colors, file.contents);
  215. });
  216. g4.forEach(file => {
  217. dark_icons[file.name] = replace_colors(dark_colors, file.contents);
  218. });
  219. console.log("Ensuring output directory...");
  220. ensure_paths();
  221. console.log("Writing icons to output directory...");
  222. for (const [file, contents] of Object.entries(light_icons)) {
  223. fs.writeFileSync(join(outputPath, "light", file), contents);
  224. }
  225. for (const [file, contents] of Object.entries(dark_icons)) {
  226. fs.writeFileSync(join(outputPath, "dark", file), contents);
  227. }
  228. }
  229. run();