townengine/apps/tools/twndel/tool.c
2025-03-08 00:50:47 +03:00

368 lines
12 KiB
C

#include "twn_game_api.h"
#include "state.h"
#include "twn_vec.h"
#include <SDL.h>
/* planned features: */
/* grid-based, bounded space (65536 / POINTS_PER_METER meters), which allows for efficient storage */
/* 65534 point limit, 16 object limit, 2048 faces per object, 32 textures per object */
/* triangles and quads only */
/* texture painting */
/* bones with mesh animations, snapping to grid, with no weights */
/* billboard render to specified angles */
/* 1 point light primitive lighting */
static State state;
static bool init;
static uint8_t new_object(const char *name) {
if (state.objects_sz >= OBJECT_LIMIT)
return INVALID_OBJECT;
state.objects_sz++;
state.objects = SDL_realloc(state.objects, state.objects_sz * sizeof (*state.objects));
SDL_memset(&state.objects[state.objects_sz-1], 0, sizeof (Object));
state.objects[state.objects_sz-1].name = SDL_strdup(name);
return state.objects_sz-1;
}
static uint16_t new_point(int16_t x, int16_t y, int16_t z) {
if (state.points_sz >= POINT_LIMIT)
return INVALID_POINT;
state.points_sz++;
state.points[state.points_sz-1] = (Point){x, y, z};
return state.points_sz-1;
}
static uint16_t push_face(uint8_t object, uint16_t p0, uint16_t p1, uint16_t p2, uint16_t p3, uint8_t texture) {
Object *o = &state.objects[object];
o->faces_sz++;
o->faces[o->faces_sz-1] = (Face) {.p = {p0, p1, p2, p3}, .texture = texture};
return o->faces_sz-1;
}
static uint8_t push_texture(uint8_t object, char *texture) {
/* TODO: search and combine if it already exists */
Object *o = &state.objects[object];
o->textures_sz++;
o->textures[o->textures_sz-1] = SDL_strdup(texture);
return o->textures_sz-1;
}
static void push_operation(Operation operation, bool active) {
state.op_stack_ptr++;
uint8_t op = state.op_stack_ptr % UNDO_STACK_SIZE;
state.op_stack[op] = operation;
state.op_active = active;
}
static inline Vec3 point_to_vec3(uint8_t object, uint16_t point) {
Object *o = &state.objects[object];
return (Vec3){ (o->position.x + state.points[point].x) / (float)POINTS_PER_METER,
(o->position.y + state.points[point].y) / (float)POINTS_PER_METER,
(o->position.z + state.points[point].z) / (float)POINTS_PER_METER };
}
static void render_object(uint8_t object) {
Object *o = &state.objects[object];
if (o->is_invisible)
return;
for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
Face *f = &o->faces[fi];
/* quads */
if (f->p[3] != INVALID_POINT) {
Vec3 p0 = point_to_vec3(object, f->p[0]);
Vec3 p1 = point_to_vec3(object, f->p[1]);
Vec3 p2 = point_to_vec3(object, f->p[2]);
Vec3 p3 = point_to_vec3(object, f->p[3]);
if (state.solid_display_mode)
draw_quad(o->textures[f->texture],
p0, p1, p2, p3,
(Rect){0,0,32,32},
(Color){255,255,255,255} );
else {
draw_line_3d(p0, p1, 1, (Color){255,255,255,255});
draw_line_3d(p1, p2, 1, (Color){255,255,255,255});
draw_line_3d(p2, p3, 1, (Color){255,255,255,255});
draw_line_3d(p3, p0, 1, (Color){255,255,255,255});
}
} else {
/* triangles */
/* TODO: */
SDL_assert_always(false);
}
}
}
static uint8_t new_cube(Point pos, Point size) {
uint8_t object = new_object("cube");
uint16_t p0 = new_point(pos.x - size.x / 2, pos.y - size.y / 2, pos.z - size.z / 2);
uint16_t p1 = new_point(pos.x - size.x / 2, pos.y + size.y / 2, pos.z - size.z / 2);
uint16_t p2 = new_point(pos.x + size.x / 2, pos.y + size.y / 2, pos.z - size.z / 2);
uint16_t p3 = new_point(pos.x + size.x / 2, pos.y - size.y / 2, pos.z - size.z / 2);
uint16_t p4 = new_point(pos.x - size.x / 2, pos.y - size.y / 2, pos.z + size.z / 2);
uint16_t p5 = new_point(pos.x - size.x / 2, pos.y + size.y / 2, pos.z + size.z / 2);
uint16_t p6 = new_point(pos.x + size.x / 2, pos.y + size.y / 2, pos.z + size.z / 2);
uint16_t p7 = new_point(pos.x + size.x / 2, pos.y - size.y / 2, pos.z + size.z / 2);
uint8_t tex = push_texture(object, "/data/placeholder.png");
push_face(object, p0, p1, p2, p3, tex);
push_face(object, p7, p6, p5, p4, tex);
push_face(object, p4, p5, p1, p0, tex);
push_face(object, p6, p7, p3, p2, tex);
push_face(object, p2, p1, p5, p6, tex);
push_face(object, p0, p3, p7, p4, tex);
return object;
}
static void process_camera_movement(void) {
input_action("camera_rotate_left", "A");
input_action("camera_rotate_right", "D");
input_action("camera_rotate_up", "W");
input_action("camera_rotate_down", "S");
float horizontal_rotation = 0;
float vertical_rotation = 0;
if (input_action_pressed("camera_rotate_left"))
horizontal_rotation -= CAMERA_ROTATION_SPEED;
if (input_action_pressed("camera_rotate_right"))
horizontal_rotation += CAMERA_ROTATION_SPEED;
if (input_action_pressed("camera_rotate_up"))
vertical_rotation -= CAMERA_ROTATION_SPEED;
if (input_action_pressed("camera_rotate_down"))
vertical_rotation += CAMERA_ROTATION_SPEED;
Vec3 front = vec3_cross(state.camera_direction, (Vec3){0,1,0});
state.camera_position = vec3_rotate(state.camera_position, horizontal_rotation, (Vec3){0,1,0});
state.camera_direction = vec3_rotate(state.camera_direction, horizontal_rotation, (Vec3){0,1,0});
Vec3 new_rot = vec3_rotate(state.camera_direction, vertical_rotation, front);
/* only apply if it's in limits */
float d = vec3_dot(new_rot, (Vec3){0,-1,0});
if (fabsf(d) <= 0.999f) {
state.camera_position = vec3_rotate(state.camera_position, vertical_rotation, front);
state.camera_direction = new_rot;
}
}
static inline DrawCameraUnprojectResult unproject_point(Vec2 point) {
return draw_camera_unproject(
point,
state.camera_position,
state.camera_direction,
(Vec3){0, 1, 0},
state.camera_is_orthographic ? 0 : CAMERA_FOV,
state.camera_zoom,
100 );
}
static bool find_closest_point(uint8_t* object_result, uint16_t *point_result) {
DrawCameraUnprojectResult pos_and_ray = unproject_point(ctx.mouse_position);
/* step over every selectable object and find points closest to the view ray */
/* by constructing triangles and finding their height, from perpendicular */
uint16_t closest_point = INVALID_POINT;
uint8_t closest_obj = INVALID_OBJECT;
float closest_distance = INFINITY;
for (uint8_t obj = 0; obj < state.objects_sz; ++obj) {
Object *o = &state.objects[obj];
if (o->is_invisible)
continue;
/* TODO: is it possible to skip repeated points? does it matter? */
/* as we limit the point could we could actually have bool array preallocated for this */
for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
Face *f = &o->faces[fi];
for (uint16_t pi = 0; pi < 4; ++pi) {
Vec3 p = point_to_vec3(obj, f->p[pi]);
Vec3 d = vec3_sub(pos_and_ray.position, p);
Vec3 b = vec3_cross(d, pos_and_ray.direction);
float ray_dist = vec3_length(b);
if (ray_dist > ((float)SELECTION_SPHERE_RADIUS / POINTS_PER_METER))
continue;
Vec3 i = vec3_add(p, b);
float dist = vec3_length(i);
if (dist < closest_distance) {
closest_distance = dist;
closest_obj = obj;
closest_point = f->p[pi];
}
}
}
}
if (closest_point == INVALID_POINT)
return false;
if (object_result)
*object_result = closest_obj;
if (point_result)
*point_result = closest_point;
return true;
}
static void process_operation_move_point(Operation *op) {
/* finish dragging around */
if (input_action_just_released("select")) {
state.op_active = false;
return;
}
float size = sinf(ctx.frame_number / 10) * 0.05f + 0.05f;
draw_billboard("/data/grab.png",
point_to_vec3(op->data.move_point.object, op->data.move_point.point),
(Vec2){size, size},
(Rect){0,0,16,16},
(Color){255,255,255,255},
false);
DrawCameraUnprojectResult pos_and_ray = unproject_point(ctx.mouse_position);
Vec3 p = point_to_vec3(op->data.move_point.object, op->data.move_point.point);
Vec3 d = vec3_sub(pos_and_ray.position, p);
Vec3 b = vec3_cross(d, pos_and_ray.direction);
/* TODO: show thresholds */
/* change along axes, delta is accumulated when threshold is met*/
int16_t zch = (int16_t)(floorf(vec3_dot(b, (Vec3){0,0,1}) * (float)POINTS_PER_METER));
op->data.move_point.delta_x += zch;
state.points[op->data.move_point.point].x += zch;
}
static void process_operations(void) {
if (!state.op_active) {
uint16_t point_select; uint8_t obj_select;
if (find_closest_point(&obj_select, &point_select)) {
draw_billboard("/data/point.png",
point_to_vec3(obj_select, point_select),
(Vec2){0.05f, 0.05f},
(Rect){0,0,16,16},
(Color){255,255,255,255},
false);
if (input_action_just_pressed("select"))
push_operation((Operation){
.kind = OPERATION_MOVE_POINT,
.data = { .move_point = { .point = point_select } },
}, true );
}
}
if (state.op_active) {
Operation *op = &state.op_stack[state.op_stack_ptr % UNDO_STACK_SIZE];
switch (op->kind) {
case OPERATION_MOVE_POINT: {
process_operation_move_point(op);
break;
}
default:
(void)0;
}
}
}
void game_tick(void) {
if (!init) {
/* default state */
new_cube((Point){0,0,0}, (Point){POINTS_PER_METER,POINTS_PER_METER,POINTS_PER_METER});
state.camera_position = (Vec3){2,1,2};
state.camera_direction = vec3_norm(((Vec3){-2,-1,-2}));
state.camera_zoom = 0.5f;
init = true;
}
input_action("toggle_display_mode", "Q");
input_action("toggle_projection", "E");
input_action("select", "LCLICK");
if (input_action_just_pressed("toggle_display_mode")) {
audio_play("/data/click.wav", NULL, false, 0.7f, 0.0f);
state.solid_display_mode = !state.solid_display_mode;
}
if (input_action_just_pressed("toggle_projection")) {
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
state.camera_is_orthographic = !state.camera_is_orthographic;
}
process_camera_movement();
process_operations();
/* axis helpers */
/* idea: black out inactives when dragging points */
/* idea: double selection of axes for diagonal edits */
draw_line_3d((Vec3){0}, (Vec3){0,0,256}, 1, (Color){255,0,0,125});
draw_billboard("/data/x.png",
(Vec3){0,0,2},
(Vec2){0.1f, 0.1f},
(Rect){0},
(Color){255,0,0,255},
false);
draw_line_3d((Vec3){0}, (Vec3){256,0,0}, 1, (Color){0,0,255,125});
draw_billboard("/data/y.png",
(Vec3){2,0,0},
(Vec2){0.1f, 0.1f},
(Rect){0},
(Color){0,0,255,255},
false);
draw_line_3d((Vec3){0}, (Vec3){0,256,0}, 1, (Color){75,125,25,125});
draw_billboard("/data/z.png",
(Vec3){0,1.5f,0},
(Vec2){0.1f, 0.1f},
(Rect){0},
(Color){75,125,25,255},
false);
for (uint8_t obj = 0; obj < state.objects_sz; ++obj)
render_object(obj);
draw_text("twndel\x03", (Vec2){0, 2}, 32, (Color){255,255,255,200}, NULL);
draw_camera(
state.camera_position,
state.camera_direction,
(Vec3){0, 1, 0},
state.camera_is_orthographic ? 0 : CAMERA_FOV,
state.camera_zoom,
100
);
}
void game_end(void) {
for (uint8_t obj = 0; obj < state.objects_sz; ++obj) {
Object *o = &state.objects[obj];
for (uint8_t t = 0; t < o->textures_sz; ++t)
SDL_free(o->textures[t]);
SDL_free(o->name);
}
if (state.objects_sz != 0)
SDL_free(state.objects);
}