techkaczynski.dev / code

summaryrefslogtreecommitdiff
path: root/editor.c
diff options
context:
space:
mode:
authortech kaczynski <tk@techkaczynski.dev>2026-10-20 07:45:23 +1000
committertech kaczynski <tk@techkaczynski.dev>2026-10-20 07:45:23 +1000
commit6bdb9fa5f2214f816b4195922b0b2b85f667e364 (patch)
tree7980d4fc7314f33a98bdfddc1ebd331c6c7b5897 /editor.c
parentbd1268267e1b0cbf6253c992bce0e5224da7fa01 (diff)
lots of editor improvements.
Diffstat (limited to 'editor.c')
-rw-r--r--editor.c571
1 files changed, 541 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();
}
}