techkaczynski.dev / code

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--editor.c790
-rw-r--r--editor.h4
-rw-r--r--res.c28
-rw-r--r--res.h3
4 files changed, 582 insertions, 243 deletions
diff --git a/editor.c b/editor.c
index 90ab378..cf74bae 100644
--- a/editor.c
+++ b/editor.c
@@ -29,10 +29,18 @@ EditorInfo ed = {
},
};
+typedef enum {
+ MDL_PICK_COMP_ADD,
+ MDL_PICK_COMP_UPDATE,
+ MDL_PICK_ENT_CREATE,
+} ModelPickerMode;
+
/* forward decl */
static bool selection_root(Entity *ent);
static void action_apply(EditorAction*, bool);
static void action_free(EditorAction*);
+static void model_picker_open(ModelPickerMode mode);
+static void model_picker_close(void);
static void
history_push(EditorAction action) {
@@ -182,11 +190,11 @@ action_begin(EditorActionTag tag, bool roots_only) {
return action;
case ED_ACTION_DELETE:
- // roots_only = true;
recursive = true;
break;
case ED_ACTION_RENAME:
+ case ED_ACTION_MODEL:
break;
default:
@@ -252,6 +260,12 @@ action_begin(EditorActionTag tag, bool roots_only) {
// Name ownership will transfer when the rename is submitted.
break;
+ case ED_ACTION_MODEL:
+ before->model = ent->mdl;
+ before->model.key = ent->mdl.key ?
+ strdup(ent->mdl.key) : NULL;
+ break;
+
default:
TraceLog(LOG_FATAL,
"action_begin: unhandled action tag.");
@@ -309,6 +323,17 @@ action_apply(EditorAction *action, bool undo) {
}
break;
+ case ED_ACTION_MODEL:
+ for (u32 i = 0; i < action->item_cnt; ++i) {
+ u64 id = entity_id_from_pid(action->item[i].pid);
+ Entity *ent = entity_get(id);
+
+ ModelCmp model = ent->mdl;
+ ent->mdl = action->item[i].before.model;
+ action->item[i].before.model = model;
+ }
+ break;
+
default:
TraceLog(LOG_FATAL, "action with invalid tag.");
break;
@@ -334,6 +359,12 @@ action_free(EditorAction *action) {
}
break;
+ case ED_ACTION_MODEL:
+ for (u32 i = 0; i < action->item_cnt; ++i) {
+ free(action->item[i].before.model.key);
+ }
+ break;
+
default:
TraceLog(LOG_FATAL, "action_free: unhandled action tag.");
break;
@@ -393,6 +424,23 @@ action_commit(EditorAction *action) {
}
break;
+ case ED_ACTION_MODEL:
+ for (u32 i = 0; i < action->item_cnt; ++i) {
+ u64 id = entity_id_from_pid(action->item[i].pid);
+ Entity *ent = entity_get(id);
+ assert(ent);
+
+ ModelCmp *before = &action->item[i].before.model;
+ ModelCmp *after = &ent->mdl;
+
+ changed |= before->has != after->has ||
+ before->draw != after->draw ||
+ strcmp(before->key ? before->key : "",
+ after->key ? after->key : "") != 0;
+
+ }
+ break;
+
default:
TraceLog(LOG_FATAL, "action_commit: unhangled action tag.");
break;
@@ -426,6 +474,7 @@ editor_init(void) {
void
editor_exit(void) {
+ model_picker_close();
action_free(&tr_action);
tr_action = (EditorAction){0};
history_clear();
@@ -900,13 +949,6 @@ editor_rotate(void) {
}
/* draw */
-// static bool
-// panel_vec3(Rect bounds, Vec3 *val, cstr label) {
- // for (int i = 0; i < 3; ++i) {
- // }
- // return true;
-// }
-
static void
statusline(void) {
char mode_str[16] = {0};
@@ -1200,8 +1242,8 @@ panel_field(PanelEdit *edit, Rect box, cstr text, bool mixed, bool numeric) {
if (!editing) {
memset(edit->text, 0, sizeof(edit->text));
- snprintf(edit->text, sizeof(edit->text), "%s",
- mixed ? "" : display);
+ // snprintf(edit->text, sizeof(edit->text), "%s",
+ // mixed ? "" : display);
edit->focus = id;
return false;
}
@@ -1211,6 +1253,24 @@ panel_field(PanelEdit *edit, Rect box, cstr text, bool mixed, bool numeric) {
}
static bool
+panel_bool(Rect row, cstr label, bool *value, bool mixed) {
+ GuiLabel((Rect){row.x, row.y, 40, row.height}, label);
+
+ if (mixed) {
+ *value = false;
+ GuiLabel((Rect){
+ row.x + 68, row.y, row.width - 68, row.height,
+ }, "(mixed)");
+ }
+
+ return GuiCheckBox((Rect){
+ row.x + 40,
+ row.y + (row.height - 20) * 0.5f,
+ 20, 20,
+ }, NULL, value);
+}
+
+static bool
panel_float(PanelEdit *edit, Rect box, f32 *value, bool mixed) {
char display[64] = "";
if (!mixed) {
@@ -1256,9 +1316,12 @@ panel_vec3(PanelEdit *edit, Rect row, cstr label, Vec3 *value, bool mixed) {
return submitted;
}
+
static void
panel_properties(EditorPanelContainer container, bool input) {
static PanelEdit edit;
+ static bool add_open;
+ static int add_choice;
Rect bounds = ed.panel[container].bounds;
int state = GuiGetState();
@@ -1271,10 +1334,15 @@ panel_properties(EditorPanelContainer container, bool input) {
edit.input = input;
if (!input) {
+ add_open = false;
edit.focus = 0;
GuiSetState(STATE_DISABLED);
}
+ if (add_open) {
+ GuiLock();
+ }
+
GuiPanel(bounds, "Entity Properties");
if (!ed.sel_cnt) {
@@ -1299,6 +1367,32 @@ panel_properties(EditorPanelContainer container, bool input) {
}
}
+ ModelCmp *model = NULL;
+ int model_cnt = 0;
+ bool key_mixed = false;
+ bool draw_mixed = false;
+
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *candidate = entity_get(entity_id_at(i));
+
+ if (!candidate->pid || !candidate->ed.selected ||
+ !candidate->mdl.has) {
+ continue;
+ }
+
+ model_cnt++;
+
+ if (!model) {
+ model = &candidate->mdl;
+ } else {
+ key_mixed |= strcmp(model->key,
+ candidate->mdl.key) != 0;
+ draw_mixed |= model->draw != candidate->mdl.draw;
+ }
+ }
+
+ bool all_have_model = model_cnt == ed.sel_cnt;
+
/* Name */
cstr name = mixed ? "<multiple entities selected>"
: (ent && ent->name ? ent->name : "");
@@ -1331,7 +1425,7 @@ panel_properties(EditorPanelContainer container, bool input) {
Rect row = {
group.x + pad,
- group.y + 10,
+ group.y + 15,
group.width - 2 * pad,
20
};
@@ -1383,10 +1477,6 @@ panel_properties(EditorPanelContainer container, bool input) {
);
Vec3 rt = QuaternionToEuler(target->tr.rot);
- // rt.x *= RAD2DEG;
- // rt.y *= RAD2DEG;
- // rt.z *= RAD2DEG;
-
f32 *component[3] = { &rt.x, &rt.y, &rt.z };
*component[axis] = value[axis];
@@ -1422,240 +1512,88 @@ panel_properties(EditorPanelContainer container, bool input) {
action_commit(&action);
}
+ model_comp:
+ if (!all_have_model) {
+ goto comp_add;
+ }
+
+ group.y += group.height + 16;
+ group.height = 80;
+ GuiGroupBox(group, "Model");
+
+ row = (Rect){
+ group.x + pad,
+ group.y + 15,
+ group.width - 2 * pad,
+ 20,
+ };
+
+ GuiLabel((Rect){row.x, row.y, 32, row.height}, "Key");
+
+ if (GuiButton(
+ (Rect){row.x + 40, row.y, row.width - 40, row.height},
+ key_mixed ? "<multiple>" : GetFileName(model->key))) {
+ edit.focus = 0;
+ model_picker_open(MDL_PICK_COMP_UPDATE);
+ }
+
+ row.y += 30;
+ bool draw = draw_mixed ? false : model->draw;
+
+ if (panel_bool(row, "Draw", &draw, draw_mixed)) {
+ edit.focus = 0;
+ EditorAction action = action_begin(ED_ACTION_MODEL, false);
+
+ for (u32 i = 0; i < action.item_cnt; ++i) {
+ Entity *target = entity_get(
+ entity_id_from_pid(action.item[i].pid)
+ );
+
+ target->mdl.draw = draw;
+ }
+
+ action_commit(&action);
+ }
+
+ row.y = group.y + group.height - 20;
+
+ comp_add:
+ GuiUnlock();
+ row.y += 40;
+
+ int add_state = GuiGetState();
+
+ if (all_have_model) {
+ add_open = false;
+ GuiSetState(STATE_DISABLED);
+ }
+
+ cstr choices = model_cnt
+ ? "Add Component;Add Model to Missing"
+ : "Add Component;Model";
+
+ if (GuiDropdownBox(row, choices, &add_choice, add_open)) {
+ add_open = !add_open;
+ edit.focus = 0;
+
+ if (!add_open && add_choice == 1) {
+ model_picker_open(MDL_PICK_COMP_ADD);
+ add_choice = 0;
+ }
+ }
+
+ GuiSetState(add_state);
+
reset:
+ GuiUnlock();
GuiSetState(state);
if (input && container != ED_PNL_CENTER) {
- ed.focus_captured = edit.focus != 0;
+ ed.focus_captured = edit.focus != 0 ||
+ add_open ||
+ ed.panel[ED_PNL_CENTER].enabled;
}
}
-// static void
-// panel_properties(EditorPanelContainer container, bool input) {
-// Rect view = {0};
-// Rect bounds = ed.panel[container].bounds;
-// Vec2 *scroll = &ed.panel[container].scroll;
-
-// int state = GuiGetState();
-
-// input = input && app.mode ==
-// APP_MODE_EDIT_NONE && state != STATE_DISABLED;
-
-// if (!input) {
-// GuiSetState(STATE_DISABLED);
-// }
-
-// if (input && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
-// ed.focus_captured = true;
-// }
-
-// GuiPanel(bounds, "Entity Properties");
-
-// /*
-// NOTE (tech kaczynski - 21/9/6):
-// I had a choice here to either create a tagged union of components and
-// iterate over them, or to write a giant function like this. I chose the
-// latter because the former would require a big refactor that I don't
-// really care for at this stage. The former still requires a loop and a
-// switch or branches for each type, which requires extra indentation or
-// abstraction into special helper functions etc. For now, the code
-// duplication and verbosity I can live with and might not be much worse
-// than the alternative anyway, plus I get to avoid refactoring every
-// system that touches entity component data.
-// */
-
-// /* needed for input field focus */
-// enum {
-// TEXT_FIELD_NONE,
-
-// TEXT_FIELD_NAME,
-
-// /* transform */
-// TEXT_FIELD_TR_POS_X,
-// TEXT_FIELD_TR_POS_Y,
-// TEXT_FIELD_TR_POS_Z,
-
-// TEXT_FIELD_TR_ROT_X,
-// TEXT_FIELD_TR_ROT_Y,
-// TEXT_FIELD_TR_ROT_Z,
-
-// TEXT_FIELD_TR_SCALE_X,
-// TEXT_FIELD_TR_SCALE_Y,
-// TEXT_FIELD_TR_SCALE_Z,
-// };
-
-// static int text_focus;
-// static char text_input[64];
-// char display_text[sizeof(text_input)];
-
-// Rect prec = bounds;
-// f32 pad = 8;
-// cstr substr = "";
-
-// if (!ed.sel_cnt) {
-// GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
-// cstr str = "no entities selected";
-// GuiLabel(bounds, str);
-// GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
-// text_focus = 0;
-// goto reset;
-// }
-
-// /* ed.sel_cnt can't be 0 because we've already checked and returned */
-// Entity *ent = NULL; // only set if sel_cnt == 1
-// if (ed.sel_cnt > 1) {
-// substr = "<multiple entities selected>";
-// } else {
-// for (u32 i = 0; i < entity_count(); ++i) {
-// ent = entity_get(entity_id_at(i));
-// if (ent && ent->pid && ent->ed.selected) {
-// substr = ent->name ? ent->name : "";
-// break;
-// }
-// }
-// }
-
-// if (text_focus != TEXT_FIELD_NAME) {
-// snprintf(display_text, sizeof(display_text), "%s", substr);
-// }
-
-// cstr name_lbl = "Name";
-// u32 name_lbl_w = editor_label_w(name_lbl);
-// prec.x += pad;
-// prec.y += 30;
-// prec.width = name_lbl_w;
-// prec.height = 20;
-// GuiLabel(prec, name_lbl);
-
-// prec.x += name_lbl_w + pad;
-// prec.width = bounds.x + bounds.width - prec.x - pad;
-
-// bool name_edit = text_focus == TEXT_FIELD_NAME;
-// bool name_toggle =
-// GuiTextBox(
-// prec,
-// name_edit ? text_input : display_text,
-// sizeof(text_input),
-// input && name_edit
-// );
-
-// if (input) {
-// if (IsKeyPressed(KEY_ESCAPE)) {
-// if (name_edit) {
-// text_focus = 0;
-// }
-// } else if (name_toggle) {
-// if (name_edit) {
-// if (IsKeyPressed(KEY_ENTER)) {
-// editor_rename_selected(text_input);
-// }
-// text_focus = 0;
-// } else {
-// snprintf(text_input,
-// sizeof(text_input),
-// "%s",
-// ed.sel_cnt > 1 ?
-// "" : display_text);
-// text_focus = TEXT_FIELD_NAME;
-// }
-// }
-// }
-
-// /* transform comp */
-// prec.x = bounds.x + pad;
-// prec.y += 30;
-// prec.width = bounds.width - pad * 2;
-// prec.height = 90 + pad * 2;
-// GuiGroupBox(prec, "Transform");
-
-// Vec3 pos = ent ? ent->tr.pos : (Vec3){0};
-// f32 val[3] = { pos.x, pos.y, pos.z };
-// Rect row = { prec.x + pad, prec.y + 10, prec.width - 2 * pad, 20 };
-
-// /* position */
-// GuiLabel((Rect){ row.x, row.y, 45, row.height }, "pos");
-
-// for (int i = 0; i < 3; ++i) {
-// int field = TEXT_FIELD_TR_POS_X + i;
-// Rect box = {
-// row.x + 45 + i * ((row.width - 45) / 3),
-// row.y,
-// (row.width - 45) / 3 - 4,
-// row.height
-// };
-
-// char display[64] = "";
-// if (ent) {
-// snprintf(display, sizeof(display), "%.3f", val[i]);
-// }
-
-// bool editing = text_focus == field;
-// f32 draft = 0;
-// bool toggle = GuiValueBoxFloat(box, NULL,
-// editing ? text_input : display,
-// &draft, input && editing);
-
-// if (!input) {
-// continue;
-// }
-
-// if (editing && IsKeyPressed(KEY_ESCAPE)) {
-// text_focus = TEXT_FIELD_NONE;
-// continue;
-// }
-
-// if (!toggle) {
-// continue;
-// }
-
-// /* clicked an inactive field, begin editing */
-// if (!editing) {
-// memset(text_input, 0, sizeof(text_input));
-// snprintf(text_input, sizeof(text_input),"%s", display);
-// text_focus = field;
-// continue;
-// }
-
-// /* editing ended. clicking away cancels, enter submits */
-// text_focus = TEXT_FIELD_NONE;
-// if (!IsKeyPressed(KEY_ENTER)) {
-// continue;
-// }
-
-// char *end;
-// f32 value = strtof(text_input, &end);
-// if (end == text_input || *end != '\0') {
-// continue;
-// }
-
-// EditorAction action = action_begin(ED_ACTION_TRANSFORM, false);
-
-// for (u32 j = 0; j < action.item_cnt; ++j) {
-// u64 id = entity_id_from_pid(action.item[j].pid);
-// Entity *target = entity_get(id);
-
-// // f32 *tval = (f32*)&target->tr.pos;
-// // *(tval + i) = value;
-
-// f32 *axis[3] = {
-// &target->tr.pos.x,
-// &target->tr.pos.y,
-// &target->tr.pos.z,
-// };
-
-// *axis[i] = value;
-// }
-
-// action_commit(&action);
-// }
-
-// /* reset */
-// reset:
-// GuiSetState(state);
-// if (input && container != ED_PNL_CENTER) {
-// ed.focus_captured = text_focus;
-// }
-// }
-
static void
panel_space(EditorPanelContainer container) {
static cstr strs[][2] = {
@@ -1696,6 +1634,350 @@ panel_space(EditorPanelContainer container) {
}
}
+/* model picker */
+typedef struct {
+ cstr key;
+ int score;
+ Asset *asset;
+ bool tried_loading;
+} ModelChoice;
+
+static struct {
+ RenderTexture preview;
+ ModelChoice *items;
+ int item_cnt, match_cnt, sel_idx, scroll;
+ ModelPickerMode mode;
+ char query[128];
+} model_picker;
+
+static bool
+editor_set_model(cstr key, ModelPickerMode mode) {
+ Asset *asset = asset_get(key);
+ if (!asset || asset->tag != ASSET_TAG_MODEL) {
+ return false;
+ }
+
+ EditorAction action = action_begin(ED_ACTION_MODEL, false);
+
+ for (u32 i = 0; i < action.item_cnt; ++i) {
+ u64 id = entity_id_from_pid(action.item[i].pid);
+ Entity *ent = entity_get(id);
+
+ if ((mode == MDL_PICK_COMP_ADD && ent->mdl.has) ||
+ (mode == MDL_PICK_COMP_UPDATE && !ent->mdl.has)) {
+ continue;
+ }
+
+ free(ent->mdl.key);
+ ent->mdl = (ModelCmp){
+ .key = strdup(key),
+ .mdl = &asset->mdl,
+ .has = true,
+ .draw = ent->mdl.has ? ent->mdl.draw : true,
+ };
+ }
+
+ action_commit(&action);
+ return true;
+}
+
+/* qsort compare func */
+static int
+model_choice_compare(const void *a, const void *b) {
+ const ModelChoice *left = a;
+ const ModelChoice *right = b;
+
+ if (left->score > right->score) {
+ return -1;
+ }
+
+ if (left->score < right->score) {
+ return 1;
+ }
+
+ int order = strcmp(GetFileName(left->key), GetFileName(right->key));
+ if (order != 0) {
+ return order;
+ }
+
+ return strcmp(left->key, right->key);
+}
+
+static void
+model_picker_close(void) {
+ if (model_picker.preview.id) {
+ UnloadRenderTexture(model_picker.preview);
+ }
+ free(model_picker.items);
+ memset(&model_picker, 0, sizeof(model_picker));
+
+ ed.panel[ED_PNL_CENTER].enabled = false;
+ ed.focus = ED_PNL_NULL;
+ ed.focus_captured = false;
+}
+
+static void
+model_picker_open(ModelPickerMode mode) {
+ model_picker.mode = mode;
+ char **keys = asset_mdl_keys(&model_picker.item_cnt);
+
+ if (model_picker.item_cnt) {
+ model_picker.items = calloc(model_picker.item_cnt,
+ sizeof(*model_picker.items));
+ if (!model_picker.items) {
+ perror("model_picker_open: alloc failed.");
+ exit(1);
+ }
+ }
+
+ for (int i = 0; i < model_picker.item_cnt; ++i) {
+ model_picker.items[i].key = keys[i];
+ }
+
+ free(keys);
+ model_picker.match_cnt = model_picker.item_cnt;
+ if (model_picker.item_cnt > 1) {
+ qsort(model_picker.items, model_picker.item_cnt,
+ sizeof(*model_picker.items), model_choice_compare);
+ }
+
+ ed.panel[ED_PNL_CENTER].type = ED_PNL_TYPE_MODEL_PICKER;
+ ed.panel[ED_PNL_CENTER].enabled = true;
+ ed.focus = ED_PNL_CENTER;
+ ed.focus_captured = true;
+}
+
+static void
+model_picker_preview(Rect bounds, Model mdl) {
+ if (model_picker.preview.texture.width != bounds.width ||
+ model_picker.preview.texture.height != bounds.height) {
+ if (model_picker.preview.texture.id) {
+ UnloadRenderTexture(model_picker.preview);
+ }
+
+ model_picker.preview =
+ LoadRenderTexture(bounds.width, bounds.height);
+ }
+
+ Matrix transform = mdl.transform;
+ mdl.transform = MatrixIdentity();
+ BoundingBox bb = GetModelBoundingBox(mdl);
+
+ Vec3 center = Vector3Scale(Vector3Add(bb.min, bb.max), 0.5);
+ center = Vector3Transform(center, transform);
+
+ f32 radius = 0.001f;
+ for (int i = 0; i < 8; ++i) {
+ Vec3 corner = {
+ (i & 1) ? bb.max.x : bb.min.x,
+ (i & 2) ? bb.max.y : bb.min.y,
+ (i & 4) ? bb.max.z : bb.min.z
+ };
+ corner = Vector3Transform(corner, transform);
+ radius = fmaxf(radius, Vector3Distance(corner, center));
+ }
+
+ transform = MatrixMultiply(transform,
+ MatrixTranslate(-center.x, -center.y, -center.z));
+ transform = MatrixMultiply(transform,
+ MatrixScale(1.0/radius, 1.0/radius, 1.0/radius));
+
+ f32 dist = 3.0f * fmaxf(1.0f, (f32)bounds.height / (f32)bounds.width);
+
+ Camera3D cam = {
+ .position =
+ Vector3Scale(Vector3Normalize((Vec3){1,0.7,1}), dist),
+ .target = {0},
+ .up = {0,1,0},
+ .fovy = 45,
+ .projection = CAMERA_PERSPECTIVE,
+ };
+
+ BeginTextureMode(model_picker.preview);
+ ClearBackground((Color){ 35, 35, 40, 255 });
+ BeginMode3D(cam);
+
+ /* we do this bc our normal draw calls override default shader */
+ for (int i = 0; i < mdl.meshCount; ++i) {
+ Material mat = mdl.materials[mdl.meshMaterial[i]];
+ mat.shader = (Shader) {
+ .id = rlGetShaderIdDefault(),
+ .locs = rlGetShaderLocsDefault(),
+ };
+ DrawMesh(mdl.meshes[i], mat, transform);
+ }
+
+ EndMode3D();
+ EndTextureMode();
+
+ DrawTexturePro(model_picker.preview.texture, (Rect) {
+ 0, 0, bounds.width, -bounds.height,
+ }, bounds, (Vec2){0}, 0, WHITE);
+}
+
+
+static cstr
+panel_model_picker(Rect bounds, bool input) {
+ if (input && IsKeyPressed(KEY_ESCAPE)) {
+ model_picker_close();
+ return NULL;
+ }
+
+ GuiPanel(bounds, "Choose Model");
+
+ f32 left_width = floorf((bounds.width - 24) * 0.45f);
+
+ Rect search = {
+ bounds.x + 8, bounds.y + 30,
+ left_width, 24,
+ };
+
+ Rect list = {
+ search.x, search.y + 32,
+ left_width, fmaxf(24, bounds.height - 100),
+ };
+
+ Rect preview = {
+ search.x + left_width + 8, search.y,
+ bounds.width - left_width - 24,
+ bounds.height - 38,
+ };
+
+ char previous[sizeof(model_picker.query)];
+ memcpy(previous, model_picker.query, sizeof(previous));
+
+ /* Ignore the toggle result: the search always keeps text focus. */
+ GuiTextBox(search, model_picker.query,
+ sizeof(model_picker.query), input);
+
+ if (strcmp(previous, model_picker.query) != 0) {
+ model_picker.match_cnt = 0;
+
+ for (int i = 0; i < model_picker.item_cnt; ++i) {
+ ModelChoice *item = &model_picker.items[i];
+
+ item->score =
+ fuzzy_match(model_picker.query,
+ GetFileName(item->key));
+
+ if (item->score != INT32_MIN) {
+ model_picker.match_cnt++;
+ }
+ }
+
+ if (model_picker.item_cnt > 1) {
+ qsort(model_picker.items,
+ model_picker.item_cnt,
+ sizeof(*model_picker.items),
+ model_choice_compare);
+ }
+
+ model_picker.sel_idx = 0;
+ model_picker.scroll = 0;
+ }
+
+ if (input && model_picker.match_cnt) {
+ int step = 0;
+
+ if (IsKeyPressed(KEY_UP)) {
+ step = -1;
+ } else if (IsKeyPressed(KEY_DOWN)) {
+ step = 1;
+ } else if (IsKeyPressed(KEY_TAB)) {
+ bool backwards = IsKeyDown(KEY_LEFT_SHIFT) ||
+ IsKeyDown(KEY_RIGHT_SHIFT);
+ step = backwards ? -1 : 1;
+ }
+
+ model_picker.sel_idx =
+ (model_picker.sel_idx + step +
+ model_picker.match_cnt) %
+ model_picker.match_cnt;
+ }
+
+ int visible = MAX(1, (int)(list.height / 24));
+
+ if (model_picker.sel_idx < model_picker.scroll) {
+ model_picker.scroll = model_picker.sel_idx;
+ }
+
+ if (model_picker.sel_idx >= model_picker.scroll + visible) {
+ model_picker.scroll = model_picker.sel_idx - visible + 1;
+ }
+
+ BeginScissorMode((int)list.x, (int)list.y,
+ (int)list.width, (int)list.height);
+
+ for (int row = 0; row < visible; ++row) {
+ int index = model_picker.scroll + row;
+
+ if (index >= model_picker.match_cnt) {
+ break;
+ }
+
+ Rect item_box = {
+ list.x, list.y + row * 24,
+ list.width, 24,
+ };
+
+ if (index == model_picker.sel_idx) {
+ DrawRectangleRec(item_box,
+ GetColor(GuiGetStyle(LISTVIEW,
+ BASE_COLOR_PRESSED)));
+ }
+
+ GuiLabel(item_box, GetFileName(model_picker.items[index].key));
+ }
+
+ if (!model_picker.match_cnt) {
+ GuiLabel((Rect) {
+ list.x, list.y, list.width, 24
+ }, "No matching models");
+ }
+
+ EndScissorMode();
+
+ GuiPanel(preview, NULL);
+
+ if (model_picker.match_cnt) {
+ ModelChoice *item = &model_picker.items[model_picker.sel_idx];
+
+ if (!item->tried_loading) {
+ item->asset = asset_get(item->key);
+ item->tried_loading = true;
+ }
+
+ /* model preview */
+ if (item->asset) {
+ Rect image = {
+ preview.x + 1,
+ preview.y + 34,
+ preview.width - 2,
+ preview.height - 35,
+ };
+ model_picker_preview(image, item->asset->mdl);
+ }
+
+ GuiLabel((Rect){
+ preview.x + 8, preview.y + 8,
+ preview.width - 16, 24,
+ }, item->asset ?
+ GetFileName(item->key) :
+ "Could not load model");
+
+ if (input && item->asset && IsKeyPressed(KEY_ENTER)) {
+ return item->key;
+ }
+ }
+
+ GuiLabel((Rect){
+ list.x, bounds.y + bounds.height - 30,
+ list.width, 22,
+ }, "Tab: cycle Enter: add Esc: cancel");
+
+ return NULL;
+}
+
static void
panel_draw(EditorPanelContainer container) {
if (!ed.panel[container].enabled) {
@@ -1754,6 +2036,28 @@ panel_draw(EditorPanelContainer container) {
case ED_PNL_TYPE_CMD:
GuiPanel(bounds, "Command Menu");
break;
+
+ case ED_PNL_TYPE_MODEL_PICKER:
+ {}
+ cstr key = panel_model_picker(bounds, input);
+ if (!key) {
+ break;
+ }
+
+ switch (model_picker.mode) {
+ case MDL_PICK_COMP_ADD:
+ case MDL_PICK_COMP_UPDATE:
+ if (editor_set_model(key, model_picker.mode)) {
+ model_picker_close();
+ }
+ break;
+
+ case MDL_PICK_ENT_CREATE:
+ /* TODO: */
+ break;
+ }
+
+ break;
}
GuiSetState(state);
diff --git a/editor.h b/editor.h
index f18cae0..07ad48d 100644
--- a/editor.h
+++ b/editor.h
@@ -9,6 +9,7 @@ typedef enum {
ED_ACTION_CREATE,
ED_ACTION_DELETE,
ED_ACTION_RENAME,
+ ED_ACTION_MODEL,
} EditorActionTag;
typedef union {
@@ -26,6 +27,8 @@ typedef union {
struct {
char *name;
} rename;
+
+ ModelCmp model;
} EditorActionData;
typedef struct {
@@ -49,6 +52,7 @@ typedef enum {
ED_PNL_TYPE_SCENE,
ED_PNL_TYPE_PROPERTIES,
ED_PNL_TYPE_SPACE,
+ ED_PNL_TYPE_MODEL_PICKER,
ED_PNL_TYPE_CMD,
} EditorPanelType;
diff --git a/res.c b/res.c
index b1ed86f..ef4f2c2 100644
--- a/res.c
+++ b/res.c
@@ -174,6 +174,34 @@ void
res_update(void) {
}
+char**
+asset_mdl_keys(int *count) {
+ *count = 0;
+
+ if (!res.assets.base.nnodes) {
+ return NULL;
+ }
+
+ char **keys = malloc(res.assets.base.nnodes * sizeof(*keys));
+ if (!keys) {
+ perror("asset_mdl_keys: mem allocation failed.");
+ exit(1);
+ }
+
+ map_iter_t iter = map_iter(&res.assets);
+ cstr key;
+
+ while ((key = map_next(&res.assets, &iter))) {
+ if (strcmp(GetFileExtension(key), ".glb") != 0) {
+ continue;
+ }
+
+ keys[(*count)++] = map_get(&res.assets, key)->path;
+ }
+
+ return keys;
+}
+
Asset*
asset_get(cstr key) {
Asset *asset = map_get(&res.assets, key);
diff --git a/res.h b/res.h
index 6824842..7fd2946 100644
--- a/res.h
+++ b/res.h
@@ -33,9 +33,12 @@ void res_init(void);
void res_exit(void);
void res_update(void);
+char** asset_mdl_keys(int *count);
+
Asset* asset_get(cstr key);
FunShader* shader_get(cstr key);
+
void fun_shader_create(cstr key, cstr vpath, cstr fpath);
void fun_shader_delete(FunShader*);
int fun_shader_uniform(cstr skey, cstr ukey);