From edc50a43a0ab064472aa7fde90f14cf456d92076 Mon Sep 17 00:00:00 2001 From: tech kaczynski Date: Mon, 19 Oct 2026 01:47:08 +1000 Subject: lots of editor work. Improved editor camera controls. --- editor.c | 1620 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 1500 insertions(+), 120 deletions(-) (limited to 'editor.c') diff --git a/editor.c b/editor.c index 0e7fd58..1feb6ca 100644 --- a/editor.c +++ b/editor.c @@ -41,6 +41,7 @@ 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 physics_pick(void); static void history_push(EditorAction action) { @@ -195,6 +196,7 @@ action_begin(EditorActionTag tag, bool roots_only) { case ED_ACTION_RENAME: case ED_ACTION_MODEL: + case ED_ACTION_PHYSICS: break; default: @@ -266,6 +268,10 @@ action_begin(EditorActionTag tag, bool roots_only) { strdup(ent->mdl.key) : NULL; break; + case ED_ACTION_PHYSICS: + before->physics = entity_physics_copy(&ent->phys); + break; + default: TraceLog(LOG_FATAL, "action_begin: unhandled action tag."); @@ -293,6 +299,12 @@ action_apply(EditorAction *action, bool undo) { ent->tr.rot = data->transform.rot; ent->tr.scale = data->transform.scale; } + + /* second pass ensures transforms are properly updated */ + for (u32 i = 0; i < action->item_cnt; ++i) { + u64 id = entity_id_from_pid(action->item[i].pid); + entity_physics_transform_changed(id); + } break; case ED_ACTION_CREATE: @@ -334,6 +346,18 @@ action_apply(EditorAction *action, bool undo) { } break; + case ED_ACTION_PHYSICS: + for (u32 i = 0; i < action->item_cnt; ++i) { + u64 id = entity_id_from_pid(action->item[i].pid); + Entity *ent = entity_get(id); + + entity_physics_clear(&ent->phys); + PhysicsCmp pre = ent->phys; + ent->phys = action->item[i].before.physics; + action->item[i].before.physics = pre; + } + break; + default: TraceLog(LOG_FATAL, "action with invalid tag."); break; @@ -365,6 +389,12 @@ action_free(EditorAction *action) { } break; + case ED_ACTION_PHYSICS: + for (u32 i = 0; i < action->item_cnt; ++i) { + entity_physics_free(&action->item[i].before.physics); + } + break; + default: TraceLog(LOG_FATAL, "action_free: unhandled action tag."); break; @@ -396,6 +426,8 @@ action_commit(EditorAction *action) { after->transform.rot) || !Vector3Equals(before->transform.scale, after->transform.scale); + + entity_physics_transform_changed(id); } if (!changed) { @@ -441,6 +473,10 @@ action_commit(EditorAction *action) { } break; + case ED_ACTION_PHYSICS: + changed = action->item_cnt != 0; + break; + default: TraceLog(LOG_FATAL, "action_commit: unhangled action tag."); break; @@ -458,6 +494,8 @@ action_commit(EditorAction *action) { void editor_init(void) { // GuiLoadStyle("style_amber.rgs"); + // GuiLoadStyle("style_candy.rgs"); + // GuiLoadStyle("style_rltech.rgs"); Asset *fnt_asset = asset_get("res/fnt/Aileron-Regular.otf"); @@ -483,6 +521,85 @@ editor_exit(void) { ed.history_cap = 0; } +static void +editor_focus_selection(void) { + Vec3 minimum = {FLT_MAX, FLT_MAX, FLT_MAX}; + Vec3 maximum = {-FLT_MAX, -FLT_MAX, -FLT_MAX}; + bool found = false; + + for (u32 i = 0; i < entity_count(); ++i) { + u64 id = entity_id_at(i); + Entity *ent = entity_get(id); + + if (!ent->ed.selected) { + continue; + } + + found = true; + Matrix world = entity_world_matrix(id); + BoundingBox bounds = {0}; + + if (ent->mdl.has && ent->mdl.draw && + ent->mdl.mdl) { + Model model = *ent->mdl.mdl; + model.transform = MatrixIdentity(); + bounds = GetModelBoundingBox(model); + } + + for (int corner = 0; corner < 8; ++corner) { + Vec3 point = { + (corner & 1) ? + bounds.max.x : bounds.min.x, + (corner & 2) ? + bounds.max.y : bounds.min.y, + (corner & 4) ? + bounds.max.z : bounds.min.z, + }; + point = Vector3Transform(point, world); + minimum = Vector3Min(minimum, point); + maximum = Vector3Max(maximum, point); + } + } + + if (!found) { + return; + } + + Vec3 center = Vector3Scale( + Vector3Add(minimum, maximum), 0.5f); + f32 radius = fmaxf( + Vector3Distance(minimum, maximum) * 0.5f, 0.5f); + + /* Leave space for panels on either side of the view. */ + f32 width = GetScreenWidth(); + f32 height = GetScreenHeight(); + f32 margin = 0; + + if (ed.panel[ED_PNL_LEFT].enabled) { + Rect bounds = ed.panel[ED_PNL_LEFT].bounds; + margin = fmaxf(margin, bounds.x + bounds.width); + } + + if (ed.panel[ED_PNL_RIGHT].enabled) { + Rect bounds = ed.panel[ED_PNL_RIGHT].bounds; + margin = fmaxf(margin, width - bounds.x); + } + + f32 visible_width = fmaxf(width - 2.0f * margin, 1.0f); + f32 aspect = visible_width / height; + f32 half_fov = main_cam.perspective_fov * DEG2RAD * 0.5f; + f32 horizontal = atanf(tanf(half_fov) * aspect); + f32 angle = fminf(half_fov, horizontal); + f32 distance = radius / sinf(angle) * 1.15f; + + f32 view_height = 0; + if (main_cam.cam.projection == CAMERA_ORTHOGRAPHIC) { + view_height = 2.0f * radius * 1.15f / fminf(aspect, 1.0f); + } + + cam_focus(center, distance, view_height); +} + void editor_update(void) { bool ctrl = IsKeyDown(KEY_LEFT_CONTROL); @@ -490,8 +607,8 @@ editor_update(void) { bool alt = IsKeyDown(KEY_LEFT_ALT); /* sorry about the indentation */ - switch (app.mode) { -case APP_MODE_EDIT_NONE: + switch (ed.interact) { +case ED_INTERACT_NONE: if (!ed.focus_captured && IsKeyPressed(KEY_SPACE)) { ed.panel[ED_PNL_CENTER].type = ED_PNL_TYPE_SPACE; ed.panel[ED_PNL_CENTER].enabled = true; @@ -524,30 +641,48 @@ case APP_MODE_EDIT_NONE: break; } - if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) { - app.mode = APP_MODE_EDIT_CTRL; - main_cam.update = true; - HideCursor(); + if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) || + (main_cam.state == CAM_MODE_EDIT_ORBIT && + IsMouseButtonPressed(MOUSE_BUTTON_MIDDLE))) { + ed.interact = ED_INTERACT_CAMERA; + main_cam.update = false; DisableCursor(); break; } + if (ed.mode == ED_MODE_PHYSICS) { + if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + physics_pick(); + } + break; + } + + if (ed.mode != ED_MODE_SCENE) { + break; + } + + + if (!ctrl && IsKeyPressed(KEY_F)) { + editor_focus_selection(); + break; + } + if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { - app.mode = APP_MODE_EDIT_SELECT; + ed.interact = ED_INTERACT_SELECT; editor_select(); // capture this frame break; } if (IsKeyPressed(KEY_G)) { if (!ctrl && ed.sel_cnt) { - app.mode = APP_MODE_EDIT_MOVE; + ed.interact = ED_INTERACT_MOVE; break; } } if (IsKeyPressed(KEY_R)) { if (!ctrl && ed.sel_cnt) { - app.mode = APP_MODE_EDIT_ROTATE; + ed.interact = ED_INTERACT_ROTATE; break; } @@ -602,38 +737,50 @@ case APP_MODE_EDIT_NONE: } break; -case APP_MODE_EDIT_SELECT: +case ED_INTERACT_SELECT: editor_select(); break; -case APP_MODE_EDIT_CTRL: - cam_ctrl_editor(); - if (IsMouseButtonReleased(MOUSE_RIGHT_BUTTON)) { - app.mode = APP_MODE_EDIT_NONE; +case ED_INTERACT_CAMERA: { + bool held = IsMouseButtonDown(MOUSE_BUTTON_RIGHT); + + if (main_cam.state == CAM_MODE_EDIT_ORBIT) { + held |= IsMouseButtonDown(MOUSE_BUTTON_MIDDLE); + } + + if (!held) { + ed.interact = ED_INTERACT_NONE; main_cam.update = false; - ShowCursor(); + EnableCursor(); + break; } + + main_cam.update = true; break; +} -case APP_MODE_EDIT_MOVE: +case ED_INTERACT_MOVE: editor_move(); break; -case APP_MODE_EDIT_ROTATE: +case ED_INTERACT_ROTATE: editor_rotate(); break; -case APP_MODE_EDIT_SPACE: - break; - -// case APP_MODE_EDIT_CMD: - // break; - default: - TraceLog(LOG_FATAL, "editor_update: unhandled app.mode value"); + TraceLog(LOG_FATAL, "editor_update: unhandled ed.interact value"); break; } + main_cam.input = + (ed.interact == ED_INTERACT_CAMERA && main_cam.update) || + (ed.interact == ED_INTERACT_NONE && + ed.focus == ED_PNL_NULL && + !ed.focus_captured && + !ed.panel[ED_PNL_CENTER].enabled); + + cam_update(); + /* reset selection data */ ed.sel_cnt = 0; ed.sel_point = Vector3Zero(); @@ -666,34 +813,81 @@ default: static void sel_ray(void) { Ray ray = GetScreenToWorldRay(end_point, main_cam.cam); + Vec3 dir = Vector3Scale(ray.direction, 1000.0f); b3QueryFilter filter = { .categoryBits = BIT(PHYS_MASK_EDITOR_RAYCAST), .maskBits = BIT(PHYS_MASK_EDITOR_SELECTABLE), }; - Vec3 dir = Vector3Scale(ray.direction, 1000.0f); - b3RayResult res = - b3World_CastRayClosest(physics.world_id, - *(b3Pos*)&ray.position, - *(b3Pos*)&dir, - filter); + b3RayResult res = b3World_CastRayClosest( + physics.world_id, + (b3Pos){ray.position.x, ray.position.y, ray.position.z}, + (b3Vec3){dir.x, dir.y, dir.z}, + filter); if (ed.focus == ED_PNL_NULL && !IsKeyDown(KEY_LEFT_SHIFT)) { entities_clear_selected(); } - if (res.hit) { - b3BodyId bid = b3Shape_GetBody(res.shapeId); - u64 eid = *(u64*)b3Body_GetUserData(bid); - Entity *ent = entity_get(eid); + if (!res.hit) { + return; + } - if (ent->pid == 0) { - return; - } + b3BodyId body = b3Shape_GetBody(res.shapeId); + u64 *id = b3Body_GetUserData(body); + if (!id) { + return; + } + + Entity *ent = entity_get(*id); + if (!ent || !ent->pid) { + return; + } + + ent->ed.selected = !ent->ed.selected; +} + +static void +physics_pick(void) { + ed.physics_body = -1; + ed.physics_shape = -1; + + Entity *ent = entity_get(entity_id_from_pid(ed.physics_pid)); + if (!ent || !ent->phys.runtime.bodies) { + return; + } + + Ray ray = GetScreenToWorldRay(GetMousePosition(), main_cam.cam); + Vec3 dir = Vector3Scale(ray.direction, 1000.0f); + + b3Pos origin = { + ray.position.x, ray.position.y, ray.position.z, + }; + b3Vec3 translation = {dir.x, dir.y, dir.z}; + f32 closest = 1.0f; + + PhysicsCmp *phys = &ent->phys; + for (u32 i = 0; i < phys->body_cnt; ++i) { + PhysicsBody *body = &phys->bodies[i]; + PhysicsBodyRuntime *runtime = &phys->runtime.bodies[i]; + + for (u32 j = 0; j < body->shape_cnt; ++j) { + b3ShapeId shape = runtime->shapes[j]; + if (!b3Shape_IsValid(shape)) { + continue; + } + + b3WorldCastOutput hit = b3Shape_RayCast( + shape, origin, translation); - ent->ed.selected = !ent->ed.selected; + if (hit.hit && hit.fraction <= closest) { + closest = hit.fraction; + ed.physics_body = (int)i; + ed.physics_shape = (int)j; + } + } } } @@ -806,7 +1000,7 @@ editor_select(void) { } started = false; sel_dist = 0; - app.mode = APP_MODE_EDIT_NONE; + ed.interact = ED_INTERACT_NONE; } } @@ -851,7 +1045,7 @@ editor_move(void) { action_commit(&tr_action); } started = false; - app.mode = APP_MODE_EDIT_NONE; + ed.interact = ED_INTERACT_NONE; return; } @@ -900,6 +1094,7 @@ editor_move(void) { Vec3 pos = Vector3Add(entity_world_pos(id), delta); entity_set_world_pos(ent, pos); + entity_physics_transform_changed(id); } } @@ -922,7 +1117,7 @@ editor_rotate(void) { } started = false; - app.mode = APP_MODE_EDIT_NONE; + ed.interact = ED_INTERACT_NONE; return; } @@ -945,6 +1140,7 @@ editor_rotate(void) { Entity *ent = entity_get(id); assert(ent); entity_set_world_rot(ent, rot); + entity_physics_transform_changed(id); } } @@ -956,8 +1152,8 @@ statusline(void) { // Color bg = { 255, 255, 255, 150 }; Color bg = GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)); - switch (app.mode) { - case APP_MODE_EDIT_NONE: + switch (ed.interact) { + case ED_INTERACT_NONE: snprintf(mode_str, 16, "EDITOR"); if (ed.sel_cnt > 0) { snprintf(ctx_str, sizeof(ctx_str), @@ -970,17 +1166,17 @@ statusline(void) { } break; - case APP_MODE_EDIT_SELECT: + case ED_INTERACT_SELECT: snprintf(mode_str, 16, "SELECT"); break; - case APP_MODE_EDIT_CTRL: + case ED_INTERACT_CAMERA: snprintf(mode_str, 16, "VIEW"); snprintf(ctx_str, sizeof(ctx_str), "controls: " "shift: boost speed, scroll: adjust speed"); break; - case APP_MODE_EDIT_MOVE: + case ED_INTERACT_MOVE: snprintf(mode_str, 16, "MOVE"); cstr axis_str = IsKeyDown(KEY_LEFT_ALT) ? @@ -995,7 +1191,7 @@ statusline(void) { ed.sel_cnt, axis_str); break; - case APP_MODE_EDIT_ROTATE: + case ED_INTERACT_ROTATE: snprintf(mode_str, 16, "ROTATE"); snprintf(ctx_str, sizeof(ctx_str), "%i Selected | " "axis: global up, pivot: each entity | " @@ -1009,6 +1205,25 @@ statusline(void) { break; }; + Color heading_bg = { 0, 0, 0, 25 }; + + if (ed.mode == ED_MODE_PHYSICS) { + u64 id = entity_id_from_pid(ed.physics_pid); + Entity *ent = entity_get(id); + cstr name = ent && ent->name ? ent->name : ""; + + snprintf(mode_str, sizeof(mode_str), "PHYSICS"); + heading_bg = (Color){70, 140, 210, 90}; + + if (ed.interact == ED_INTERACT_CAMERA) { + snprintf(ctx_str, sizeof(ctx_str), + "%s | MMB: pan, scroll: zoom", name); + } else { + snprintf(ctx_str, sizeof(ctx_str), + "%s | RMB: orbit, scroll: zoom", name); + } + } + /* statusline bg */ Rect rec = { // 0, 0, /* top bar */ @@ -1016,12 +1231,13 @@ statusline(void) { GetScreenWidth(), 25 // editor_label_w(mode_str) , 25 }; + DrawRectangleRec(rec, bg); /* mode str */ f32 mode_str_w = editor_label_w(mode_str); rec.width = 70; - DrawRectangleRec(rec, (Color){0,0,0,25}); + DrawRectangleRec(rec, heading_bg); GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); GuiLabel(rec, mode_str); GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); @@ -1131,7 +1347,7 @@ panel_scene(EditorPanelContainer container, bool input) { Rect bounds = ed.panel[container].bounds; Vec2 *scroll = &ed.panel[container].scroll; - input = input && app.mode == APP_MODE_EDIT_NONE; + input = input && ed.interact == ED_INTERACT_NONE; int state = GuiGetState(); if (!input) { @@ -1143,7 +1359,7 @@ panel_scene(EditorPanelContainer container, bool input) { ed.focus_captured = true; } - GuiPanel(bounds, "Entities"); + GuiPanel(bounds, "Scene"); f32 btn_w = editor_label_w("Create Entity") + 16; GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); @@ -1155,10 +1371,18 @@ panel_scene(EditorPanelContainer container, bool input) { } GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); + GuiLabel((Rect){ + bounds.x + 8, bounds.y + 64, 110, 20, + }, "Draw Physics"); + + GuiCheckBox((Rect){ + bounds.x + 126, bounds.y + 64, 20, 20, + }, "", &ed.draw_physics); + bounds.x += 8; - bounds.y += 64; + bounds.y += 94; bounds.width -= 16; - bounds.height -= 72; + bounds.height -= 102; u32 count = 0; for (u32 i = 0; i < entity_count(); ++i) { @@ -1175,9 +1399,14 @@ panel_scene(EditorPanelContainer container, bool input) { (Rect){ 0, 0, width, count * 26.0f }, scroll, &view); - BeginScissorMode( - (int)view.x, (int)view.y, - (int)view.width, (int)view.height); + BeginScissorMode(view.x, view.y, view.width, view.height); + + if (input && state != STATE_DISABLED && !GuiIsLocked() && + IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && + !IsKeyDown(KEY_LEFT_SHIFT) && + CheckCollisionPointRec(GetMousePosition(), view)) { + entities_clear_selected(); + } f32 y = view.y + scroll->y; for (u32 i = 0; i < entity_count(); ++i) { @@ -1316,7 +1545,6 @@ 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; @@ -1327,7 +1555,7 @@ panel_properties(EditorPanelContainer container, bool input) { int state = GuiGetState(); f32 pad = 8; - input = input && app.mode == APP_MODE_EDIT_NONE + input = input && ed.interact == ED_INTERACT_NONE && state != STATE_DISABLED; edit.next_id = 1; @@ -1367,16 +1595,23 @@ panel_properties(EditorPanelContainer container, bool input) { } } + /* selection info prep */ ModelCmp *model = NULL; int model_cnt = 0; + int physics_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) { + if (!candidate->pid || !candidate->ed.selected) { + continue; + } + + physics_cnt += candidate->phys.has; + + if (!candidate->mdl.has) { continue; } @@ -1392,6 +1627,7 @@ panel_properties(EditorPanelContainer container, bool input) { } bool all_have_model = model_cnt == ed.sel_cnt; + bool all_have_physics = physics_cnt == ed.sel_cnt; /* Name */ cstr name = mixed ? "" @@ -1512,12 +1748,14 @@ panel_properties(EditorPanelContainer container, bool input) { action_commit(&action); } + f32 next_y = group.y + group.height + 16; + model_comp: if (!all_have_model) { - goto comp_add; + goto physics_comp; } - group.y += group.height + 16; + group.y = next_y; group.height = 80; GuiGroupBox(group, "Model"); @@ -1578,38 +1816,171 @@ panel_properties(EditorPanelContainer container, bool input) { action_commit(&action); } - row.y = group.y + group.height + 20; + next_y = group.y + group.height + pad + 20 + 16; + + physics_comp: + if (!all_have_physics) { + goto comp_add; + } + + group.y = next_y; + group.height = 80; + GuiGroupBox(group, "Physics"); + + row = (Rect){ + group.x + pad, + group.y + 15, + group.width - 2 * pad, + 20, + }; + + GuiLabel(row, mixed ? "" : + TextFormat("%u bodies, %u joints", + ent->phys.body_cnt, ent->phys.joint_cnt)); + + row.y += 30; + if (ent) { + if (GuiButton(row, "Edit Physics")) { + ed.scene_cam = main_cam; + + main_cam.allow_fly = false; + main_cam.input = false; + main_cam.update = false; + + u64 id = entity_id_from_pid(ent->pid); + + f32 view_height = 10.0f * + tanf(main_cam.perspective_fov * + DEG2RAD * 0.5f); + + cam_focus(entity_world_pos(id), 5.0f, view_height); + + ed.physics_pid = ent->pid; + ed.physics_body = -1; + ed.physics_shape = -1; + ed.physics_scroll = (Vec2){0}; + ed.prev_mode = ed.mode; + ed.mode = ED_MODE_PHYSICS; + ed.panel[ED_PNL_LEFT].type = + ED_PNL_TYPE_PHYSICS_ELEMENTS; + ed.panel[ED_PNL_RIGHT].type = + ED_PNL_TYPE_PHYSICS_PROPERTIES; + + edit.focus = 0; + add_open = false; + ed.focus = ED_PNL_NULL; + ed.focus_captured = false; + goto reset; + } + } else { + GuiLabel(row, "Select one entity to edit physics"); + } + + if (GuiButton((Rect){ + group.x, + group.y + group.height + pad, + 120, + 20, + }, "Remove Physics")) { + edit.focus = 0; + + EditorAction action = + action_begin(ED_ACTION_PHYSICS, false); + + for (u32 i = 0; i < action.item_cnt; ++i) { + u64 id = entity_id_from_pid(action.item[i].pid); + Entity *target = entity_get(id); + entity_physics_free(&target->phys); + } + + action_commit(&action); + goto reset; + } + + next_y = group.y + group.height + pad + 20 + 16; comp_add: GuiUnlock(); - row.y += 40; + + row = (Rect){ + bounds.x + pad, + next_y, + bounds.width - 2 * pad, + 20, + }; + + char choices[128] = "Add Component"; + int model_choice = -1; + int physics_choice = -1; + int choice_cnt = 1; + + if (!all_have_model) { + model_choice = choice_cnt++; + strcat(choices, model_cnt + ? ";Add Model to Missing" : ";Model"); + } + + if (!all_have_physics) { + physics_choice = choice_cnt++; + strcat(choices, physics_cnt + ? ";Add Physics to Missing" : ";Physics"); + } int add_state = GuiGetState(); - if (all_have_model) { + if (choice_cnt == 1) { add_open = false; GuiSetState(STATE_DISABLED); } - cstr choices = model_cnt - ? "Add Component;Add Model to Missing" - : "Add Component;Model"; + bool toggle = GuiDropdownBox( + row, choices, &add_choice, add_open); - if (GuiDropdownBox(row, choices, &add_choice, add_open)) { - add_open = !add_open; - edit.focus = 0; + GuiSetState(add_state); + + if (!toggle) { + goto reset; + } + + add_open = !add_open; + edit.focus = 0; + + if (add_open) { + goto reset; + } + + int choice = add_choice; + add_choice = 0; + + if (choice == model_choice) { + model_picker_open(MDL_PICK_COMP_ADD); + goto reset; + } + + if (choice != physics_choice) { + goto reset; + } - if (!add_open && add_choice == 1) { - model_picker_open(MDL_PICK_COMP_ADD); - add_choice = 0; + EditorAction action = action_begin(ED_ACTION_PHYSICS, false); + + for (u32 i = 0; i < action.item_cnt; ++i) { + Entity *target = entity_get( + entity_id_from_pid(action.item[i].pid) + ); + + if (target->phys.has) { + continue; } + + target->phys.has = true; } - GuiSetState(add_state); + action_commit(&action); reset: GuiUnlock(); GuiSetState(state); + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); if (input && container != ED_PNL_CENTER) { ed.focus_captured = edit.focus != 0 || add_open || @@ -2002,63 +2373,900 @@ panel_model_picker(Rect bounds, bool input) { } static void -panel_draw(EditorPanelContainer container) { - if (!ed.panel[container].enabled) { +panel_physics_elements(EditorPanelContainer container) { + Rect bounds = ed.panel[container].bounds; + GuiPanel(bounds, "Physics Elements"); + + Entity *ent = entity_get(entity_id_from_pid(ed.physics_pid)); + if (!ent) { return; } - bool input = app.mode == APP_MODE_EDIT_NONE && - ed.focus == container && - (!ed.panel[ED_PNL_CENTER].enabled || - container == ED_PNL_CENTER); - - Rect bounds = ed.panel[container].bounds; - int state = GuiGetState(); + PhysicsCmp *phys = &ent->phys; - if (!input) { - GuiSetState(STATE_DISABLED); - } + Rect row = { + bounds.x + 8, + bounds.y + 30, + bounds.width - 16, + 20, + }; - switch (ed.panel[container].type) { - case ED_PNL_TYPE_SCENE: - panel_scene(container, input); - break; + if (GuiButton(row, "Add Body")) { + EditorAction action = + action_begin(ED_ACTION_PHYSICS, false); - case ED_PNL_TYPE_PROPERTIES: - panel_properties(container, input); - break; + entity_physics_clear(phys); - case ED_PNL_TYPE_SPACE: - bool exit = false; + PhysicsBody *bodies = realloc( + phys->bodies, + (phys->body_cnt + 1) * sizeof(*bodies)); - if (IsKeyPressed(KEY_ESCAPE)) { - exit = true; + if (!bodies) { + perror("physics body allocation failed"); + exit(EXIT_FAILURE); } - if (IsKeyPressed(KEY_Q)) { - ed.panel[ED_PNL_LEFT].enabled = - !ed.panel[ED_PNL_LEFT].enabled; - exit = true; - } + phys->bodies = bodies; + phys->bodies[phys->body_cnt] = (PhysicsBody) { + .rot = QuaternionIdentity(), + .gravity_scale = 1.0f, + .mode = PHYS_MODE_DYNAMIC, + .enable_sleep = true, + }; - if (IsKeyPressed(KEY_E)) { - ed.panel[ED_PNL_RIGHT].enabled = - !ed.panel[ED_PNL_RIGHT].enabled; - exit = true; + if (!phys->body_cnt) { + phys->root_body = 0; } - if (exit) { - ed.panel[container].enabled = false; - ed.focus = ED_PNL_NULL; - ed.focus_captured = false; - app.mode = APP_MODE_EDIT_NONE; - } - panel_space(container); - break; + ed.physics_body = (int)phys->body_cnt; + ed.physics_shape = -1; + phys->body_cnt++; - case ED_PNL_TYPE_CMD: - GuiPanel(bounds, "Command Menu"); - break; + action_commit(&action); + } + + Rect list = { + bounds.x + 8, + bounds.y + 60, + bounds.width - 16, + bounds.height - 68, + }; + + Rect view = {0}; + f32 width = list.width + - 2 * GuiGetStyle(DEFAULT, BORDER_WIDTH) + - GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH); + + u32 row_count = phys->body_cnt; + for (u32 i = 0; i < phys->body_cnt; ++i) { + row_count += phys->bodies[i].shape_cnt; + } + + GuiScrollPanel(list, NULL, + (Rect){0, 0, width, row_count * 26.0f}, + &ed.physics_scroll, &view); + + bool input = GuiGetState() != STATE_DISABLED && !GuiIsLocked(); + Vec2 mouse = GetMousePosition(); + + if (input && IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && + CheckCollisionPointRec(mouse, view)) { + ed.physics_body = -1; + ed.physics_shape = -1; + } + + BeginScissorMode( + (int)view.x, (int)view.y, + (int)view.width, (int)view.height); + + cstr shape_names[] = { + "Box", + "Sphere", + "Capsule", + "Cylinder", + "Convex Hull", + "Triangle Mesh", + }; + + f32 y = view.y + ed.physics_scroll.y; + + for (u32 i = 0; i < phys->body_cnt; ++i) { + PhysicsBody *body = &phys->bodies[i]; + + for (int j = -1; j < (int)body->shape_cnt; ++j) { + row = (Rect){view.x, y, width, 26}; + y += 26; + + bool hovered = input && + CheckCollisionPointRec(mouse, view) && + CheckCollisionPointRec(mouse, row); + + if (hovered && + IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + ed.physics_body = (int)i; + ed.physics_shape = j; + } + + bool selected = ed.physics_body == (int)i && + ed.physics_shape == j; + + if (selected || hovered) { + int colour = selected + ? BASE_COLOR_PRESSED + : BASE_COLOR_FOCUSED; + + DrawRectangleRec(row, GetColor( + GuiGetStyle(DEFAULT, colour))); + } + + f32 indent = j < 0 ? 8 : 26; + row.x += indent; + row.width -= indent; + + if (j < 0) { + GuiLabel(row, TextFormat("Body %u%s", + i + 1, i == phys->root_body ? + " (root)" : "")); + } else { + GuiLabel(row, TextFormat("%s %i", + shape_names[body->shapes[j].tag], + j + 1)); + } + } + } + + EndScissorMode(); +} + +static void +panel_physics_shape(PanelEdit *edit, Rect row, + Entity *ent, PhysicsBody *body) { + u32 index = (u32)ed.physics_shape; + if (index >= body->shape_cnt) { + edit->focus = 0; + return; + } + + PhysicsShape *shape = &body->shapes[index]; + + GuiLabel(row, TextFormat("Body %i | Shape %u", + ed.physics_body + 1, index + 1)); + + row.y += 30; + + if (GuiButton(row, "Remove Shape")) { + edit->focus = 0; + + EditorAction action = + action_begin(ED_ACTION_PHYSICS, false); + + entity_physics_clear(&ent->phys); + + switch (shape->tag) { + case PHYS_SHAPE_CONVEX_HULL: + free(shape->ConvexHull.points); + break; + + case PHYS_SHAPE_STATIC_TRIMESH: + free(shape->TriMesh.points); + free(shape->TriMesh.indices); + break; + + default: + break; + } + + memmove(shape, shape + 1, + (body->shape_cnt - index - 1) * sizeof(*shape)); + body->shape_cnt--; + + if (!body->shape_cnt) { + free(body->shapes); + body->shapes = NULL; + } + + ed.physics_shape = -1; + action_commit(&action); + return; + } + + Vec3 values[3] = { + shape->pos, + Vector3Scale(QuaternionToEuler(shape->rot), RAD2DEG), + {0}, + }; + + cstr labels[3] = {"Pos", "Rot", "Size"}; + bool changed[3] = {false}; + int count = 2; + + if (shape->tag == PHYS_SHAPE_BOX) { + values[2] = shape->Box.sz; + count = 3; + } + + for (int i = 0; i < count; ++i) { + row.y += 30; + + Vec3 value = values[i]; + int axis = panel_vec3(edit, row, labels[i], &value, false); + + if (axis < 0) { + continue; + } + + if (!isfinite(value.x) || + !isfinite(value.y) || + !isfinite(value.z)) { + continue; + } + + if (i == 2 && + (value.x <= 0 || value.y <= 0 || value.z <= 0)) { + continue; + } + + changed[i] = !Vector3Equals(value, values[i]); + values[i] = value; + } + + f32 total_height = 0; + + if (shape->tag == PHYS_SHAPE_CAPSULE) { + total_height = shape->Capsule.height + + 2.0f * shape->Capsule.radius; + } + + f32 radius = 0; + bool radius_changed = false; + + if (shape->tag == PHYS_SHAPE_SPHERE || + shape->tag == PHYS_SHAPE_CAPSULE || + shape->tag == PHYS_SHAPE_CYLINDER) { + row.y += 30; + + GuiLabel((Rect){ + row.x, row.y, 60, row.height, + }, "Radius"); + + Rect box = { + row.x + 60, row.y, + row.width - 60, row.height, + }; + + f32 orig_radius; + if (shape->tag == PHYS_SHAPE_SPHERE) { + orig_radius = shape->Sphere.radius; + } else if (shape->tag == PHYS_SHAPE_CAPSULE) { + orig_radius = shape->Capsule.radius; + } else { + orig_radius = shape->Cylinder.radius; + } + radius = orig_radius; + + if (panel_float(edit, box, &radius, false)) { + radius_changed = isfinite(radius) && + radius > 0 && + radius != orig_radius; + + if (shape->tag == PHYS_SHAPE_CAPSULE && + radius > total_height * 0.5f) { + radius_changed = false; + } + } + } + + f32 height = 0; + bool height_changed = false; + + if (shape->tag == PHYS_SHAPE_CAPSULE || + shape->tag == PHYS_SHAPE_CYLINDER) { + row.y += 30; + + GuiLabel((Rect){ + row.x, row.y, 100, row.height, + }, "Height"); + + Rect box = { + row.x + 100, row.y, + row.width - 100, row.height, + }; + + f32 original_height = shape->tag == PHYS_SHAPE_CAPSULE + ? total_height : shape->Cylinder.height; + + height = original_height; + + if (panel_float(edit, box, &height, false)) { + height_changed = isfinite(height) && + height > 0 && + height != original_height; + + if (shape->tag == PHYS_SHAPE_CAPSULE) { + f32 effective_radius = radius_changed + ? radius : shape->Capsule.radius; + + if (height < 2.0f * effective_radius) { + height_changed = false; + } + } + } + } + + cstr material_labels[3] = { + "Density", + "Friction", + "Restitution", + }; + + f32 material[3] = { + shape->density, + shape->friction, + shape->restitution, + }; + + bool material_changed = false; + + for (int i = 0; i < 3; ++i) { + row.y += 30; + + GuiLabel((Rect){ + row.x, row.y, 100, row.height, + }, material_labels[i]); + + Rect box = { + row.x + 100, row.y, + row.width - 100, row.height, + }; + + f32 value = material[i]; + if (!panel_float(edit, box, &value, false)) { + continue; + } + + if (!isfinite(value) || value < 0) { + continue; + } + + if (i == 2 && value > 1) { + continue; + } + + material_changed |= value != material[i]; + material[i] = value; + } + + if (!changed[0] && !changed[1] && !changed[2] && + !radius_changed && !height_changed && + !material_changed) { + return; + } + + EditorAction action = action_begin(ED_ACTION_PHYSICS, false); + + entity_physics_clear(&ent->phys); + + if (changed[0]) { + shape->pos = values[0]; + } + + if (changed[1]) { + shape->rot = QuaternionFromEuler( + values[1].x * DEG2RAD, + values[1].y * DEG2RAD, + values[1].z * DEG2RAD); + } + + if (changed[2]) { + shape->Box.sz = values[2]; + } + + if (shape->tag == PHYS_SHAPE_SPHERE && radius_changed) { + shape->Sphere.radius = radius; + } + + if (shape->tag == PHYS_SHAPE_CAPSULE && + (radius_changed || height_changed)) { + f32 new_radius = radius_changed + ? radius : shape->Capsule.radius; + f32 new_height = height_changed ? height : total_height; + + shape->Capsule.radius = new_radius; + shape->Capsule.height = + fmaxf(0, new_height - 2.0f * new_radius); + } + + if (shape->tag == PHYS_SHAPE_CYLINDER) { + if (radius_changed) { + shape->Cylinder.radius = radius; + } + + if (height_changed) { + shape->Cylinder.height = height; + } + } + + shape->density = material[0]; + shape->friction = material[1]; + shape->restitution = material[2]; + + action_commit(&action); +} + +static void +panel_physics_properties(EditorPanelContainer container, bool input) { + Rect bounds = ed.panel[container].bounds; + + static bool type_open; + static PanelEdit edit; + + edit.next_id = 1; + edit.input = input && !type_open; + + if (!edit.input) { + edit.focus = 0; + } + + if (!input) { + type_open = false; + } + + if (type_open) { + GuiLock(); + } + + GuiPanel(bounds, "Physics Properties"); + + Rect button = { + bounds.x + 8, + bounds.y + 30, + bounds.width - 16, + 20, + }; + + if (GuiButton(button, "Done")) { + main_cam = ed.scene_cam; + edit.focus = 0; + ed.mode = ed.prev_mode; + ed.physics_pid = 0; + ed.panel[ED_PNL_LEFT].type = ED_PNL_TYPE_SCENE; + ed.panel[ED_PNL_RIGHT].type = ED_PNL_TYPE_PROPERTIES; + ed.focus = ED_PNL_NULL; + ed.focus_captured = false; + return; + } + + Entity *ent = entity_get(entity_id_from_pid(ed.physics_pid)); + + Rect info = { + bounds.x + 8, + bounds.y + 60, + bounds.width - 16, + 20, + }; + + if (ent && ed.physics_body >= 0 && + (u32)ed.physics_body < ent->phys.body_cnt) { + PhysicsBody *body = &ent->phys.bodies[ed.physics_body]; + + if (ed.physics_shape >= 0) { + type_open = false; + GuiUnlock(); + + panel_physics_shape(&edit, info, ent, body); + goto reset; + } + + GuiLabel(info, TextFormat("Body %i | %u shapes", + ed.physics_body + 1, body->shape_cnt)); + + info.y += 30; + + if (ent->phys.root_body == (u32)ed.physics_body) { + GuiLabel(info, "Root body"); + } else if (GuiButton(info, "Make Root Body")) { + edit.focus = 0; + + EditorAction action = + action_begin(ED_ACTION_PHYSICS, false); + + ent->phys.root_body = (u32)ed.physics_body; + + action_commit(&action); + } + + info.y += 30; + + if (GuiButton(info, "Remove Body")) { + edit.focus = 0; + + EditorAction action = + action_begin(ED_ACTION_PHYSICS, false); + + PhysicsCmp *phys = &ent->phys; + u32 index = (u32)ed.physics_body; + + entity_physics_clear(phys); + + for (u32 i = 0; i < body->shape_cnt; ++i) { + PhysicsShape *shape = &body->shapes[i]; + + switch (shape->tag) { + case PHYS_SHAPE_CONVEX_HULL: + free(shape->ConvexHull.points); + break; + + case PHYS_SHAPE_STATIC_TRIMESH: + free(shape->TriMesh.points); + free(shape->TriMesh.indices); + break; + + default: + break; + } + } + + free(body->shapes); + + memmove(body, body + 1, + (phys->body_cnt - index - 1) * sizeof(*body)); + + phys->body_cnt--; + + if (phys->body_cnt == index) { + phys->root_body = 0; + } else if (phys->root_body > index) { + phys->root_body -= 1; + } + + u32 kept = 0; + for (u32 i = 0; i < phys->joint_cnt; ++i) { + PhysicsJoint joint = phys->joints[i]; + + if (joint.body_a == index || + joint.body_b == index) { + continue; + } + + if (joint.body_a > index) { + joint.body_a--; + } + + if (joint.body_b > index) { + joint.body_b--; + } + + phys->joints[kept++] = joint; + } + phys->joint_cnt = kept; + + if (!phys->body_cnt) { + free(phys->bodies); + phys->bodies = NULL; + } + + if (!phys->joint_cnt) { + free(phys->joints); + phys->joints = NULL; + } + + ed.physics_body = -1; + ed.physics_shape = -1; + + action_commit(&action); + goto reset; + } + + info.y += 30; + + Vec3 pos = body->pos; + int pos_axis = panel_vec3( + &edit, info, "Pos", &pos, false); + + bool pos_changed = pos_axis >= 0 && + isfinite(pos.x) && + isfinite(pos.y) && + isfinite(pos.z) && + !Vector3Equals(pos, body->pos); + + info.y += 30; + + Vec3 original_rot = Vector3Scale( + QuaternionToEuler(body->rot), RAD2DEG); + + Vec3 rot = original_rot; + int rot_axis = panel_vec3( + &edit, info, "Rot", &rot, false); + + bool rot_changed = rot_axis >= 0 && + isfinite(rot.x) && + isfinite(rot.y) && + isfinite(rot.z) && + !Vector3Equals(rot, original_rot); + + info.y += 30; + + GuiLabel((Rect){ + info.x, info.y, 45, info.height, + }, "Type"); + + Rect type_box = { + info.x + 45, info.y, + info.width - 45, info.height, + }; + + info.y += 30; + + bool sleep = body->enable_sleep; + panel_bool(info, "Enable Sleep", &sleep, false); + + f32 values[3] = { + body->gravity_scale, + body->linear_damping, + body->angular_damping, + }; + + cstr labels[3] = { + "Gravity scale", + "Linear damping", + "Angular damping", + }; + + bool floats_changed = false; + + for (int i = 0; i < 3; ++i) { + info.y += 30; + + GuiLabel((Rect){ + info.x, info.y, 130, info.height, + }, labels[i]); + + Rect box = { + info.x + 130, info.y, + info.width - 130, info.height, + }; + + f32 value = values[i]; + if (panel_float(&edit, box, &value, false)) { + if (!isfinite(value)) { + continue; + } + + if (i > 0 && value < 0) { + continue; + } + + floats_changed |= value != values[i]; + values[i] = value; + } + } + + info.y += 30; + bool add_box = GuiButton(info, "Add Box"); + + info.y += 30; + bool add_sphere = GuiButton(info, "Add Sphere"); + + info.y += 30; + bool add_capsule = GuiButton(info, "Add Capsule"); + + info.y += 30; + bool add_cylinder = GuiButton(info, "Add Cylinder"); + + info.y += 30; + int button_state = GuiGetState(); + + if (!ent->mdl.has) { + GuiSetState(STATE_DISABLED); + } + + bool fit_model = GuiButton(info, "Add Model Box"); + GuiSetState(button_state); + + Model *model = NULL; + if (fit_model) { + Asset *asset = asset_get(ent->mdl.key); + if (asset && asset->tag == ASSET_TAG_MODEL && + asset->mdl.meshCount > 0) { + model = &asset->mdl; + } + } + + if (add_box || add_sphere || add_capsule || add_cylinder || + model) { + edit.focus = 0; + + EditorAction action = + action_begin(ED_ACTION_PHYSICS, false); + + entity_physics_clear(&ent->phys); + + PhysicsShape *shapes = realloc( + body->shapes, + (body->shape_cnt + 1) * sizeof(*shapes)); + + if (!shapes) { + perror("physics shape allocation failed"); + exit(1); + } + + body->shapes = shapes; + body->shapes[body->shape_cnt] = (PhysicsShape){ + .tag = PHYS_SHAPE_BOX, + .Box.sz = {1, 1, 1}, + .rot = QuaternionIdentity(), + .density = 1.0f, + .friction = 0.5f, + .category_bits = BIT(PHYS_MASK_DEFAULT), + .mask_bits = UINT32_MAX, + }; + + if (add_sphere) { + PhysicsShape *shape = + &body->shapes[body->shape_cnt]; + + shape->tag = PHYS_SHAPE_SPHERE; + shape->Sphere.radius = 0.5f; + } + + if (add_capsule) { + PhysicsShape *shape = + &body->shapes[body->shape_cnt]; + + shape->tag = PHYS_SHAPE_CAPSULE; + shape->Capsule.radius = 0.5f; + shape->Capsule.height = 1.0f; + } + + if (add_cylinder) { + PhysicsShape *shape = + &body->shapes[body->shape_cnt]; + + shape->tag = PHYS_SHAPE_CYLINDER; + shape->Cylinder.radius = 0.5f; + shape->Cylinder.height = 1.0f; + } + + if (model) { + Model local_model = *model; + local_model.transform = MatrixIdentity(); + + BoundingBox bounds = + GetModelBoundingBox(local_model); + + Vec3 center = Vector3Scale( + Vector3Add( + bounds.min, + bounds.max), 0.5f); + + PhysicsShape *shape = + &body->shapes[body->shape_cnt]; + + shape->Box.sz = + Vector3Subtract(bounds.max, + bounds.min); + shape->rot = QuaternionInvert(body->rot); + shape->pos = Vector3RotateByQuaternion( + Vector3Subtract(center, body->pos), + shape->rot); + } + + body->shape_cnt++; + + action_commit(&action); + } + + GuiUnlock(); + int mode = body->mode; + if (GuiDropdownBox(type_box, + "Static;Kinematic;Dynamic", + &mode, type_open)) { + type_open = !type_open; + edit.focus = 0; + } + + if (input && IsKeyPressed(KEY_ESCAPE)) { + type_open = false; + } + + if (mode != body->mode || + sleep != body->enable_sleep || + floats_changed || + pos_changed || + rot_changed) { + EditorAction action = + action_begin(ED_ACTION_PHYSICS, false); + + entity_physics_clear(&ent->phys); + + if (pos_changed) { + body->pos = pos; + } + + if (rot_changed) { + body->rot = QuaternionFromEuler( + rot.x * DEG2RAD, + rot.y * DEG2RAD, + rot.z * DEG2RAD); + } + + body->mode = (PhysicsMode)mode; + body->enable_sleep = sleep; + + body->gravity_scale = values[0]; + body->linear_damping = values[1]; + body->angular_damping = values[2]; + + action_commit(&action); + } + } else { + GuiLabel(info, "Select a body"); + } + + reset: + GuiUnlock(); + if (input) { + ed.focus_captured = type_open || edit.focus != 0; + } +} + +static void +panel_draw(EditorPanelContainer container) { + if (!ed.panel[container].enabled) { + return; + } + + bool input = ed.interact == ED_INTERACT_NONE && + ed.focus == container && + (!ed.panel[ED_PNL_CENTER].enabled || + container == ED_PNL_CENTER); + + Rect bounds = ed.panel[container].bounds; + int state = GuiGetState(); + + if (!input) { + GuiSetState(STATE_DISABLED); + } + + switch (ed.panel[container].type) { + case ED_PNL_TYPE_SCENE: + panel_scene(container, input); + break; + + case ED_PNL_TYPE_PROPERTIES: + panel_properties(container, input); + break; + + case ED_PNL_TYPE_SPACE: + bool exit = false; + + if (IsKeyPressed(KEY_ESCAPE)) { + exit = true; + } + + if (IsKeyPressed(KEY_Q)) { + ed.panel[ED_PNL_LEFT].enabled = + !ed.panel[ED_PNL_LEFT].enabled; + exit = true; + } + + if (IsKeyPressed(KEY_E)) { + ed.panel[ED_PNL_RIGHT].enabled = + !ed.panel[ED_PNL_RIGHT].enabled; + exit = true; + } + + if (exit) { + ed.panel[container].enabled = false; + ed.focus = ED_PNL_NULL; + ed.focus_captured = false; + } + panel_space(container); + break; + + case ED_PNL_TYPE_CMD: + GuiPanel(bounds, "Command Menu"); + break; case ED_PNL_TYPE_MODEL_PICKER: {} @@ -2081,6 +3289,14 @@ panel_draw(EditorPanelContainer container) { } break; + + case ED_PNL_TYPE_PHYSICS_ELEMENTS: + panel_physics_elements(container); + break; + + case ED_PNL_TYPE_PHYSICS_PROPERTIES: + panel_physics_properties(container, input); + break; } GuiSetState(state); @@ -2115,7 +3331,7 @@ editor_draw_ui(void) { panel_draw(ED_PNL_LEFT); panel_draw(ED_PNL_RIGHT); - if (app.mode == APP_MODE_EDIT_SELECT) { + if (ed.interact == ED_INTERACT_SELECT) { if (started && sel_dist > 20) { sel_rec_draw(); } @@ -2186,13 +3402,177 @@ gizmo_rotate(void) { } } +static void +physics_draw_shapes(u64 id) { + Entity *ent = entity_get(id); + if (!ent) { + return; + } + + Matrix entity_world = entity_world_matrix(id); + Quaternion entity_rot = entity_world_rot(id); + + for (u32 i = 0; i < ent->phys.body_cnt; ++i) { + PhysicsBody *body = &ent->phys.bodies[i]; + + Matrix body_local = MatrixMultiply( + QuaternionToMatrix(body->rot), + MatrixTranslate( + body->pos.x, body->pos.y, body->pos.z)); + + Matrix body_world = MatrixMultiply(body_local, entity_world); + + for (u32 j = 0; j < body->shape_cnt; ++j) { + PhysicsShape *shape = &body->shapes[j]; + + bool selected = ent->ed.selected; + + if (ed.mode == ED_MODE_PHYSICS) { + selected = ent->pid == ed.physics_pid && + ed.physics_body == (int)i && + (ed.physics_shape < 0 || + ed.physics_shape == (int)j); + } + + Color colour = selected ? ORANGE : SKYBLUE; + + Matrix shape_local = MatrixMultiply( + QuaternionToMatrix(shape->rot), + MatrixTranslate( + shape->pos.x, + shape->pos.y, + shape->pos.z)); + + Matrix world = MatrixMultiply(shape_local, body_world); + + Vec3 scale = { + Vector3Length( + (Vec3){world.m0, world.m1, world.m2}), + Vector3Length( + (Vec3){world.m4, world.m5, world.m6}), + Vector3Length( + (Vec3){world.m8, world.m9, world.m10}), + }; + + Vec3 center = {world.m12, world.m13, world.m14}; + + Quaternion rotation = QuaternionMultiply( + QuaternionMultiply(entity_rot, body->rot), + shape->rot); + + Matrix transform = MatrixMultiply( + QuaternionToMatrix(rotation), + MatrixTranslate(center.x, center.y, center.z)); + + rlPushMatrix(); + rlMultMatrixf(MatrixToFloat(transform)); + + switch (shape->tag) { + case PHYS_SHAPE_BOX: { + Vec3 size = Vector3Multiply( + shape->Box.sz, scale); + + DrawCubeWires((Vec3){0}, + size.x, size.y, size.z, colour); + break; + } + + case PHYS_SHAPE_SPHERE: { + Vec3 entity_scale = { + Vector3Length((Vec3){ + entity_world.m0, + entity_world.m1, + entity_world.m2, + }), + Vector3Length((Vec3){ + entity_world.m4, + entity_world.m5, + entity_world.m6, + }), + Vector3Length((Vec3){ + entity_world.m8, + entity_world.m9, + entity_world.m10, + }), + }; + + f32 largest = fmaxf(entity_scale.x, + fmaxf(entity_scale.y, + entity_scale.z)); + + DrawSphereWires((Vec3){0}, + shape->Sphere.radius * largest, + 8, 12, colour); + break; + } + + case PHYS_SHAPE_CAPSULE: { + f32 radius = shape->Capsule.radius * + fmaxf(scale.x, scale.z); + f32 half_height = + shape->Capsule.height * scale.y * 0.5; + + if (half_height == 0) { + DrawSphereWires((Vec3){0}, radius, 8, + 12, colour); + } else { + DrawCapsuleWires( + (Vec3) {0,-half_height,0}, + (Vec3){0,half_height,0}, + radius, 4, 8, colour); + } + break; + } + + case PHYS_SHAPE_CYLINDER: { + f32 radius = shape->Cylinder.radius * + fmaxf(scale.x, scale.z); + f32 half_height = + shape->Cylinder.height * scale.y * 0.5f; + + DrawCylinderWiresEx( + (Vec3){0, -half_height, 0}, + (Vec3){0, half_height, 0}, + radius, radius, 12, colour); + break; + } + + default: + break; + } + + rlPopMatrix(); + } + } +} + void editor_draw_3d(void) { - if (app.mode == APP_MODE_EDIT_MOVE) { + if (ed.mode == ED_MODE_PHYSICS) { + physics_draw_shapes(entity_id_from_pid(ed.physics_pid)); + return; + } + + if (ed.mode != ED_MODE_SCENE) { + return; + } + + if (ed.draw_physics) { + for (u32 i = 0; i < entity_count(); ++i) { + u64 id = entity_id_at(i); + Entity *ent = entity_get(id); + + if (ent->phys.has) { + physics_draw_shapes(id); + } + } + } + + if (ed.interact == ED_INTERACT_MOVE) { gizmo_move(); } - if (app.mode == APP_MODE_EDIT_ROTATE) { + if (ed.interact == ED_INTERACT_ROTATE) { gizmo_rotate(); } -- cgit v1.2.3