فهرست منبع

Add initial resource importers

Daniele Bartolini 9 سال پیش
والد
کامیت
8144bca6e6

+ 1 - 1
tools/level_editor/level.vala

@@ -72,7 +72,7 @@ namespace Crown
 		/// Loads the empty level template.
 		public void load_empty_level()
 		{
-			load(_toolchain_dir + "core/editors/levels/empty.level");
+			load(_toolchain_dir + "/" + "core/editors/levels/empty.level");
 		}
 
 		public void spawn_unit(Guid id, string name, Vector3 pos, Quaternion rot, Vector3 scl)

+ 148 - 51
tools/level_editor/level_editor.vala

@@ -89,45 +89,49 @@ namespace Crown
 
 		const Gtk.ActionEntry[] action_entries =
 		{
-			{ "menu-file",            null,  "_File",            null,             null, null                   },
-			{ "new",                  null,  "New",              "<ctrl>N",        null, on_new                 },
-			{ "open",                 null,  "Open",             "<ctrl>O",        null, on_open                },
-			{ "save",                 null,  "Save",             "<ctrl>S",        null, on_save                },
-			{ "save-as",              null,  "Save As...",       null,             null, on_save_as             },
-			{ "import",               null,  "Import...",        null,             null, on_import              },
-			{ "preferences",          null,  "Preferences",      null,             null, on_preferences         },
-			{ "quit",                 null,  "Quit",             "<ctrl>Q",        null, on_quit                },
-			{ "menu-edit",            null,  "_Edit",            null,             null, null                   },
-			{ "undo",                 null,  "Undo",             "<ctrl>Z",        null, on_undo                },
-			{ "redo",                 null,  "Redo",             "<shift><ctrl>Z", null, on_redo                },
-			{ "duplicate",            null,  "Duplicate",        "<ctrl>D",        null, on_duplicate           },
-			{ "delete",               null,  "Delete",           "<ctrl>K",        null, on_delete              },
-			{ "menu-grid",            null,  "Grid",             null,             null, null                   },
-			{ "grid-custom",          null,  "Custom",           "G",              null, on_custom_grid         },
-			{ "menu-rotation-snap",   null,  "Rotation Snap",    null,             null, null                   },
-			{ "rotation-snap-custom", null,  "Custom",           "H",              null, on_rotation_snap       },
-			{ "menu-create",          null,  "Create",           null,             null, null                   },
-			{ "menu-primitives",      null,  "Primitives",       null,             null, null                   },
-			{ "primitive-cube",       null,  "Cube",             null,             null, on_create_cube         },
-			{ "primitive-sphere",     null,  "Sphere",           null,             null, on_create_sphere       },
-			{ "primitive-cone",       null,  "Cone",             null,             null, on_create_cone         },
-			{ "primitive-cylinder",   null,  "Cylinder",         null,             null, on_create_cylinder     },
-			{ "primitive-plane",      null,  "Plane",            null,             null, on_create_plane        },
-			{ "camera",               null,  "Camera",           null,             null, on_create_camera       },
-			{ "light",                null,  "Light",            null,             null, on_create_light        },
-			{ "sound-source",         null,  "Sound Source",     null,             null, on_create_sound_source },
-			{ "menu-engine",          null,  "En_gine",          null,             null, null                   },
-			{ "menu-view",            null,  "View",             null,             null, null                   },
-			{ "resource-browser",     null,  "Resource Browser", "<ctrl>P",        null, on_resource_browser    },
-			{ "restart",              null,  "_Restart",         null,             null, on_engine_restart      },
-			{ "reload-lua",           null,  "Reload Lua",       "F7",             null, on_reload_lua          },
-			{ "menu-run",             null,  "_Run",             null,             null, null                   },
-			{ "game-run",             "run", "Run Game",         "F5",             null, on_run_game            },
-			{ "menu-help",            null,  "Help",             null,             null, null                   },
-			{ "manual",               null,  "Manual",           "F1",             null, on_manual              },
-			{ "report-issue",         null,  "Report an Issue",  null,             null, on_report_issue        },
-			{ "open-last-log",        null,  "Open last.log",    null,             null, on_open_last_log       },
-			{ "about",                null,  "About",            null,             null, on_about               }
+			{ "menu-file",            null,  "_File",              null,             null, null                   },
+			{ "new",                  null,  "New",                "<ctrl>N",        null, on_new                 },
+			{ "open",                 null,  "Open...",            "<ctrl>O",        null, on_open                },
+			{ "save",                 null,  "Save",               "<ctrl>S",        null, on_save                },
+			{ "save-as",              null,  "Save As...",         null,             null, on_save_as             },
+			{ "import",               null,  "Import",             null,             null, null                   },
+			{ "import-sprites",       null,  "Sprites...",         null,             null, on_import_sprites      },
+			{ "import-meshes",        null,  "Meshes...",          null,             null, on_import_meshes       },
+			{ "import-sounds",        null,  "Sounds...",          null,             null, on_import_sounds       },
+			{ "import-textures",      null,  "Textures...",        null,             null, on_import_textures     },
+			{ "preferences",          null,  "Preferences",        null,             null, on_preferences         },
+			{ "quit",                 null,  "Quit",               "<ctrl>Q",        null, on_quit                },
+			{ "menu-edit",            null,  "_Edit",              null,             null, null                   },
+			{ "undo",                 null,  "Undo",               "<ctrl>Z",        null, on_undo                },
+			{ "redo",                 null,  "Redo",               "<shift><ctrl>Z", null, on_redo                },
+			{ "duplicate",            null,  "Duplicate",          "<ctrl>D",        null, on_duplicate           },
+			{ "delete",               null,  "Delete",             "<ctrl>K",        null, on_delete              },
+			{ "menu-grid",            null,  "Grid",               null,             null, null                   },
+			{ "grid-custom",          null,  "Custom",             "G",              null, on_custom_grid         },
+			{ "menu-rotation-snap",   null,  "Rotation Snap",      null,             null, null                   },
+			{ "rotation-snap-custom", null,  "Custom",             "H",              null, on_rotation_snap       },
+			{ "menu-create",          null,  "Create",             null,             null, null                   },
+			{ "menu-primitives",      null,  "Primitives",         null,             null, null                   },
+			{ "primitive-cube",       null,  "Cube",               null,             null, on_create_cube         },
+			{ "primitive-sphere",     null,  "Sphere",             null,             null, on_create_sphere       },
+			{ "primitive-cone",       null,  "Cone",               null,             null, on_create_cone         },
+			{ "primitive-cylinder",   null,  "Cylinder",           null,             null, on_create_cylinder     },
+			{ "primitive-plane",      null,  "Plane",              null,             null, on_create_plane        },
+			{ "camera",               null,  "Camera",             null,             null, on_create_camera       },
+			{ "light",                null,  "Light",              null,             null, on_create_light        },
+			{ "sound-source",         null,  "Sound Source",       null,             null, on_create_sound_source },
+			{ "menu-engine",          null,  "En_gine",            null,             null, null                   },
+			{ "menu-view",            null,  "View",               null,             null, null                   },
+			{ "resource-browser",     null,  "Resource Browser",   "<ctrl>P",        null, on_resource_browser    },
+			{ "restart",              null,  "_Restart",           null,             null, on_engine_restart      },
+			{ "reload-lua",           null,  "Reload Lua",         "F7",             null, on_reload_lua          },
+			{ "menu-run",             null,  "_Run",               null,             null, null                   },
+			{ "game-run",             "run", "Run Game",           "F5",             null, on_run_game            },
+			{ "menu-help",            null,  "Help",               null,             null, null                   },
+			{ "manual",               null,  "Manual",             "F1",             null, on_manual              },
+			{ "report-issue",         null,  "Report an Issue",    null,             null, on_report_issue        },
+			{ "open-last-log",        null,  "Open last.log",      null,             null, on_open_last_log       },
+			{ "about",                null,  "About",              null,             null, on_about               }
 		};
 
 		const Gtk.RadioActionEntry[] grid_entries =
@@ -323,6 +327,12 @@ namespace Crown
 			_file_filter.set_filter_name("Level (*.level)");
 			_file_filter.add_pattern("*.level");
 
+			_resource_browser = new ResourceBrowser(_project);
+			_resource_browser.relative_to = _toolbar;
+			_resource_browser.resource_selected.connect(on_resource_browser_resource_selected);
+			_resource_browser.delete_event.connect(() => { _resource_browser.hide(); return true; });
+			_resource_browser.modal = true;
+
 			// Save level once every 5 minutes.
 			GLib.Timeout.add_seconds(5*3600, save_timeout);
 
@@ -903,7 +913,7 @@ namespace Crown
 			save_as();
 		}
 
-		private void on_import(Gtk.Action action)
+		private void on_import_begin(Gtk.FileFilter ff, out SList<string> filenames, out string filename)
 		{
 			FileChooserDialog fcd = new FileChooserDialog("Import..."
 				, this
@@ -914,13 +924,109 @@ namespace Crown
 				, ResponseType.ACCEPT
 				);
 			fcd.select_multiple = true;
+			fcd.add_filter(ff);
 
-			if (fcd.run() == (int)ResponseType.ACCEPT)
+			if (fcd.run() != (int)ResponseType.ACCEPT)
 			{
-				// TODO
+				fcd.destroy();
+				return;
 			}
 
+			filenames = fcd.get_filenames();
 			fcd.destroy();
+
+			FileChooserDialog dst = new FileChooserDialog("Select destination folder..."
+				, this
+				, FileChooserAction.SELECT_FOLDER
+				, "Cancel"
+				, ResponseType.CANCEL
+				, "Select"
+				, ResponseType.ACCEPT
+				);
+			dst.set_current_folder(_project.source_dir());
+
+			if (dst.run() != (int)ResponseType.ACCEPT)
+			{
+				dst.destroy();
+				return;
+			}
+
+			filename = dst.get_filename();
+			dst.destroy();
+		}
+
+		private void on_import_end()
+		{
+			_resource_compiler.compile.begin(_project.data_dir(), _project.platform(), (obj, res) => {
+				if (_resource_compiler.compile.end(res))
+				{
+					_project.scan_source_dir();
+				}
+			});
+		}
+
+		private void on_import_sprites(Gtk.Action action)
+		{
+			Gtk.FileFilter ff = new FileFilter();
+			ff.set_filter_name("Sprite (*.png)");
+			ff.add_pattern("*.png");
+
+			SList<string> filenames;
+			string filename;
+			on_import_begin(ff, out filenames, out filename);
+
+			_project.import_sprites(filenames, filename);
+
+			on_import_end();
+		}
+
+		private void on_import_meshes(Gtk.Action action)
+		{
+			Gtk.FileFilter ff = new FileFilter();
+			ff.set_filter_name("Mesh (*.mesh)");
+			ff.add_pattern("*.mesh");
+
+			SList<string> filenames;
+			string filename;
+			on_import_begin(ff, out filenames, out filename);
+
+			_project.import_meshes(filenames, filename);
+
+			on_import_end();
+		}
+
+		private void on_import_sounds(Gtk.Action action)
+		{
+			Gtk.FileFilter ff = new FileFilter();
+			ff.set_filter_name("Sound (*.wav)");
+			ff.add_pattern("*.wav");
+
+			SList<string> filenames;
+			string filename;
+			on_import_begin(ff, out filenames, out filename);
+
+			_project.import_sounds(filenames, filename);
+
+			on_import_end();
+		}
+
+		private void on_import_textures(Gtk.Action action)
+		{
+			Gtk.FileFilter ff = new FileFilter();
+			ff.set_filter_name("Texture (*.png, *.tga, *.dds, *.ktx, *.pvr)");
+			ff.add_pattern("*.png");
+			ff.add_pattern("*.tga");
+			ff.add_pattern("*.dds");
+			ff.add_pattern("*.ktx");
+			ff.add_pattern("*.pvr");
+
+			SList<string> filenames;
+			string filename;
+			on_import_begin(ff, out filenames, out filename);
+
+			_project.import_textures(filenames, filename);
+
+			on_import_end();
 		}
 
 		private void on_preferences(Gtk.Action action)
@@ -1033,15 +1139,6 @@ namespace Crown
 
 		private void on_resource_browser(Gtk.Action action)
 		{
-			if (_resource_browser == null)
-			{
-				_resource_browser = new ResourceBrowser(_project);
-				_resource_browser.relative_to = _toolbar;
-				_resource_browser.resource_selected.connect(on_resource_browser_resource_selected);
-				_resource_browser.delete_event.connect(() => { _resource_browser.hide(); return true; });
-				_resource_browser.modal = true;
-			}
-
 			_resource_browser.show_all();
 		}
 

+ 285 - 16
tools/level_editor/project.vala

@@ -3,18 +3,23 @@
  * License: https://github.com/taylor001/crown/blob/master/LICENSE-GPLv2
  */
 
+using Gtk;
+using Gee;
+
 namespace Crown
 {
 	public class Project
 	{
 		// Data
-		private string _source_dir;
-		private string _toolchain_dir;
-		private string _data_dir;
+		private File _source_dir;
+		private File _toolchain_dir;
+		private File _data_dir;
 		private string _platform;
 
 		private Database _files;
 
+		public signal void changed();
+
 		public Project()
 		{
 			_source_dir = null;
@@ -27,26 +32,26 @@ namespace Crown
 
 		public void load(string source_dir, string toolchain_dir, string data_dir)
 		{
-			_source_dir = source_dir;
-			_toolchain_dir = toolchain_dir;
-			_data_dir = data_dir;
+			_source_dir    = File.new_for_path(source_dir);
+			_toolchain_dir = File.new_for_path(toolchain_dir);
+			_data_dir      = File.new_for_path(data_dir);
 
 			scan_source_dir();
 		}
 
 		public string source_dir()
 		{
-			return _source_dir;
+			return _source_dir.get_path();
 		}
 
 		public string toolchain_dir()
 		{
-			return _toolchain_dir;
+			return _toolchain_dir.get_path();
 		}
 
 		public string data_dir()
 		{
-			return _data_dir;
+			return _data_dir.get_path();
 		}
 
 		public string platform()
@@ -59,9 +64,11 @@ namespace Crown
 			return _files;
 		}
 
-		private void scan_source_dir()
+		public void scan_source_dir()
 		{
-			list_directory_entries(File.new_for_path(_source_dir));
+			_files.reset();
+			list_directory_entries(_source_dir);
+			changed();
 		}
 
 		private void list_directory_entries(File dir, Cancellable? cancellable = null) throws Error
@@ -72,7 +79,7 @@ namespace Crown
 				);
 
 			FileInfo info = null;
-			while (cancellable.is_cancelled () == false && ((info = enumerator.next_file (cancellable)) != null))
+			while (!cancellable.is_cancelled() && ((info = enumerator.next_file (cancellable)) != null))
 			{
 				if (info.get_file_type () == FileType.DIRECTORY)
 				{
@@ -81,10 +88,10 @@ namespace Crown
 				}
 				else
 				{
-					string path = dir.get_path() + "/" + info.get_name();
-					string path_rel = File.new_for_path(_source_dir).get_relative_path(File.new_for_path(path));
-					string name = path_rel.substring(0, path_rel.last_index_of("."));
-					string type = path_rel.substring(path_rel.last_index_of(".") + 1);
+					string path     = dir.get_path() + "/" + info.get_name();
+					string path_rel = _source_dir.get_relative_path(File.new_for_path(path));
+					string name     = path_rel.substring(0, path_rel.last_index_of("."));
+					string type     = path_rel.substring(path_rel.last_index_of(".") + 1);
 
 					Guid id = Guid.new_guid();
 					_files.create(id);
@@ -100,5 +107,267 @@ namespace Crown
 				throw new IOError.CANCELLED("Operation was cancelled");
 			}
 		}
+
+		public void import_sprites(SList<string> filenames, string destination_dir)
+		{
+			SpriteImportDialog sid = new SpriteImportDialog(filenames.nth_data(0));
+
+			if (sid.run() != Gtk.ResponseType.OK)
+			{
+				sid.destroy();
+				return;
+			}
+
+			int width     = (int)sid._pixbuf.width;
+			int height    = (int)sid._pixbuf.height;
+			int num_h     = (int)sid.cells_h.value;
+			int num_v     = (int)sid.cells_v.value;
+			int cell_w    = (int)sid.cell_w.value;
+			int cell_h    = (int)sid.cell_h.value;
+			int offset_x  = (int)sid.offset_x.value;
+			int offset_y  = (int)sid.offset_y.value;
+			int spacing_x = (int)sid.spacing_x.value;
+			int spacing_y = (int)sid.spacing_y.value;
+
+			Vector2 pivot_xy = sprite_cell_pivot_xy(cell_w, cell_h, sid.pivot.active);
+
+			sid.destroy();
+
+			foreach (unowned string filename_i in filenames)
+			{
+				GLib.File file_src = File.new_for_path(filename_i);
+				GLib.File file_dst = File.new_for_path(destination_dir + "/" + file_src.get_basename());
+
+				string dst_dir_rel    = _source_dir.get_relative_path(File.new_for_path(destination_dir));
+				string basename       = file_src.get_basename();
+				string basename_noext = basename.substring(0, basename.last_index_of_char('.'));
+				string dst_noext      = file_dst.get_path().substring(0, file_dst.get_path().last_index_of_char('.'));
+
+				if (!filename_i.has_suffix(".png"))
+					continue;
+
+				Hashtable textures = new Hashtable();
+				textures["u_albedo"] = dst_dir_rel + "/" + basename_noext;
+
+				Hashtable uniform = new Hashtable();
+				uniform["type"]  = "vector4";
+				uniform["value"] = Vector4(1.0, 1.0, 1.0, 1.0).to_array();
+
+				Hashtable uniforms = new Hashtable();
+				uniforms["u_color"] = uniform;
+
+				Hashtable material = new Hashtable();
+				material["shader"]   = "sprite";
+				material["textures"] = textures;
+				material["uniforms"] = uniforms;
+				SJSON.save(material, dst_noext + ".material");
+
+				file_src.copy(file_dst, FileCopyFlags.OVERWRITE);
+
+				Hashtable texture = new Hashtable();
+				texture["source"]        = dst_dir_rel + "/" + basename;
+				texture["generate_mips"] = false;
+				texture["is_normalmap"]  = false;
+				SJSON.save(texture, dst_noext + ".texture");
+
+				Hashtable sprite = new Hashtable();
+				sprite["width"]  = width;
+				sprite["height"] = height;
+
+				ArrayList<Value?> frames = new ArrayList<Value?>();
+				for (int r = 0; r < num_v; ++r)
+				{
+					for (int c = 0; c < num_h; ++c)
+					{
+						Vector2 cell_xy = sprite_cell_xy(r
+							, c
+							, offset_x
+							, offset_y
+							, cell_w
+							, cell_h
+							, spacing_x
+							, spacing_y
+							);
+
+						// Pivot is relative to the top-left corner of the cell
+						int x = (int)cell_xy.x;
+						int y = (int)cell_xy.y;
+
+						Hashtable data = new Hashtable();
+						data["name"]   = "sprite_%d".printf(c+num_h*r);
+						data["region"] = Vector4(x, y, cell_w, cell_h).to_array();
+						data["pivot"]  = Vector2(x+pivot_xy.x, y+pivot_xy.y).to_array();
+						frames.add(data);
+					}
+				}
+				sprite["frames"] = frames;
+
+				SJSON.save(sprite, dst_noext + ".sprite");
+
+				Hashtable data = new Hashtable();
+				data["position"] = VECTOR3_ZERO.to_array();
+				data["rotation"] = QUATERNION_IDENTITY.to_array();
+				data["scale"]    = VECTOR3_ONE.to_array();
+
+				Hashtable comp = new Hashtable();
+				comp["data"] = data;
+				comp["type"] = "transform";
+
+				Hashtable components = new Hashtable();
+				components[Guid.new_guid().to_string()] = comp;
+
+				data = new Hashtable();
+				data["material"]        = dst_dir_rel + "/" + basename_noext;
+				data["sprite_resource"] = dst_dir_rel + "/" + basename_noext;
+				data["visible"]         = true;
+
+				comp = new Hashtable();
+				comp["data"] = data;
+				comp["type"] = "sprite_renderer";
+
+				components[Guid.new_guid().to_string()] = comp;
+
+				Hashtable unit = new Hashtable();
+				unit["components"] = components;
+
+				SJSON.save(unit, dst_noext + ".unit");
+			}
+		}
+
+		public void import_meshes(SList<string> filenames, string destination_dir)
+		{
+			foreach (unowned string filename_i in filenames)
+			{
+				GLib.File file_src = File.new_for_path(filename_i);
+				GLib.File file_dst = File.new_for_path(destination_dir + "/" + file_src.get_basename());
+
+				string dst_dir_rel    = _source_dir.get_relative_path(File.new_for_path(destination_dir));
+				string basename       = file_src.get_basename();
+				string basename_noext = basename.substring(0, basename.last_index_of_char('.'));
+				string dst_noext      = file_dst.get_path().substring(0, file_dst.get_path().last_index_of_char('.'));
+
+				if (!filename_i.has_suffix(".mesh"))
+					continue;
+
+				// Choose material or create new one
+				FileChooserDialog mtl = new FileChooserDialog("Select material... (Cancel to create a new one)"
+					, null
+					, FileChooserAction.OPEN
+					, "Cancel"
+					, ResponseType.CANCEL
+					, "Select"
+					, ResponseType.ACCEPT
+					);
+				mtl.set_current_folder(_source_dir.get_path());
+
+				FileFilter fltr = new FileFilter();
+				fltr.set_filter_name("Material (*.material)");
+				fltr.add_pattern("*.material");
+				mtl.add_filter(fltr);
+
+				string material_name = dst_dir_rel + "/" + basename_noext;
+				if (mtl.run() == (int)ResponseType.ACCEPT)
+				{
+					material_name = _source_dir.get_relative_path(File.new_for_path(mtl.get_filename()));
+					material_name = material_name.substring(0, material_name.last_index_of_char('.'));
+				}
+				else
+				{
+					Hashtable material = new Hashtable();
+					material["shader"]   = "mesh+DIFFUSE_MAP";
+					material["textures"] = new Hashtable();
+					material["uniforms"] = new Hashtable();
+					SJSON.save(material, dst_noext + ".material");
+				}
+				mtl.destroy();
+
+				file_src.copy(file_dst, FileCopyFlags.OVERWRITE);
+
+				Hashtable data = new Hashtable();
+				data["position"] = VECTOR3_ZERO.to_array();
+				data["rotation"] = QUATERNION_IDENTITY.to_array();
+				data["scale"]    = VECTOR3_ONE.to_array();
+
+				Hashtable comp = new Hashtable();
+				comp["data"] = data;
+				comp["type"] = "transform";
+
+				Hashtable components = new Hashtable();
+				components[Guid.new_guid().to_string()] = comp;
+
+				Hashtable mesh = SJSON.load(filename_i);
+				Hashtable mesh_nodes = (Hashtable)mesh["nodes"];
+				foreach (var entry in mesh_nodes.entries)
+				{
+					string node_name = (string)entry.key;
+
+					data = new Hashtable();
+					data["geometry_name"] = node_name;
+					data["material"]      = material_name;
+					data["mesh_resource"] = dst_dir_rel + "/" + basename_noext;
+					data["visible"]       = true;
+
+					comp = new Hashtable();
+					comp["data"] = data;
+					comp["type"] = "mesh_renderer";
+
+					components[Guid.new_guid().to_string()] = comp;
+				}
+
+				Hashtable unit = new Hashtable();
+				unit["components"] = components;
+
+				SJSON.save(unit, dst_noext + ".unit");
+			}
+		}
+
+		public void import_sounds(SList<string> filenames, string destination_dir)
+		{
+			foreach (unowned string filename_i in filenames)
+			{
+				GLib.File file_src = File.new_for_path(filename_i);
+				GLib.File file_dst = File.new_for_path(destination_dir + "/" + file_src.get_basename());
+
+				string dst_dir_rel    = _source_dir.get_relative_path(File.new_for_path(destination_dir));
+				string basename       = file_src.get_basename();
+				string basename_noext = basename.substring(0, basename.last_index_of_char('.'));
+				string dst_noext      = file_dst.get_path().substring(0, file_dst.get_path().last_index_of_char('.'));
+
+				if (!filename_i.has_suffix(".wav"))
+					continue;
+
+				file_src.copy(file_dst, FileCopyFlags.OVERWRITE);
+
+				Hashtable sound = new Hashtable();
+				sound["source"] = dst_dir_rel + "/" + basename;
+
+				SJSON.save(sound, dst_noext + ".sound");
+			}
+		}
+
+		public void import_textures(SList<string> filenames, string destination_dir)
+		{
+			foreach (unowned string filename_i in filenames)
+			{
+				GLib.File file_src = File.new_for_path(filename_i);
+				GLib.File file_dst = File.new_for_path(destination_dir + "/" + file_src.get_basename());
+
+				string dst_dir_rel    = _source_dir.get_relative_path(File.new_for_path(destination_dir));
+				string basename       = file_src.get_basename();
+				string basename_noext = basename.substring(0, basename.last_index_of_char('.'));
+				string dst_noext      = file_dst.get_path().substring(0, file_dst.get_path().last_index_of_char('.'));
+
+				if (!filename_i.has_suffix(".png"))
+
+				file_src.copy(file_dst, FileCopyFlags.OVERWRITE);
+
+				Hashtable texture = new Hashtable();
+				texture["source"]        = dst_dir_rel + "/" + basename;
+				texture["generate_mips"] = true;
+				texture["is_normalmap"]  = false;
+
+				SJSON.save(texture, dst_noext + ".texture");
+			}
+		}
 	}
 }

+ 25 - 11
tools/level_editor/resource_browser.vala

@@ -50,6 +50,8 @@ namespace Crown
 		{
 			// Data
 			_project = project;
+			_project.changed.connect(on_project_changed);
+
 			_console_client = new ConsoleClient();
 
 			// Widgets
@@ -92,17 +94,7 @@ namespace Crown
 			_scrolled_window.add(_tree_view);
 			_scrolled_window.set_size_request(300, 400);
 
-			Database db = _project.files();
-			HashSet<Guid?> files = db.get_property(GUID_ZERO, "data") as HashSet<Guid?>;
-			files.foreach((id) => {
-				Gtk.TreeIter resource_iter;
-				_tree_store.append(out resource_iter, null);
-				string name = (string)db.get_property(id, "name");
-				string type = (string)db.get_property(id, "type");
-				_tree_store.set(resource_iter, 0, name, 1, type, -1);
-				return true;
-			});
-			_tree_filter.refilter();
+			read_project();
 
 			_engine_view = new EngineView(_console_client, false);
 			_engine_view.realized.connect(on_engine_view_realized);
@@ -300,5 +292,27 @@ namespace Crown
 				_console_client.send_script(UnitPreviewApi.set_preview_resource((string)type, (string)name));
 			}
 		}
+
+		private void read_project()
+		{
+			_tree_store.clear();
+
+			Database db = _project.files();
+			HashSet<Guid?> files = db.get_property(GUID_ZERO, "data") as HashSet<Guid?>;
+			files.foreach((id) => {
+				Gtk.TreeIter resource_iter;
+				_tree_store.append(out resource_iter, null);
+				string name = (string)db.get_property(id, "name");
+				string type = (string)db.get_property(id, "type");
+				_tree_store.set(resource_iter, 0, name, 1, type, -1);
+				return true;
+			});
+			_tree_filter.refilter();
+		}
+
+		private void on_project_changed()
+		{
+			read_project();
+		}
 	}
 }

+ 328 - 0
tools/level_editor/sprite_import_dialog.vala

@@ -0,0 +1,328 @@
+/*
+ * Copyright (c) 2012-2017 Daniele Bartolini and individual contributors.
+ * License: https://github.com/taylor001/crown/blob/master/LICENSE-GPLv2
+ */
+
+using Cairo;
+using Gdk;
+using Gtk;
+
+namespace Crown
+{
+public enum Pivot
+{
+	TOP_LEFT,
+	TOP_CENTER,
+	TOP_RIGHT,
+	LEFT,
+	CENTER,
+	RIGHT,
+	BOTTOM_LEFT,
+	BOTTOM_CENTER,
+	BOTTOM_RIGHT
+}
+
+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)
+{
+	int x0 = offset_x + c*cell_w + c*spacing_x;
+	int y0 = offset_y + r*cell_h + r*spacing_y;
+	return Vector2(x0, y0);
+}
+
+Vector2 sprite_cell_pivot_xy(int cell_w, int cell_h, int pivot)
+{
+	int pivot_x = 0;
+	int pivot_y = 0;
+
+	switch (pivot)
+	{
+	case Pivot.TOP_LEFT:
+		pivot_x = 0;
+		pivot_y = 0;
+		break;
+
+	case Pivot.TOP_CENTER:
+		pivot_x = cell_w / 2;
+		pivot_y = 0;
+		break;
+
+	case Pivot.TOP_RIGHT:
+		pivot_x = cell_w;
+		pivot_y = 0;
+		break;
+
+	case Pivot.BOTTOM_LEFT:
+		pivot_x = 0;
+		pivot_y = cell_h;
+		break;
+
+	case Pivot.BOTTOM_CENTER:
+		pivot_x = cell_w / 2;
+		pivot_y = cell_h;
+		break;
+
+	case Pivot.BOTTOM_RIGHT:
+		pivot_x = cell_w;
+		pivot_y = cell_h;
+		break;
+
+	case Pivot.LEFT:
+		pivot_x = 0;
+		pivot_y = cell_h / 2;
+		break;
+
+	case Pivot.CENTER:
+		pivot_x = cell_w / 2;
+		pivot_y = cell_h / 2;
+		break;
+
+	case Pivot.RIGHT:
+		pivot_x = cell_w;
+		pivot_y = cell_h / 2;
+		break;
+
+	default:
+		assert(false);
+		break;
+	}
+
+	return Vector2(pivot_x, pivot_y);
+}
+
+Gtk.Label label_with_alignment(string text, Gtk.Align align)
+{
+	var l = new Label(text);
+	l.halign = align;
+	return l;
+}
+
+public class SpriteImportDialog : Gtk.Dialog
+{
+	public Gdk.Pixbuf _pixbuf;
+	public Gtk.DrawingArea da;
+
+	public Gtk.Label resolution;
+	public Gtk.SpinButton cells_h;
+	public Gtk.SpinButton cells_v;
+	public Gtk.CheckButton cell_wh_auto;
+	public Gtk.SpinButton cell_w;
+	public Gtk.SpinButton cell_h;
+	public Gtk.SpinButton offset_x;
+	public Gtk.SpinButton offset_y;
+	public Gtk.SpinButton spacing_x;
+	public Gtk.SpinButton spacing_y;
+	public Gtk.ComboBoxText pivot;
+
+	// Widgets
+	public SpriteImportDialog(string png)
+	{
+		this.border_width = 18;
+		this.title = "Import Sprite...";
+
+		try {
+			_pixbuf = new Gdk.Pixbuf.from_file(png);
+		} catch(GLib.Error err) {
+			stdout.printf("Pixbuf.from_file: error");
+		}
+
+		da = new Gtk.DrawingArea();
+		da.set_size_request(_pixbuf.width, _pixbuf.height);
+
+		da.draw.connect((cr) => {
+				Gdk.cairo_set_source_pixbuf(cr, _pixbuf, 0, 0);
+				cr.paint();
+
+				// Pivot is relative to the top-left corner of the cell
+				Vector2 pivot = sprite_cell_pivot_xy((int)cell_w.value
+					, (int)cell_h.value
+					, (int)pivot.active
+					);
+
+				int num_v = (int)cells_v.value;
+				int num_h = (int)cells_h.value;
+
+				for (int h = 0; h < num_v; ++h)
+				{
+					for (int w = 0; w < num_h; ++w)
+					{
+						Vector2 cell = sprite_cell_xy(h
+							, w
+							, (int)offset_x.value
+							, (int)offset_y.value
+							, (int)cell_w.value
+							, (int)cell_h.value
+							, (int)spacing_x.value
+							, (int)spacing_y.value
+							);
+
+						int x0 = (int)cell.x;
+						int y0 = (int)cell.y;
+						int x1 = x0+(int)cell_w.value;
+						int y1 = y0;
+						int x2 = x1;
+						int y2 = y0+(int)cell_h.value;
+						int x3 = x0;
+						int y3 = y2;
+						cr.move_to(x0, y0);
+						cr.line_to(x1, y1);
+						cr.move_to(x1, y1);
+						cr.line_to(x2, y2);
+						cr.move_to(x2, y2);
+						cr.line_to(x3, y3);
+						cr.move_to(x3, y3);
+						cr.line_to(x0, y0);
+
+						cr.set_source_rgba(0.9, 0.1, 0.1, 0.6);
+						cr.stroke();
+
+						cr.arc(x0 + pivot.x, y0 + pivot.y, 5.0, 0, 2*Math.PI);
+						cr.set_source_rgba(0.1, 0.1, 0.9, 0.6);
+						cr.fill();
+					}
+				}
+
+				return true;
+			});
+
+		resolution = new Gtk.Label(_pixbuf.width.to_string() + " × " + _pixbuf.height.to_string());
+		resolution.halign = Gtk.Align.START;
+
+		cells_h = new Gtk.SpinButton.with_range(1.0, 256.0, 1.0);
+		cells_h.value = 4;
+		cells_v = new Gtk.SpinButton.with_range(1.0, 256.0, 1.0);
+		cells_v.value = 4;
+		cell_wh_auto = new Gtk.CheckButton();
+		cell_wh_auto.active = true;
+		cell_w = new Gtk.SpinButton.with_range(1.0, 4096.0, 1.0);
+		cell_w.value = _pixbuf.width / cells_h.value;
+		cell_w.sensitive = !cell_wh_auto.active;
+		cell_h = new Gtk.SpinButton.with_range(1.0, 4096.0, 1.0);
+		cell_h.value = _pixbuf.height / cells_v.value;
+		cell_h.sensitive = !cell_wh_auto.active;
+		offset_x = new Gtk.SpinButton.with_range(0, 128.0, 1.0);
+		offset_y = new Gtk.SpinButton.with_range(0, 128.0, 1.0);
+		spacing_x = new Gtk.SpinButton.with_range(0, 128.0, 1.0);
+		spacing_y = new Gtk.SpinButton.with_range(0, 128.0, 1.0);
+
+		cells_h.value_changed.connect (() => {
+			if (cell_wh_auto.active)
+			{
+				cell_w.value = _pixbuf.width / cells_h.value;
+				cell_h.value = _pixbuf.height / cells_v.value;
+			}
+			da.queue_draw();
+		});
+
+		cells_v.value_changed.connect(() => {
+			if (cell_wh_auto.active)
+			{
+				cell_w.value = _pixbuf.width / cells_h.value;
+				cell_h.value = _pixbuf.height / cells_v.value;
+			}
+			da.queue_draw();
+		});
+
+		cell_wh_auto.toggled.connect(() => {
+			cell_w.sensitive = !cell_wh_auto.active;
+			cell_h.sensitive = !cell_wh_auto.active;
+			cell_w.value = _pixbuf.width / cells_h.value;
+			cell_h.value = _pixbuf.height / cells_v.value;
+			da.queue_draw();
+		});
+
+		cell_w.value_changed.connect (() => {
+			da.queue_draw();
+		});
+
+		cell_h.value_changed.connect(() => {
+			da.queue_draw();
+		});
+
+		offset_x.value_changed.connect(() => {
+			da.queue_draw();
+		});
+
+		offset_y.value_changed.connect(() => {
+			da.queue_draw();
+		});
+
+		spacing_x.value_changed.connect(() => {
+			da.queue_draw();
+		});
+
+		spacing_y.value_changed.connect(() => {
+			da.queue_draw();
+		});
+
+		pivot = new Gtk.ComboBoxText();
+		pivot.append_text("Top left");      // TOP_LEFT
+		pivot.append_text("Top center");    // TOP_CENTER
+		pivot.append_text("Top right");     // TOP_RIGHT
+		pivot.append_text("Left");          // LEFT
+		pivot.append_text("Center");        // CENTER
+		pivot.append_text("Right");         // RIGHT
+		pivot.append_text("Bottom left");   // BOTTOM_LEFT
+		pivot.append_text("Bottom center"); // BOTTOM_CENTER
+		pivot.append_text("Bottom right");  // BOTTOM_RIGHT
+		pivot.active = Pivot.CENTER;
+
+		pivot.changed.connect(() => {
+			da.queue_draw();
+		});
+
+		Gtk.Grid grid = new Gtk.Grid();
+		grid.attach(label_with_alignment("Resolution", Gtk.Align.END),   0,  0, 1, 1);
+		grid.attach(label_with_alignment("Cells H", Gtk.Align.END),      0,  1, 1, 1);
+		grid.attach(label_with_alignment("Cells V", Gtk.Align.END),      0,  2, 1, 1);
+		grid.attach(label_with_alignment("Cell WH auto", Gtk.Align.END), 0,  3, 1, 1);
+		grid.attach(label_with_alignment("Cell W", Gtk.Align.END),       0,  4, 1, 1);
+		grid.attach(label_with_alignment("Cell H", Gtk.Align.END),       0,  5, 1, 1);
+		grid.attach(label_with_alignment("Offset X", Gtk.Align.END),     0,  6, 1, 1);
+		grid.attach(label_with_alignment("Offset Y", Gtk.Align.END),     0,  7, 1, 1);
+		grid.attach(label_with_alignment("Spacing X", Gtk.Align.END),    0,  8, 1, 1);
+		grid.attach(label_with_alignment("Spacing Y", Gtk.Align.END),    0,  9, 1, 1);
+		grid.attach(label_with_alignment("Pivot", Gtk.Align.END),        0, 10, 1, 1);
+
+		grid.attach(resolution,   1,  0, 1, 1);
+		grid.attach(cells_h,      1,  1, 1, 1);
+		grid.attach(cells_v,      1,  2, 1, 1);
+		grid.attach(cell_wh_auto, 1,  3, 1, 1);
+		grid.attach(cell_w,       1,  4, 1, 1);
+		grid.attach(cell_h,       1,  5, 1, 1);
+		grid.attach(offset_x,     1,  6, 1, 1);
+		grid.attach(offset_y,     1,  7, 1, 1);
+		grid.attach(spacing_x,    1,  8, 1, 1);
+		grid.attach(spacing_y,    1,  9, 1, 1);
+		grid.attach(pivot,        1, 10, 1, 1);
+		grid.row_spacing = 6;
+		grid.column_spacing = 12;
+
+		Gtk.Box box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 12);
+		box.add(da);
+		box.add(grid);
+		box.margin_bottom = 18;
+
+		get_content_area().add(box);
+		get_content_area().show_all();
+
+		add_button("Cancel", Gtk.ResponseType.CANCEL);
+		add_button("OK", Gtk.ResponseType.OK);
+
+		this.response.connect(on_response);
+	}
+
+	private void on_response(Gtk.Dialog source, int response_id)
+	{
+		switch (response_id)
+		{
+		case Gtk.ResponseType.OK:
+			break;
+
+		case Gtk.ResponseType.CANCEL:
+			destroy();
+			break;
+		}
+	}
+}
+
+}

+ 6 - 1
tools/ui/level_editor_menu.xml

@@ -7,7 +7,12 @@
 			<menuitem action="save"></menuitem>
 			<menuitem action="save-as"></menuitem>
 			<separator></separator>
-			<menuitem action="import"></menuitem>
+			<menu action="import">
+				<menuitem action="import-sprites"></menuitem>
+				<menuitem action="import-meshes"></menuitem>
+				<menuitem action="import-sounds"></menuitem>
+				<menuitem action="import-textures"></menuitem>
+			</menu>
 			<separator></separator>
 			<menuitem action="preferences"></menuitem>
 			<separator></separator>