Files
pebble/src/fw/applib/rockyjs/api/rocky_api_graphics_path2d.c
T

392 lines
12 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 "rocky_api_graphics_path2d.h"
#include "rocky_api_graphics.h"
#include "rocky_api_errors.h"
#include "applib/graphics/gpath.h"
#include "applib/graphics/graphics_circle.h"
#include "applib/graphics/graphics_line.h"
#include "kernel/pbl_malloc.h"
#include "rocky_api_util.h"
#include "rocky_api_util_args.h"
#include "system/passert.h"
#include "util/size.h"
#include "util/trig.h"
#define PATH2D_ARC "arc"
#define PATH2D_RECT "rect"
#define PATH2D_BEGINPATH "beginPath"
#define PATH2D_MOVETO "moveTo"
#define PATH2D_LINETO "lineTo"
#define PATH2D_CLOSEPATH "closePath"
#define ROCKY_CONTEXT2D_STROKE "stroke"
#define ROCKY_CONTEXT2D_FILL "fill"
#define MINIMUM_ARRAY_LEN (8)
// TODO: PBL-35780 make this part of app_state_get_rocky_runtime_context()
SECTION(".rocky_bss") T_STATIC RockyAPIPathStep *s_rocky_path_steps;
SECTION(".rocky_bss") T_STATIC size_t s_rocky_path_steps_array_len;
SECTION(".rocky_bss") T_STATIC size_t s_rocky_path_steps_num;
void rocky_api_graphics_path2d_reset_state(void) {
s_rocky_path_steps_num = 0;
task_free(s_rocky_path_steps);
s_rocky_path_steps = NULL;
s_rocky_path_steps_array_len = 0;
}
JERRY_FUNCTION(prv_begin_path) {
rocky_api_graphics_path2d_reset_state();
return jerry_create_undefined();
}
static size_t prv_get_realloc_array_len(const size_t required_array_len) {
size_t len = s_rocky_path_steps_array_len ?: MINIMUM_ARRAY_LEN;
while (required_array_len > len) {
len *= 2;
}
return len;
}
static jerry_value_t prv_try_allocate_steps(const size_t num_steps_increment) {
const size_t required_array_len = (s_rocky_path_steps_num + num_steps_increment);
if (required_array_len <= s_rocky_path_steps_array_len) {
goto success;
}
const size_t new_array_len = prv_get_realloc_array_len(required_array_len);
void *new_steps_array = task_realloc(s_rocky_path_steps,
sizeof(RockyAPIPathStep) * new_array_len);
if (!new_steps_array) {
return rocky_error_oom("can't create more path steps");
}
s_rocky_path_steps = new_steps_array;
s_rocky_path_steps_array_len = new_array_len;
success:
return jerry_create_undefined();
}
#define TRY_ALLOCATE_STEPS_OR_RETURN_ERROR(num_steps_increment) \
ROCKY_RETURN_IF_ERROR(prv_try_allocate_steps(num_steps_increment))
static jerry_value_t prv_add_pt(jerry_length_t argc, const jerry_value_t *argv,
RockyAPIPathStepType step_type) {
const double raw_x = argc > 0 ? (jerry_get_number_value(argv[0]) - 0.5) * FIXED_S16_3_FACTOR : 0;
const double raw_y = argc > 1 ? (jerry_get_number_value(argv[1]) - 0.5) * FIXED_S16_3_FACTOR : 0;
if (raw_x < INT16_MIN || raw_x > INT16_MAX || raw_y < INT16_MIN || raw_x > INT16_MAX) {
return rocky_error_argument_invalid("Value out of bounds");
}
TRY_ALLOCATE_STEPS_OR_RETURN_ERROR(1);
s_rocky_path_steps[s_rocky_path_steps_num++] = (RockyAPIPathStep) {
.type = step_type,
.pt.xy = GPointPrecise((int16_t)raw_x, (int16_t)raw_y),
};
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_move_to) {
return prv_add_pt(argc, argv, RockyAPIPathStepType_MoveTo);
}
JERRY_FUNCTION(prv_line_to) {
return prv_add_pt(argc, argv, RockyAPIPathStepType_LineTo);
}
JERRY_FUNCTION(prv_stroke) {
GContext *const ctx = rocky_api_graphics_get_gcontext();
GPoint p = {0};
GPointPrecise pp = {.x = {0}, .y = {0}};
bool moved_already = false;
#define ASSIGN_P(new_p) do { \
moved_already = true; \
p = GPointFromGPointPrecise(new_p); \
pp = new_p; \
} while (0)
for (size_t i = 0; i < s_rocky_path_steps_num; i++) {
RockyAPIPathStep *const step = &s_rocky_path_steps[i];
switch (step->type) {
case RockyAPIPathStepType_MoveTo: {
ASSIGN_P(step->pt.xy);
break;
}
case RockyAPIPathStepType_LineTo: {
if (moved_already) {
graphics_line_draw_precise_stroked(ctx, pp, step->pt.xy);
}
ASSIGN_P(step->pt.xy);
break;
}
case RockyAPIPathStepType_Arc: {
if (moved_already) {
const GPointPrecise pt_from = gpoint_from_polar_precise(
&step->arc.center, (uint16_t)step->arc.radius.raw_value, step->arc.angle_start);
graphics_line_draw_precise_stroked(ctx, pp, pt_from);
}
int32_t angle_start = step->arc.angle_start;
int32_t angle_end = step->arc.angle_end;
if (step->arc.anti_clockwise) {
const int32_t t = angle_start;
angle_start = angle_end;
angle_end = t;
}
while (angle_end < angle_start) {
angle_end += TRIG_MAX_ANGLE;
}
graphics_draw_arc_precise_internal(ctx, step->arc.center, step->arc.radius,
angle_start, angle_end);
const GPointPrecise pt_to = gpoint_from_polar_precise(
&step->arc.center, (uint16_t)step->arc.radius.raw_value, step->arc.angle_end);
ASSIGN_P(pt_to);
break;
}
default:
WTF;
}
}
#undef ASSIGN_P
return jerry_create_undefined();
}
static void prv_fill_points(GPoint *points, size_t num) {
if (num < 3) {
return;
}
GPath path = {
.num_points = num,
.points = points,
};
GContext *const ctx = rocky_api_graphics_get_gcontext();
gpath_draw_filled(ctx, &path);
}
static GPointPrecise prv_point_add_vector_precise(GPointPrecise *pt, GVectorPrecise *v) {
return GPointPrecise(
pt->x.raw_value + v->dx.raw_value,
pt->y.raw_value + v->dy.raw_value);
}
JERRY_FUNCTION(prv_fill) {
GPoint *const points = task_zalloc(sizeof(*points) * s_rocky_path_steps_num);
if (!points) {
return rocky_error_oom("too many points to fill");
}
size_t points_num = 0;
jerry_value_t rv;
#define ADD_P(pt) \
PBL_ASSERTN(points_num < s_rocky_path_steps_num); \
points[points_num++] = GPointFromGPointPrecise( \
prv_point_add_vector_precise(&(pt).xy, &(pt).fill_delta));
for (size_t i = 0; i < s_rocky_path_steps_num; i++) {
RockyAPIPathStep *const step = &s_rocky_path_steps[i];
switch (step->type) {
case RockyAPIPathStepType_MoveTo: {
prv_fill_points(points, points_num);
points_num = 0;
ADD_P(step->pt);
break;
}
case RockyAPIPathStepType_LineTo: {
ADD_P(step->pt);
break;
}
case RockyAPIPathStepType_Arc: {
rv = rocky_error_argument_invalid("fill() does not support arc()");
goto cleanup;
}
}
}
rv = jerry_create_undefined();
prv_fill_points(points, points_num);
cleanup:
task_free(points);
return rv;
}
JERRY_FUNCTION(prv_arc) {
GPointPrecise center;
Fixed_S16_3 radius;
double angle_1, angle_2;
ROCKY_ARGS_ASSIGN_OR_RETURN_ERROR(
ROCKY_ARG(center.x),
ROCKY_ARG(center.y),
ROCKY_ARG(radius),
ROCKY_ARG_ANGLE(angle_1),
ROCKY_ARG_ANGLE(angle_2));
TRY_ALLOCATE_STEPS_OR_RETURN_ERROR(1);
const bool anti_clockwise = (argc >= 6) ? jerry_get_boolean_value(argv[5]) : false;
// adjust for coordinate system
center.x.raw_value -= FIXED_S16_3_HALF.raw_value;
center.y.raw_value -= FIXED_S16_3_HALF.raw_value;
s_rocky_path_steps[s_rocky_path_steps_num++] = (RockyAPIPathStep) {
.type = RockyAPIPathStepType_Arc,
.arc = (RockyAPIPathStepArc) {
.center = center,
.radius = radius,
// TODO: PBL-40555 consolidate angle handling here and in rocky_api_graphics.c
.angle_start = jerry_get_angle_value(argv[3]),
.angle_end = jerry_get_angle_value(argv[4]),
.anti_clockwise = anti_clockwise,
},
};
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_rect) {
TRY_ALLOCATE_STEPS_OR_RETURN_ERROR(5);
if (argc >= 4) {
GRectPrecise rect;
ROCKY_ARGS_ASSIGN_OR_RETURN_ERROR(ROCKY_ARG(rect));
grect_precise_standardize(&rect);
// shift rectangle into coordinate system
const int16_t half_pt = FIXED_S16_3_HALF.raw_value;
rect.origin.x.raw_value -= half_pt;
rect.origin.y.raw_value -= half_pt;
// special casing for our filling algorithm to match fillRect()
const int16_t full_pt = FIXED_S16_3_ONE.raw_value;
const int16_t delta_t = full_pt;
const int16_t delta_r = full_pt;
const int16_t delta_b = 0;
const int16_t delta_l = 0;
const GVectorPrecise delta_tl = GVectorPrecise(delta_l, delta_t);
const GVectorPrecise delta_tr = GVectorPrecise(delta_r, delta_t);
const GVectorPrecise delta_br = GVectorPrecise(delta_r, delta_b);
const GVectorPrecise delta_bl = GVectorPrecise(delta_l, delta_b);
const Fixed_S16_3 right = grect_precise_get_max_x(&rect);
const Fixed_S16_3 bottom = grect_precise_get_max_y(&rect);
// top left
s_rocky_path_steps[s_rocky_path_steps_num++] = (RockyAPIPathStep) {
.type = RockyAPIPathStepType_MoveTo,
.pt.xy = rect.origin,
.pt.fill_delta = delta_tl,
};
// top right
s_rocky_path_steps[s_rocky_path_steps_num++] = (RockyAPIPathStep) {
.type = RockyAPIPathStepType_LineTo,
.pt.xy = GPointPrecise(right.raw_value, rect.origin.y.raw_value),
.pt.fill_delta = delta_tr,
};
// bottom right
s_rocky_path_steps[s_rocky_path_steps_num++] = (RockyAPIPathStep) {
.type = RockyAPIPathStepType_LineTo,
.pt.xy = GPointPrecise(right.raw_value, bottom.raw_value),
.pt.fill_delta = delta_br,
};
// bottom left
s_rocky_path_steps[s_rocky_path_steps_num++] = (RockyAPIPathStep) {
.type = RockyAPIPathStepType_LineTo,
.pt.xy = GPointPrecise(rect.origin.x.raw_value, bottom.raw_value),
.pt.fill_delta = delta_bl,
};
// top left again, to close path
s_rocky_path_steps[s_rocky_path_steps_num++] = (RockyAPIPathStep) {
.type = RockyAPIPathStepType_LineTo,
.pt.xy = rect.origin,
.pt.fill_delta = delta_tl,
};
}
return jerry_create_undefined();
}
JERRY_FUNCTION(prv_close_path) {
// lineTo() back to most-recent .moveTo()
if (s_rocky_path_steps_num < 2) {
return jerry_create_undefined();
}
RockyAPIPathStep *step = &s_rocky_path_steps[s_rocky_path_steps_num - 1];
// if the last step was a moveTo(), there's nothing to do
if (step->type == RockyAPIPathStepType_MoveTo) {
return jerry_create_undefined();
}
TRY_ALLOCATE_STEPS_OR_RETURN_ERROR(1);
do {
step--;
if (step->type == RockyAPIPathStepType_MoveTo) {
// add a lintTo() at the end
s_rocky_path_steps[s_rocky_path_steps_num++] = (RockyAPIPathStep) {
.type = RockyAPIPathStepType_LineTo,
.pt = step->pt,
};
break;
}
} while (step > &s_rocky_path_steps[0]);
return jerry_create_undefined();
}
void rocky_api_graphics_path2d_add_canvas_methods(jerry_value_t obj) {
rocky_add_function(obj, PATH2D_BEGINPATH, prv_begin_path);
rocky_add_function(obj, PATH2D_MOVETO, prv_move_to);
rocky_add_function(obj, PATH2D_LINETO, prv_line_to);
rocky_add_function(obj, PATH2D_ARC, prv_arc);
rocky_add_function(obj, PATH2D_RECT, prv_rect);
rocky_add_function(obj, PATH2D_CLOSEPATH, prv_close_path);
rocky_add_function(obj, ROCKY_CONTEXT2D_STROKE, prv_stroke);
rocky_add_function(obj, ROCKY_CONTEXT2D_FILL, prv_fill);
}
//! For unit testing
jerry_value_t rocky_api_graphics_path2d_try_allocate_steps(size_t inc_steps) {
TRY_ALLOCATE_STEPS_OR_RETURN_ERROR(inc_steps);
return jerry_create_undefined();
}
size_t rocky_api_graphics_path2d_min_array_len(void) {
return MINIMUM_ARRAY_LEN;
}
size_t rocky_api_graphics_path2d_array_len(void) {
return s_rocky_path_steps_array_len;
}
jerry_value_t rocky_api_graphics_path2d_call_fill(void) {
// Args aren't correct, but it doesn't matter right now because the function doesn't use them:
return prv_fill(jerry_create_undefined(), jerry_create_undefined(), NULL, 0);
}