techkaczynski.dev / code

summaryrefslogtreecommitdiff
path: root/editor.c
diff options
context:
space:
mode:
authortech kaczynski <tk@techkaczynski.dev>2026-09-30 08:00:41 +1000
committertech kaczynski <tk@techkaczynski.dev>2026-09-30 08:00:41 +1000
commit7f42c8ec0d3faf22499ebc0b6644ca3009cabd03 (patch)
treed3508645d9b313feaa854d826c9c99878e824ab7 /editor.c
parentea8db55dc48a374063f10ac1c3c1615fb538943c (diff)
added editable transform component for entity properties panel in editor.
Diffstat (limited to 'editor.c')
-rw-r--r--editor.c600
1 files changed, 381 insertions, 219 deletions
diff --git a/editor.c b/editor.c
index 6550ecf..90ab378 100644
--- a/editor.c
+++ b/editor.c
@@ -155,15 +155,15 @@ action_collect(EditorAction *action, u64 id, bool recursive) {
}
static EditorAction
-action_begin(EditorActionTag tag) {
+action_begin(EditorActionTag tag, bool roots_only) {
EditorAction action = { .tag = tag };
- bool roots_only = false;
+ // bool roots_only = false;
bool recursive = false;
switch (tag) {
case ED_ACTION_TRANSFORM:
- roots_only = true;
+ // roots_only = true;
break;
case ED_ACTION_CREATE:
@@ -182,7 +182,7 @@ action_begin(EditorActionTag tag) {
return action;
case ED_ACTION_DELETE:
- roots_only = true;
+ // roots_only = true;
recursive = true;
break;
@@ -227,12 +227,14 @@ action_begin(EditorActionTag tag) {
entity_id_from_pid(action.item[i].pid));
assert(ent);
EditorActionData *before = &action.item[i].before;
+
switch (tag) {
case ED_ACTION_TRANSFORM:
before->transform.pos = ent->tr.pos;
before->transform.rot = ent->tr.rot;
before->transform.scale = ent->tr.scale;
break;
+
case ED_ACTION_DELETE: {
Entity *parent = entity_get(ent->tr.parent);
Entity *prev = entity_get(ent->tr.prev);
@@ -245,9 +247,11 @@ action_begin(EditorActionTag tag) {
before->stored.next = next ? next->pid : 0;
break;
}
+
case ED_ACTION_RENAME:
// Name ownership will transfer when the rename is submitted.
break;
+
default:
TraceLog(LOG_FATAL,
"action_begin: unhandled action tag.");
@@ -520,7 +524,7 @@ case APP_MODE_EDIT_NONE:
if (IsKeyPressed(KEY_X)) {
if (!ctrl && ed.sel_cnt) {
- EditorAction action = action_begin(ED_ACTION_DELETE);
+ EditorAction action = action_begin(ED_ACTION_DELETE, true);
if (action.item_cnt) {
action_apply(&action, false);
action_commit(&action);
@@ -783,7 +787,7 @@ editor_move(void) {
static Vec3 mv_start = {0};
if (!tr_action.item) {
- tr_action = action_begin(ED_ACTION_TRANSFORM);
+ tr_action = action_begin(ED_ACTION_TRANSFORM, true);
started = false;
}
@@ -853,7 +857,7 @@ editor_move(void) {
void
editor_rotate(void) {
if (!tr_action.item) {
- tr_action = action_begin(ED_ACTION_TRANSFORM);
+ tr_action = action_begin(ED_ACTION_TRANSFORM, true);
started = false;
}
@@ -896,10 +900,12 @@ editor_rotate(void) {
}
/* draw */
-static void
-panel_vec3(Rect bounds, Vec3 *val, cstr label) {
-
-}
+// static bool
+// panel_vec3(Rect bounds, Vec3 *val, cstr label) {
+ // for (int i = 0; i < 3; ++i) {
+ // }
+ // return true;
+// }
static void
statusline(void) {
@@ -1102,7 +1108,7 @@ panel_scene(EditorPanelContainer container, bool input) {
if (GuiButton(
(Rect){ bounds.x + 8, bounds.y + 30, btn_w, 26 },
"Create Entity")) {
- EditorAction action = action_begin(ED_ACTION_CREATE);
+ EditorAction action = action_begin(ED_ACTION_CREATE, false);
action_commit(&action);
}
GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
@@ -1151,6 +1157,278 @@ panel_scene(EditorPanelContainer container, bool input) {
}
}
+typedef struct {
+ int focus;
+ int next_id;
+ bool input;
+ char text[64];
+} PanelEdit;
+
+// mixed means “show a blank field and start editing with an empty buffer”,
+// currently used for when there's multiple entities selected.
+static bool
+panel_field(PanelEdit *edit, Rect box, cstr text, bool mixed, bool numeric) {
+ int id = edit->next_id++;
+ bool editing = edit->focus == id;
+
+ char display[sizeof(edit->text)];
+ snprintf(display, sizeof(display), "%s", text);
+
+ bool toggle;
+ if (numeric) {
+ f32 draft = 0;
+ toggle = GuiValueBoxFloat(box, NULL,
+ editing ? edit->text : display, &draft,
+ edit->input && editing);
+ } else {
+ toggle = GuiTextBox(box, editing ? edit->text : display,
+ sizeof(edit->text), edit->input && editing);
+ }
+
+ if (!edit->input) {
+ return false;
+ }
+
+ if (editing && IsKeyPressed(KEY_ESCAPE)) {
+ edit->focus = 0;
+ return false;
+ }
+
+ if (!toggle) {
+ return false;
+ }
+
+ if (!editing) {
+ memset(edit->text, 0, sizeof(edit->text));
+ snprintf(edit->text, sizeof(edit->text), "%s",
+ mixed ? "" : display);
+ edit->focus = id;
+ return false;
+ }
+
+ edit->focus = 0;
+ return IsKeyPressed(KEY_ENTER);
+}
+
+static bool
+panel_float(PanelEdit *edit, Rect box, f32 *value, bool mixed) {
+ char display[64] = "";
+ if (!mixed) {
+ snprintf(display, sizeof(display), "%.3f", *value);
+ }
+
+ if (!panel_field(edit, box, display, mixed, true)) {
+ return false;
+ }
+
+ char *end;
+ f32 parsed = strtof(edit->text, &end);
+ if (end == edit->text || *end != '\0') {
+ return false;
+ }
+
+ *value = parsed;
+ return true;
+}
+
+/* Returns the submitted axis, or -1 when nothing was submitted. */
+static int
+panel_vec3(PanelEdit *edit, Rect row, cstr label, Vec3 *value, bool mixed) {
+ GuiLabel((Rect){ row.x, row.y, 45, row.height }, label);
+
+ f32 *axis[3] = { &value->x, &value->y, &value->z };
+ f32 width = (row.width - 45) / 3;
+ int submitted = -1;
+
+ for (int i = 0; i < 3; ++i) {
+ Rect box = {
+ row.x + 45 + i * width,
+ row.y,
+ width - 4,
+ row.height
+ };
+
+ if (panel_float(edit, box, axis[i], mixed)) {
+ submitted = i;
+ }
+ }
+
+ return submitted;
+}
+
+static void
+panel_properties(EditorPanelContainer container, bool input) {
+ static PanelEdit edit;
+
+ Rect bounds = ed.panel[container].bounds;
+ int state = GuiGetState();
+ f32 pad = 8;
+
+ input = input && app.mode == APP_MODE_EDIT_NONE
+ && state != STATE_DISABLED;
+
+ edit.next_id = 1;
+ edit.input = input;
+
+ if (!input) {
+ edit.focus = 0;
+ GuiSetState(STATE_DISABLED);
+ }
+
+ GuiPanel(bounds, "Entity Properties");
+
+ if (!ed.sel_cnt) {
+ edit.focus = 0;
+ GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
+ GuiLabel(bounds, "no entities selected");
+ GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT);
+ goto reset;
+ }
+
+ Entity *ent = NULL;
+ bool mixed = ed.sel_cnt > 1;
+
+ if (!mixed) {
+ for (u32 i = 0; i < entity_count(); ++i) {
+ Entity *candidate = entity_get(entity_id_at(i));
+ if (candidate && candidate->pid &&
+ candidate->ed.selected) {
+ ent = candidate;
+ break;
+ }
+ }
+ }
+
+ /* Name */
+ cstr name = mixed ? "<multiple entities selected>"
+ : (ent && ent->name ? ent->name : "");
+
+ f32 label_w = editor_label_w("Name");
+ Rect name_box = {
+ bounds.x + pad,
+ bounds.y + 30,
+ label_w,
+ 20
+ };
+
+ GuiLabel(name_box, "Name");
+ name_box.x += label_w + pad;
+ name_box.width = bounds.x + bounds.width - name_box.x - pad;
+
+ if (panel_field(&edit, name_box, name, mixed, false)) {
+ editor_rename_selected(edit.text);
+ }
+
+ /* Transform */
+ Rect group = {
+ bounds.x + pad,
+ bounds.y + 60,
+ bounds.width - 2 * pad,
+ 90 + 2 * pad
+ };
+
+ GuiGroupBox(group, "Transform");
+
+ Rect row = {
+ group.x + pad,
+ group.y + 10,
+ group.width - 2 * pad,
+ 20
+ };
+
+ Vec3 pos = ent ? ent->tr.pos : (Vec3){0};
+ int axis = panel_vec3(&edit, row, "pos", &pos, mixed);
+
+ if (axis >= 0) {
+ EditorAction action =
+ action_begin(ED_ACTION_TRANSFORM, false);
+
+ f32 value[3] = { pos.x, pos.y, pos.z };
+ for (u32 j = 0; j < action.item_cnt; ++j) {
+ Entity *target = entity_get(
+ entity_id_from_pid(action.item[j].pid)
+ );
+
+ f32 *component[3] = {
+ &target->tr.pos.x,
+ &target->tr.pos.y,
+ &target->tr.pos.z,
+ };
+ *component[axis] = value[axis];
+ }
+
+ action_commit(&action);
+ }
+
+ row.y += 30;
+ Vec3 rot = ent ? QuaternionToEuler(ent->tr.rot) : (Vec3) {0};
+ rot.x *= RAD2DEG;
+ rot.y *= RAD2DEG;
+ rot.z *= RAD2DEG;
+ axis = panel_vec3(&edit, row, "rot", &rot, mixed);
+
+ if (axis >= 0) {
+ EditorAction action =
+ action_begin(ED_ACTION_TRANSFORM, false);
+
+ f32 value[3] = {
+ rot.x * DEG2RAD,
+ rot.y * DEG2RAD,
+ rot.z * DEG2RAD
+ };
+
+ for (u32 j = 0; j < action.item_cnt; ++j) {
+ Entity *target = entity_get(
+ entity_id_from_pid(action.item[j].pid)
+ );
+
+ 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];
+
+ target->tr.rot = QuaternionFromEuler(rt.x, rt.y, rt.z);
+ }
+
+ action_commit(&action);
+ }
+
+ row.y += 30;
+ Vec3 scale = ent ? ent->tr.scale : (Vec3){0};
+ axis = panel_vec3(&edit, row, "scale", &scale, mixed);
+
+ if (axis >= 0) {
+ EditorAction action =
+ action_begin(ED_ACTION_TRANSFORM, false);
+
+ f32 value[3] = { scale.x, scale.y, scale.z };
+ for (u32 j = 0; j < action.item_cnt; ++j) {
+ Entity *target = entity_get(
+ entity_id_from_pid(action.item[j].pid)
+ );
+
+ f32 *component[3] = {
+ &target->tr.scale.x,
+ &target->tr.scale.y,
+ &target->tr.scale.z,
+ };
+
+ *component[axis] = value[axis];
+ }
+
+ action_commit(&action);
+ }
+
+ reset:
+ GuiSetState(state);
+ if (input && container != ED_PNL_CENTER) {
+ ed.focus_captured = edit.focus != 0;
+ }
+}
+
// static void
// panel_properties(EditorPanelContainer container, bool input) {
// Rect view = {0};
@@ -1172,6 +1450,19 @@ panel_scene(EditorPanelContainer container, bool input) {
// 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,
@@ -1182,9 +1473,11 @@ panel_scene(EditorPanelContainer container, bool input) {
// 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,
@@ -1208,11 +1501,12 @@ panel_scene(EditorPanelContainer container, bool input) {
// }
// /* 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) {
-// Entity *ent = entity_get(entity_id_at(i));
+// ent = entity_get(entity_id_at(i));
// if (ent && ent->pid && ent->ed.selected) {
// substr = ent->name ? ent->name : "";
// break;
@@ -1266,233 +1560,101 @@ panel_scene(EditorPanelContainer container, bool input) {
// }
// }
-// /*
-// 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.
-// */
-
// /* transform comp */
-// {
// prec.x = bounds.x + pad;
// prec.y += 30;
// prec.width = bounds.width - pad * 2;
// prec.height = 90 + pad * 2;
// GuiGroupBox(prec, "Transform");
-// /* pos */
-// prec.height = 20;
-// prec.y += 15;
-// prec.x += pad;
-// prec.width -= pad;
-// GuiLabel(prec, "Pos");
-// // panel_vec3(prec, ent->pos, cstr label)
-
-// /* rot */
-// static Vec3 rot; // euler angle representation of entity rot
-// prec.y += 30;
-// GuiLabel(prec, "Rot");
-// {
-
+// 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]);
// }
-// /* scale */
-// prec.y += 30;
-// GuiLabel(prec, "Scale");
-
-// // GuiTextBox(prec, char *text, int textSize, false);
-// }
-
-// /* reset */
-// reset:
-// GuiSetState(state);
-// if (input && container != ED_PNL_CENTER) {
-// ed.focus_captured = text_focus;
-// }
-// }
-
-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();
+// bool editing = text_focus == field;
+// f32 draft = 0;
+// bool toggle = GuiValueBoxFloat(box, NULL,
+// editing ? text_input : display,
+// &draft, input && editing);
- 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");
-
- /* 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)];
+// if (!input) {
+// continue;
+// }
- Rect prec = bounds;
- f32 pad = 8;
- cstr substr = "";
+// if (editing && IsKeyPressed(KEY_ESCAPE)) {
+// text_focus = TEXT_FIELD_NONE;
+// continue;
+// }
- 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;
- }
+// if (!toggle) {
+// continue;
+// }
- /* 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;
- }
- }
- }
+// /* 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;
+// }
- if (text_focus != TEXT_FIELD_NAME) {
- snprintf(display_text, sizeof(display_text), "%s", substr);
- }
+// /* editing ended. clicking away cancels, enter submits */
+// text_focus = TEXT_FIELD_NONE;
+// if (!IsKeyPressed(KEY_ENTER)) {
+// continue;
+// }
- 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);
+// char *end;
+// f32 value = strtof(text_input, &end);
+// if (end == text_input || *end != '\0') {
+// continue;
+// }
- prec.x += name_lbl_w + pad;
- prec.width = bounds.x + bounds.width - prec.x - pad;
+// EditorAction action = action_begin(ED_ACTION_TRANSFORM, false);
- 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
- );
+// for (u32 j = 0; j < action.item_cnt; ++j) {
+// u64 id = entity_id_from_pid(action.item[j].pid);
+// Entity *target = entity_get(id);
- 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;
- }
- }
- }
+// // f32 *tval = (f32*)&target->tr.pos;
+// // *(tval + i) = value;
- /*
- 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.
- */
-
- /* transform comp */
- {
- prec.x = bounds.x + pad;
- prec.y += 30;
- prec.width = bounds.width - pad * 2;
- prec.height = 90 + pad * 2;
- GuiGroupBox(prec, "Transform");
-
- static Vec3 pos, rot, scale;
- // pos = panel_vec3(prec, pos, "pos");
- // rot = panel_vec3(prec, rot, "rot");
- // scale = panel_vec3(prec, scale, "scale");
-
- // /* pos */
- // prec.height = 20;
- // prec.y += 15;
- // prec.x += pad;
- // prec.width -= pad;
- // GuiLabel(prec, "Pos");
- // // panel_vec3(prec, ent->pos, cstr label)
-
- // /* rot */
- // static Vec3 rot; // euler angle representation of entity rot
- // prec.y += 30;
- // GuiLabel(prec, "Rot");
- // if (ent) {
- // rot = QuaternionToEuler(ent->tr.rot);
- // }
- // for (int i = 0; i < 3; ++i) {
+// f32 *axis[3] = {
+// &target->tr.pos.x,
+// &target->tr.pos.y,
+// &target->tr.pos.z,
+// };
- // }
-
- // /* scale */
- // prec.y += 30;
- // GuiLabel(prec, "Scale");
+// *axis[i] = value;
+// }
- // GuiTextBox(prec, char *text, int textSize, false);
- }
+// action_commit(&action);
+// }
- /* reset */
- reset:
- GuiSetState(state);
- if (input && container != ED_PNL_CENTER) {
- ed.focus_captured = text_focus;
- }
-}
+// /* reset */
+// reset:
+// GuiSetState(state);
+// if (input && container != ED_PNL_CENTER) {
+// ed.focus_captured = text_focus;
+// }
+// }
static void
panel_space(EditorPanelContainer container) {
@@ -1723,7 +1885,7 @@ editor_label_w(cstr text) {
void
editor_rename_selected(cstr name) {
- EditorAction action = action_begin(ED_ACTION_RENAME);
+ EditorAction action = action_begin(ED_ACTION_RENAME, false);
for (u32 i = 0; i < action.item_cnt; ++i) {
u64 id = entity_id_from_pid(action.item[i].pid);