#include "editor.h" #include "save.h" #include "cam.h" #include "physics.h" #include "ent.h" #include "app.h" #include "res.h" #include "raygui.h" static bool started = false; static Vec2 start_point = {0}; static Vec2 end_point = {0}; static Rect sel_rec = {0}; static f32 sel_dist = 0; static EditorAction tr_action = {0}; EditorInfo ed = { .panel = { [ED_PNL_LEFT] = { .type = ED_PNL_TYPE_SCENE, // .enabled = true, }, [ED_PNL_RIGHT] = { .type = ED_PNL_TYPE_PROPERTIES, }, [ED_PNL_CENTER] = { .type = ED_PNL_TYPE_SPACE, }, }, }; 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 physics_pick(void); static void history_push(EditorAction action) { if (ed.history_save_csr > ed.history_csr) { ed.history_save_csr = UINT32_MAX; } /* discard redo branch */ while (ed.history_cnt > ed.history_csr) { action_free(&ed.history[--ed.history_cnt]); } if (ed.history_cnt == ed.history_cap) { u32 cap = ed.history_cap ? ed.history_cap * 2 : 64; void *history = realloc(ed.history, cap * sizeof(action)); if (!history) { perror("editor history alloc failed."); exit(1); } ed.history = history; ed.history_cap = cap; } ed.history[ed.history_cnt++] = action; ed.history_csr = ed.history_cnt; } static void history_undo(void) { if (ed.history_csr > 0) { action_apply(&ed.history[ed.history_csr - 1], true); ed.history_csr -= 1; } } static void history_redo(void) { if (ed.history_csr < ed.history_cnt) { action_apply(&ed.history[ed.history_csr], false); ed.history_csr += 1; } } static void history_clear(void) { for (u32 i = 0; i < ed.history_cnt; ++i) { action_free(&ed.history[i]); } ed.history_cnt = 0; ed.history_csr = 0; ed.history_save_csr = UINT32_MAX; } static void action_entity_extract(EditorAction *action) { for (u32 i = 0; i < action->item_cnt; ++i) { Entity *held = &action->item[i].before.stored.ent; assert(!held->pid); u64 id = entity_id_from_pid(action->item[i].pid); *held = entity_extract_now(id); } } static void action_entity_restore(EditorAction *action) { /* Restore all entities before resolving their relationships. */ for (u32 i = 0; i < action->item_cnt; ++i) { Entity *held = &action->item[i].before.stored.ent; assert(held->pid == action->item[i].pid); u64 id = entity_restore(action->item[i].pid); assert(id); *entity_get(id) = *held; *held = (Entity){0}; } for (u32 i = 0; i < action->item_cnt; ++i) { const EditorActionData *data = &action->item[i].before; u64 id = entity_id_from_pid(action->item[i].pid); Entity *ent = entity_get(id); ent->tr.parent = entity_id_from_pid(data->stored.parent); ent->tr.prev = entity_id_from_pid(data->stored.prev); ent->tr.next = entity_id_from_pid(data->stored.next); 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; } } } /* collects PIDs, recursive visits children first. */ static void action_collect(EditorAction *action, u64 id, bool recursive) { Entity *ent = entity_get(id); assert(ent && ent->pid); if (recursive) { for (u64 child = ent->tr.first_child; child; child = entity_get(child)->tr.next) { action_collect(action, child, true); } } u32 i = action->item_cnt++; /* First pass counts, second pass fills the allocated array. */ if (action->item) { action->item[i].pid = ent->pid; } } static EditorAction action_begin(EditorActionTag tag, bool roots_only) { EditorAction action = { .tag = tag }; // bool roots_only = false; bool recursive = false; switch (tag) { case ED_ACTION_TRANSFORM: // roots_only = true; break; case ED_ACTION_CREATE: action.item_cnt = 1; action.item = calloc(1, sizeof(*action.item)); if (!action.item) { perror("action_begin: create entity alloc failed."); exit(1); } u64 id = entity_create(false); Entity *ent = entity_get(id); action.item[0].pid = ent->pid; entities_clear_selected(); ent->ed.selected = true; return action; case ED_ACTION_DELETE: recursive = true; break; case ED_ACTION_RENAME: case ED_ACTION_MODEL: case ED_ACTION_PHYSICS: break; default: TraceLog(LOG_FATAL, "action_begin: unsupported tag."); } /* Count, allocate, then collect PIDs. */ for (u32 pass = 0; pass < 2; ++pass) { for (u32 i = 0; i < entity_count(); ++i) { Entity *ent = &entity_data()[i]; if (!ent || !ent->pid) { continue; } if (roots_only ? !selection_root(ent) : !ent->ed.selected) { continue; } action_collect(&action, entity_id_at(i), recursive); } if (pass == 0) { if (!action.item_cnt) { return action; } action.item = calloc(action.item_cnt, sizeof(*action.item)); if (!action.item) { perror("editor action alloc failed."); exit(1); } action.item_cnt = 0; } } /* Capture action-specific data for the collected entities. */ for (u32 i = 0; i < action.item_cnt; ++i) { Entity *ent = entity_get( 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); Entity *next = entity_get(ent->tr.next); assert(!parent || parent->pid); assert(!prev || prev->pid); assert(!next || next->pid); before->stored.parent = parent ? parent->pid : 0; before->stored.prev = prev ? prev->pid : 0; before->stored.next = next ? next->pid : 0; break; } case ED_ACTION_RENAME: // 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; case ED_ACTION_PHYSICS: before->physics = entity_physics_copy(&ent->phys); break; default: TraceLog(LOG_FATAL, "action_begin: unhandled action tag."); break; } } return action; } static void action_apply(EditorAction *action, bool undo) { switch (action->tag) { case ED_ACTION_TRANSFORM: 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); const EditorActionData *data = undo ? &action->item[i].before : &action->item[i].after; ent->tr.pos = data->transform.pos; 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: if (undo) { action_entity_extract(action); } else { action_entity_restore(action); } break; case ED_ACTION_DELETE: if (undo) { action_entity_restore(action); } else { action_entity_extract(action); } break; case ED_ACTION_RENAME: 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); char *name = ent->name; ent->name = action->item[i].before.rename.name; action->item[i].before.rename.name = name; } 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; 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; } } static void action_free(EditorAction *action) { switch (action->tag) { case ED_ACTION_TRANSFORM: break; case ED_ACTION_CREATE: case ED_ACTION_DELETE: for (u32 i = 0; i < action->item_cnt; ++i) { entity_clean(&action->item[i].before.stored.ent); } break; case ED_ACTION_RENAME: for (u32 i = 0; i < action->item_cnt; ++i) { free(action->item[i].before.rename.name); } break; case ED_ACTION_MODEL: for (u32 i = 0; i < action->item_cnt; ++i) { free(action->item[i].before.model.key); } 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; } free(action->item); } static void action_commit(EditorAction *action) { bool changed = false; switch (action->tag) { case ED_ACTION_TRANSFORM: 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); const EditorActionData *before = &action->item[i].before; EditorActionData *after = &action->item[i].after; after->transform.pos = ent->tr.pos; after->transform.rot = ent->tr.rot; after->transform.scale = ent->tr.scale; changed |= !Vector3Equals(before->transform.pos, after->transform.pos) || !QuaternionEquals(before->transform.rot, after->transform.rot) || !Vector3Equals(before->transform.scale, after->transform.scale); entity_physics_transform_changed(id); } if (!changed) { action_apply(action, true); } break; case ED_ACTION_CREATE: case ED_ACTION_DELETE: changed = action->item_cnt != 0; break; case ED_ACTION_RENAME: 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); cstr before = action->item[i].before.rename.name; changed |= strcmp(before ? before : "", ent->name ? ent->name : "") != 0; } if (!changed) { action_apply(action, true); } 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; case ED_ACTION_PHYSICS: changed = action->item_cnt != 0; break; default: TraceLog(LOG_FATAL, "action_commit: unhangled action tag."); break; } if (changed) { history_push(*action); } else { action_free(action); } *action = (EditorAction){0}; } 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"); if (fnt_asset) { GuiSetFont(fnt_asset->fnt); GuiSetStyle(DEFAULT, TEXT_SIZE, 16); } /* probably will delete this later */ ed.history_save_csr = save_file_read("res/save/scene.fgs") ? ed.history_csr : UINT32_MAX; } void editor_exit(void) { model_picker_close(); action_free(&tr_action); tr_action = (EditorAction){0}; history_clear(); free(ed.history); ed.history = NULL; 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); bool shift = IsKeyDown(KEY_LEFT_SHIFT); bool alt = IsKeyDown(KEY_LEFT_ALT); /* sorry about the indentation */ 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; } if (ed.panel[ED_PNL_CENTER].enabled) { ed.focus = ED_PNL_CENTER; ed.focus_captured = true; break; } if (!ed.focus_captured) { ed.focus = 0; Vec2 mpos = GetMousePosition(); if (ed.panel[ED_PNL_LEFT].enabled && CheckCollisionPointRec(mpos, ed.panel[ED_PNL_LEFT].bounds)) { ed.focus = ED_PNL_LEFT; } if (ed.panel[ED_PNL_RIGHT].enabled && CheckCollisionPointRec(mpos, ed.panel[ED_PNL_RIGHT].bounds)) { ed.focus = ED_PNL_RIGHT; } } if (ed.focus != 0) { break; } 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)) { ed.interact = ED_INTERACT_SELECT; editor_select(); // capture this frame break; } if (IsKeyPressed(KEY_G)) { if (!ctrl && ed.sel_cnt) { ed.interact = ED_INTERACT_MOVE; break; } } if (IsKeyPressed(KEY_R)) { if (!ctrl && ed.sel_cnt) { ed.interact = ED_INTERACT_ROTATE; break; } if (ctrl) { history_clear(); entity_system_clear(); ed.history_save_csr = save_file_read("res/save/scene.fgs") ? ed.history_csr : UINT32_MAX; } } if (IsKeyPressed(KEY_S)) { if (ctrl) { ed.history_save_csr = save_file_write("res/save/scene.fgs") ? ed.history_csr : UINT32_MAX; } /* scale */ } if (IsKeyPressed(KEY_X)) { if (!ctrl && ed.sel_cnt) { EditorAction action = action_begin(ED_ACTION_DELETE, true); if (action.item_cnt) { action_apply(&action, false); action_commit(&action); } } } 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: editor_select(); break; 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; EnableCursor(); break; } main_cam.update = true; break; } case ED_INTERACT_MOVE: editor_move(); break; case ED_INTERACT_ROTATE: editor_rotate(); break; default: 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(); ed.sel_rot = QuaternionIdentity(); for (u32 i = 0; i < entity_count(); ++i) { u64 id = entity_id_at(i); Entity *ent = entity_get(id); if (!ent->ed.selected) { continue; } ed.sel_cnt++; ed.sel_point = Vector3Add(ed.sel_point, entity_world_pos(id)); ed.sel_rot = entity_world_rot(id); } if (ed.sel_cnt > 0) { ed.sel_point = Vector3Scale( ed.sel_point, 1.0f / ed.sel_cnt); } if (ed.sel_cnt != 1) { ed.sel_rot = QuaternionIdentity(); } } /* tools */ 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), }; 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) { 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); if (hit.hit && hit.fraction <= closest) { closest = hit.fraction; ed.physics_body = (int)i; ed.physics_shape = (int)j; } } } } static void sel_area(void) { Vec3 fwd = Vector3Subtract(main_cam.cam.target, main_cam.cam.position); for (u32 i = 0; i < entity_count(); ++i) { u64 id = entity_id_at(i); Entity *ent = entity_get(id); if (ent->pid == 0) { continue; } if (!IsKeyDown(KEY_LEFT_SHIFT)) { ent->ed.selected = false; } Vec3 world_pos = entity_world_pos(id); Vec3 offset = Vector3Subtract(world_pos, main_cam.cam.position); // Only the dot product’s sign matters, so neither vector // needs normalization. if (Vector3DotProduct(fwd, offset) <= 0) { continue; } Vec2 pos = GetWorldToScreen(world_pos, main_cam.cam); if (CheckCollisionPointRec(pos, sel_rec)) { ent->ed.selected = true; } } } static void sel_rec_draw(void) { DrawRectangleRec(sel_rec, (Color) { 255, 255, 255, 40 }); DrawRectangleLines(sel_rec.x, sel_rec.y, sel_rec.width, sel_rec.height, (Color) { 255, 255, 255, 80 }); } static bool selection_root(Entity *ent) { if (!ent->ed.selected) { return false; } for (u64 parent = ent->tr.parent; parent;) { Entity *ancestor = entity_get(parent); assert(ancestor); if (ancestor->ed.selected) { return false; } parent = ancestor->tr.parent; } return true; } static Vec3 selected_pivot(void) { Vec3 pivot = {0}; int count = 0; for (int i = 0; i < entity_count(); ++i) { u64 id = entity_id_at(i); Entity *ent = entity_get(id); if (!ent || !ent->ed.selected) { continue; } pivot = Vector3Add(pivot, entity_world_pos(id)); count++; } if (count > 0) { pivot = Vector3Scale(pivot, 1.0f / (f32)count); } return pivot; } void editor_select(void) { if (!started) { start_point = GetMousePosition(); started = true; } end_point = GetMousePosition(); sel_dist = Vector2Distance(start_point, end_point); sel_rec = (Rect) { fminf(start_point.x, end_point.x), fminf(start_point.y, end_point.y), fabs(end_point.x - start_point.x), fabs(end_point.y - start_point.y) }; if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { if (sel_dist < 20) { sel_ray(); } else { sel_area(); } started = false; sel_dist = 0; ed.interact = ED_INTERACT_NONE; } } static bool ray_plane_hit(Vec3 plane_pos, Vec3 plane_dir, Vec3 *out) { Ray ray = GetScreenToWorldRay(GetMousePosition(), main_cam.cam); f32 denom = Vector3DotProduct(ray.direction, plane_dir); if (fabsf(denom) < 0.0001f) { return false; } Vec3 offset = Vector3Subtract(plane_pos, ray.position); f32 dist = Vector3DotProduct(offset, plane_dir) / denom; if (dist < 0.0f) { return false; } *out = Vector3Add(ray.position, Vector3Scale(ray.direction, dist)); return true; } void editor_move(void) { static Vec3 mv_start = {0}; if (!tr_action.item) { tr_action = action_begin(ED_ACTION_TRANSFORM, true); started = false; } bool cancel = IsKeyPressed(KEY_ESCAPE) || !tr_action.item_cnt; if (cancel || IsKeyPressed(KEY_ENTER) || IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { if (cancel) { action_apply(&tr_action, true); action_free(&tr_action); tr_action = (EditorAction){0}; } else { action_commit(&tr_action); } started = false; ed.interact = ED_INTERACT_NONE; return; } Vec3 pivot = selected_pivot(); Vec3 axis = { 0, 1, 0 }; // mv plane normal Vec3 delta = {0}; if (IsKeyDown(KEY_LEFT_ALT)) { started = false; Vec2 a = GetWorldToScreen(pivot, main_cam.cam); Vec2 b = GetWorldToScreen( Vector3Add(pivot, axis), main_cam.cam); Vec2 scr_axis = Vector2Subtract(b, a); f32 lensqr = Vector2LengthSqr(scr_axis); if (lensqr <= 0.001f) { return; } f32 amount = Vector2DotProduct(GetMouseDelta(), scr_axis) / lensqr; delta = Vector3Scale(axis, amount); } else { Vec3 hit; if (!ray_plane_hit(pivot, axis, &hit)) { return; } if (!started) { mv_start = hit; started = true; return; } delta = Vector3Subtract(hit, mv_start); mv_start = hit; } for (u32 i = 0; i < tr_action.item_cnt; ++i) { u64 id = entity_id_from_pid(tr_action.item[i].pid); Entity *ent = entity_get(id); assert(ent); Vec3 pos = Vector3Add(entity_world_pos(id), delta); entity_set_world_pos(ent, pos); entity_physics_transform_changed(id); } } void editor_rotate(void) { if (!tr_action.item) { tr_action = action_begin(ED_ACTION_TRANSFORM, true); started = false; } bool cancel = IsKeyPressed(KEY_ESCAPE) || !tr_action.item_cnt; if (cancel || IsKeyPressed(KEY_ENTER) || IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { if (cancel) { action_apply(&tr_action, true); action_free(&tr_action); tr_action = (EditorAction){0}; } else { action_commit(&tr_action); } started = false; ed.interact = ED_INTERACT_NONE; return; } Vec3 pivot = selected_pivot(); Vec3 mouse_hit = {0}; if (!ray_plane_hit(pivot, (Vec3) {0,1,0}, &mouse_hit)) { return; } Vec3 dir = Vector3Subtract(mouse_hit, pivot); if (Vector3LengthSqr(dir) < 0.0001f) { return; } f32 yaw = atan2f(dir.x, dir.z); Quaternion rot = QuaternionFromAxisAngle((Vec3){0,1,0}, yaw); for (u32 i = 0; i < tr_action.item_cnt; ++i) { u64 id = entity_id_from_pid(tr_action.item[i].pid); Entity *ent = entity_get(id); assert(ent); entity_set_world_rot(ent, rot); entity_physics_transform_changed(id); } } /* draw */ static void statusline(void) { char mode_str[16] = {0}; char ctx_str[256] = {0}; // Color bg = { 255, 255, 255, 150 }; Color bg = GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)); switch (ed.interact) { case ED_INTERACT_NONE: snprintf(mode_str, 16, "EDITOR"); if (ed.sel_cnt > 0) { snprintf(ctx_str, sizeof(ctx_str), "%i Selected | " "G: move, R: rotate, D: duplicate, " "X: delete", ed.sel_cnt); } else { snprintf(ctx_str, sizeof(ctx_str), "Nothing selected"); } break; case ED_INTERACT_SELECT: snprintf(mode_str, 16, "SELECT"); break; case ED_INTERACT_CAMERA: snprintf(mode_str, 16, "VIEW"); snprintf(ctx_str, sizeof(ctx_str), "controls: " "shift: boost speed, scroll: adjust speed"); break; case ED_INTERACT_MOVE: snprintf(mode_str, 16, "MOVE"); cstr axis_str = IsKeyDown(KEY_LEFT_ALT) ? "up axis" : "xz plane"; snprintf(ctx_str, sizeof(ctx_str), "%i Selected | " "current axis: %s | " "controls: shift: grid snap, " "alt: up axis, X/Y/Z: toggle axis", ed.sel_cnt, axis_str); break; case ED_INTERACT_ROTATE: snprintf(mode_str, 16, "ROTATE"); snprintf(ctx_str, sizeof(ctx_str), "%i Selected | " "axis: global up, pivot: each entity | " "controls: ctrl: change pivot, alt: local/global, " "X/Y/Z: toggle axis", ed.sel_cnt); break; default: return; 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 */ 0, GetScreenHeight() - 25, 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, heading_bg); GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); GuiLabel(rec, mode_str); GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); /* context str */ f32 ctx_str_w = editor_label_w(ctx_str); rec.x = 80; rec.width = ctx_str_w; GuiLabel(rec, ctx_str); rec.x += ctx_str_w; /* scene file status str */ char scene_str[96]; if (ed.history_save_csr == UINT32_MAX) { snprintf(scene_str, sizeof(scene_str), "| scene.fgs * - unsaved changes"); } else { u32 changes = ed.history_csr > ed.history_save_csr ? ed.history_csr - ed.history_save_csr : ed.history_save_csr - ed.history_csr; snprintf(scene_str, sizeof(scene_str), "| scene.fgs%s - %u change%s", changes ? " *" : "", changes, changes == 1 ? "" : "s"); } f32 scene_str_w = editor_label_w(scene_str); rec.x = GetScreenWidth() - scene_str_w - 5; rec.width = scene_str_w; DrawRectangleRec(rec, bg); GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); GuiLabel(rec, scene_str); GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); } /* Scene panel */ static void scene_entity(u64 id, Rect view, f32 *y, int depth, bool input) { Entity *ent = entity_get(id); if (!ent) { return; } /* Transient entities are hidden, but their children are visited. */ if (ent->pid) { Rect row = { view.x, *y, view.width, 26 }; bool hovered = 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; } } if (row.y + row.height > view.y && row.y < view.y + view.height) { if (ent->ed.selected || hovered) { int colour = ent->ed.selected ? BASE_COLOR_PRESSED : BASE_COLOR_FOCUSED; DrawRectangleRec(row, GetColor(GuiGetStyle(DEFAULT, colour))); } Rect label = { row.x + 8 + depth * 18, row.y, fmaxf(0, row.width - 8 - depth * 18), row.height }; if (label.width > 0) { GuiLabel(label, ent->name && ent->name[0] ? ent->name : "Entity"); } } *y += row.height; depth++; } for (u64 child = ent->tr.first_child; child;) { Entity *next = entity_get(child); if (!next) { break; } u64 sibling = next->tr.next; scene_entity(child, view, y, depth, input); child = sibling; } } static void panel_scene(EditorPanelContainer container, bool input) { Rect view = {0}; Rect bounds = ed.panel[container].bounds; Vec2 *scroll = &ed.panel[container].scroll; input = input && ed.interact == ED_INTERACT_NONE; int state = GuiGetState(); if (!input) { GuiSetState(STATE_DISABLED); } if (input && state != STATE_DISABLED && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { ed.focus_captured = true; } GuiPanel(bounds, "Scene"); f32 btn_w = editor_label_w("Create Entity") + 16; GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); if (GuiButton( (Rect){ bounds.x + 8, bounds.y + 30, btn_w, 26 }, "Create Entity")) { EditorAction action = action_begin(ED_ACTION_CREATE, false); action_commit(&action); } 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 += 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) { count++; } } f32 width = bounds.width - 2 * GuiGetStyle(DEFAULT, BORDER_WIDTH) - GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH); GuiScrollPanel(bounds, NULL, (Rect){ 0, 0, width, count * 26.0f }, scroll, &view); 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) { 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); } } EndScissorMode(); GuiSetState(state); if (input && container != ED_PNL_CENTER && !IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { ed.focus_captured = false; } } 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_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, }, "", value); } 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; static bool add_open; static int add_choice; Rect bounds = ed.panel[container].bounds; int state = GuiGetState(); f32 pad = 8; input = input && ed.interact == ED_INTERACT_NONE && state != STATE_DISABLED; edit.next_id = 1; 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) { 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; } } } /* 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) { continue; } physics_cnt += candidate->phys.has; if (!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; bool all_have_physics = physics_cnt == ed.sel_cnt; /* Name */ cstr name = mixed ? "" : (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 + 15, 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); 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); } f32 next_y = group.y + group.height + 16; model_comp: if (!all_have_model) { goto physics_comp; } group.y = next_y; group.height = 80; GuiGroupBox(group, "Model"); row = (Rect){ group.x + pad, group.y + 15, group.width - 2 * pad, 20, }; GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); if (GuiButton((Rect){ group.x, group.y + group.height + pad, 120, 20 }, "Remove Model")) { 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) ); free(target->mdl.key); target->mdl = (ModelCmp){0}; } action_commit(&action); goto reset; } GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); 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 ? "" : 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); } 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 = (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 (choice_cnt == 1) { add_open = false; GuiSetState(STATE_DISABLED); } bool toggle = GuiDropdownBox( row, choices, &add_choice, add_open); 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; } 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; } 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 || ed.panel[ED_PNL_CENTER].enabled; } } static void panel_space(EditorPanelContainer container) { static cstr strs[][2] = { { "Q", "Toggle left panel" }, { "E", "Toggle right panel" }, { "/", "Command Menu" }, }; static const int str_cnt = sizeof(strs) / sizeof(strs[0]); Rect bounds = { .x = (f32)GetScreenWidth() / 2 - 125, .y = (f32)GetScreenHeight() / 2 - 65, .width = 250, .height = 130, }; Color bg = GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)); // GuiPanel(bounds, "Space Menu"); DrawRectangleRec(bounds, bg); GuiGroupBox((Rect) { .x = bounds.x + 5, .y = bounds.y + 10, .width = bounds.width - 10, .height = bounds.height - 15, }, "Space Menu"); Rect lrec = bounds; lrec.height = 20; lrec.x += 16; for (int i = 0; i < str_cnt; ++i) { lrec.y += 30; GuiLabel(lrec, strs[i][0]); lrec.x += 50; GuiLabel(lrec, strs[i][1]); lrec.x -= 50; } } /* 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_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; } PhysicsCmp *phys = &ent->phys; Rect row = { bounds.x + 8, bounds.y + 30, bounds.width - 16, 20, }; if (GuiButton(row, "Add Body")) { EditorAction action = action_begin(ED_ACTION_PHYSICS, false); entity_physics_clear(phys); PhysicsBody *bodies = realloc( phys->bodies, (phys->body_cnt + 1) * sizeof(*bodies)); if (!bodies) { perror("physics body allocation failed"); exit(EXIT_FAILURE); } phys->bodies = bodies; phys->bodies[phys->body_cnt] = (PhysicsBody) { .rot = QuaternionIdentity(), .gravity_scale = 1.0f, .mode = PHYS_MODE_DYNAMIC, .enable_sleep = true, }; if (!phys->body_cnt) { phys->root_body = 0; } ed.physics_body = (int)phys->body_cnt; ed.physics_shape = -1; phys->body_cnt++; 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: {} 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; 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); } void editor_draw_ui(void) { statusline(); /* L/R panel info */ f32 screen_w = GetScreenWidth(); f32 screen_h = GetScreenHeight(); f32 left_w = Clamp(screen_w * 0.16f, 210.0f, 280.0f); f32 right_w = Clamp(screen_w * 0.22f, 260.0f, 360.0f); f32 status_h = 25.0f; f32 margin = 8.0f; ed.panel[ED_PNL_LEFT].bounds = (Rect) { margin, margin, left_w, screen_h - status_h - margin * 2.0f }; ed.panel[ED_PNL_RIGHT].bounds = (Rect) { screen_w - right_w - margin, margin, right_w, screen_h - status_h - margin * 2.0f }; panel_draw(ED_PNL_LEFT); panel_draw(ED_PNL_RIGHT); if (ed.interact == ED_INTERACT_SELECT) { if (started && sel_dist > 20) { sel_rec_draw(); } } if (ed.panel[ED_PNL_CENTER].enabled) { f32 width = fminf(640, screen_w - margin * 2); f32 height = fminf(420, screen_h - status_h - margin * 2); ed.panel[ED_PNL_CENTER].bounds = (Rect){ (screen_w - width) * 0.5f, (screen_h - status_h - height) * 0.5f, width, height }; DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Fade(BLACK, 0.35f)); panel_draw(ED_PNL_CENTER); } } /* 3D gizmos */ static void gizmo_transform(void) { Vec3 point = ed.sel_point; Quaternion rot = ed.sel_rot; Vec3 right = Vector3RotateByQuaternion((Vec3){1, 0, 0}, rot); Vec3 up = Vector3RotateByQuaternion((Vec3){0, 1, 0}, rot); Vec3 fwd = Vector3RotateByQuaternion((Vec3){0, 0, 1}, rot); f32 len = 0.75f; right = Vector3Scale(right, len); up = Vector3Scale(up, len); fwd = Vector3Scale(fwd, len); DrawLine3D(point, Vector3Add(point, fwd), BLUE); DrawLine3D(point, Vector3Add(point, up), GREEN); DrawLine3D(point, Vector3Add(point, right), RED); } static void gizmo_move(void) { Color col = { 255, 255, 255, 150 }; Vec3 point = ed.sel_point; if (IsKeyDown(KEY_LEFT_ALT)) { DrawLine3D(point, Vector3Add(point, (Vec3){0,100,0}), col); // GREEN DrawLine3D(point, Vector3Add(point, (Vec3){0,-100,0}), col); } else { DrawLine3D(point, Vector3Add(point, (Vec3){100,0,0}), col); //RED DrawLine3D(point, Vector3Add(point, (Vec3){-100,0,0}), col); DrawLine3D(point, Vector3Add(point, (Vec3){0,0,100}), col); //BLUE DrawLine3D(point, Vector3Add(point, (Vec3){0,0,-100}), col); } } static void gizmo_rotate(void) { Vec3 point = ed.sel_point; Color line_col = { 255, 255, 255, 150 }; Color circle_col = { 255, 255, 255, 100 }; if (ray_plane_hit(point, (Vec3){0,1,0}, &point)) { DrawLine3D(ed.sel_point, point, line_col); DrawCircle3D(ed.sel_point, 1.5, (Vec3){1,0,0}, 90, circle_col); } } 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 (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 (ed.interact == ED_INTERACT_ROTATE) { gizmo_rotate(); } if (ed.sel_cnt > 0) { gizmo_transform(); } } /* utils */ f32 editor_label_w(cstr text) { return (f32)GuiGetTextWidth(text) + 2.0f * GuiGetStyle(LABEL, BORDER_WIDTH) + 2.0f * GuiGetStyle(LABEL, TEXT_PADDING) + 2.0f; } void editor_rename_selected(cstr name) { 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); Entity *ent = entity_get(id); assert(ent); char *new_name = strdup(name); if (!new_name) { perror("rename alloc failed."); exit(1); } action.item[i].before.rename.name = ent->name; ent->name = new_name; } action_commit(&action); }