]> git.lizzy.rs Git - nothing.git/blobdiff - src/game/level/level_editor/point_layer.c
Try to remove malloc from LevelEditor
[nothing.git] / src / game / level / level_editor / point_layer.c
index 99f7e5eebfadcd33d6d0ee5ed2355cd68199035f..56d7318c0541df25cdf521059a6ba158d0e984e2 100644 (file)
@@ -4,15 +4,14 @@
 
 #include "dynarray.h"
 #include "game/camera.h"
-#include "system/line_stream.h"
 #include "system/log.h"
-#include "system/lt.h"
 #include "system/nth_alloc.h"
 #include "system/stacktrace.h"
 #include "system/str.h"
 #include "ui/edit_field.h"
 #include "./point_layer.h"
 #include "math/extrema.h"
+#include "math/mat3x3.h"
 #include "./color_picker.h"
 #include "undo_history.h"
 
 #define POINT_LAYER_ID_TEXT_SIZE vec(2.0f, 2.0f)
 #define POINT_LAYER_ID_TEXT_COLOR COLOR_BLACK
 
-// TODO(#1002): PointLayer does not fully support UndoHistory
+static int point_clipboard = 0;
+static Color point_clipboard_color;
+
+// TODO(#1140): PointLayer does not support snapping
 
 typedef enum {
-    POINT_LAYER_IDLE = 0,
-    POINT_LAYER_EDIT_ID,
-    POINT_LAYER_MOVE
-} PointLayerState;
+    POINT_UNDO_ADD,
+    POINT_UNDO_DELETE,
+    POINT_UNDO_UPDATE,
+    POINT_UNDO_SWAP
+} PointUndoType;
 
-struct PointLayer
-{
-    Lt *lt;
-    PointLayerState state;
-    Dynarray/*<Point>*/ *positions;
-    Dynarray/*<Color>*/ *colors;
-    Dynarray/*<char[ID_MAX_SIZE]>*/ *ids;
-    Edit_field *edit_field;
-    int selected;
-    ColorPicker color_picker;
-    Color prev_color;
-    Point prev_position;
-};
+typedef struct {
+    PointUndoType type;
+    PointLayer *layer;
+    Vec2f position;
+    Color color;
+    char id[ID_MAX_SIZE];
+    size_t index;
+    size_t index2;
+} PointUndoContext;
 
-LayerPtr point_layer_as_layer(PointLayer *point_layer)
+static
+PointUndoContext create_point_undo_swap_context(PointLayer *point_layer,
+                                     size_t index, size_t index2)
 {
-    LayerPtr layer = {
-        .type = LAYER_POINT,
-        .ptr = point_layer
-    };
-    return layer;
+    trace_assert(point_layer);
+    trace_assert(index < point_layer->positions.count);
+    trace_assert(index2 < point_layer->positions.count);
+
+    PointUndoContext undo_context;
+    undo_context.type = POINT_UNDO_SWAP;
+    undo_context.layer = point_layer;
+    undo_context.index = index;
+    undo_context.index2 = index2;
+    return undo_context;
 }
 
-PointLayer *create_point_layer(void)
+static
+PointUndoContext create_point_undo_context(PointLayer *point_layer,
+                                PointUndoType type)
 {
-    Lt *lt = create_lt();
+    trace_assert(type != POINT_UNDO_SWAP);
 
-    PointLayer *point_layer = PUSH_LT(lt, nth_calloc(1, sizeof(PointLayer)), free);
-    if (point_layer == NULL) {
-        RETURN_LT(lt, NULL);
-    }
-    point_layer->lt = lt;
+    (void) create_point_undo_swap_context;
 
-    point_layer->state = POINT_LAYER_IDLE;
+    PointUndoContext undo_context;
 
-    point_layer->positions = PUSH_LT(lt, create_dynarray(sizeof(Point)), destroy_dynarray);
-    if (point_layer->positions == NULL) {
-        RETURN_LT(lt, NULL);
-    }
+    size_t index =
+        type == POINT_UNDO_ADD
+        ? point_layer->positions.count - 1
+        : (size_t) point_layer->selection;
 
-    point_layer->colors = PUSH_LT(lt, create_dynarray(sizeof(Color)), destroy_dynarray);
-    if (point_layer->colors == NULL) {
-        RETURN_LT(lt, NULL);
-    }
+    undo_context.type = type;
+    undo_context.layer = point_layer;
+    dynarray_copy_to(&point_layer->positions, &undo_context.position, index);
+    dynarray_copy_to(&point_layer->colors, &undo_context.color, index);
+    dynarray_copy_to(&point_layer->ids, &undo_context.id, index);
+    undo_context.index = index;
 
-    point_layer->ids = PUSH_LT(lt, create_dynarray(sizeof(char) * ID_MAX_SIZE), destroy_dynarray);
-    if (point_layer->ids == NULL) {
-        RETURN_LT(lt, NULL);
-    }
-
-    point_layer->edit_field = PUSH_LT(
-        lt,
-        create_edit_field(
-            POINT_LAYER_ID_TEXT_SIZE,
-            POINT_LAYER_ID_TEXT_COLOR),
-        destroy_edit_field);
-    if (point_layer->edit_field == NULL) {
-        RETURN_LT(lt, NULL);
-    }
-
-    return point_layer;
+    return undo_context;
 }
 
-PointLayer *create_point_layer_from_line_stream(LineStream *line_stream)
+static
+void point_layer_undo(void *context, size_t context_size)
 {
-    trace_assert(line_stream);
-
-    PointLayer *point_layer = create_point_layer();
+    trace_assert(context);
+    trace_assert(sizeof(PointUndoContext) == context_size);
+
+    PointUndoContext *undo_context = context;
+    PointLayer *point_layer = undo_context->layer;
+
+    switch (undo_context->type) {
+    case POINT_UNDO_ADD: {
+        dynarray_pop(&point_layer->positions, NULL);
+        dynarray_pop(&point_layer->colors, NULL);
+        dynarray_pop(&point_layer->ids, NULL);
+        point_layer->selection = -1;
+    } break;
 
-    size_t count = 0;
-    if (sscanf(
-            line_stream_next(line_stream),
-            "%zu",
-            &count) == EOF) {
-        log_fail("Could not read amount of points");
-        RETURN_LT(point_layer->lt, NULL);
-    }
+    case POINT_UNDO_DELETE: {
+        dynarray_insert_before(&point_layer->positions, undo_context->index, &undo_context->position);
+        dynarray_insert_before(&point_layer->colors, undo_context->index, &undo_context->color);
+        dynarray_insert_before(&point_layer->ids, undo_context->index, &undo_context->id);
+        point_layer->selection = -1;
+    } break;
 
-    char color_name[7];
-    char id[ID_MAX_SIZE];
-    float x, y;
-    for (size_t i = 0; i < count; ++i) {
-        if (sscanf(
-                line_stream_next(line_stream),
-                "%"STRINGIFY(ID_MAX_SIZE)"s%f%f%6s",
-                id, &x, &y, color_name) < 0) {
-            log_fail("Could not read %dth goal\n", i);
-            RETURN_LT(point_layer->lt, NULL);
-        }
-        const Color color = hexstr(color_name);
-        const Point point = vec(x, y);
+    case POINT_UNDO_UPDATE: {
+        dynarray_replace_at(&point_layer->positions, undo_context->index, &undo_context->position);
+        dynarray_replace_at(&point_layer->colors, undo_context->index, &undo_context->color);
+        dynarray_replace_at(&point_layer->ids, undo_context->index, &undo_context->id);
+    } break;
 
-        dynarray_push(point_layer->colors, &color);
-        dynarray_push(point_layer->positions, &point);
-        dynarray_push(point_layer->ids, id);
+    case POINT_UNDO_SWAP: {
+        dynarray_swap(&point_layer->positions, undo_context->index, undo_context->index2);
+        dynarray_swap(&point_layer->colors, undo_context->index, undo_context->index2);
+        dynarray_swap(&point_layer->ids, undo_context->index, undo_context->index2);
+    } break;
     }
+}
 
-    point_layer->selected = -1;
+#define POINT_UNDO_PUSH(HISTORY, CONTEXT)                                     \
+    do {                                                                \
+        PointUndoContext context = (CONTEXT);                                \
+        undo_history_push(                                              \
+            HISTORY,                                                    \
+            point_layer_undo,                                           \
+            &context,                                                   \
+            sizeof(context));                                           \
+    } while(0)
 
-    point_layer->color_picker = create_color_picker_from_rgba(COLOR_RED);
-    point_layer->prev_color = COLOR_RED;
+LayerPtr point_layer_as_layer(PointLayer *point_layer)
+{
+    LayerPtr layer = {
+        .type = LAYER_POINT,
+        .ptr = point_layer
+    };
+    return layer;
+}
 
-    return point_layer;
+PointLayer create_point_layer(const char *id_name_prefix)
+{
+    PointLayer result = {0};
+    result.state = POINT_LAYER_IDLE;
+    result.positions = create_dynarray(sizeof(Vec2f));
+    result.colors = create_dynarray(sizeof(Color));
+    result.ids = create_dynarray(sizeof(char) * ID_MAX_SIZE);
+    result.edit_field.font_size = POINT_LAYER_ID_TEXT_SIZE;
+    result.edit_field.font_color = POINT_LAYER_ID_TEXT_COLOR;
+    result.id_name_prefix = id_name_prefix;
+    return result;
 }
 
-void destroy_point_layer(PointLayer *point_layer)
+PointLayer *create_point_layer_from_memory(Memory *memory,
+                                           const char *id_name_prefix)
+{
+    trace_assert(memory);
+    trace_assert(id_name_prefix);
+
+    PointLayer *result = memory_alloc(memory, sizeof(PointLayer));
+    memset(result, 0, sizeof(PointLayer));
+    result->state = POINT_LAYER_IDLE;
+    result->positions = create_dynarray_from_memory(memory, sizeof(Vec2f));
+    result->colors = create_dynarray_from_memory(memory, sizeof(Color));
+    result->ids = create_dynarray_from_memory(memory, sizeof(char) * ID_MAX_SIZE);
+    result->edit_field.font_size = POINT_LAYER_ID_TEXT_SIZE;
+    result->edit_field.font_color = POINT_LAYER_ID_TEXT_COLOR;
+    result->id_name_prefix = id_name_prefix;
+    return result;
+}
+
+void point_layer_load(PointLayer *point_layer,
+                      Memory *memory,
+                      String *input)
 {
     trace_assert(point_layer);
-    RETURN_LT0(point_layer->lt);
+    trace_assert(memory);
+    trace_assert(input);
+
+    int n = atoi(string_to_cstr(memory, trim(chop_by_delim(input, '\n'))));
+    char id[ENTITY_MAX_ID_SIZE];
+    for (int i = 0; i < n; ++i) {
+        String line = trim(chop_by_delim(input, '\n'));
+        String string_id = trim(chop_word(&line));
+        Vec2f point;
+        point.x = strtof(string_to_cstr(memory, trim(chop_word(&line))), NULL);
+        point.y = strtof(string_to_cstr(memory, trim(chop_word(&line))), NULL);
+        Color color = hexs(trim(chop_word(&line)));
+
+        memset(id, 0, ENTITY_MAX_ID_SIZE);
+        memcpy(id, string_id.data, min_size_t(ENTITY_MAX_ID_SIZE - 1, string_id.count));
+
+        dynarray_push(&point_layer->positions, &point);
+        dynarray_push(&point_layer->colors, &color);
+        dynarray_push(&point_layer->ids, id);
+    }
+}
+
+static inline
+Triangle element_shape(Vec2f position, float scale)
+{
+    return triangle_mat3x3_product(
+        equilateral_triangle(),
+        mat3x3_product(
+            trans_mat_vec(position),
+            scale_mat(scale)));
 }
 
 int point_layer_render(const PointLayer *point_layer,
-                       Camera *camera,
+                       const Camera *camera,
                        int active)
 {
     trace_assert(point_layer);
     trace_assert(camera);
 
-    const int n = (int) dynarray_count(point_layer->positions);
-    Point *positions = dynarray_data(point_layer->positions);
-    Color *colors = dynarray_data(point_layer->colors);
-    char *ids = dynarray_data(point_layer->ids);
+    const int n = (int)point_layer->positions.count;
+    Vec2f *positions = (Vec2f *)point_layer->positions.data;
+    Color *colors = (Color *)point_layer->colors.data;
+    char *ids = (char *)point_layer->ids.data;
 
     for (int i = 0; i < n; ++i) {
-        const Triangle t = triangle_mat3x3_product(
-            equilateral_triangle(),
-            mat3x3_product(
-                trans_mat(positions[i].x, positions[i].y),
-                scale_mat(POINT_LAYER_ELEMENT_RADIUS)));
-
         const Color color = color_scale(
-            colors[i],
+            point_layer->state == POINT_LAYER_RECOLOR && i == point_layer->selection
+            ? point_layer->inter_color
+            : colors[i],
             rgba(1.0f, 1.0f, 1.0f, active ? 1.0f : 0.5f));
 
-        if (i == point_layer->selected) {
-            const Triangle t0 = triangle_mat3x3_product(
-                equilateral_triangle(),
-                mat3x3_product(
-                    trans_mat(positions[i].x, positions[i].y),
-                    scale_mat(15.0f)));
+        const Vec2f position =
+            point_layer->state == POINT_LAYER_MOVE && i == point_layer->selection
+            ? point_layer->inter_position
+            : positions[i];
 
-            if (camera_fill_triangle(camera, t0, color_invert(color)) < 0) {
+        // Selection Layer
+        if (active && i == point_layer->selection) {
+            if (camera_fill_triangle(
+                    camera,
+                    element_shape(
+                        position,
+                        POINT_LAYER_ELEMENT_RADIUS + 5.0f),
+                    color_invert(color)) < 0) {
                 return -1;
             }
 
@@ -179,27 +248,31 @@ int point_layer_render(const PointLayer *point_layer,
                     ids + ID_MAX_SIZE * i,
                     POINT_LAYER_ID_TEXT_SIZE,
                     POINT_LAYER_ID_TEXT_COLOR,
-                    positions[i]) < 0) {
+                    position) < 0) {
                 return -1;
             }
         }
 
-        if (camera_fill_triangle(camera, t, color) < 0) {
+        if (camera_fill_triangle(
+                camera,
+                element_shape(
+                    position,
+                    POINT_LAYER_ELEMENT_RADIUS),
+                color) < 0) {
             return -1;
         }
-
     }
 
     if (point_layer->state == POINT_LAYER_EDIT_ID) {
         if (edit_field_render_world(
-                point_layer->edit_field,
+                &point_layer->edit_field,
                 camera,
-                positions[point_layer->selected]) < 0) {
+                positions[point_layer->selection]) < 0) {
             return -1;
         }
     }
 
-    if (active && color_picker_render(&point_layer->color_picker, camera, vec(0.0f, 0.0f)) < 0) {
+    if (active && color_picker_render(&point_layer->color_picker, camera) < 0) {
         return -1;
     }
 
@@ -209,14 +282,14 @@ int point_layer_render(const PointLayer *point_layer,
 
 static
 int point_layer_element_at(const PointLayer *point_layer,
-                           Point position)
+                           Vec2f position)
 {
     trace_assert(point_layer);
 
-    int n = (int) dynarray_count(point_layer->positions);
-    Point *positions = dynarray_data(point_layer->positions);
+    int n = (int) point_layer->positions.count;
+    Vec2f *positions = (Vec2f *)point_layer->positions.data;
 
-    for (int i = 0; i < n; ++i) {
+    for (int i = n - 1; i >= 0; --i) {
         if (vec_length(vec_sub(positions[i], position)) < POINT_LAYER_ELEMENT_RADIUS) {
             return i;
         }
@@ -225,22 +298,9 @@ int point_layer_element_at(const PointLayer *point_layer,
     return -1;
 }
 
-static
-void point_layer_pop_element(void *layer, Context context)
-{
-    trace_assert(layer);
-    (void) context;
-
-    PointLayer *point_layer = layer;
-
-    dynarray_pop(point_layer->positions, NULL);
-    dynarray_pop(point_layer->colors, NULL);
-    dynarray_pop(point_layer->ids, NULL);
-}
-
 static
 int point_layer_add_element(PointLayer *point_layer,
-                            Point position,
+                            Vec2f position,
                             Color color,
                             UndoHistory *undo_history)
 {
@@ -248,43 +308,38 @@ int point_layer_add_element(PointLayer *point_layer,
     trace_assert(undo_history);
 
     char id[ID_MAX_SIZE];
-    for (size_t i = 0; i < ID_MAX_SIZE - 1; ++i) {
-        id[i] = (char) ('a' + rand() % ('z' - 'a' + 1));
-    }
-    id[ID_MAX_SIZE - 1] = '\0';
+    snprintf(id, ID_MAX_SIZE, "%s_%d",
+             point_layer->id_name_prefix,
+             point_layer->id_name_counter++);
 
-    dynarray_push(point_layer->positions, &position);
-    dynarray_push(point_layer->colors, &color);
-    dynarray_push(point_layer->ids, id);
+    dynarray_push(&point_layer->positions, &position);
+    dynarray_push(&point_layer->colors, &color);
+    dynarray_push(&point_layer->ids, id);
 
-    Action action =  {
-        .revert = point_layer_pop_element,
-        .layer = point_layer
-    };
-    undo_history_push(undo_history, action);
+    POINT_UNDO_PUSH(
+        undo_history,
+        create_point_undo_context(point_layer, POINT_UNDO_ADD));
 
     return 0;
 }
 
-typedef struct {
-    Point position;
-    Color color;
-    char id[ID_MAX_SIZE];
-    size_t index;
-} DeleteContext;
-
 static
-void point_layer_revert_delete(void *layer, Context context)
+void point_layer_swap_elements(PointLayer *point_layer,
+                               size_t a, size_t b,
+                               UndoHistory *undo_history)
 {
-    trace_assert(layer);
-    PointLayer *point_layer = layer;
+    trace_assert(point_layer);
+    trace_assert(undo_history);
+    trace_assert(a < point_layer->positions.count);
+    trace_assert(b < point_layer->positions.count);
 
-    trace_assert(sizeof(DeleteContext) <= CONTEXT_SIZE);
-    DeleteContext *delete_context = (DeleteContext *)context.data;
+    dynarray_swap(&point_layer->positions, a, b);
+    dynarray_swap(&point_layer->colors, a, b);
+    dynarray_swap(&point_layer->ids, a, b);
 
-    dynarray_insert_before(point_layer->positions, delete_context->index, &delete_context->position);
-    dynarray_insert_before(point_layer->colors, delete_context->index, &delete_context->color);
-    dynarray_insert_before(point_layer->ids, delete_context->index, delete_context->id);
+    POINT_UNDO_PUSH(
+        undo_history,
+        create_point_undo_swap_context(point_layer, a, b));
 }
 
 static
@@ -294,54 +349,15 @@ void point_layer_delete_nth_element(PointLayer *point_layer,
 {
     trace_assert(point_layer);
 
-    Action action = {
-        .revert = point_layer_revert_delete,
-        .layer = point_layer
-    };
-
-    trace_assert(sizeof(DeleteContext) <= CONTEXT_SIZE);
-    DeleteContext *delete_context = (DeleteContext *)action.context.data;
-
-    Point *positions = dynarray_data(point_layer->positions);
-    Color *colors = dynarray_data(point_layer->colors);
-    char *ids = dynarray_data(point_layer->ids);
-
-    delete_context->position = positions[i];
-    delete_context->color = colors[i];
-    memcpy(
-        delete_context->id,
-        ids + i * ID_MAX_SIZE,
-        ID_MAX_SIZE);
-    delete_context->index = i;
-
-    undo_history_push(undo_history, action);
-
-    dynarray_delete_at(point_layer->positions, i);
-    dynarray_delete_at(point_layer->colors, i);
-    dynarray_delete_at(point_layer->ids, i);
-}
-
-typedef struct {
-    size_t index;
-    Color color;
-} ColorContext;
-
-static
-void point_layer_revert_color(void *layer, Context context)
-{
-    log_info("point_layer_revert_color\n");
-
-    trace_assert(layer);
-    PointLayer *point_layer = layer;
+    POINT_UNDO_PUSH(
+        undo_history,
+        create_point_undo_context(
+            point_layer,
+            POINT_UNDO_DELETE));
 
-    trace_assert(sizeof(ColorContext) <= CONTEXT_SIZE);
-    ColorContext *color_context = (ColorContext*)context.data;
-
-    const size_t n = dynarray_count(point_layer->colors);
-    Color *colors = dynarray_data(point_layer->colors);
-    trace_assert(color_context->index < n);
-
-    colors[color_context->index] = color_context->color;
+    dynarray_delete_at(&point_layer->positions, i);
+    dynarray_delete_at(&point_layer->colors, i);
+    dynarray_delete_at(&point_layer->ids, i);
 }
 
 static
@@ -358,35 +374,16 @@ int point_layer_idle_event(PointLayer *point_layer,
     if (color_picker_event(
             &point_layer->color_picker,
             event,
+            camera,
             &selected) < 0) {
         return -1;
     }
 
     if (selected) {
-        if (point_layer->selected >= 0) {
-            Color *colors = dynarray_data(point_layer->colors);
-
-            if (!color_picker_drag(&point_layer->color_picker)) {
-                Action action = {
-                    .layer = point_layer,
-                    .revert = point_layer_revert_color
-                };
-
-                *((ColorContext*)action.context.data) = (ColorContext) {
-                    .index = (size_t) point_layer->selected,
-                    .color = point_layer->prev_color
-                };
-
-                undo_history_push(undo_history, action);
-
-                point_layer->prev_color =
-                    color_picker_rgba(&point_layer->color_picker);
-            }
-
-            colors[point_layer->selected] =
-                color_picker_rgba(&point_layer->color_picker);
+        if (point_layer->selection >= 0) {
+            point_layer->inter_color = color_picker_rgba(&point_layer->color_picker);
+            point_layer->state = POINT_LAYER_RECOLOR;
         }
-
         return 0;
     }
 
@@ -394,27 +391,25 @@ int point_layer_idle_event(PointLayer *point_layer,
     case SDL_MOUSEBUTTONDOWN: {
         switch (event->button.button) {
         case SDL_BUTTON_LEFT: {
-            const Point position = camera_map_screen(camera, event->button.x, event->button.y);
+            const Vec2f position = camera_map_screen(camera, event->button.x, event->button.y);
 
-            point_layer->selected = point_layer_element_at(
+            point_layer->selection = point_layer_element_at(
                 point_layer, position);
 
-            if (point_layer->selected < 0) {
+            if (point_layer->selection < 0) {
                 point_layer_add_element(
                     point_layer,
                     position,
                     color_picker_rgba(&point_layer->color_picker),
                     undo_history);
             } else {
-                Color *colors = dynarray_data(point_layer->colors);
-                Point *positions = dynarray_data(point_layer->positions);
+                Color *colors = (Color*)point_layer->colors.data;
+                Vec2f *positions = (Vec2f*)point_layer->positions.data;
 
                 point_layer->state = POINT_LAYER_MOVE;
                 point_layer->color_picker =
-                    create_color_picker_from_rgba(colors[point_layer->selected]);
-
-                point_layer->prev_color = colors[point_layer->selected];
-                point_layer->prev_position = positions[point_layer->selected];
+                    create_color_picker_from_rgba(colors[point_layer->selection]);
+                point_layer->inter_position = positions[point_layer->selection];
             }
         } break;
         }
@@ -422,26 +417,73 @@ int point_layer_idle_event(PointLayer *point_layer,
 
     case SDL_KEYDOWN: {
         switch (event->key.keysym.sym) {
+        case SDLK_UP: {
+            if ((event->key.keysym.mod & KMOD_SHIFT)
+                && (point_layer->selection >= 0)
+                && ((size_t)(point_layer->selection + 1) < point_layer->positions.count)) {
+                point_layer_swap_elements(
+                    point_layer,
+                    (size_t) point_layer->selection,
+                    (size_t) point_layer->selection + 1,
+                    undo_history);
+                point_layer->selection++;
+            }
+        } break;
+
+        case SDLK_DOWN: {
+            if ((event->key.keysym.mod & KMOD_SHIFT)
+                && (point_layer->selection > 0)
+                && ((size_t) point_layer->selection < point_layer->positions.count)) {
+                point_layer_swap_elements(
+                    point_layer,
+                    (size_t) point_layer->selection,
+                    (size_t) point_layer->selection - 1,
+                    undo_history);
+                point_layer->selection--;
+            }
+        } break;
+
         case SDLK_DELETE: {
-            if (0 <= point_layer->selected && point_layer->selected < (int) dynarray_count(point_layer->positions)) {
+            if (0 <= point_layer->selection && point_layer->selection < (int) point_layer->positions.count) {
                 point_layer_delete_nth_element(
                     point_layer,
-                    (size_t)point_layer->selected,
+                    (size_t)point_layer->selection,
                     undo_history);
-                point_layer->selected = -1;
+                point_layer->selection = -1;
             }
         } break;
 
         case SDLK_F2: {
-            if (point_layer->selected >= 0) {
-                char *ids = dynarray_data(point_layer->ids);
+            if (point_layer->selection >= 0) {
+                char *ids = (char*)point_layer->ids.data;
                 point_layer->state = POINT_LAYER_EDIT_ID;
                 edit_field_replace(
-                    point_layer->edit_field,
-                    ids + ID_MAX_SIZE * point_layer->selected);
+                    &point_layer->edit_field,
+                    ids + ID_MAX_SIZE * point_layer->selection);
                 SDL_StartTextInput();
             }
         } break;
+
+        case SDLK_c: {
+            if ((event->key.keysym.mod & KMOD_LCTRL) && point_layer->selection >= 0) {
+                point_clipboard = 1;
+                dynarray_copy_to(&point_layer->colors, &point_clipboard_color, (size_t)point_layer->selection);
+            }
+        } break;
+
+        case SDLK_v: {
+            if ((event->key.keysym.mod & KMOD_LCTRL) && point_clipboard) {
+                int x, y;
+                SDL_GetMouseState(&x, &y);
+                Vec2f position = camera_map_screen(camera, x, y);
+
+                point_layer_add_element(
+                    point_layer,
+                    position,
+                    point_clipboard_color,
+                    undo_history);
+            }
+        } break;
         }
     } break;
     }
@@ -449,30 +491,6 @@ int point_layer_idle_event(PointLayer *point_layer,
     return 0;
 }
 
-typedef struct {
-    size_t index;
-    char id[ID_MAX_SIZE];
-} RenameContext;
-
-static
-void point_layer_revert_rename(void *layer,
-                               Context context)
-{
-    trace_assert(layer);
-    PointLayer *point_layer = layer;
-
-    ASSERT_CONTEXT_SIZE(RenameContext);
-    RenameContext *rename_context = (RenameContext *)context.data;
-
-    trace_assert(rename_context->index < dynarray_count(point_layer->ids));
-
-    char *ids = dynarray_data(point_layer->ids);
-    memcpy(
-        ids + rename_context->index * ID_MAX_SIZE,
-        rename_context->id,
-        ID_MAX_SIZE);
-}
-
 static
 int point_layer_edit_id_event(PointLayer *point_layer,
                               const SDL_Event *event,
@@ -487,28 +505,17 @@ int point_layer_edit_id_event(PointLayer *point_layer,
     case SDL_KEYDOWN: {
         switch(event->key.keysym.sym) {
         case SDLK_RETURN: {
-            char *ids = dynarray_data(point_layer->ids);
-            const char *text = edit_field_as_text(point_layer->edit_field);
-
-            Action action = {
-                .revert = point_layer_revert_rename,
-                .layer = point_layer
-            };
-
-            ASSERT_CONTEXT_SIZE(RenameContext);
-            RenameContext *rename_context = (RenameContext *)action.context.data;
-
-            memcpy(
-                rename_context->id,
-                ids + point_layer->selected * ID_MAX_SIZE,
-                ID_MAX_SIZE);
-            rename_context->index = (size_t) point_layer->selected;
-
-            undo_history_push(undo_history, action);
+            POINT_UNDO_PUSH(
+                undo_history,
+                create_point_undo_context(
+                    point_layer,
+                    POINT_UNDO_UPDATE));
 
-            size_t n = max_size_t(strlen(text), ID_MAX_SIZE - 1);
-            memcpy(ids + point_layer->selected * ID_MAX_SIZE, text, n);
-            *(ids + point_layer->selected * ID_MAX_SIZE + n) = '\0';
+            char *id = dynarray_pointer_at(&point_layer->ids, (size_t) point_layer->selection);
+            const char *text = edit_field_as_text(&point_layer->edit_field);
+            size_t n = min_size_t(strlen(text), ID_MAX_SIZE - 1);
+            memcpy(id, text, n);
+            memset(id + n, 0, ID_MAX_SIZE - n);
 
             point_layer->state = POINT_LAYER_IDLE;
             SDL_StopTextInput();
@@ -524,26 +531,7 @@ int point_layer_edit_id_event(PointLayer *point_layer,
     } break;
     }
 
-    return edit_field_event(point_layer->edit_field, event);
-}
-
-typedef struct {
-    size_t index;
-    Point position;
-} MoveContext;
-
-static
-void point_layer_revert_move(void *layer, Context context)
-{
-    trace_assert(layer);
-    PointLayer *point_layer = layer;
-
-    ASSERT_CONTEXT_SIZE(MoveContext);
-    MoveContext *move_context = (MoveContext *)context.data;
-
-    trace_assert(move_context->index < dynarray_count(point_layer->positions));
-    Point *positions = dynarray_data(point_layer->positions);
-    positions[move_context->index] = move_context->position;
+    return edit_field_event(&point_layer->edit_field, event);
 }
 
 static
@@ -555,7 +543,9 @@ int point_layer_move_event(PointLayer *point_layer,
     trace_assert(point_layer);
     trace_assert(event);
     trace_assert(camera);
-    trace_assert(point_layer->selected >= 0);
+    trace_assert(point_layer->selection >= 0);
+
+    Vec2f *positions = (Vec2f*)point_layer->positions.data;
 
     switch (event->type) {
     case SDL_MOUSEBUTTONUP: {
@@ -563,33 +553,93 @@ int point_layer_move_event(PointLayer *point_layer,
         case SDL_BUTTON_LEFT: {
             point_layer->state = POINT_LAYER_IDLE;
 
-            Action action = {
-                .revert = point_layer_revert_move,
-                .layer = point_layer
-            };
-
-            MoveContext *context = (MoveContext *)action.context.data;
-            ASSERT_CONTEXT_SIZE(MoveContext);
-
-            context->index = (size_t) point_layer->selected;
-            context->position = point_layer->prev_position;
-
-            // TODO(#1014): just click (without moving) on the point creates an undo history entry
-            undo_history_push(undo_history, action);
+            const float distance = vec_length(
+                vec_sub(point_layer->inter_position,
+                        positions[point_layer->selection]));
+
+            if (distance > 1e-6) {
+                POINT_UNDO_PUSH(
+                    undo_history,
+                    create_point_undo_context(
+                        point_layer,
+                        POINT_UNDO_UPDATE));
+
+                dynarray_replace_at(
+                    &point_layer->positions,
+                    (size_t) point_layer->selection,
+                    &point_layer->inter_position);
+            }
         } break;
         }
     } break;
 
     case SDL_MOUSEMOTION: {
-        Point *positions = dynarray_data(point_layer->positions);
-        positions[point_layer->selected] =
-            camera_map_screen(camera, event->motion.x, event->motion.y);
+        const Uint8 *state = SDL_GetKeyboardState(NULL);
+        const Vec2f mouse_pos = camera_map_screen(camera, event->motion.x, event->motion.y);
+        const Vec2f point_pos = positions[point_layer->selection];
+
+        if (!(state[SDL_SCANCODE_LCTRL] || state[SDL_SCANCODE_RCTRL])) {
+            point_layer->inter_position = mouse_pos;
+        } else {
+            const float dx = fabsf(point_pos.x - mouse_pos.x);
+            const float dy = fabsf(point_pos.y - mouse_pos.y);
+
+            if (dx > dy) {
+                point_layer->inter_position = vec(mouse_pos.x, point_pos.y);
+            } else {
+                point_layer->inter_position = vec(point_pos.x, mouse_pos.y);
+            }
+        }
     } break;
     }
 
     return 0;
 }
 
+static
+int point_layer_recolor_event(PointLayer *point_layer,
+                              const SDL_Event *event,
+                              const Camera *camera,
+                              UndoHistory *undo_history)
+{
+    trace_assert(point_layer);
+    trace_assert(event);
+    trace_assert(camera);
+    trace_assert(undo_history);
+    trace_assert(point_layer->selection >= 0);
+
+    int selected = 0;
+    if (color_picker_event(
+            &point_layer->color_picker,
+            event,
+            camera,
+            &selected) < 0) {
+        return -1;
+    }
+
+    if (selected) {
+        point_layer->inter_color = color_picker_rgba(&point_layer->color_picker);
+
+        if (!color_picker_drag(&point_layer->color_picker)) {
+            POINT_UNDO_PUSH(
+                undo_history,
+                create_point_undo_context(
+                    point_layer,
+                    POINT_UNDO_UPDATE));
+
+            dynarray_replace_at(
+                &point_layer->colors,
+                (size_t) point_layer->selection,
+                &point_layer->inter_color);
+
+            point_layer->state = POINT_LAYER_IDLE;
+        }
+    }
+
+
+    return 0;
+}
+
 int point_layer_event(PointLayer *point_layer,
                       const SDL_Event *event,
                       const Camera *camera,
@@ -609,6 +659,9 @@ int point_layer_event(PointLayer *point_layer,
 
     case POINT_LAYER_MOVE:
         return point_layer_move_event(point_layer, event, camera, undo_history);
+
+    case POINT_LAYER_RECOLOR:
+        return point_layer_recolor_event(point_layer, event, camera, undo_history);
     }
 
     return 0;
@@ -617,25 +670,25 @@ int point_layer_event(PointLayer *point_layer,
 size_t point_layer_count(const PointLayer *point_layer)
 {
     trace_assert(point_layer);
-    return dynarray_count(point_layer->positions);
+    return point_layer->positions.count;
 }
 
-const Point *point_layer_positions(const PointLayer *point_layer)
+const Vec2f *point_layer_positions(const PointLayer *point_layer)
 {
     trace_assert(point_layer);
-    return dynarray_data(point_layer->positions);
+    return (const Vec2f *)point_layer->positions.data;
 }
 
 const Color *point_layer_colors(const PointLayer *point_layer)
 {
     trace_assert(point_layer);
-    return dynarray_data(point_layer->colors);
+    return (const Color *)point_layer->colors.data;
 }
 
 const char *point_layer_ids(const PointLayer *point_layer)
 {
     trace_assert(point_layer);
-    return dynarray_data(point_layer->ids);
+    return (const char *)point_layer->ids.data;
 }
 
 int point_layer_dump_stream(const PointLayer *point_layer,
@@ -644,10 +697,10 @@ int point_layer_dump_stream(const PointLayer *point_layer,
     trace_assert(point_layer);
     trace_assert(filedump);
 
-    size_t n = dynarray_count(point_layer->ids);
-    char *ids = dynarray_data(point_layer->ids);
-    Point *positions = dynarray_data(point_layer->positions);
-    Color *colors = dynarray_data(point_layer->colors);
+    size_t n = point_layer->ids.count;
+    char *ids = (char *) point_layer->ids.data;
+    Vec2f *positions = (Vec2f *) point_layer->positions.data;
+    Color *colors = (Color *) point_layer->colors.data;
 
     fprintf(filedump, "%zd\n", n);
     for (size_t i = 0; i < n; ++i) {