sprite_resource.vala 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024
  1. /*
  2. * Copyright (c) 2012-2026 Daniele Bartolini et al.
  3. * SPDX-License-Identifier: GPL-3.0-or-later
  4. */
  5. namespace Crown
  6. {
  7. public Gdk.RGBA collider_color = { 1.0, 0.5, 0.0, 1.0 };
  8. public enum Pivot
  9. {
  10. TOP_LEFT,
  11. TOP_CENTER,
  12. TOP_RIGHT,
  13. LEFT,
  14. CENTER,
  15. RIGHT,
  16. BOTTOM_LEFT,
  17. BOTTOM_CENTER,
  18. BOTTOM_RIGHT
  19. }
  20. Vector2 sprite_cell_xy(int r, int c, int offset_x, int offset_y, int cell_w, int cell_h, int spacing_x, int spacing_y)
  21. {
  22. int x0 = offset_x + c*cell_w + c*spacing_x;
  23. int y0 = offset_y + r*cell_h + r*spacing_y;
  24. return Vector2(x0, y0);
  25. }
  26. Vector2 sprite_cell_pivot_xy(int cell_w, int cell_h, int pivot)
  27. {
  28. int pivot_x = 0;
  29. int pivot_y = 0;
  30. switch (pivot) {
  31. case Pivot.TOP_LEFT:
  32. pivot_x = 0;
  33. pivot_y = 0;
  34. break;
  35. case Pivot.TOP_CENTER:
  36. pivot_x = cell_w / 2;
  37. pivot_y = 0;
  38. break;
  39. case Pivot.TOP_RIGHT:
  40. pivot_x = cell_w;
  41. pivot_y = 0;
  42. break;
  43. case Pivot.BOTTOM_LEFT:
  44. pivot_x = 0;
  45. pivot_y = cell_h;
  46. break;
  47. case Pivot.BOTTOM_CENTER:
  48. pivot_x = cell_w / 2;
  49. pivot_y = cell_h;
  50. break;
  51. case Pivot.BOTTOM_RIGHT:
  52. pivot_x = cell_w;
  53. pivot_y = cell_h;
  54. break;
  55. case Pivot.LEFT:
  56. pivot_x = 0;
  57. pivot_y = cell_h / 2;
  58. break;
  59. case Pivot.CENTER:
  60. pivot_x = cell_w / 2;
  61. pivot_y = cell_h / 2;
  62. break;
  63. case Pivot.RIGHT:
  64. pivot_x = cell_w;
  65. pivot_y = cell_h / 2;
  66. break;
  67. default:
  68. assert(false);
  69. break;
  70. }
  71. return Vector2(pivot_x, pivot_y);
  72. }
  73. void sprite_cell_from_index(out int r, out int c, int num_cols, int index)
  74. {
  75. r = (int)(index / num_cols);
  76. c = index - r * num_cols;
  77. }
  78. public class SpriteImportDialog : Gtk.Window
  79. {
  80. public Project _project;
  81. public string _destination_dir;
  82. public GLib.SList<string> _filenames;
  83. public unowned Import _import_result;
  84. public string _image_type;
  85. public Gdk.Pixbuf _pixbuf;
  86. public InputResourceBasename _unit_name;
  87. public Gtk.Label resolution;
  88. public InputVector2 cells;
  89. public Gtk.CheckButton cell_wh_auto;
  90. public InputVector2 cell;
  91. public InputVector2 offset;
  92. public InputVector2 spacing;
  93. public Gtk.ComboBoxText pivot;
  94. public InputDouble layer;
  95. public InputDouble depth;
  96. public Gtk.CheckButton collision_enabled;
  97. public Gtk.CheckButton mirror_cell;
  98. public string shape_active_name;
  99. public Gtk.StackSwitcher shape_switcher;
  100. public Gtk.Stack shape;
  101. public InputVector2 circle_collision_center;
  102. public InputDouble circle_collision_radius;
  103. public InputVector2 capsule_collision_center;
  104. public InputDouble capsule_collision_height;
  105. public InputDouble capsule_collision_radius;
  106. public InputVector2 collision_xy;
  107. public InputVector2 collision_wh;
  108. public InputEnum actor_class;
  109. public InputDouble mass;
  110. public Gtk.CheckButton lock_rotation_z;
  111. public Gtk.Button _previous_frame;
  112. public Gtk.Button _next_frame;
  113. public InputDouble _current_frame;
  114. public Gtk.Box _frame_selector_box;
  115. public Gtk.Overlay _preview_overlay;
  116. public PixbufView _slices;
  117. public PixbufView _preview;
  118. public Gtk.ScrolledWindow _scrolled_window;
  119. public Gtk.Notebook _notebook;
  120. public Gtk.Box _box;
  121. public Gtk.Button _import;
  122. public Gtk.Button _cancel;
  123. public Gtk.HeaderBar _header_bar;
  124. public SpriteImportDialog(Database database, string destination_dir, GLib.SList<string> filenames, Import import_result)
  125. {
  126. this.set_icon_name(CROWN_EDITOR_ICON_NAME);
  127. _project = database._project;
  128. _destination_dir = destination_dir;
  129. _filenames = new GLib.SList<string>();
  130. foreach (var f in filenames)
  131. _filenames.append(f);
  132. _import_result = import_result;
  133. string settings_path;
  134. string image_path;
  135. string image_name;
  136. {
  137. GLib.File file_src = File.new_for_path(_filenames.nth_data(0));
  138. image_path = file_src.get_path();
  139. _image_type = image_path.substring(image_path.last_index_of_char('.') + 1
  140. , image_path.length - image_path.last_index_of_char('.') - 1
  141. );
  142. GLib.File file_dst = File.new_for_path(Path.build_filename(destination_dir, file_src.get_basename()));
  143. string resource_filename = _project.resource_filename(file_dst.get_path());
  144. string resource_path = ResourceId.normalize(resource_filename);
  145. string resource_name = ResourceId.name(resource_path);
  146. settings_path = _project.absolute_path(resource_name) + ".importer_settings";
  147. int last_slash = resource_name.last_index_of_char('/');
  148. if (last_slash == -1)
  149. image_name = resource_name;
  150. else
  151. image_name = resource_name.substring(last_slash + 1, resource_name.length - last_slash - 1);
  152. }
  153. try {
  154. _pixbuf = new Gdk.Pixbuf.from_file(image_path);
  155. } catch (GLib.Error e) {
  156. loge(e.message);
  157. }
  158. _unit_name = new InputResourceBasename(image_name);
  159. _unit_name.sensitive = _filenames.length() == 1;
  160. resolution = new Gtk.Label(_pixbuf.width.to_string() + " × " + _pixbuf.height.to_string());
  161. resolution.halign = Gtk.Align.START;
  162. cells = new InputVector2(Vector2(1.0, 1.0), Vector2(1.0, 1.0), Vector2(256.0, 256.0));
  163. cell_wh_auto = new Gtk.CheckButton();
  164. cell_wh_auto.active = true;
  165. cell = new InputVector2(Vector2(_pixbuf.width / cells.value.x, _pixbuf.height / cells.value.y), Vector2(1.0, 1.0), Vector2(double.MAX, double.MAX));
  166. cell.sensitive = !cell_wh_auto.active;
  167. offset = new InputVector2(Vector2(0.0, 0.0), Vector2(0.0, 0.0), Vector2(double.MAX, double.MAX));
  168. spacing = new InputVector2(Vector2(0.0, 0.0), Vector2(0.0, 0.0), Vector2(double.MAX, double.MAX));
  169. collision_enabled = new Gtk.CheckButton();
  170. collision_enabled.active = true;
  171. collision_enabled.toggled.connect(() => {
  172. _preview.queue_draw();
  173. });
  174. mirror_cell = new Gtk.CheckButton();
  175. mirror_cell.active = true;
  176. collision_xy = new InputVector2(Vector2(0.0, 0.0), Vector2(-double.MAX, -double.MAX), Vector2(double.MAX, double.MAX));
  177. collision_wh = new InputVector2(Vector2(32.0, 32.0), Vector2(-double.MAX, -double.MAX), Vector2(double.MAX, double.MAX));
  178. actor_class = new InputEnum();
  179. actor_class.append("static", "static");
  180. actor_class.append("dynamic", "dynamic");
  181. actor_class.append("keyframed", "keyframed");
  182. actor_class.append("trigger", "trigger");
  183. actor_class.value = "static";
  184. lock_rotation_z = new Gtk.CheckButton();
  185. lock_rotation_z.active = true;
  186. mass = new InputDouble(10.0, 0.0, double.MAX);
  187. circle_collision_center = new InputVector2(Vector2(cell.value.x/2.0, cell.value.y/2.0), Vector2(-double.MAX, -double.MAX), Vector2(double.MAX, double.MAX));
  188. circle_collision_radius = new InputDouble(32.0, 0.5, double.MAX);
  189. capsule_collision_center = new InputVector2(Vector2(cell.value.x/2.0, cell.value.y/2.0), Vector2(-double.MAX, -double.MAX), Vector2(double.MAX, double.MAX));
  190. capsule_collision_radius = new InputDouble(32.0, 0.5, double.MAX);
  191. capsule_collision_height = new InputDouble(64.0, 2.0*capsule_collision_radius.value, double.MAX);
  192. cells.value_changed.connect(() => {
  193. if (cell_wh_auto.active)
  194. cell.value = Vector2(_pixbuf.width / cells.value.x, _pixbuf.height / cells.value.y);
  195. calc_collider_shape();
  196. _current_frame.set_max(cells.value.x * cells.value.y - 1);
  197. _slices.queue_draw();
  198. set_preview_frame();
  199. _preview.queue_draw();
  200. });
  201. cell_wh_auto.toggled.connect(() => {
  202. cell.sensitive = !cell_wh_auto.active;
  203. cell.value = Vector2(_pixbuf.width / cells.value.x, _pixbuf.height / cells.value.y);
  204. _slices.queue_draw();
  205. _preview.queue_draw();
  206. });
  207. cell.value_changed.connect(() => {
  208. calc_collider_shape();
  209. _slices.queue_draw();
  210. set_preview_frame();
  211. _preview.queue_draw();
  212. });
  213. offset.value_changed.connect(() => {
  214. _slices.queue_draw();
  215. set_preview_frame();
  216. _preview.queue_draw();
  217. });
  218. spacing.value_changed.connect(() => {
  219. _slices.queue_draw();
  220. set_preview_frame();
  221. _preview.queue_draw();
  222. });
  223. collision_enabled.toggled.connect(() => {
  224. collision_xy.sensitive = !collision_xy.sensitive;
  225. collision_wh.sensitive = !collision_wh.sensitive;
  226. shape_switcher.sensitive = !shape_switcher.sensitive;
  227. mirror_cell.sensitive = !mirror_cell.sensitive;
  228. circle_collision_center.sensitive = !circle_collision_center.sensitive;
  229. circle_collision_radius.sensitive = !circle_collision_radius.sensitive;
  230. capsule_collision_center.sensitive = !capsule_collision_center.sensitive;
  231. capsule_collision_radius.sensitive = !capsule_collision_radius.sensitive;
  232. capsule_collision_height.sensitive = !capsule_collision_height.sensitive;
  233. actor_class.sensitive = !actor_class.sensitive;
  234. mass.sensitive = !mass.sensitive;
  235. lock_rotation_z.sensitive = !lock_rotation_z.sensitive;
  236. });
  237. collision_xy.value_changed.connect(() => {
  238. _preview.queue_draw();
  239. });
  240. collision_wh.value_changed.connect(() => {
  241. _preview.queue_draw();
  242. });
  243. circle_collision_center.value_changed.connect(() => {
  244. _preview.queue_draw();
  245. });
  246. circle_collision_radius.value_changed.connect(() => {
  247. _preview.queue_draw();
  248. });
  249. capsule_collision_center.value_changed.connect(() => {
  250. _preview.queue_draw();
  251. });
  252. capsule_collision_radius.value_changed.connect(() => {
  253. capsule_collision_height.set_min(2.0 * capsule_collision_radius.value);
  254. _preview.queue_draw();
  255. });
  256. capsule_collision_height.value_changed.connect(() => {
  257. _preview.queue_draw();
  258. });
  259. pivot = new Gtk.ComboBoxText();
  260. pivot.append_text("Top left"); // TOP_LEFT
  261. pivot.append_text("Top center"); // TOP_CENTER
  262. pivot.append_text("Top right"); // TOP_RIGHT
  263. pivot.append_text("Left"); // LEFT
  264. pivot.append_text("Center"); // CENTER
  265. pivot.append_text("Right"); // RIGHT
  266. pivot.append_text("Bottom left"); // BOTTOM_LEFT
  267. pivot.append_text("Bottom center"); // BOTTOM_CENTER
  268. pivot.append_text("Bottom right"); // BOTTOM_RIGHT
  269. pivot.active = Pivot.CENTER;
  270. pivot.changed.connect(() => {
  271. _slices.queue_draw();
  272. _preview.queue_draw();
  273. });
  274. layer = new InputDouble(0.0, 0.0, 7.0);
  275. depth = new InputDouble(0.0, 0.0, 9999.0);
  276. PropertyGridSet sprite_set = new PropertyGridSet();
  277. // Slices.
  278. PropertyGrid cv;
  279. cv = new PropertyGrid();
  280. cv.add_row("Name", _unit_name, "Name of the imported unit.");
  281. sprite_set.add_property_grid(cv, "Unit");
  282. cv = new PropertyGrid();
  283. cv.add_row("Resolution", resolution, "Resolution of the source image.");
  284. cv.add_row("Cells", cells, "Split the image into X columns and Y rows.");
  285. cv.add_row("Auto Size", cell_wh_auto, "Compute the cell size automatically.");
  286. cv.add_row("Cell", cell, "Size of each cell.");
  287. cv.add_row("Offset", offset, "Starting offset of the cells.");
  288. cv.add_row("Spacing", spacing, "Spacing between the cells.");
  289. cv.add_row("Pivot", pivot, "Origin of the sprite.");
  290. cv.add_row("Collision", collision_enabled, "Enable collision detection.");
  291. sprite_set.add_property_grid(cv, "Image");
  292. // Sprite Renderer.
  293. cv = new PropertyGrid();
  294. cv.add_row("Layer", layer, "Sorting layer. Higher values makes the sprite appear in front.");
  295. cv.add_row("Depth", depth, "Higher values make the sprite apper in front of other sprites in the same layer.");
  296. sprite_set.add_property_grid(cv, "Sprite Renderer");
  297. // Collider.
  298. shape = new Gtk.Stack();
  299. shape.sensitive = false;
  300. shape.vhomogeneous = false;
  301. shape.notify["visible-child"].connect(() => {
  302. calc_collider_shape();
  303. _preview.queue_draw();
  304. });
  305. cv = new PropertyGrid();
  306. cv.add_row("Origin", collision_xy);
  307. cv.add_row("Size", collision_wh);
  308. shape.add_titled(cv, "square_collider", "Square");
  309. cv = new PropertyGrid();
  310. cv.add_row("Origin", circle_collision_center);
  311. cv.add_row("Radius", circle_collision_radius);
  312. shape.add_titled(cv, "circle_collider", "Circle");
  313. cv = new PropertyGrid();
  314. cv.add_row("Origin", capsule_collision_center);
  315. cv.add_row("Radius", capsule_collision_radius);
  316. cv.add_row("Height", capsule_collision_height);
  317. shape.add_titled(cv, "capsule_collider", "Capsule");
  318. shape_switcher = new Gtk.StackSwitcher();
  319. shape_switcher.set_stack(shape);
  320. cv = new PropertyGrid();
  321. cv.row_homogeneous = false;
  322. cv.add_row("Shape Type", shape_switcher, "Shape to use as collider.");
  323. cv.add_row("Mirror Cell", mirror_cell, "Compute the shape size based on the cell size.");
  324. cv.add_row("Shape Data", shape, "Set the shape size manually.");
  325. sprite_set.add_property_grid(cv, "Collider");
  326. mirror_cell.toggled.connect(() => {
  327. shape.sensitive = !shape.sensitive;
  328. calc_collider_shape();
  329. _slices.queue_draw();
  330. _preview.queue_draw();
  331. });
  332. // Actor.
  333. cv = new PropertyGrid();
  334. cv.add_row("Class", actor_class, "Actor class.");
  335. cv.add_row("Mass", mass, "Actor physical mass.");
  336. cv.add_row("Lock Rotation", lock_rotation_z, "Prevent the actor from rotating around the Z axis.");
  337. sprite_set.add_property_grid(cv, "Actor");
  338. _previous_frame = new Gtk.Button.from_icon_name("go-previous-symbolic", Gtk.IconSize.LARGE_TOOLBAR);
  339. _previous_frame.clicked.connect(() => {
  340. _current_frame.value -= 1;
  341. set_preview_frame();
  342. _preview.queue_draw();
  343. });
  344. _next_frame = new Gtk.Button.from_icon_name("go-next-symbolic", Gtk.IconSize.LARGE_TOOLBAR);
  345. _next_frame.clicked.connect(() => {
  346. _current_frame.value += 1;
  347. set_preview_frame();
  348. _preview.queue_draw();
  349. });
  350. _current_frame = new InputDouble(0.0, 0.0, cells.value.x * cells.value.y - 1);
  351. _current_frame.value_changed.connect(() => {
  352. set_preview_frame();
  353. _preview.queue_draw();
  354. });
  355. _frame_selector_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
  356. _frame_selector_box.homogeneous = true;
  357. _frame_selector_box.halign = Gtk.Align.CENTER;
  358. _frame_selector_box.valign = Gtk.Align.END;
  359. _frame_selector_box.margin_bottom = 24;
  360. _frame_selector_box.pack_start(_previous_frame);
  361. _frame_selector_box.pack_start(_current_frame);
  362. _frame_selector_box.pack_end(_next_frame);
  363. _slices = new PixbufView();
  364. _slices.set_size_request(_pixbuf.width, _pixbuf.height);
  365. _slices.set_pixbuf(_pixbuf);
  366. _slices.draw.connect((cr) => {
  367. int allocated_width = _preview.get_allocated_width();
  368. int allocated_height = _preview.get_allocated_height();
  369. double original_line_width = cr.get_line_width();
  370. cr.translate(allocated_width * 0.5, allocated_height * 0.5);
  371. cr.scale(_slices._zoom, _slices._zoom);
  372. cr.set_line_width(original_line_width / _slices._zoom);
  373. cr.translate(-_pixbuf.width * 0.5, -_pixbuf.height * 0.5);
  374. int num_v = (int)cells.value.y;
  375. int num_h = (int)cells.value.x;
  376. for (int h = 0; h < num_v; ++h) {
  377. for (int w = 0; w < num_h; ++w) {
  378. Vector2 sc = sprite_cell_xy(h
  379. , w
  380. , (int)offset.value.x
  381. , (int)offset.value.y
  382. , (int)cell.value.x
  383. , (int)cell.value.y
  384. , (int)spacing.value.x
  385. , (int)spacing.value.y
  386. );
  387. int x0 = (int)sc.x;
  388. int y0 = (int)sc.y;
  389. int x1 = x0 + (int)cell.value.x;
  390. int y1 = y0;
  391. int x2 = x1;
  392. int y2 = y0 + (int)cell.value.y;
  393. int x3 = x0;
  394. int y3 = y2;
  395. // https://www.cairographics.org/FAQ/#sharp_lines
  396. cr.move_to((double)x0, (double)y0 + 0.5);
  397. cr.line_to((double)x1, (double)y1 + 0.5);
  398. cr.move_to((double)x1 + 0.5, (double)y1);
  399. cr.line_to((double)x2 + 0.5, (double)y2);
  400. cr.move_to((double)x2, (double)y2 + 0.5);
  401. cr.line_to((double)x3, (double)y3 + 0.5);
  402. cr.move_to((double)x3 + 0.5, (double)y3);
  403. cr.line_to((double)x0 + 0.5, (double)y0);
  404. cr.set_source_rgba(0.9, 0.1, 0.1, 0.9);
  405. cr.stroke();
  406. }
  407. }
  408. return Gdk.EVENT_STOP;
  409. });
  410. _preview = new PixbufView();
  411. _preview._zoom = 4.0;
  412. _preview.set_size_request(128, 128);
  413. set_preview_frame();
  414. _preview.draw.connect((cr) => {
  415. int allocated_width = _preview.get_allocated_width();
  416. int allocated_height = _preview.get_allocated_height();
  417. double original_line_width = cr.get_line_width();
  418. cr.translate(allocated_width * 0.5, allocated_height * 0.5);
  419. cr.scale(_preview._zoom, _preview._zoom);
  420. cr.set_line_width(original_line_width / _preview._zoom);
  421. cr.translate(-cell.value.x * 0.5, -cell.value.y * 0.5);
  422. // Draw collider.
  423. if (collision_enabled.active) {
  424. if (shape.visible_child_name == "square_collider") {
  425. cr.rectangle(collision_xy.value.x, collision_xy.value.y, collision_wh.value.x, collision_wh.value.y);
  426. cr.set_source_rgba(collider_color.red, collider_color.green, collider_color.blue, collider_color.alpha);
  427. cr.stroke();
  428. } else if (shape.visible_child_name == "circle_collider") {
  429. cr.arc(circle_collision_center.value.x, circle_collision_center.value.y, circle_collision_radius.value, 0, 2*Math.PI);
  430. cr.set_source_rgba(collider_color.red, collider_color.green, collider_color.blue, collider_color.alpha);
  431. cr.stroke();
  432. } else if (shape.visible_child_name == "capsule_collider") {
  433. double x = capsule_collision_center.value.x;
  434. double y = capsule_collision_center.value.y;
  435. double radius = capsule_collision_radius.value;
  436. double height = capsule_collision_height.value - 2*radius;
  437. cr.arc(x - height/2, y, radius, Math.PI/2, 3*Math.PI/2);
  438. cr.rectangle(x - height/2, y - radius, height, 2*radius);
  439. cr.arc(x + height/2, y, radius, 3*Math.PI/2, Math.PI/2);
  440. cr.set_source_rgba(collider_color.red, collider_color.green, collider_color.blue, collider_color.alpha);
  441. cr.stroke();
  442. }
  443. }
  444. // Draw pivot.
  445. // Pivot is relative to the top-left corner of the cell.
  446. Vector2 pivot = sprite_cell_pivot_xy((int)cell.value.x
  447. , (int)cell.value.y
  448. , (int)pivot.active
  449. );
  450. cr.arc(pivot.x, pivot.y, 5.0, 0, 2*Math.PI);
  451. cr.set_source_rgba(0.1, 0.1, 0.9, 0.6);
  452. cr.fill();
  453. return Gdk.EVENT_STOP;
  454. });
  455. _preview_overlay = new Gtk.Overlay();
  456. _preview_overlay.add(_preview);
  457. _preview_overlay.add_overlay(_frame_selector_box);
  458. _scrolled_window = new Gtk.ScrolledWindow(null, null);
  459. _scrolled_window.min_content_width = 640;
  460. _scrolled_window.min_content_height = 640;
  461. _scrolled_window.add(_slices);
  462. _notebook = new Gtk.Notebook();
  463. _notebook.append_page(_preview_overlay, new Gtk.Label("Preview"));
  464. _notebook.append_page(_scrolled_window, new Gtk.Label("Slices"));
  465. _notebook.show_border = false;
  466. Gtk.Paned pane;
  467. pane = new Gtk.Paned(Gtk.Orientation.HORIZONTAL);
  468. pane.pack1(_notebook, false, false);
  469. pane.pack2(sprite_set, true, false);
  470. _box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
  471. _box.pack_start(pane, false, false);
  472. _cancel = new Gtk.Button.with_label("Cancel");
  473. _cancel.clicked.connect(() => {
  474. close();
  475. });
  476. _import = new Gtk.Button.with_label("Import");
  477. _import.get_style_context().add_class("suggested-action");
  478. _import.clicked.connect(on_import);
  479. _header_bar = new Gtk.HeaderBar();
  480. _header_bar.title = "Import Sprite...";
  481. _header_bar.show_close_button = true;
  482. _header_bar.pack_start(_cancel);
  483. _header_bar.pack_end(_import);
  484. this.set_titlebar(_header_bar);
  485. this.add(_box);
  486. this.map_event.connect(on_map_event);
  487. if (File.new_for_path(settings_path).query_exists()) {
  488. try {
  489. decode(SJSON.load_from_path(settings_path));
  490. } catch (JsonSyntaxError e) {
  491. // No-op.
  492. }
  493. }
  494. }
  495. public bool on_map_event(Gdk.EventAny ev)
  496. {
  497. _unit_name.grab_focus();
  498. return Gdk.EVENT_PROPAGATE;
  499. }
  500. public void set_preview_frame()
  501. {
  502. assert(cells != null);
  503. assert(_current_frame != null);
  504. assert(offset != null);
  505. assert(cell != null);
  506. assert(spacing != null);
  507. int r, c;
  508. sprite_cell_from_index(out r, out c, (int)cells.value.x, (int)_current_frame.value);
  509. Vector2 sc = sprite_cell_xy(r
  510. , c
  511. , (int)offset.value.x
  512. , (int)offset.value.y
  513. , (int)cell.value.x
  514. , (int)cell.value.y
  515. , (int)spacing.value.x
  516. , (int)spacing.value.y
  517. );
  518. Gdk.Pixbuf frame_pixbuf = new Gdk.Pixbuf.subpixbuf(_pixbuf
  519. , (int)sc.x
  520. , (int)sc.y
  521. , (int)cell.value.x
  522. , (int)cell.value.y
  523. );
  524. _preview.set_pixbuf(frame_pixbuf);
  525. }
  526. public void calc_collider_shape()
  527. {
  528. if (!mirror_cell.active)
  529. return;
  530. if (shape.visible_child_name == "square_collider") {
  531. collision_xy.value = Vector2(0, 0);
  532. collision_wh.value = cell.value;
  533. } else if (shape.visible_child_name == "circle_collider") {
  534. circle_collision_center.value = Vector2(cell.value.x * 0.5, cell.value.y * 0.5);
  535. circle_collision_radius.value = double.min(cell.value.x * 0.5, cell.value.y * 0.5);
  536. } else if (shape.visible_child_name == "capsule_collider") {
  537. capsule_collision_center.value = Vector2(cell.value.x * 0.5, cell.value.y * 0.5);
  538. capsule_collision_radius.value = double.min(cell.value.x * 0.5, cell.value.y * 0.5);
  539. capsule_collision_height.value = cell.value.y;
  540. }
  541. }
  542. public void decode(Hashtable obj)
  543. {
  544. // Load settings
  545. cells.value = Vector2((double)obj["num_h"], (double)obj["num_v"]);
  546. cell.value = Vector2((double)obj["cell_w"], (double)obj["cell_h"]);
  547. offset.value = Vector2((double)obj["offset_x"], (double)obj["offset_y"]);
  548. spacing.value = Vector2((double)obj["spacing_x"], (double)obj["spacing_y"]);
  549. layer.value = (double)obj["layer"];
  550. depth.value = (double)obj["depth"];
  551. pivot.active = (int)(double)obj["pivot"];
  552. collision_enabled.active = (bool)obj["collision_enabled"];
  553. collision_xy.value = Vector2((double)obj["collision_x"], (double)obj["collision_y"]);
  554. collision_wh.value = Vector2((double)obj["collision_w"], (double)obj["collision_h"]);
  555. circle_collision_center.value = Vector2(obj.has_key("circle_collision_center_x") ? (double)obj["circle_collision_center_x"] : cell.value.x/2.0, obj.has_key("circle_collision_center_y") ? (double)obj["circle_collision_center_y"] : cell.value.y/2.0);
  556. circle_collision_radius.value = obj.has_key("circle_collision_radius") ? (double)obj["circle_collision_radius"] : 32;
  557. capsule_collision_center.value = Vector2(obj.has_key("capsule_collision_center_x") ? (double)obj["capsule_collision_center_x"] : cell.value.x/2.0, obj.has_key("capsule_collision_center_y") ? (double)obj["capsule_collision_center_y"] : cell.value.y/2.0);
  558. capsule_collision_radius.value = obj.has_key("capsule_collision_radius") ? (double)obj["capsule_collision_radius"] : 32;
  559. capsule_collision_height.value = obj.has_key("capsule_collision_height") ? (double)obj["capsule_collision_height"] : 64;
  560. shape.visible_child_name = obj.has_key("shape_active_name") ? (string)obj["shape_active_name"] : "square_collider";
  561. actor_class.value = obj.has_key("actor_class") ? (string)obj["actor_class"] : "static";
  562. lock_rotation_z.active = obj.has_key("lock_rotation_z") ? (bool)obj["lock_rotation_z"] : true;
  563. mass.value = obj.has_key("mass") ? (double)obj["mass"] : 10.0;
  564. }
  565. public Hashtable encode()
  566. {
  567. Hashtable obj = new Hashtable();
  568. obj["num_h"] = cells.value.x;
  569. obj["num_v"] = cells.value.y;
  570. obj["cell_w"] = cell.value.x;
  571. obj["cell_h"] = cell.value.y;
  572. obj["offset_x"] = offset.value.x;
  573. obj["offset_y"] = offset.value.y;
  574. obj["spacing_x"] = spacing.value.x;
  575. obj["spacing_y"] = spacing.value.y;
  576. obj["layer"] = layer.value;
  577. obj["depth"] = depth.value;
  578. obj["pivot"] = pivot.active;
  579. obj["collision_enabled"] = collision_enabled.active;
  580. obj["collision_x"] = collision_xy.value.x;
  581. obj["collision_y"] = collision_xy.value.y;
  582. obj["collision_w"] = collision_wh.value.x;
  583. obj["collision_h"] = collision_wh.value.y;
  584. obj["circle_collision_center_x"] = circle_collision_center.value.x;
  585. obj["circle_collision_center_y"] = circle_collision_center.value.y;
  586. obj["circle_collision_radius"] = circle_collision_radius.value;
  587. obj["capsule_collision_center_x"] = capsule_collision_center.value.x;
  588. obj["capsule_collision_center_y"] = capsule_collision_center.value.y;
  589. obj["capsule_collision_radius"] = capsule_collision_radius.value;
  590. obj["capsule_collision_height"] = capsule_collision_height.value;
  591. obj["shape_active_name"] = shape.visible_child_name;
  592. obj["actor_class"] = actor_class.value;
  593. obj["lock_rotation_z"] = lock_rotation_z.active;
  594. obj["mass"] = mass.value;
  595. return obj;
  596. }
  597. public void on_import()
  598. {
  599. _import_result(SpriteResource.do_import(this, _project, _destination_dir, _filenames));
  600. close();
  601. }
  602. }
  603. public class SpriteResource
  604. {
  605. public static ImportResult do_import(SpriteImportDialog dlg, Project project, string destination_dir, GLib.SList<string> filenames)
  606. {
  607. int width = (int)dlg._pixbuf.width;
  608. int height = (int)dlg._pixbuf.height;
  609. int num_h = (int)dlg.cells.value.x;
  610. int num_v = (int)dlg.cells.value.y;
  611. int cell_w = (int)dlg.cell.value.x;
  612. int cell_h = (int)dlg.cell.value.y;
  613. int offset_x = (int)dlg.offset.value.x;
  614. int offset_y = (int)dlg.offset.value.y;
  615. int spacing_x = (int)dlg.spacing.value.x;
  616. int spacing_y = (int)dlg.spacing.value.y;
  617. double layer = dlg.layer.value;
  618. double depth = dlg.depth.value;
  619. Vector2 pivot_xy = sprite_cell_pivot_xy(cell_w, cell_h, dlg.pivot.active);
  620. bool collision_enabled = dlg.collision_enabled.active;
  621. string shape_active_name = (string)dlg.shape.visible_child_name;
  622. int circle_collision_center_x = (int)dlg.circle_collision_center.value.x;
  623. int circle_collision_center_y = (int)dlg.circle_collision_center.value.y;
  624. int circle_collision_radius = (int)dlg.circle_collision_radius.value;
  625. int capsule_collision_center_x = (int)dlg.capsule_collision_center.value.x;
  626. int capsule_collision_center_y = (int)dlg.capsule_collision_center.value.y;
  627. int capsule_collision_radius = (int)dlg.capsule_collision_radius.value;
  628. int capsule_collision_height = (int)dlg.capsule_collision_height.value;
  629. int collision_x = (int)dlg.collision_xy.value.x;
  630. int collision_y = (int)dlg.collision_xy.value.y;
  631. int collision_w = (int)dlg.collision_wh.value.x;
  632. int collision_h = (int)dlg.collision_wh.value.y;
  633. string actor_class = (string)dlg.actor_class.value;
  634. bool lock_rotation_z = dlg.lock_rotation_z.active;
  635. double mass = (double)dlg.mass.value;
  636. foreach (unowned string filename_i in filenames) {
  637. GLib.File file_src = File.new_for_path(filename_i);
  638. string resource_basename;
  639. if (filenames.length() == 1)
  640. resource_basename = dlg._unit_name.value + "." + dlg._image_type;
  641. else
  642. resource_basename = file_src.get_basename();
  643. GLib.File file_dst = File.new_for_path(Path.build_filename(destination_dir, resource_basename));
  644. string resource_filename = project.resource_filename(file_dst.get_path());
  645. string resource_path = ResourceId.normalize(resource_filename);
  646. string resource_name = ResourceId.name(resource_path);
  647. try {
  648. SJSON.save(dlg.encode(), project.absolute_path(resource_name) + ".importer_settings");
  649. } catch (JsonWriteError e) {
  650. loge(e.message);
  651. return ImportResult.ERROR;
  652. }
  653. Database db = new Database(project);
  654. MaterialResource material_resource = MaterialResource.sprite(db, Guid.new_guid(), resource_name);
  655. if (material_resource.save(project, resource_name) != 0)
  656. return ImportResult.ERROR;
  657. try {
  658. file_src.copy(file_dst, FileCopyFlags.OVERWRITE);
  659. } catch (Error e) {
  660. loge(e.message);
  661. return ImportResult.ERROR;
  662. }
  663. var texture_resource = TextureResource.sprite(db, Guid.new_guid(), resource_path);
  664. if (texture_resource.save(project, resource_name) != 0)
  665. return ImportResult.ERROR;
  666. db.reset();
  667. // Generate .sprite.
  668. Guid sprite_id = Guid.new_guid();
  669. db.create(sprite_id, OBJECT_TYPE_SPRITE);
  670. db.set_double(sprite_id, "width", width);
  671. db.set_double(sprite_id, "height", height);
  672. // Create 'animations' folder.
  673. string directory_name = "animations";
  674. string animations_path = destination_dir;
  675. {
  676. GLib.File animations_file = File.new_for_path(Path.build_filename(destination_dir, directory_name));
  677. try {
  678. animations_file.make_directory();
  679. } catch (GLib.IOError.EXISTS e) {
  680. // Ignore.
  681. } catch (GLib.Error e) {
  682. loge(e.message);
  683. return ImportResult.ERROR;
  684. }
  685. animations_path = animations_file.get_path();
  686. }
  687. // Generate .sprite_animation.
  688. Guid sprite_animation_id = Guid.new_guid();
  689. SpriteAnimation sa = SpriteAnimation(db, sprite_animation_id);
  690. double frame_index = 0.0;
  691. for (int r = 0; r < num_v; ++r) {
  692. for (int c = 0; c < num_h; ++c) {
  693. Vector2 cell_xy = sprite_cell_xy(r
  694. , c
  695. , offset_x
  696. , offset_y
  697. , cell_w
  698. , cell_h
  699. , spacing_x
  700. , spacing_y
  701. );
  702. // Pivot is relative to the top-left corner of the cell
  703. int x = (int)cell_xy.x;
  704. int y = (int)cell_xy.y;
  705. Guid frame_id = Guid.new_guid();
  706. db.create(frame_id, "sprite_frame");
  707. db.set_string (frame_id, "name", "sprite_%d".printf(c + num_h*r));
  708. db.set_quaternion(frame_id, "region", Quaternion(x, y, cell_w, cell_h));
  709. db.set_vector3 (frame_id, "pivot", Vector3(x + pivot_xy.x, y + pivot_xy.y, 0.0));
  710. db.set_double (frame_id, "index", frame_index);
  711. db.add_to_set(sprite_id, "frames", frame_id);
  712. Guid anim_frame = Guid.new_guid();
  713. AnimationFrame af = AnimationFrame(db
  714. , anim_frame
  715. , (int)frame_index
  716. , (int)frame_index
  717. );
  718. sa.add_frame(af);
  719. frame_index++;
  720. }
  721. }
  722. if (db.save(project.absolute_path(resource_name) + ".sprite", sprite_id) != 0)
  723. return ImportResult.ERROR;
  724. string anim_basename = GLib.File.new_for_path(resource_name).get_basename();
  725. string anim_filename = Path.build_filename(animations_path, anim_basename + "_default" + "." + OBJECT_TYPE_SPRITE_ANIMATION);
  726. GLib.File anim_file = GLib.File.new_for_path(anim_filename);
  727. string anim_path = anim_file.get_path();
  728. string anim_resource_filename = project.resource_filename(anim_path);
  729. string anim_resource_path = ResourceId.normalize(anim_resource_filename);
  730. string anim_resource_name = ResourceId.name(anim_resource_path);
  731. if (sa.save(project, anim_resource_name) != 0)
  732. return ImportResult.ERROR;
  733. // Generate .state_machine.
  734. Guid state_machine_id = Guid.new_guid();
  735. StateMachineResource smr = StateMachineResource.sprite(db, state_machine_id, anim_resource_name);
  736. if (smr.save(project, resource_name) != 0)
  737. return ImportResult.ERROR;
  738. db.reset();
  739. // Generate or modify existing .unit.
  740. Guid unit_id;
  741. if (db.add_from_resource_path(out unit_id, resource_name + ".unit") != 0) {
  742. unit_id = Guid.new_guid();
  743. db.create(unit_id, OBJECT_TYPE_UNIT);
  744. }
  745. Unit unit = Unit(db, unit_id);
  746. // Create transform
  747. {
  748. Guid component_id;
  749. if (!unit.has_component(out component_id, OBJECT_TYPE_TRANSFORM)) {
  750. component_id = Guid.new_guid();
  751. db.create(component_id, OBJECT_TYPE_TRANSFORM);
  752. db.add_to_set(unit_id, "components", component_id);
  753. }
  754. unit.set_component_vector3 (component_id, "data.position", VECTOR3_ZERO);
  755. unit.set_component_quaternion(component_id, "data.rotation", QUATERNION_IDENTITY);
  756. unit.set_component_vector3 (component_id, "data.scale", VECTOR3_ONE);
  757. }
  758. // Create sprite_renderer
  759. {
  760. Guid component_id;
  761. if (!unit.has_component(out component_id, OBJECT_TYPE_SPRITE_RENDERER)) {
  762. component_id = Guid.new_guid();
  763. db.create(component_id, OBJECT_TYPE_SPRITE_RENDERER);
  764. db.add_to_set(unit_id, "components", component_id);
  765. }
  766. unit.set_component_string(component_id, "data.material", resource_name);
  767. unit.set_component_string(component_id, "data.sprite_resource", resource_name);
  768. unit.set_component_double(component_id, "data.layer", layer);
  769. unit.set_component_double(component_id, "data.depth", depth);
  770. unit.set_component_bool (component_id, "data.visible", true);
  771. }
  772. // Create state_machine_component.
  773. {
  774. Guid component_id;
  775. if (!unit.has_component(out component_id, OBJECT_TYPE_ANIMATION_STATE_MACHINE)) {
  776. component_id = Guid.new_guid();
  777. db.create(component_id, OBJECT_TYPE_ANIMATION_STATE_MACHINE);
  778. db.add_to_set(unit_id, "components", component_id);
  779. }
  780. unit.set_component_string(component_id, "data.state_machine_resource", resource_name);
  781. }
  782. if (collision_enabled) {
  783. // Create collider
  784. double PIXELS_PER_METER = 32.0;
  785. {
  786. Quaternion rotation = QUATERNION_IDENTITY;
  787. Guid component_id;
  788. if (!unit.has_component(out component_id, OBJECT_TYPE_COLLIDER)) {
  789. component_id = Guid.new_guid();
  790. db.create(component_id, OBJECT_TYPE_COLLIDER);
  791. db.add_to_set(unit_id, "components", component_id);
  792. }
  793. unit.set_component_string(component_id, "data.source", "inline");
  794. if (shape_active_name == "square_collider") {
  795. double pos_x = (collision_x + collision_w/2.0 - pivot_xy.x) / PIXELS_PER_METER;
  796. double pos_y = -(collision_y + collision_h/2.0 - pivot_xy.y) / PIXELS_PER_METER;
  797. Vector3 position = Vector3(pos_x, pos_y, 0);
  798. Vector3 half_extents = Vector3(collision_w/2/PIXELS_PER_METER, collision_h/2/PIXELS_PER_METER, 0.5/PIXELS_PER_METER);
  799. unit.set_component_vector3 (component_id, "data.collider_data.position", position);
  800. unit.set_component_quaternion(component_id, "data.collider_data.rotation", rotation);
  801. unit.set_component_string (component_id, "data.shape", "box");
  802. unit.set_component_vector3 (component_id, "data.collider_data.half_extents", half_extents);
  803. } else if (shape_active_name == "circle_collider") {
  804. double pos_x = (circle_collision_center_x - pivot_xy.x) / PIXELS_PER_METER;
  805. double pos_y = -(circle_collision_center_y - pivot_xy.y) / PIXELS_PER_METER;
  806. Vector3 position = Vector3(pos_x, pos_y, 0);
  807. double radius = circle_collision_radius / PIXELS_PER_METER;
  808. unit.set_component_vector3 (component_id, "data.collider_data.position", position);
  809. unit.set_component_quaternion(component_id, "data.collider_data.rotation", rotation);
  810. unit.set_component_string (component_id, "data.shape", "sphere");
  811. unit.set_component_double (component_id, "data.collider_data.radius", radius);
  812. } else if (shape_active_name == "capsule_collider") {
  813. double pos_x = (capsule_collision_center_x - pivot_xy.x) / PIXELS_PER_METER;
  814. double pos_y = -(capsule_collision_center_y - pivot_xy.y) / PIXELS_PER_METER;
  815. Vector3 position = Vector3(pos_x, pos_y, 0);
  816. double radius = capsule_collision_radius / PIXELS_PER_METER;
  817. double capsule_height = (capsule_collision_height - 2*capsule_collision_radius) / PIXELS_PER_METER;
  818. unit.set_component_vector3 (component_id, "data.collider_data.position", position);
  819. unit.set_component_quaternion(component_id, "data.collider_data.rotation", Quaternion.from_axis_angle(Vector3(0, 0, 1), (float)Math.PI/2));
  820. unit.set_component_string (component_id, "data.shape", "capsule");
  821. unit.set_component_double (component_id, "data.collider_data.radius", radius);
  822. unit.set_component_double (component_id, "data.collider_data.height", capsule_height);
  823. }
  824. }
  825. // Create actor
  826. {
  827. Guid component_id;
  828. if (!unit.has_component(out component_id, OBJECT_TYPE_ACTOR)) {
  829. component_id = Guid.new_guid();
  830. db.create(component_id, OBJECT_TYPE_ACTOR);
  831. db.add_to_set(unit_id, "components", component_id);
  832. }
  833. unit.set_component_string(component_id, "data.class", actor_class);
  834. unit.set_component_string(component_id, "data.collision_filter", "default");
  835. unit.set_component_bool (component_id, "data.lock_rotation_x", true);
  836. unit.set_component_bool (component_id, "data.lock_rotation_z", true);
  837. unit.set_component_bool (component_id, "data.lock_rotation_z", lock_rotation_z);
  838. unit.set_component_bool (component_id, "data.lock_translation_x", false);
  839. unit.set_component_bool (component_id, "data.lock_translation_y", false);
  840. unit.set_component_bool (component_id, "data.lock_translation_z", true);
  841. unit.set_component_double(component_id, "data.mass", mass);
  842. unit.set_component_string(component_id, "data.material", "default");
  843. }
  844. } else { /* if (collision_enabled) */
  845. // Destroy collider and actor if any
  846. Guid component_id;
  847. if (unit.has_component(out component_id, OBJECT_TYPE_COLLIDER)) {
  848. db.remove_from_set(unit_id, "components", component_id);
  849. db.destroy(component_id);
  850. }
  851. if (unit.has_component(out component_id, OBJECT_TYPE_ACTOR)) {
  852. db.remove_from_set(unit_id, "components", component_id);
  853. db.destroy(component_id);
  854. }
  855. }
  856. if (db.save(project.absolute_path(resource_name) + ".unit", unit_id) != 0)
  857. return ImportResult.ERROR;
  858. }
  859. return ImportResult.SUCCESS;
  860. }
  861. public static void import(Import import_result, Database database, string destination_dir, SList<string> filenames, Gtk.Window? parent_window)
  862. {
  863. SpriteImportDialog dlg = new SpriteImportDialog(database, destination_dir, filenames, import_result);
  864. dlg.set_transient_for(parent_window);
  865. dlg.set_modal(true);
  866. dlg.show_all();
  867. }
  868. }
  869. } /* namespace Crown */