From 6bdb9fa5f2214f816b4195922b0b2b85f667e364 Mon Sep 17 00:00:00 2001 From: tech kaczynski Date: Tue, 20 Oct 2026 07:45:23 +1000 Subject: lots of editor improvements. --- editor.c | 571 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- editor.h | 11 ++ 2 files changed, 552 insertions(+), 30 deletions(-) diff --git a/editor.c b/editor.c index 5ae5233..9f5eb21 100644 --- a/editor.c +++ b/editor.c @@ -14,6 +14,15 @@ static Rect sel_rec = {0}; static f32 sel_dist = 0; static EditorAction tr_action = {0}; +typedef struct { + Vec3 pos; + Quaternion rot; +} RotationRecord; + +static RotationRecord *rot_record; +static u32 rot_record_len, rot_record_cap; +static const f32 rotation_steps[] = {1, 5, 15, 30, 45, 90}; + EditorInfo ed = { .panel = { [ED_PNL_LEFT] = { @@ -510,11 +519,21 @@ editor_init(void) { ed.history_csr : UINT32_MAX; ed.grid_sz = 1.0f; + ed.rotation_snap = 2; // 15 degrees + + rot_record_cap = 16; + rot_record_len = 0; + rot_record = malloc(rot_record_cap * sizeof(*rot_record)); + if (!rot_record) { + perror("rot_record_start allocation failed"); + exit(1); + } } void editor_exit(void) { model_picker_close(); + free(rot_record); action_free(&tr_action); tr_action = (EditorAction){0}; history_clear(); @@ -703,9 +722,10 @@ case ED_INTERACT_NONE: ed.history_save_csr = save_file_write("res/save/scene.fgs") ? ed.history_csr : UINT32_MAX; + } else if (ed.sel_cnt) { + ed.interact = ED_INTERACT_SCALE; + break; } - - /* scale */ } if (IsKeyPressed(KEY_DELETE)) { @@ -769,6 +789,10 @@ case ED_INTERACT_ROTATE: editor_rotate(); break; +case ED_INTERACT_SCALE: + editor_scale(); + break; + default: TraceLog(LOG_FATAL, "editor_update: unhandled ed.interact value"); break; @@ -1233,9 +1257,41 @@ editor_move(void) { void editor_rotate(void) { + static f32 mouse_angle; + if (!tr_action.item) { tr_action = action_begin(ED_ACTION_TRANSFORM, true); + started = false; + ed.rotate_axis = -1; + mouse_angle = 0; + rot_record_len = tr_action.item_cnt; + ed.rotate_pivot = selected_pivot(); + + if (rot_record_len > rot_record_cap) { + u32 capacity = rot_record_cap; + + while (capacity < rot_record_len) { + capacity *= 2; + } + + RotationRecord *resized = realloc(rot_record, + capacity * sizeof(*rot_record)); + + if (!resized) { + perror("rotation_start resize failed"); + exit(EXIT_FAILURE); + } + + rot_record = resized; + rot_record_cap = capacity; + } + + for (u32 i = 0; i < rot_record_len; ++i) { + u64 id = entity_id_from_pid(tr_action.item[i].pid); + rot_record[i].pos = entity_world_pos(id); + rot_record[i].rot = entity_world_rot(id); + } } bool cancel = IsKeyPressed(KEY_ESCAPE) || !tr_action.item_cnt; @@ -1249,30 +1305,267 @@ editor_rotate(void) { action_commit(&tr_action); } + rot_record_len = 0; 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)) { + if (IsKeyDown(KEY_LEFT_CONTROL)) { + f32 wheel = -GetMouseWheelMove(); + + if (wheel > 0 && ed.rotation_snap > 0) { + ed.rotation_snap--; + } else if (wheel < 0 && ed.rotation_snap < 5) { + ed.rotation_snap++; + } + } + + bool reset = false; + + if (IsKeyPressed(KEY_L)) { + main_cam.local_space = !main_cam.local_space; + reset = true; + } + + if (IsKeyPressed(KEY_TAB)) { + ed.rotate_group = !ed.rotate_group; + reset = true; + } + + int keys[3] = {KEY_X, KEY_Y, KEY_Z}; + for (int i = 0; i < 3; ++i) { + if (IsKeyPressed(keys[i])) { + ed.rotate_axis = ed.rotate_axis == i ? -1 : i; + reset = true; + } + } + + if (reset) { + for (u32 i = 0; i < rot_record_len; ++i) { + u64 id = entity_id_from_pid(tr_action.item[i].pid); + Entity *ent = entity_get(id); + assert(ent); + + entity_set_world_pos(ent, rot_record[i].pos); + entity_set_world_rot(ent, rot_record[i].rot); + entity_physics_transform_changed(id); + } + + mouse_angle = 0; return; } - Vec3 dir = Vector3Subtract(mouse_hit, pivot); - if (Vector3LengthSqr(dir) < 0.0001f) { + bool local = main_cam.local_space && ed.sel_cnt == 1; + Vec3 mouse_hit = {0}; + + if (ed.rotate_axis < 0) { + if (!ray_plane_hit(ed.rotate_pivot, + (Vec3){0, 1, 0}, &mouse_hit)) { + return; + } + } else { + mouse_angle += GetMouseDelta().x * 0.01f; + } + + Vec3 axes[3] = { + {1, 0, 0}, + {0, 1, 0}, + {0, 0, 1}, + }; + + for (u32 i = 0; i < rot_record_len; ++i) { + u64 id = entity_id_from_pid(tr_action.item[i].pid); + Entity *ent = entity_get(id); + assert(ent); + + Quaternion original = rot_record[i].rot; + Quaternion basis = original; + + if (ed.rotate_group && ed.sel_cnt > 1) { + basis = QuaternionIdentity(); + } + + int axis_index = ed.rotate_axis < 0 ? 1 : ed.rotate_axis; + Vec3 axis = axes[axis_index]; + + if (local) { + axis = Vector3RotateByQuaternion(axis, basis); + } + + /* A perpendicular reference: X uses Y, Y uses Z, Z uses X. */ + Vec3 reference = axes[(axis_index + 1) % 3]; + Vec3 forward = Vector3RotateByQuaternion(reference, basis); + forward = Vector3Subtract(forward, + Vector3Scale(axis, Vector3DotProduct(forward, axis))); + + if (Vector3LengthSqr(forward) < 0.0001f) { + if (ed.rotate_axis < 0) { + continue; + } + + forward = Vector3RotateByQuaternion( + axes[(axis_index + 2) % 3], basis); + forward = Vector3Subtract(forward, + Vector3Scale(axis, + Vector3DotProduct(forward, + axis))); + } + + forward = Vector3Normalize(forward); + f32 angle = mouse_angle; + + if (ed.rotate_axis < 0) { + Vec3 pivot = ed.rotate_group ? + ed.rotate_pivot : + rot_record[i].pos; + + Vec3 direction = Vector3Subtract(mouse_hit, pivot); + direction = Vector3Subtract(direction, + Vector3Scale(axis, + Vector3DotProduct(direction, + axis))); + + if (Vector3LengthSqr(direction) < 0.0001f) { + continue; + } + + direction = Vector3Normalize(direction); + angle = atan2f(Vector3DotProduct(axis, + Vector3CrossProduct(forward, + direction)), + Vector3DotProduct(forward, direction)); + } + + if (IsKeyDown(KEY_LEFT_CONTROL)) { + f32 step = rotation_steps[ed.rotation_snap] * DEG2RAD; + f32 heading = 0; + + if (!local) { + heading = atan2f( + Vector3DotProduct(axis, + Vector3CrossProduct(reference, + forward)), + Vector3DotProduct(reference, forward)); + } + + angle = roundf((heading + angle) / step) * + step -heading; + } + + Quaternion turn = QuaternionFromAxisAngle(axis, angle); + Quaternion rotation = QuaternionNormalize( + QuaternionMultiply(turn, original)); + + if (ed.rotate_group) { + Vec3 offset = Vector3Subtract( + rot_record[i].pos, ed.rotate_pivot); + offset = Vector3RotateByQuaternion(offset, turn); + + entity_set_world_pos(ent, + Vector3Add(ed.rotate_pivot, offset)); + } + + entity_set_world_rot(ent, rotation); + entity_physics_transform_changed(id); + } +} + +void +editor_scale(void) { + if (!tr_action.item) { + tr_action = action_begin(ED_ACTION_TRANSFORM, true); + ed.scale_pivot = selected_pivot(); + ed.scale_factor = 1.0f; + ed.scale_axis = -1; + } + + 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); + } + + ed.interact = ED_INTERACT_NONE; return; } - f32 yaw = atan2f(dir.x, dir.z); - Quaternion rot = QuaternionFromAxisAngle((Vec3){0,1,0}, yaw); + bool reset = false; + + if (IsKeyPressed(KEY_TAB)) { + ed.scale_group = !ed.scale_group; + reset = true; + } + + int keys[3] = {KEY_X, KEY_Y, KEY_Z}; + for (int i = 0; i < 3; ++i) { + if (IsKeyPressed(keys[i])) { + ed.scale_axis = ed.scale_axis == i ? -1 : i; + reset = true; + } + } + + if (reset) { + ed.scale_factor = 1.0f; + } else { + f32 movement = GetMouseDelta().x; + if (movement == 0.0f) { + return; + } + + ed.scale_factor = Clamp( + ed.scale_factor * exp2f(movement * 0.01f), + 0.001f, 1000.0f); + } + + Vec3 factors = { + ed.scale_axis < 0 || ed.scale_axis == 0 ? + ed.scale_factor : 1.0f, + ed.scale_axis < 0 || ed.scale_axis == 1 ? + ed.scale_factor : 1.0f, + ed.scale_axis < 0 || ed.scale_axis == 2 ? + ed.scale_factor : 1.0f, + }; 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); + + EditorActionData *before = &tr_action.item[i].before; + Vec3 position = before->transform.pos; + + if (ed.scale_group) { + Vec3 pivot; + if (!entity_point_to_local(ent->tr.parent, + ed.scale_pivot, &pivot)) { + continue; + } + + Vec3 offset = Vector3Subtract(position, pivot); + + if (ed.scale_axis < 0) { + offset = Vector3Scale(offset, ed.scale_factor); + } else { + Quaternion rotation = before->transform.rot; + offset = Vector3RotateByQuaternion( + offset, QuaternionInvert(rotation)); + offset = Vector3Multiply(offset, factors); + offset = Vector3RotateByQuaternion( + offset, rotation); + } + + position = Vector3Add(pivot, offset); + } + + ent->tr.pos = position; + ent->tr.scale = Vector3Multiply( + before->transform.scale, factors); entity_physics_transform_changed(id); } } @@ -1285,17 +1578,15 @@ statusline(void) { // Color bg = { 255, 255, 255, 150 }; Color bg = GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)); - cstr view_space = "Global"; - if (main_cam.local_space) { - view_space = ed.sel_cnt == 1 ? "Local" : "Local (fallback)"; - } + cstr view_space = main_cam.local_space ? "Local" : "Global"; switch (ed.interact) { case ED_INTERACT_NONE: snprintf(mode_str, sizeof(mode_str), "EDITOR"); snprintf(ctx_str, sizeof(ctx_str), - "%i selected | View: %s [L] | " - "F: focus, XYZ: axis, P: projection, Del: delete", + "%i selected | View: %s | " + "F: focus, XYZ: axis, L: space " + "P: projection, Del: delete", ed.sel_cnt, view_space); break; @@ -1338,14 +1629,37 @@ statusline(void) { 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); + case ED_INTERACT_ROTATE: { + cstr axes[3] = {"X", "Y", "Z"}; + cstr constraint = ed.rotate_axis < 0 ? + "Aim" : axes[ed.rotate_axis]; + + snprintf(mode_str, sizeof(mode_str), "ROTATE"); + snprintf(ctx_str, sizeof(ctx_str), + "%i | %s %s | %s | Snap %.0f deg %s | " + "Tab: pivot, XYZ: axis, L: space, Ctrl+wheel: step", + ed.sel_cnt, + main_cam.local_space && ed.sel_cnt == 1 ? + "Local" : "Global", + constraint, + ed.rotate_group ? "Center" : "Individual", + rotation_steps[ed.rotation_snap], + IsKeyDown(KEY_LEFT_CONTROL) ? "[on]" : "[off]"); + break; + } + + case ED_INTERACT_SCALE: { + cstr axes[3] = {"Local X", "Local Y", "Local Z"}; + cstr constraint = ed.scale_axis < 0 ? + "Uniform" : axes[ed.scale_axis]; + + snprintf(mode_str, sizeof(mode_str), "SCALE"); + snprintf(ctx_str, sizeof(ctx_str), + "%i | %s %.3fx | %s | XYZ: axis, Tab: pivot", + ed.sel_cnt, constraint, ed.scale_factor, + ed.scale_group ? "Center" : "Individual"); break; + } default: return; @@ -3637,12 +3951,203 @@ gizmo_move(void) { 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); + Vec3 axes[3] = { + {1, 0, 0}, + {0, 1, 0}, + {0, 0, 1}, + }; + Color colors[3] = {RED, GREEN, BLUE}; + int axis_index = ed.rotate_axis < 0 ? 1 : ed.rotate_axis; + bool local = main_cam.local_space && ed.sel_cnt == 1; + + for (u32 i = 0; i < rot_record_len; ++i) { + if (ed.rotate_group && i > 0) { + break; + } + + u64 id = entity_id_from_pid(tr_action.item[i].pid); + Vec3 pivot = ed.rotate_group ? + ed.rotate_pivot : entity_world_pos(id); + Quaternion original = rot_record[i].rot; + Quaternion current = entity_world_rot(id); + + if (ed.rotate_group && ed.sel_cnt > 1) { + current = QuaternionNormalize(QuaternionMultiply( + current, QuaternionInvert(original))); + original = QuaternionIdentity(); + } + + Vec3 axis = axes[axis_index]; + if (local) { + axis = Vector3RotateByQuaternion(axis, original); + } + + Vec3 reference = axes[(axis_index + 1) % 3]; + Vec3 model_axis = reference; + Vec3 start = Vector3RotateByQuaternion(model_axis, original); + start = Vector3Subtract(start, + Vector3Scale(axis, Vector3DotProduct(start, axis))); + + if (Vector3LengthSqr(start) < 0.0001f) { + if (ed.rotate_axis < 0) { + continue; + } + + model_axis = axes[(axis_index + 2) % 3]; + start = Vector3RotateByQuaternion(model_axis, + original); + start = Vector3Subtract(start, Vector3Scale(axis, + Vector3DotProduct(start, axis))); + } + + start = Vector3Normalize(start); + + Vec3 applied = Vector3RotateByQuaternion(model_axis, current); + applied = Vector3Normalize(Vector3Subtract(applied, + Vector3Scale(axis, + Vector3DotProduct(applied, axis)))); + + Vec3 u = local ? start : reference; + Vec3 v = Vector3Normalize(Vector3CrossProduct(axis, u)); + + /* Keep the ring roughly the same size on screen. */ + f32 view_height = main_cam.cam.fovy; + if (main_cam.cam.projection == CAMERA_PERSPECTIVE) { + Vec3 forward = Vector3Normalize(Vector3Subtract( + main_cam.cam.target, main_cam.cam.position)); + f32 depth = Vector3DotProduct( + Vector3Subtract(pivot, + main_cam.cam.position), + forward); + + if (depth <= 0.0f) { + continue; + } + + view_height = 2.0f * depth * + tanf(main_cam.cam.fovy * DEG2RAD * 0.5f); + } + + f32 radius = view_height * 65.0f / GetScreenHeight(); + + if (ed.rotate_group) { + DrawSphereEx(pivot, radius * 0.04f, 4, 6, WHITE); + } + + Color color = Fade(colors[axis_index], 0.8f); + Vec3 previous = Vector3Add(pivot, Vector3Scale(u, radius)); + + for (int j = 1; j <= 64; ++j) { + f32 angle = 2.0f * PI * j / 64.0f; + Vec3 direction = Vector3Add( + Vector3Scale(u, cosf(angle)), + Vector3Scale(v, sinf(angle))); + Vec3 point = Vector3Add( + pivot, Vector3Scale(direction, radius)); + + DrawLine3D(previous, point, color); + previous = point; + } + + if (IsKeyDown(KEY_LEFT_CONTROL)) { + f32 degrees = rotation_steps[ed.rotation_snap]; + + /* At one-degree snapping, show every fifth tick. */ + int stride = degrees < 5.0f ? 5 : 1; + int count = (int)roundf(360.0f / degrees); + + for (int j = 0; j < count; j += stride) { + f32 angle = j * degrees * DEG2RAD; + Vec3 direction = Vector3Add( + Vector3Scale(u, cosf(angle)), + Vector3Scale(v, sinf(angle))); + + DrawLine3D( + Vector3Add(pivot, + Vector3Scale(direction, + radius * 0.94f)), + Vector3Add(pivot, + Vector3Scale(direction, + radius * 1.06f)), + Fade(LIGHTGRAY, 0.8f)); + } + } + + /* Zero reference, applied angle, and rotation axis. */ + DrawLine3D(pivot, + Vector3Add(pivot, Vector3Scale(u, radius)), + Fade(LIGHTGRAY, 0.4f)); + + DrawLine3D(pivot, + Vector3Add(pivot, Vector3Scale(applied, radius)), + WHITE); + + Vec3 extent = Vector3Scale(axis, radius * 1.3f); + DrawLine3D( + Vector3Subtract(pivot, extent), + Vector3Add(pivot, extent), color); + } +} + +static void +gizmo_scale(void) { + Vec3 axes[3] = { + {1, 0, 0}, + {0, 1, 0}, + {0, 0, 1}, + }; + Color colors[3] = {RED, GREEN, BLUE}; + + for (u32 i = 0; i < tr_action.item_cnt; ++i) { + u64 id = entity_id_from_pid(tr_action.item[i].pid); + Vec3 position = entity_world_pos(id); + Vec3 pivot = ed.scale_group ? ed.scale_pivot : position; + + f32 view_height = main_cam.cam.fovy; + if (main_cam.cam.projection == CAMERA_PERSPECTIVE) { + Vec3 forward = Vector3Normalize(Vector3Subtract( + main_cam.cam.target, main_cam.cam.position)); + f32 depth = Vector3DotProduct( + Vector3Subtract(pivot, main_cam.cam.position), + forward); + + if (depth <= 0.0f) { + continue; + } + + view_height = 2.0f * depth * + tanf(main_cam.cam.fovy * DEG2RAD * 0.5f); + } + + f32 pixel = view_height / GetScreenHeight(); + + if (!ed.scale_group || i == 0) { + DrawSphereEx(pivot, pixel * 3.0f, 4, 6, WHITE); + } + + if (ed.scale_group) { + DrawLine3D(pivot, position, Fade(LIGHTGRAY, 0.6f)); + } + + if (ed.scale_axis < 0) { + continue; + } + + Matrix world = entity_world_matrix(id); + Vec3 endpoint = Vector3Transform(axes[ed.scale_axis], world); + Vec3 direction = Vector3Subtract(endpoint, position); + + if (Vector3LengthSqr(direction) == 0.0f) { + continue; + } + + direction = Vector3Normalize(direction); + Vec3 extent = Vector3Scale(direction, 100.0f); + + DrawLine3D( + Vector3Subtract(pivot, extent), + Vector3Add(pivot, extent), + Fade(colors[ed.scale_axis], 0.8f)); } } @@ -3820,7 +4325,13 @@ editor_draw_3d(void) { gizmo_rotate(); } - if (ed.sel_cnt > 0 && ed.interact != ED_INTERACT_MOVE) { + if (ed.interact == ED_INTERACT_SCALE) { + gizmo_scale(); + } + + if (ed.sel_cnt > 0 && + ed.interact != ED_INTERACT_MOVE && + ed.interact != ED_INTERACT_SCALE) { gizmo_transform(); } } diff --git a/editor.h b/editor.h index 78a8850..a63ccbf 100644 --- a/editor.h +++ b/editor.h @@ -82,6 +82,7 @@ typedef struct { int sel_cnt; f32 grid_sz; + int rotation_snap; u64 physics_pid; int physics_body, physics_shape; @@ -97,8 +98,17 @@ typedef struct { int move_axis; bool move_plane; Vec3 move_snap_origin; + + int rotate_axis; + bool rotate_group; + Vec3 rotate_pivot; Quaternion move_rotation; + int scale_axis; + bool scale_group; + Vec3 scale_pivot; + f32 scale_factor; + struct { EditorPanelType type; Rect bounds; @@ -123,6 +133,7 @@ void editor_update(void); void editor_select(void); void editor_move(void); void editor_rotate(void); +void editor_scale(void); /* draw */ void editor_draw_ui(void); -- cgit v1.2.3