From 6fa680c739fcf9cf61de68e903976532ce9639f3 Mon Sep 17 00:00:00 2001 From: tech kaczynski Date: Sat, 24 Oct 2026 14:35:19 +1000 Subject: lots of work done to scene hierarchy, added support for reparenting by dragging elements around. Things function much more intuitively now. --- editor.c | 524 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 484 insertions(+), 40 deletions(-) (limited to 'editor.c') diff --git a/editor.c b/editor.c index c31c9a5..785a46e 100644 --- a/editor.c +++ b/editor.c @@ -376,6 +376,48 @@ action_apply(EditorAction *action, bool undo) { } break; + case ED_ACTION_REPARENT: { + /* Detach everything before restoring any recorded links. */ + for (u32 i = 0; i < action->item_cnt; ++i) { + u64 id = entity_id_from_pid(action->item[i].pid); + entity_set_parent(id, 0); + } + + for (u32 i = 0; i < action->item_cnt; ++i) { + u64 id = entity_id_from_pid(action->item[i].pid); + Entity *ent = entity_get(id); + const EditorActionData *data = undo + ? &action->item[i].before + : &action->item[i].after; + + ent->tr.parent = entity_id_from_pid(data->reparent.parent); + ent->tr.prev = entity_id_from_pid(data->reparent.prev); + ent->tr.next = entity_id_from_pid(data->reparent.next); + ent->tr.pos = data->reparent.pos; + ent->tr.rot = data->reparent.rot; + ent->tr.scale = data->reparent.scale; + } + + /* All moved entities now have their final relationships. */ + 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 (ent->tr.prev) { + entity_get(ent->tr.prev)->tr.next = id; + } else if (ent->tr.parent) { + entity_get(ent->tr.parent)->tr.first_child = id; + } + + if (ent->tr.next) { + entity_get(ent->tr.next)->tr.prev = id; + } + + entity_physics_transform_changed(id); + } + break; + } + case ED_ACTION_CREATE: case ED_ACTION_DUPLICATE: if (undo) { @@ -450,6 +492,9 @@ action_free(EditorAction *action) { case ED_ACTION_TRANSFORM: break; + case ED_ACTION_REPARENT: + break; + case ED_ACTION_CREATE: case ED_ACTION_DELETE: case ED_ACTION_DUPLICATE: @@ -858,6 +903,14 @@ case ED_INTERACT_NONE: } } + if (!ed.focus_captured && ctrl && IsKeyPressed(KEY_Z)) { + if (shift) { + history_redo(); + } else { + history_undo(); + } + } + if (ed.focus != 0) { break; } @@ -949,25 +1002,18 @@ case ED_INTERACT_NONE: } } - if (IsKeyPressed(KEY_A)) { - if (ctrl) { - for (u32 i = 0; i < entity_count(); ++i) { - u64 id = entity_id_at(i); - Entity *ent = entity_get(id); - if (ent && ent->pid) { - ent->ed.selected = true; - } - } - } - } + // if (IsKeyPressed(KEY_A)) { + // if (ctrl) { + // for (u32 i = 0; i < entity_count(); ++i) { + // u64 id = entity_id_at(i); + // Entity *ent = entity_get(id); + // if (ent && ent->pid) { + // ent->ed.selected = true; + // } + // } + // } + // } - if (ctrl && IsKeyPressed(KEY_Z)) { - if (!shift) { - history_undo(); - } else { - history_redo(); - } - } break; case ED_INTERACT_SELECT: @@ -1959,6 +2005,239 @@ statusline(void) { } /* Scene panel */ +static void +scene_collect_reparent(u64 id, EditorAction *action) { + Entity *ent = entity_get(id); + + if (ent->pid && selection_root(ent)) { + action->item[action->item_cnt++].pid = ent->pid; + return; + } + + for (u64 child = ent->tr.first_child; child;) { + scene_collect_reparent(child, action); + child = entity_get(child)->tr.next; + } +} + +static void +scene_reparent(u64 parent_id) { + Entity *parent = entity_get(parent_id); + + if (parent_id && (!parent || !parent->pid)) { + return; + } + + /* The destination cannot be within any selected subtree. */ + for (u64 ancestor = parent_id; ancestor;) { + Entity *ent = entity_get(ancestor); + + if (ent->ed.selected) { + return; + } + + ancestor = ent->tr.parent; + } + + EditorAction action = { + .tag = ED_ACTION_REPARENT, + }; + + action.item = calloc(entity_count(), sizeof(*action.item)); + if (!action.item) { + perror("scene_reparent: action allocation failed."); + exit(1); + } + + for (u32 i = 0; i < entity_count(); ++i) { + u64 id = entity_id_at(i); + + if (!entity_get(id)->tr.parent) { + scene_collect_reparent(id, &action); + } + } + + /* Leave entities already under this parent in place. */ + u32 count = 0; + for (u32 i = 0; i < action.item_cnt; ++i) { + u64 pid = action.item[i].pid; + Entity *ent = entity_get(entity_id_from_pid(pid)); + + if (ent->tr.parent != parent_id) { + action.item[count++].pid = pid; + } + } + action.item_cnt = count; + + if (!action.item_cnt) { + action_free(&action); + return; + } + + Matrix parent_world = entity_world_matrix(parent_id); + f32 determinant = MatrixDeterminant(parent_world); + + if (!isfinite(determinant) || determinant == 0.0f) { + TraceLog(LOG_WARNING, + "Cannot reparent through a zero-scale transform."); + action_free(&action); + return; + } + + Matrix parent_inverse = MatrixInvert(parent_world); + Entity *first = parent ? entity_get(parent->tr.first_child) : NULL; + + /* Prepare every item before changing the hierarchy. */ + for (u32 i = 0; i < action.item_cnt; ++i) { + u64 id = entity_id_from_pid(action.item[i].pid); + Entity *ent = entity_get(id); + EditorActionData *before = &action.item[i].before; + EditorActionData *after = &action.item[i].after; + + Entity *old_parent = entity_get(ent->tr.parent); + Entity *prev = entity_get(ent->tr.prev); + Entity *next = entity_get(ent->tr.next); + + before->reparent.parent = old_parent ? old_parent->pid : 0; + before->reparent.prev = prev ? prev->pid : 0; + before->reparent.next = next ? next->pid : 0; + before->reparent.pos = ent->tr.pos; + before->reparent.rot = ent->tr.rot; + before->reparent.scale = ent->tr.scale; + + Matrix local = MatrixMultiply( + entity_world_matrix(id), parent_inverse); + + Vec3 pos, scale; + Quaternion rot; + MatrixDecompose(local, &pos, &rot, &scale); + rot = QuaternionNormalize(rot); + + Matrix rebuilt = MatrixMultiply( + MatrixMultiply( + MatrixScale(scale.x, scale.y, scale.z), + QuaternionToMatrix(rot)), + MatrixTranslate(pos.x, pos.y, pos.z)); + + float16 expected = MatrixToFloatV(local); + float16 actual = MatrixToFloatV(rebuilt); + + for (int j = 0; j < 16; ++j) { + f32 tolerance = 0.0001f * + fmaxf(1.0f, fabsf(expected.v[j])); + + if (!isfinite(expected.v[j]) || + !isfinite(actual.v[j]) || + fabsf(expected.v[j] - actual.v[j]) > + tolerance) { + TraceLog(LOG_WARNING, + "Cannot preserve all selected" + " transforms."); + action_free(&action); + return; + } + } + + after->reparent.parent = parent ? parent->pid : 0; + after->reparent.pos = pos; + after->reparent.rot = rot; + after->reparent.scale = scale; + + if (parent_id) { + after->reparent.prev = i > 0 + ? action.item[i - 1].pid : 0; + after->reparent.next = i + 1 < action.item_cnt + ? action.item[i + 1].pid + : (first ? first->pid : 0); + } + } + + action_apply(&action, false); + history_push(action); +} + +static struct { + u64 id; + u64 hovered; + u64 expand_id; + Vec2 start; + f32 expand_time; + bool dragging; + bool invalid; +} scene_drag; + +static u64 scene_anchor_pid; + +static void +scene_collect_visible(u64 id, u64 *rows, u32 *count) { + Entity *ent = entity_get(id); + + if (ent->pid) { + rows[(*count)++] = id; + + if (ent->ed.collapsed) { + return; + } + } + + for (u64 child = ent->tr.first_child; child;) { + scene_collect_visible(child, rows, count); + child = entity_get(child)->tr.next; + } +} + +static void +scene_select_range(u64 id, bool additive) { + u64 *rows = malloc(entity_count() * sizeof(*rows)); + if (!rows) { + perror("scene_select_range: allocation failed."); + exit(1); + } + + u32 count = 0; + for (u32 i = 0; i < entity_count(); ++i) { + u64 root = entity_id_at(i); + + if (!entity_get(root)->tr.parent) { + scene_collect_visible(root, rows, &count); + } + } + + int anchor = -1; + int clicked = -1; + + for (u32 i = 0; i < count; ++i) { + if (entity_get(rows[i])->pid == scene_anchor_pid) { + anchor = (int)i; + } + + if (rows[i] == id) { + clicked = (int)i; + } + } + + assert(clicked >= 0); + + /* A missing or hidden anchor starts a new range here. */ + if (anchor < 0) { + anchor = clicked; + scene_anchor_pid = entity_get(id)->pid; + } + + if (!additive) { + entities_clear_selected(); + } + + int first = MIN(anchor, clicked); + int last = MAX(anchor, clicked); + + for (int i = first; i <= last; ++i) { + entity_get(rows[i])->ed.selected = true; + } + + free(rows); +} + static void scene_entity(u64 id, Rect view, f32 *y, int depth, bool input) { Entity *ent = entity_get(id); @@ -1973,12 +2252,60 @@ scene_entity(u64 id, Rect view, f32 *y, int depth, bool input) { CheckCollisionPointRec(GetMousePosition(), view) && CheckCollisionPointRec(GetMousePosition(), row); - if (hovered && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { - if (IsKeyDown(KEY_LEFT_SHIFT)) { - ent->ed.selected = !ent->ed.selected; - } else { - entities_clear_selected(); - ent->ed.selected = true; + Rect arrow = { + row.x + 8 + depth * 18, + row.y, + 18, + row.height + }; + + bool arrow_hovered = hovered && ent->tr.first_child && + CheckCollisionPointRec(GetMousePosition(), arrow); + + if (arrow_hovered && + IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + ent->ed.collapsed = !ent->ed.collapsed; + } + + if (hovered) { + scene_drag.hovered = id; + + if (!arrow_hovered && + IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + bool ctrl = IsKeyDown(KEY_LEFT_CONTROL) || + IsKeyDown(KEY_RIGHT_CONTROL); + bool shift = IsKeyDown(KEY_LEFT_SHIFT) || + IsKeyDown(KEY_RIGHT_SHIFT); + + if (shift) { + scene_select_range(id, ctrl); + } else if (ctrl) { + ent->ed.selected = !ent->ed.selected; + scene_anchor_pid = ent->pid; + } else { + if (!ent->ed.selected) { + entities_clear_selected(); + ent->ed.selected = true; + } + + scene_anchor_pid = ent->pid; + scene_drag.id = id; + scene_drag.start = GetMousePosition(); + scene_drag.dragging = false; + } + } + + if (scene_drag.dragging) { + for (u64 anc = id; anc;) { + Entity *ancestor = entity_get(anc); + + if (ancestor->ed.selected) { + scene_drag.invalid = true; + break; + } + + anc = ancestor->tr.parent; + } } } @@ -1994,13 +2321,22 @@ scene_entity(u64 id, Rect view, f32 *y, int depth, bool input) { colour))); } + if (ent->tr.first_child) { + GuiLabel(arrow, ent->ed.collapsed ? "+" : "-"); + } + Rect label = { - row.x + 8 + depth * 18, + arrow.x + arrow.width, row.y, - fmaxf(0, row.width - 8 - depth * 18), + fmaxf(0, row.width - 26 - depth * 18), row.height }; + if (hovered && scene_drag.dragging) { + DrawRectangleLinesEx(row, 2.0f, + scene_drag.invalid ? RED : GREEN); + } + if (label.width > 0) { GuiLabel(label, ent->name && ent->name[0] ? @@ -2012,6 +2348,10 @@ scene_entity(u64 id, Rect view, f32 *y, int depth, bool input) { depth++; } + if (ent->pid && ent->ed.collapsed) { + return; + } + for (u64 child = ent->tr.first_child; child;) { Entity *next = entity_get(child); if (!next) { @@ -2062,14 +2402,31 @@ panel_scene(EditorPanelContainer container, bool input) { bounds.x + 126, bounds.y + 64, 20, 20, }, "", &ed.draw_physics); - bounds.x += 8; - bounds.y += 94; - bounds.width -= 16; - bounds.height -= 102; + bounds.x += 8; + bounds.y += 94; + bounds.width -= 16; + bounds.height -= 102; u32 count = 0; for (u32 i = 0; i < entity_count(); ++i) { - if (entity_get(entity_id_at(i))->pid) { + Entity *ent = entity_get(entity_id_at(i)); + if (!ent->pid) { + continue; + } + + bool hidden = false; + for (u64 ancestor = ent->tr.parent; ancestor;) { + Entity *parent = entity_get(ancestor); + + if (parent->pid && parent->ed.collapsed) { + hidden = true; + break; + } + + ancestor = parent->tr.parent; + } + + if (!hidden) { count++; } } @@ -2079,30 +2436,117 @@ panel_scene(EditorPanelContainer container, bool input) { - GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH); GuiScrollPanel(bounds, NULL, - (Rect){ 0, 0, width, count * 26.0f }, + (Rect){ 0, 0, width, (count + 1) * 26.0f }, scroll, &view); - BeginScissorMode(view.x, view.y, view.width, view.height); + bool tree_input = input && + state != STATE_DISABLED && !GuiIsLocked(); - if (input && state != STATE_DISABLED && !GuiIsLocked() && - IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && - !IsKeyDown(KEY_LEFT_SHIFT) && - CheckCollisionPointRec(GetMousePosition(), view)) { - entities_clear_selected(); + Vec2 mouse = GetMousePosition(); + bool inside = tree_input && CheckCollisionPointRec(mouse, view); + + if (scene_drag.dragging && inside && + IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { + f32 direction = 0.0f; + + if (mouse.y < view.y + 24.0f) { + direction = 1.0f; + } else if (mouse.y > view.y + view.height - 24.0f) { + direction = -1.0f; + } + + f32 content_height = (count + 1) * 26.0f; + f32 max_scroll = fmaxf(0.0f, content_height - view.height); + + scroll->y = Clamp( + scroll->y + direction * 240.0f * GetFrameTime(), + -max_scroll, 0.0f); + } + + scene_drag.hovered = 0; + scene_drag.invalid = false; + + if (scene_drag.id) { + if (!tree_input || !entity_valid(scene_drag.id) || + IsKeyPressed(KEY_ESCAPE)) { + scene_drag.id = 0; + scene_drag.dragging = false; + } else if (IsMouseButtonDown(MOUSE_LEFT_BUTTON) && + Vector2Distance(mouse, scene_drag.start) >= 5.0f) { + scene_drag.dragging = true; + } } + BeginScissorMode(view.x, view.y, view.width, view.height); + f32 y = view.y + scroll->y; + Rect root_row = { view.x, y, view.width, 26 }; + + GuiLabel(root_row, "Scene Root"); + y += root_row.height; + for (u32 i = 0; i < entity_count(); ++i) { u64 id = entity_id_at(i); Entity *ent = entity_get(id); if (!ent->tr.parent) { - scene_entity(id, view, &y, 0, - input && state != STATE_DISABLED); + scene_entity(id, view, &y, 0, tree_input); } } + /* The root row and empty space both mean "make root". */ + if (scene_drag.dragging && inside && !scene_drag.hovered) { + DrawRectangleLinesEx(root_row, 2.0f, GREEN); + } + + /* Unmodified clicks on empty space clear the selection. */ + if (inside && !scene_drag.hovered && + IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && + !IsKeyDown(KEY_LEFT_CONTROL) && + !IsKeyDown(KEY_RIGHT_CONTROL) && + !IsKeyDown(KEY_LEFT_SHIFT) && + !IsKeyDown(KEY_RIGHT_SHIFT)) { + entities_clear_selected(); + scene_anchor_pid = 0; + } + EndScissorMode(); + + /* Apply changes after traversal has finished. */ + if (scene_drag.id && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { + if (scene_drag.dragging) { + if (inside && !scene_drag.invalid) { + scene_reparent(scene_drag.hovered); + } + } else if (inside && scene_drag.hovered == scene_drag.id) { + entities_clear_selected(); + entity_get(scene_drag.id)->ed.selected = true; + } + + scene_drag.id = 0; + scene_drag.dragging = false; + } + + Entity *target = entity_get(scene_drag.hovered); + + if (scene_drag.dragging && inside && !scene_drag.invalid && + IsMouseButtonDown(MOUSE_LEFT_BUTTON) && + target && target->ed.collapsed && target->tr.first_child) { + if (scene_drag.expand_id != scene_drag.hovered) { + scene_drag.expand_id = scene_drag.hovered; + scene_drag.expand_time = 0.0f; + } + + scene_drag.expand_time += GetFrameTime(); + + if (scene_drag.expand_time >= 0.6f) { + target->ed.collapsed = false; + } + } else { + scene_drag.expand_id = 0; + scene_drag.expand_time = 0.0f; + } + GuiSetState(state); if (input && container != ED_PNL_CENTER && -- cgit v1.2.3