diff --git a/paint/sources/functions.h b/paint/sources/functions.h index 13591a8e..e71e5156 100644 --- a/paint/sources/functions.h +++ b/paint/sources/functions.h @@ -73,6 +73,7 @@ slot_layer_t *layers_new_layer(bool clear, i32 position, slot_layer_ slot_layer_t *layers_new_mask(bool clear, slot_layer_t *parent, i32 position); slot_layer_t *layers_new_group(); slot_layer_t *layers_new_path_layer(bool curved); +slot_layer_t *layers_new_text_layer(); void layers_create_fill_layer(uv_type_t uv_type, mat4_t decal_mat, i32 position); void layers_create_image_mask(asset_t *asset); void layers_create_color_layer(i32 base_color, f32 occlusion, f32 roughness, f32 metallic, i32 position); diff --git a/paint/sources/globals.h b/paint/sources/globals.h index 638f92b4..30ffc644 100644 --- a/paint/sources/globals.h +++ b/paint/sources/globals.h @@ -7,7 +7,7 @@ char *manifest_title = "ArmorPaint"; char *manifest_version = "1.0 rc2"; -char *manifest_version_project = "13"; +char *manifest_version_project = "14"; char *manifest_version_config = "1"; char *manifest_url = "https://armorpaint.org"; char *manifest_url_android = "https://play.google.com/store/apps/details?id=org.armorpaint"; diff --git a/paint/sources/io/export_arm.c b/paint/sources/io/export_arm.c index b9521fab..019dcf16 100644 --- a/paint/sources/io/export_arm.c +++ b/paint/sources/io/export_arm.c @@ -258,6 +258,7 @@ void export_arm_run_project() { .path_points_parent = l->path_points_parent, .path_tool = l->path_tool, .path_curved = l->path_curved, + .path_text = l->path_text, .path_material = l->path_material != NULL ? array_index_of(g_project->_->materials, l->path_material) : -1}); any_array_push(ld, d); } diff --git a/paint/sources/io/import_arm.c b/paint/sources/io/import_arm.c index ac5baa22..f0b16b56 100644 --- a/paint/sources/io/import_arm.c +++ b/paint/sources/io/import_arm.c @@ -585,6 +585,7 @@ void import_arm_run_project(char *path) { l->path_points_parent = ld->path_points_parent; l->path_tool = ld->path_tool; l->path_curved = ld->path_curved; + l->path_text = ld->path_text; gpu_delete_texture(_texpaint); if (_texpaint_nor != NULL) { diff --git a/paint/sources/io/import_legacy.c b/paint/sources/io/import_legacy.c index 952f89eb..013448f3 100644 --- a/paint/sources/io/import_legacy.c +++ b/paint/sources/io/import_legacy.c @@ -210,6 +210,14 @@ project_t *import_arm_from_map_to_arm(any_map_t *old) { ld->paint_emis = armpack_map_get_i32(old, "paint_emis") > 0; ld->paint_subs = armpack_map_get_i32(old, "paint_subs") > 0; ld->uv_map = armpack_map_get_i32(old, "uv_map"); + ld->path_points = any_map_get(old, "path_points"); + ld->path_points_world = any_map_get(old, "path_points_world"); + ld->path_points_camera = any_map_get(old, "path_points_camera"); + ld->path_points_parent = any_map_get(old, "path_points_parent"); + ld->path_tool = armpack_map_get_i32(old, "path_tool"); + ld->path_curved = armpack_map_get_i32(old, "path_curved") > 0; + ld->path_material = ld->path_points != NULL ? armpack_map_get_i32(old, "path_material") : -1; + ld->path_text = armpack_map_get_i32(old, "path_text") > 0; any_array_push(project->layer_datas, ld); } } @@ -303,9 +311,20 @@ project_t *import_arm_from_map_to_arm(any_map_t *old) { return project; } +project_t *import_arm_from_version_13(any_map_t *old) { + any_array_t *lds = any_map_get(old, "layer_datas"); + if (lds != NULL) { + for (i32 i = 0; i < lds->length; ++i) { + any_map_t *ld = lds->buffer[i]; + armpack_map_set_i32(ld, "path_text", 0); + } + } + return import_arm_from_map_to_arm(old); +} + project_t *import_arm_from_version_12(any_map_t *old) { any_map_set(old, "mesh_skins", NULL); - return import_arm_from_map_to_arm(old); + return import_arm_from_version_13(old); } project_t *import_arm_from_version_11(any_map_t *old) { @@ -434,7 +453,7 @@ project_t *import_arm_from_old(buffer_t *b) { project_t *(*fns[])(any_map_t *) = { import_arm_from_version_0, import_arm_from_version_1, import_arm_from_version_2, import_arm_from_version_3, import_arm_from_version_4, import_arm_from_version_5, import_arm_from_version_6, import_arm_from_version_7, import_arm_from_version_8, import_arm_from_version_9, - import_arm_from_version_10, import_arm_from_version_11, import_arm_from_version_12, + import_arm_from_version_10, import_arm_from_version_11, import_arm_from_version_12, import_arm_from_version_13, }; for (i32 v = sizeof(fns) / sizeof(fns[0]) - 1; v >= 0; --v) { if (import_arm_is_version(b, i32_to_string(v))) { diff --git a/paint/sources/slot_layer.c b/paint/sources/slot_layer.c index f9c1aa1a..629d62da 100644 --- a/paint/sources/slot_layer.c +++ b/paint/sources/slot_layer.c @@ -372,6 +372,7 @@ slot_layer_t *slot_layer_duplicate(slot_layer_t *raw) { l->path_points_parent = i32_array_create_from_raw(raw->path_points_parent->buffer, raw->path_points_parent->length); l->path_tool = raw->path_tool; l->path_curved = raw->path_curved; + l->path_text = raw->path_text; l->path_material = raw->path_material; } @@ -488,6 +489,7 @@ void slot_layer_to_paint_layer(slot_layer_t *raw) { if (raw->path_material != NULL) { raw->path_material = NULL; raw->path_points = NULL; + raw->path_text = false; util_layer_update_path(); } make_material_parse_paint_material(true); diff --git a/paint/sources/types.h b/paint/sources/types.h index 05d3f1d8..6972ba87 100644 --- a/paint/sources/types.h +++ b/paint/sources/types.h @@ -52,6 +52,7 @@ typedef struct slot_layer { i32_array_t *path_points_parent; i32 path_tool; bool path_curved; + bool path_text; struct slot_material *path_material; } slot_layer_t; @@ -645,6 +646,7 @@ typedef struct layer_data { i32 path_tool; bool path_curved; i32 path_material; + bool path_text; } layer_data_t; typedef struct timeline_layer_keyframe_data { diff --git a/paint/sources/ui/tab_layers.c b/paint/sources/ui/tab_layers.c index 0cdbcd5e..db395fad 100644 --- a/paint/sources/ui/tab_layers.c +++ b/paint/sources/ui/tab_layers.c @@ -18,6 +18,12 @@ void tab_layers_button_2d_view() { } } +void tab_layers_repaint_text_layer(slot_layer_t *l) { + util_layer_repaint_path(l); + g_context->layer_preview_dirty = true; + g_context->layers_preview_dirty = true; +} + void tab_layers_set_drag_layer(slot_layer_t *layer, f32 off_x, f32 off_y) { base_drag_off_x = off_x; base_drag_off_y = off_y; @@ -409,6 +415,9 @@ void tab_layers_draw_layer_slot_full(slot_layer_t *l, i32 i) { tab_layers_layer_name_handle->text = string_copy(l->name); char *new_name = string_copy(ui_text_input(tab_layers_layer_name_handle, "", UI_ALIGN_LEFT, true, false)); tab_stages_rename_layer(l->name, new_name); + if (l->path_text && slot_layer_is_path(l) && !string_equals(l->name, new_name)) { + sys_notify_on_next_frame(&tab_layers_repaint_text_layer, l); + } l->name = new_name; if (g_ui->text_selected_handle != tab_layers_layer_name_handle) { tab_layers_layer_name_edit = -1; @@ -1058,6 +1067,10 @@ void tab_layers_button_new_menu() { if (ui_menu_button(tr("Decal"), "", ICON_DECAL)) { layers_create_fill_layer(UV_TYPE_PROJECT, mat4_nan(), -1); } + if (ui_menu_button(tr("Text"), "", ICON_FONT)) { + layers_new_text_layer(); + history_new_layer(); + } if (ui_menu_button(tr("Path"), "", ICON_PATH)) { layers_new_path_layer(false); history_new_layer(); diff --git a/paint/sources/util/util_encode.c b/paint/sources/util/util_encode.c index 611d6402..4ac4a01e 100644 --- a/paint/sources/util/util_encode.c +++ b/paint/sources/util/util_encode.c @@ -366,7 +366,7 @@ buffer_t *util_encode_project(project_t *raw) { if (raw->layer_datas != NULL) { armpack_encode_array(raw->layer_datas->length); for (i32 i = 0; i < raw->layer_datas->length; ++i) { - armpack_encode_map(36); + armpack_encode_map(37); armpack_encode_string("name"); armpack_encode_string(raw->layer_datas->buffer[i]->name); armpack_encode_string("res"); @@ -439,6 +439,8 @@ buffer_t *util_encode_project(project_t *raw) { armpack_encode_bool(raw->layer_datas->buffer[i]->path_curved); armpack_encode_string("path_material"); armpack_encode_i32(raw->layer_datas->buffer[i]->path_material); + armpack_encode_string("path_text"); + armpack_encode_bool(raw->layer_datas->buffer[i]->path_text); } } else { diff --git a/paint/sources/util/util_layer.c b/paint/sources/util/util_layer.c index 897a49c7..287a4f11 100644 --- a/paint/sources/util/util_layer.c +++ b/paint/sources/util/util_layer.c @@ -684,6 +684,17 @@ slot_layer_t *layers_new_path_layer(bool curved) { return l; } +slot_layer_t *layers_new_text_layer() { + slot_layer_t *l = layers_new_path_layer(true); + if (l == NULL) { + return NULL; + } + l->path_text = true; + l->path_tool = TOOL_TYPE_TEXT; + l->name = string("Text %d", l->id + 1); + return l; +} + void layers_create_fill_layer_on_next_frame(void *_) { slot_layer_t *l = layers_new_layer(false, _layers_position, NULL); history_new_layer(); diff --git a/paint/sources/util/util_path.c b/paint/sources/util/util_path.c index f1d7448e..3d3e147d 100644 --- a/paint/sources/util/util_path.c +++ b/paint/sources/util/util_path.c @@ -275,6 +275,159 @@ static void path_paint_straight(f32_array_t *points, f32_array_t *points_world, render_path_paint_dilate(true, true); } +static void path_text_stamp(char *letter, f32 px, f32 py, f32 angle, f32 *prev_px, f32 *prev_py) { + g_context->text_tool_text = string_copy(letter); + util_render_make_text_preview(); + g_context->brush_angle = angle; + g_context->prev_paint_vec_x = px; + g_context->prev_paint_vec_y = py; + path_paint(px, py, prev_px, prev_py, true); +} + +static void path_paint_text(slot_layer_t *l) { + f32_array_t *points = l->path_points; + f32_array_t *points_world = l->path_points_world; + f32_array_t *points_camera = l->path_points_camera; + i32_array_t *points_parent = l->path_points_parent; + i32 num_world = points_world->length / 3; + i32 num_camera = points_camera->length / 9; + i32 num_parent = points_parent->length; + + char *text = l->name; + i32 n = 0; // Codepoint count + for (i32 i = 0; text[i] != '\0'; i++) { + if ((text[i] & 0xc0) != 0x80) { + n++; + } + } + if (n == 0) { + return; + } + + char *_text = g_context->text_tool_text; + gpu_texture_t *_image = g_context->text_tool_image; + g_context->text_tool_image = NULL; + + f32 prev_px = 0.5f; + f32 prev_py = 0.5f; + + if (num_world < 3) { + // Single anchor + path_set_camera(points_camera, num_camera, 0); + f32 px = points->length >= 2 ? points->buffer[0] : 0.5f; + f32 py = points->length >= 2 ? points->buffer[1] : 0.5f; + path_text_stamp(text, px, py, 0.0f, &prev_px, &prev_py); + } + else { + // Sample the bezier segments + i32 num_segments = (num_world - 1) / 2; + i32 num_samples = num_segments * 32 + 1; + + f32_array_t *spos = f32_array_create(num_samples * 3); + f32_array_t *slen = f32_array_create(num_samples); + i32_array_t *scam = i32_array_create(num_samples); + + i32 si = 0; + for (i32 j = 2; j < num_world; j += 2) { + i32 p = (j < num_parent) ? points_parent->buffer[j] : j - 2; + f32 wx0 = points_world->buffer[p * 3]; + f32 wy0 = points_world->buffer[p * 3 + 1]; + f32 wz0 = points_world->buffer[p * 3 + 2]; + f32 wcx = points_world->buffer[(j - 1) * 3]; + f32 wcy = points_world->buffer[(j - 1) * 3 + 1]; + f32 wcz = points_world->buffer[(j - 1) * 3 + 2]; + f32 wx1 = points_world->buffer[j * 3]; + f32 wy1 = points_world->buffer[j * 3 + 1]; + f32 wz1 = points_world->buffer[j * 3 + 2]; + + if (si == 0) { + spos->buffer[0] = wx0; + spos->buffer[1] = wy0; + spos->buffer[2] = wz0; + slen->buffer[0] = 0.0f; + scam->buffer[0] = p; + si = 1; + } + for (i32 k = 1; k <= 32; k++) { + f32 kt = k / 32.0f; + f32 bwx = bezier_eval(wx0, wcx, wx1, kt); + f32 bwy = bezier_eval(wy0, wcy, wy1, kt); + f32 bwz = bezier_eval(wz0, wcz, wz1, kt); + f32 dx = bwx - spos->buffer[(si - 1) * 3]; + f32 dy = bwy - spos->buffer[(si - 1) * 3 + 1]; + f32 dz = bwz - spos->buffer[(si - 1) * 3 + 2]; + spos->buffer[si * 3] = bwx; + spos->buffer[si * 3 + 1] = bwy; + spos->buffer[si * 3 + 2] = bwz; + slen->buffer[si] = slen->buffer[si - 1] + sqrtf(dx * dx + dy * dy + dz * dz); + scam->buffer[si] = (k == 32 && j + 2 >= num_world) ? j : p; + si++; + } + } + + f32 total = slen->buffer[num_samples - 1]; + f32 aspect = sys_w() / (f32)sys_h(); + i32 cur_cam = -1; + i32 byte_i = 0; + for (i32 i = 0; i < n; i++) { + // Extract letter + i32 start = byte_i; + byte_i++; + while (text[byte_i] != '\0' && (text[byte_i] & 0xc0) == 0x80) { + byte_i++; + } + char letter[8]; + i32 len_b = byte_i - start > 7 ? 7 : byte_i - start; + memcpy(letter, text + start, len_b); + letter[len_b] = '\0'; + if (letter[0] == ' ' || letter[0] == '\t') { + continue; + } + + // Evenly spaced along the arc + f32 target = n == 1 ? 0.0f : total * i / (f32)(n - 1); + i32 k = 1; + while (k < num_samples - 1 && slen->buffer[k] < target) { + k++; + } + f32 d = slen->buffer[k] - slen->buffer[k - 1]; + f32 a = d > 0.0f ? (target - slen->buffer[k - 1]) / d : 1.0f; + f32 wx = spos->buffer[(k - 1) * 3] + a * (spos->buffer[k * 3] - spos->buffer[(k - 1) * 3]); + f32 wy = spos->buffer[(k - 1) * 3 + 1] + a * (spos->buffer[k * 3 + 1] - spos->buffer[(k - 1) * 3 + 1]); + f32 wz = spos->buffer[(k - 1) * 3 + 2] + a * (spos->buffer[k * 3 + 2] - spos->buffer[(k - 1) * 3 + 2]); + + i32 cam = scam->buffer[k]; + if (cam != cur_cam) { + path_set_camera(points_camera, num_camera, cam); + cur_cam = cam; + } + + f32 px, py; + if (!project_to_screen((vec4_t){wx, wy, wz, 1.0f}, &px, &py)) { + continue; + } + + // Rotate the letter + f32 ax, ay, bx, by; + f32 angle = 0.0f; + if (project_to_screen((vec4_t){spos->buffer[(k - 1) * 3], spos->buffer[(k - 1) * 3 + 1], spos->buffer[(k - 1) * 3 + 2], 1.0f}, &ax, &ay) && + project_to_screen((vec4_t){spos->buffer[k * 3], spos->buffer[k * 3 + 1], spos->buffer[k * 3 + 2], 1.0f}, &bx, &by)) { + angle = -atan2f(-(by - ay), (bx - ax) * aspect) * (180.0f / math_pi()); + } + + path_text_stamp(letter, px, py, angle, &prev_px, &prev_py); + } + } + + if (g_context->text_tool_image != NULL) { + gpu_delete_texture(g_context->text_tool_image); + } + g_context->text_tool_text = _text; + g_context->text_tool_image = _image; + + render_path_paint_dilate(true, true); +} + static void path_repaint(slot_layer_t *l) { if (l->path_material == NULL) { return; @@ -284,15 +437,20 @@ static void path_repaint(slot_layer_t *l) { return; } - slot_layer_t *_layer = g_context->layer; - slot_material_t *_material = g_context->material; - tool_type_t _tool = g_context->tool; - f32 _last_x = g_context->last_paint_vec_x; - f32 _last_y = g_context->last_paint_vec_y; - vec4_t _paint_vec = g_context->paint_vec; - g_context->layer = l; - g_context->material = l->path_material; - g_context->tool = l->path_tool; + slot_layer_t *_layer = g_context->layer; + slot_material_t *_material = g_context->material; + tool_type_t _tool = g_context->tool; + f32 _last_x = g_context->last_paint_vec_x; + f32 _last_y = g_context->last_paint_vec_y; + vec4_t _paint_vec = g_context->paint_vec; + f32 _brush_angle = g_context->brush_angle; + g_context->layer = l; + g_context->material = l->path_material; + g_context->tool = l->path_tool; + if (l->path_text) { + // Add rotation uniform to paint shader + g_context->brush_angle = 0.001f; + } make_material_save_paint_material(); make_material_parse_paint_material(false); @@ -320,7 +478,10 @@ static void path_repaint(slot_layer_t *l) { f32 dot_spacing = sphere_mode ? g_context->brush_lazy_radius * g_context->brush_lazy_step * r_world * 3.0 : 0.0f; if (num_world >= 1) { - if (l->path_curved) { + if (l->path_text) { + path_paint_text(l); + } + else if (l->path_curved) { path_paint_curved(points, points_world, points_camera, points_parent, num_world, num_camera, num_parent, sphere_mode, dot_spacing); } else { @@ -343,6 +504,7 @@ static void path_repaint(slot_layer_t *l) { g_context->last_paint_vec_x = _last_x; g_context->last_paint_vec_y = _last_y; g_context->paint_vec = _paint_vec; + g_context->brush_angle = _brush_angle; make_material_restore_paint_material(); } @@ -354,7 +516,7 @@ void util_layer_clear_path_points(slot_layer_t *l) { l->path_points_world->length = 0; l->path_points_camera->length = 0; l->path_points_parent->length = 0; - l->path_tool = -1; + l->path_tool = l->path_text ? TOOL_TYPE_TEXT : -1; path_layer_last_active = -1; } @@ -463,7 +625,7 @@ void util_layer_add_path_point(slot_layer_t *l, f32 screen_x, f32 screen_y) { if (l->path_tool == -1) { l->path_tool = g_context->tool; } - if ((i32)g_context->tool != l->path_tool) { + if (!l->path_text && (i32)g_context->tool != l->path_tool) { return; }