pebble/tests/fw/javascript/test_rocky_api_graphics_path2d.c
2025-01-27 11:38:16 -08:00

472 lines
16 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <applib/rockyjs/api/rocky_api_graphics_path2d.h>
#include "clar.h"
#include "test_jerry_port_common.h"
#include "test_rocky_common.h"
#include "applib/graphics/gpath.h"
#include "applib/graphics/gtypes.h"
#include "applib/rockyjs/api/rocky_api_global.h"
#include "applib/rockyjs/api/rocky_api_graphics.h"
#include "applib/rockyjs/api/rocky_api_graphics_color.h"
#include "applib/rockyjs/pbl_jerry_port.h"
#include "util/trig.h"
// Standard
#include "string.h"
// Fakes
#include "fake_app_timer.h"
#include "fake_logging.h"
#include "fake_pbl_malloc.h"
#include "fake_time.h"
// Stubs
#include "stubs_app_manager.h"
#include "stubs_app_state.h"
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_resources.h"
#include "stubs_sleep.h"
#include "stubs_serial.h"
#include "stubs_syscalls.h"
#include "stubs_sys_exit.h"
size_t heap_bytes_free(void) {
return 123456;
}
void layer_get_unobstructed_bounds(const Layer *layer, GRect *bounds_out) {
*bounds_out = layer->bounds;
}
bool rocky_api_graphics_color_parse(const char *color_value, GColor8 *parsed_color) {
return false;
}
bool rocky_api_graphics_color_from_value(jerry_value_t value, GColor *result) {
return false;
}
static Window s_app_window_stack_get_top_window;
Window *app_window_stack_get_top_window() {
return &s_app_window_stack_get_top_window;
}
GPointPrecise gpoint_from_polar_precise(const GPointPrecise *precise_center,
uint16_t precise_radius, int32_t angle) {
return GPointPreciseFromGPoint(GPointZero);
}
GContext s_context;
void rocky_api_graphics_text_init(void) {}
void rocky_api_graphics_text_deinit(void) {}
void rocky_api_graphics_text_add_canvas_methods(jerry_value_t obj) {}
void rocky_api_graphics_text_reset_state(void) {}
void graphics_context_set_fill_color(GContext* ctx, GColor color) {}
void graphics_context_set_stroke_color(GContext* ctx, GColor color) {}
void graphics_context_set_stroke_width(GContext* ctx, uint8_t stroke_width) {}
// mocks
static MockCallRecordings s_graphics_line_draw_precise_stroked;
void graphics_line_draw_precise_stroked(GContext* ctx, GPointPrecise p0, GPointPrecise p1) {
record_mock_call(s_graphics_line_draw_precise_stroked) {
.ctx = ctx, .pp0 = p0, .pp1 = p1,
};
}
void graphics_draw_line(GContext* ctx, GPoint p0, GPoint p1) {
// TODO: remove me PBL-42458 (still used for drawing arc)
record_mock_call(s_graphics_line_draw_precise_stroked) {.ctx = ctx};
}
MockCallRecordings s_graphics_draw_arc_precise;
void graphics_draw_arc_precise_internal(GContext *ctx, GPointPrecise center, Fixed_S16_3 radius,
int32_t angle_start, int32_t angle_end) {
record_mock_call(s_graphics_draw_arc_precise) {
.draw_arc.center = center,
.draw_arc.radius = radius,
.draw_arc.angle_start = angle_start,
.draw_arc.angle_end = angle_end,
};
}
MockCallRecordings s_gpath_draw_filled;
void gpath_draw_filled(GContext* ctx, GPath *path) {
record_mock_call(s_gpath_draw_filled) {
.path.num_points = path->num_points,
};
memcpy(s_gpath_draw_filled.last_call.path.points,
path->points, sizeof(path->points[0]) * path->num_points);
}
void graphics_fill_rect(GContext *ctx, const GRect *rect) {}
void graphics_fill_round_rect_by_value(GContext *ctx, GRect rect, uint16_t corner_radius,
GCornerMask corner_mask) {}
void graphics_draw_rect_precise(GContext *ctx, const GRectPrecise *rect) {}
void graphics_fill_radial_precise_internal(GContext *ctx, GPointPrecise center,
Fixed_S16_3 radius_inner, Fixed_S16_3 radius_outer,
int32_t angle_start, int32_t angle_end) {}
void layer_mark_dirty(Layer *layer) {}
jerry_value_t prv_create_canvas_context_2d_for_layer(Layer *layer);
static void prv_create_global_ctx(void) {
// make this easily testable by putting it int JS context as global
Layer l = {.bounds = GRect(0, 0, 144, 168)};
const jerry_value_t ctx = prv_create_canvas_context_2d_for_layer(&l);
cl_assert_equal_b(jerry_value_is_object(ctx), true);
jerry_set_object_field(jerry_get_global_object(), "ctx", ctx);
}
void test_rocky_api_graphics_path2d__initialize(void) {
fake_malloc_set_largest_free_block(~0);
s_log_internal__expected = NULL;
rocky_runtime_context_init();
fake_app_timer_init();
jerry_init(JERRY_INIT_EMPTY);
s_app_window_stack_get_top_window = (Window){};
s_context = (GContext){};
s_app_state_get_graphics_context = &s_context;
s_app_event_loop_callback = NULL;
s_graphics_line_draw_precise_stroked = (MockCallRecordings){0};
s_graphics_draw_arc_precise = (MockCallRecordings){0};
s_gpath_draw_filled = (MockCallRecordings){0};
}
void test_rocky_api_graphics_path2d__cleanup(void) {
fake_app_timer_deinit();
// Frees the internal path steps array ():
rocky_api_graphics_path2d_reset_state();
// some tests deinitialize the engine, avoid double de-init
if (app_state_get_rocky_runtime_context() != NULL) {
jerry_cleanup();
rocky_runtime_context_deinit();
}
fake_pbl_malloc_check_net_allocs();
}
static const RockyGlobalAPI *s_graphics_api[] = {
&GRAPHIC_APIS,
NULL,
};
#define PP(x, y) \
GPointPrecise((int16_t)(((x)) * FIXED_S16_3_FACTOR), \
(int16_t)(((y)) * FIXED_S16_3_FACTOR))
void test_rocky_api_graphics_path2d__invalid_coords(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
EXECUTE_SCRIPT("ctx.moveTo(4095.375, -4095.5);");
EXECUTE_SCRIPT_EXPECT_ERROR("ctx.moveTo(4096.5, 0);", "TypeError: Value out of bounds");
EXECUTE_SCRIPT_EXPECT_ERROR("ctx.moveTo(0, -4095.625);", "TypeError: Value out of bounds");
EXECUTE_SCRIPT("ctx.lineTo(4095.375, -4095.5);");
EXECUTE_SCRIPT_EXPECT_ERROR("ctx.lineTo(4096.5, 0);", "TypeError: Value out of bounds");
EXECUTE_SCRIPT_EXPECT_ERROR("ctx.lineTo(0, -4095.625);", "TypeError: Value out of bounds");
}
void test_rocky_api_graphics_path2d__minimal_path(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
EXECUTE_SCRIPT(
"ctx.beginPath();\n"
"ctx.moveTo(1, 2);\n"
"ctx.lineTo(3.5, -4.5);\n"
"ctx.stroke();\n"
);
cl_assert_equal_i(1, s_graphics_line_draw_precise_stroked.call_count);
cl_assert_equal_point_precise(PP(0.5, 1.5), s_graphics_line_draw_precise_stroked.last_call.pp0);
cl_assert_equal_point_precise(PP(3, -5), s_graphics_line_draw_precise_stroked.last_call.pp1);
EXECUTE_SCRIPT(
"ctx.fill();\n"
);
cl_assert_equal_i(0, s_gpath_draw_filled.call_count);
}
void test_rocky_api_graphics_path2d__more_lines(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
EXECUTE_SCRIPT(
"ctx.beginPath();\n"
"ctx.moveTo(1, 2);\n"
"ctx.lineTo(3, 4);\n"
"ctx.lineTo(5, 6);\n"
"ctx.lineTo(7, 8);\n"
"ctx.moveTo(9, 10);\n"
"ctx.lineTo(11, 12);\n"
"ctx.stroke();\n"
);
cl_assert_equal_i(4, s_graphics_line_draw_precise_stroked.call_count);
cl_assert_equal_point_precise(PP(8.5, 9.5), s_graphics_line_draw_precise_stroked.last_call.pp0);
cl_assert_equal_point_precise(PP(10.5, 11.5), s_graphics_line_draw_precise_stroked.last_call.pp1);
EXECUTE_SCRIPT(
"ctx.fill();\n"
);
// only first shape has at least 3 points
cl_assert_equal_i(1, s_gpath_draw_filled.call_count);
MockCallRecording *lc = &s_gpath_draw_filled.last_call;
cl_assert_equal_i(4, lc->path.num_points);
cl_assert_equal_point(GPoint(0, 1), lc->path.points[0]);
cl_assert_equal_point(GPoint(2, 3), lc->path.points[1]);
cl_assert_equal_point(GPoint(4, 5), lc->path.points[2]);
cl_assert_equal_point(GPoint(6, 7), lc->path.points[3]);
}
void test_rocky_api_graphics_path2d__fill(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
EXECUTE_SCRIPT(
"ctx.moveTo(1, 2);\n"
"ctx.lineTo(3, 4);\n"
"ctx.fill();\n"
);
// only 2 points
cl_assert_equal_i(0, s_gpath_draw_filled.call_count);
EXECUTE_SCRIPT(
"ctx.lineTo(5, 6);\n"
"ctx.fill();\n"
);
cl_assert_equal_i(1, s_gpath_draw_filled.call_count);
MockCallRecording *lc = &s_gpath_draw_filled.last_call;
cl_assert_equal_i(3, lc->path.num_points);
cl_assert_equal_point(GPoint(0, 1), lc->path.points[0]);
cl_assert_equal_point(GPoint(2, 3), lc->path.points[1]);
cl_assert_equal_point(GPoint(4, 5), lc->path.points[2]);
s_gpath_draw_filled.call_count = 0;
EXECUTE_SCRIPT(
"ctx.moveTo(7, 8);\n"
"ctx.lineTo(9, 10);\n"
"ctx.fill();\n"
);
// still only the first part (before the .moveTo()) as the second only has two points
cl_assert_equal_i(1, s_gpath_draw_filled.call_count);
cl_assert_equal_i(3, lc->path.num_points);
cl_assert_equal_point(GPoint(0, 1), lc->path.points[0]);
cl_assert_equal_point(GPoint(2, 3), lc->path.points[1]);
cl_assert_equal_point(GPoint(4, 5), lc->path.points[2]);
s_gpath_draw_filled.call_count = 0;
EXECUTE_SCRIPT(
"ctx.lineTo(11.5, 12.7);\n"
"ctx.fill();\n"
);
// still only the first part (before the .moveTo()) as the second only has two points
cl_assert_equal_i(2, s_gpath_draw_filled.call_count);
cl_assert_equal_i(3, lc->path.num_points);
cl_assert_equal_point(GPoint(6, 7), lc->path.points[0]);
cl_assert_equal_point(GPoint(8, 9), lc->path.points[1]);
cl_assert_equal_point(GPoint(11, 12), lc->path.points[2]);
EXECUTE_SCRIPT_EXPECT_ERROR(
"ctx.arc(1, 2, 3, 4, 5);\n"
"ctx.fill();\n"
, "TypeError: fill() does not support arc()");
}
void test_rocky_api_graphics_path2d__fill_oom(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
EXECUTE_SCRIPT(
"ctx.moveTo(1, 2);\n"
"ctx.lineTo(3, 4);\n"
"ctx.lineTo(5, 6);\n"
);
// OOM!
fake_malloc_set_largest_free_block(0);
// Call implementation directly instead of executing a script, to avoid mallocs by the VM itself:
extern jerry_value_t rocky_api_graphics_path2d_call_fill(void);
const jerry_value_t error_value = rocky_api_graphics_path2d_call_fill();
ASSERT_JS_ERROR(error_value, "RangeError: Out of memory: too many points to fill");
}
void test_rocky_api_graphics_path2d__arc(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
EXECUTE_SCRIPT(
"ctx.beginPath();\n"
"ctx.moveTo(1, 2);\n"
"ctx.arc(50, 40, 30, Math.PI, 0);\n"
"ctx.arc(60, 80.1, 20.5, 0, Math.PI, false);\n"
"ctx.stroke();\n"
);
cl_assert_equal_i(2, s_graphics_line_draw_precise_stroked.call_count);
cl_assert_equal_i(2, s_graphics_draw_arc_precise.call_count);
MockCallRecording *lc = &s_graphics_draw_arc_precise.last_call;
cl_assert_equal_point_precise(PP(59.5, 79.625), lc->draw_arc.center);
cl_assert_equal_i(20.5 * 8, lc->draw_arc.radius.raw_value);
cl_assert_equal_i(TRIG_MAX_ANGLE * 1 / 4, lc->draw_arc.angle_start);
cl_assert_equal_i(TRIG_MAX_ANGLE * 3 / 4, lc->draw_arc.angle_end);
}
void test_rocky_api_graphics_path2d__anti_clockwise(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
EXECUTE_SCRIPT(
"ctx.beginPath();\n"
"ctx.moveTo(80, 40);\n"
"ctx.arc(60, 80, 20, 0, Math.PI, true);\n"
"ctx.stroke();\n"
);
cl_assert_equal_i(1, s_graphics_line_draw_precise_stroked.call_count);
cl_assert_equal_i(1, s_graphics_draw_arc_precise.call_count);
MockCallRecording *lc = &s_graphics_draw_arc_precise.last_call;
cl_assert_equal_point_precise(PP(59.5, 79.5), lc->draw_arc.center);
cl_assert_equal_i(20 * 8, lc->draw_arc.radius.raw_value);
cl_assert_equal_i(TRIG_MAX_ANGLE * 3 / 4, lc->draw_arc.angle_start);
cl_assert_equal_i(TRIG_MAX_ANGLE * 5 / 4, lc->draw_arc.angle_end);
}
void test_rocky_api_graphics_path2d__unsupported(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
EXECUTE_SCRIPT_EXPECT_UNDEFINED("ctx.arcTo");
EXECUTE_SCRIPT_EXPECT_UNDEFINED("ctx.bezierCurveTo");
EXECUTE_SCRIPT_EXPECT_UNDEFINED("ctx.quadraticCurveTo");
}
extern size_t s_rocky_path_steps_num;
extern RockyAPIPathStep *s_rocky_path_steps;
void test_rocky_api_graphics_path2d__state_initialized_between_renders(void) {
rocky_global_init(s_graphics_api);
s_rocky_path_steps_num = 2;
EXECUTE_SCRIPT("_rocky.on('draw', function(e) {});");
Layer *l = &app_window_stack_get_top_window()->layer;
l->update_proc(l, NULL);
cl_assert_equal_i(0, s_rocky_path_steps_num);
}
void test_rocky_api_graphics_path2d__rect(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
cl_assert_equal_i(0, s_rocky_path_steps_num);
EXECUTE_SCRIPT(
"ctx.moveTo(1, 2);\n"
"ctx.rect(3, 4, 5, 6);\n"
);
cl_assert_equal_i(6, s_rocky_path_steps_num);
EXECUTE_SCRIPT(
"ctx.rect(7, 8, 9, 10);\n"
);
cl_assert_equal_i(11, s_rocky_path_steps_num);
cl_assert_equal_i(RockyAPIPathStepType_MoveTo, s_rocky_path_steps[0].type);
cl_assert_equal_i(RockyAPIPathStepType_MoveTo, s_rocky_path_steps[1].type);
cl_assert_equal_i(RockyAPIPathStepType_LineTo, s_rocky_path_steps[5].type);
cl_assert_equal_i(RockyAPIPathStepType_MoveTo, s_rocky_path_steps[6].type);
cl_assert_equal_point_precise((GPointPrecise(20, 28)), s_rocky_path_steps[1].pt.xy);
cl_assert_equal_point_precise((GPointPrecise(60, 76)), s_rocky_path_steps[3].pt.xy);
cl_assert_equal_point_precise((GPointPrecise(20, 28)), s_rocky_path_steps[5].pt.xy);
// actual correctness of these values is test in test_rocky_api_graphics_rendering.c
cl_assert_equal_vector_precise((GVectorPrecise(0, 8)), s_rocky_path_steps[1].pt.fill_delta);
cl_assert_equal_vector_precise((GVectorPrecise(8, 0)), s_rocky_path_steps[3].pt.fill_delta);
cl_assert_equal_vector_precise((GVectorPrecise(0, 8)), s_rocky_path_steps[5].pt.fill_delta);
}
void test_rocky_api_graphics_path2d__close_path(void) {
rocky_global_init(s_graphics_api);
prv_create_global_ctx();
cl_assert_equal_i(0, s_rocky_path_steps_num);
EXECUTE_SCRIPT(
"ctx.moveTo(1, 2);\n"
"ctx.closePath();\n"
);
cl_assert_equal_i(1, s_rocky_path_steps_num);
EXECUTE_SCRIPT(
"ctx.lineTo(3, 4);\n"
"ctx.closePath();\n"
);
cl_assert_equal_i(3, s_rocky_path_steps_num);
cl_assert_equal_i(RockyAPIPathStepType_LineTo, s_rocky_path_steps[2].type);
cl_assert_equal_point_precise(GPointPrecise(4, 12), (s_rocky_path_steps[0].pt.xy));
cl_assert_equal_point_precise(GPointPrecise(4, 12), (s_rocky_path_steps[2].pt.xy));
}
extern jerry_value_t rocky_api_graphics_path2d_try_allocate_steps(size_t increment_steps);
extern size_t rocky_api_graphics_path2d_min_array_len(void);
extern size_t rocky_api_graphics_path2d_array_len(void);
void test_rocky_api_graphics_path2d__initial_increment_larger_than_initial_size(void) {
cl_assert_equal_i(rocky_api_graphics_path2d_array_len(), 0);
const size_t min_size = rocky_api_graphics_path2d_min_array_len();
const jerry_value_t rv = rocky_api_graphics_path2d_try_allocate_steps(min_size + 1);
ASSERT_JS_ERROR(rv, NULL);
jerry_release_value(rv);
const size_t actual_size = rocky_api_graphics_path2d_array_len();
cl_assert(actual_size >= min_size + 1);
}
void test_rocky_api_graphics_path2d__array_realloc_oom(void) {
fake_malloc_set_largest_free_block(0);
const jerry_value_t rv = rocky_api_graphics_path2d_try_allocate_steps(1);
ASSERT_JS_ERROR(rv, "RangeError: Out of memory: can't create more path steps");
jerry_release_value(rv);
}