react-native-vroom-chart 0.1.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cpp/VroomChartHostObject.cpp +76 -1
- package/cpp/_core_include/vroom/vroom_chart.h +126 -11
- package/cpp/_core_src/candles.cpp +75 -10
- package/cpp/_core_src/candles.h +8 -1
- package/cpp/_core_src/chart.cpp +28 -3
- package/cpp/_core_src/chart.h +45 -4
- package/cpp/_core_src/chart_facade.cpp +207 -4
- package/cpp/_core_src/drawings.cpp +313 -15
- package/cpp/_core_src/drawings.h +23 -0
- package/cpp/_core_src/liquidity.cpp +112 -0
- package/cpp/_core_src/liquidity.h +35 -0
- package/cpp/_core_src/ma_overlay.cpp +9 -1
- package/cpp/_core_src/ma_overlay.h +2 -1
- package/cpp/_core_src/theme.cpp +5 -0
- package/cpp/_core_src/volume.cpp +15 -3
- package/lib/index.d.mts +179 -15
- package/lib/index.d.ts +179 -15
- package/lib/index.js +96 -4
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +96 -4
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/VroomChart.tsx +71 -0
- package/src/index.ts +1 -0
- package/src/jsi.d.ts +15 -0
- package/src/theme.ts +31 -6
- package/src/types.ts +1 -0
- package/src/useChartCore.ts +24 -1
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
#include <limits>
|
|
13
13
|
|
|
14
14
|
#include "chart.h"
|
|
15
|
+
#include "drawings.h"
|
|
15
16
|
#include "labels.h"
|
|
16
17
|
#include "viewport.h"
|
|
17
18
|
|
|
@@ -54,10 +55,37 @@ int64_t damp_future_delta(int64_t delta_ms, int64_t cur_future,
|
|
|
54
55
|
return static_cast<int64_t>(static_cast<double>(delta_ms) * resist);
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
// Frame the default view
|
|
58
|
-
//
|
|
58
|
+
// Frame the default view. When the consumer has set a target candle body width
|
|
59
|
+
// (default_candle_px), size the window so each candle renders ~that wide with the
|
|
60
|
+
// right edge pinned to the latest candle; otherwise fall back to the legacy
|
|
61
|
+
// "most recent ~80 candles". No-op when there are no candles.
|
|
59
62
|
void apply_default_framing(VroomChart* chart) {
|
|
60
63
|
if (chart->candles.empty()) return;
|
|
64
|
+
|
|
65
|
+
// Px-driven framing. Reuses the inversion from vroom_chart_scale_time_axis:
|
|
66
|
+
// body_w = usable × (dur / window) × ratio → window = usable × dur × ratio / body_w.
|
|
67
|
+
// Needs a valid layout (width_px); until size is known it falls through to
|
|
68
|
+
// the candle-count default below.
|
|
69
|
+
if (chart->default_candle_px > 0.f) {
|
|
70
|
+
const auto lay = chart->layout();
|
|
71
|
+
const double usable =
|
|
72
|
+
lay.width_px - lay.y_axis_width_px - lay.right_padding_px;
|
|
73
|
+
const double ratio = chart->theme.floats[VROOM_FLOAT_CANDLE_WIDTH_RATIO];
|
|
74
|
+
const double dur = static_cast<double>(chart->candle_duration_ms);
|
|
75
|
+
const double body = std::clamp(static_cast<double>(chart->default_candle_px),
|
|
76
|
+
kMinCandleBodyPx, kMaxCandleBodyPx);
|
|
77
|
+
if (usable > 0.0 && ratio > 0.0 && dur > 0.0 && body > 0.0) {
|
|
78
|
+
int64_t window_ms = static_cast<int64_t>((usable * dur * ratio) / body);
|
|
79
|
+
const int64_t span =
|
|
80
|
+
chart->candles.back().time_ms - chart->candles.front().time_ms;
|
|
81
|
+
window_ms = std::clamp<int64_t>(window_ms, chart->candle_duration_ms,
|
|
82
|
+
span > 0 ? span : window_ms);
|
|
83
|
+
chart->visible_end_ms = chart->candles.back().time_ms;
|
|
84
|
+
chart->visible_start_ms = chart->visible_end_ms - window_ms;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
61
89
|
constexpr size_t kDefaultVisible = 80;
|
|
62
90
|
const size_t start_idx = chart->candles.size() > kDefaultVisible
|
|
63
91
|
? chart->candles.size() - kDefaultVisible
|
|
@@ -167,6 +195,36 @@ extern "C" void vroom_chart_set_visible_range(VroomChart* chart, int64_t start_m
|
|
|
167
195
|
chart->mark_dirty();
|
|
168
196
|
}
|
|
169
197
|
|
|
198
|
+
extern "C" void vroom_chart_set_default_candle_width(VroomChart* chart, float px) {
|
|
199
|
+
if (!chart) return;
|
|
200
|
+
chart->default_candle_px = px;
|
|
201
|
+
// Re-frame so a value that arrives after set_candles still drives the initial
|
|
202
|
+
// view. Intended for initial framing only — calling it after the user has
|
|
203
|
+
// panned would snap the view back to the default frame.
|
|
204
|
+
if (!chart->candles.empty()) apply_default_framing(chart);
|
|
205
|
+
chart->mark_dirty();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
extern "C" void vroom_chart_set_chart_type(VroomChart* chart, int32_t mode) {
|
|
209
|
+
if (!chart) return;
|
|
210
|
+
chart->chart_type = mode;
|
|
211
|
+
// Snap the morph blend to the target so a direct set (no animation) renders
|
|
212
|
+
// the requested mode immediately. The JS animation loop uses set_morph for
|
|
213
|
+
// the in-between frames and then calls this to lock the final state.
|
|
214
|
+
const float t = mode == 1 ? 1.f : 0.f;
|
|
215
|
+
chart->morph_collapse = t;
|
|
216
|
+
chart->morph_fade = t;
|
|
217
|
+
chart->mark_dirty();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
extern "C" void vroom_chart_set_morph(VroomChart* chart, float collapse,
|
|
221
|
+
float fade) {
|
|
222
|
+
if (!chart) return;
|
|
223
|
+
chart->morph_collapse = collapse;
|
|
224
|
+
chart->morph_fade = fade;
|
|
225
|
+
chart->mark_dirty();
|
|
226
|
+
}
|
|
227
|
+
|
|
170
228
|
extern "C" void vroom_chart_get_visible_range(VroomChart* chart,
|
|
171
229
|
int64_t* out_start_ms,
|
|
172
230
|
int64_t* out_end_ms) {
|
|
@@ -712,14 +770,49 @@ extern "C" void vroom_chart_set_drawings(VroomChart* chart,
|
|
|
712
770
|
const VroomDrawing* drawings,
|
|
713
771
|
size_t count) {
|
|
714
772
|
if (!chart) return;
|
|
715
|
-
chart
|
|
773
|
+
// Deep-copy into the owning form: a pencil's path lives in the chart, so the
|
|
774
|
+
// caller's `points` buffer may be freed as soon as this returns.
|
|
775
|
+
chart->drawings.clear();
|
|
776
|
+
chart->drawings.reserve(count);
|
|
777
|
+
for (size_t i = 0; i < count; ++i) {
|
|
778
|
+
const VroomDrawing& src = drawings[i];
|
|
779
|
+
VroomChart::StoredDrawing d;
|
|
780
|
+
d.a = src.a;
|
|
781
|
+
d.b = src.b;
|
|
782
|
+
d.color = src.color;
|
|
783
|
+
d.width = src.width;
|
|
784
|
+
d.kind = src.kind;
|
|
785
|
+
if (src.kind == 2 && src.points && src.point_count > 0) {
|
|
786
|
+
d.points.assign(src.points, src.points + src.point_count);
|
|
787
|
+
// Keep a/b mirroring the path ends so bounds/handle code is uniform.
|
|
788
|
+
d.a = d.points.front();
|
|
789
|
+
d.b = d.points.back();
|
|
790
|
+
}
|
|
791
|
+
chart->drawings.push_back(std::move(d));
|
|
792
|
+
}
|
|
793
|
+
// Drop selection if the new set no longer contains that index.
|
|
794
|
+
if (chart->selected_drawing >= static_cast<int32_t>(count)) {
|
|
795
|
+
chart->selected_drawing = -1;
|
|
796
|
+
chart->grabbed_endpoint = -1;
|
|
797
|
+
}
|
|
798
|
+
chart->mark_dirty();
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// ---- Liquidity bands (order-book depth overlay) ---------------------------
|
|
802
|
+
|
|
803
|
+
extern "C" void vroom_chart_set_liquidity(VroomChart* chart,
|
|
804
|
+
const VroomBand* bands, size_t count,
|
|
805
|
+
const VroomLiquidityStyle* style) {
|
|
806
|
+
if (!chart) return;
|
|
807
|
+
chart->bands.assign(bands, bands + count);
|
|
808
|
+
if (style) chart->liquidity_style = *style;
|
|
716
809
|
chart->mark_dirty();
|
|
717
810
|
}
|
|
718
811
|
|
|
719
812
|
extern "C" void vroom_chart_set_draft(VroomChart* chart, int64_t a_time,
|
|
720
813
|
double a_price, bool has_b, int64_t b_time,
|
|
721
814
|
double b_price, bool guide, uint32_t color,
|
|
722
|
-
float width) {
|
|
815
|
+
float width, int32_t kind) {
|
|
723
816
|
if (!chart) return;
|
|
724
817
|
chart->draft_active = true;
|
|
725
818
|
chart->draft_a = VroomDrawPoint{a_time, a_price};
|
|
@@ -728,6 +821,30 @@ extern "C" void vroom_chart_set_draft(VroomChart* chart, int64_t a_time,
|
|
|
728
821
|
chart->draft_guide = guide;
|
|
729
822
|
chart->draft_color = color;
|
|
730
823
|
chart->draft_width = width;
|
|
824
|
+
chart->draft_kind = kind;
|
|
825
|
+
chart->mark_dirty();
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
extern "C" void vroom_chart_start_draft_stroke(VroomChart* chart, uint32_t color,
|
|
829
|
+
float width) {
|
|
830
|
+
if (!chart) return;
|
|
831
|
+
chart->draft_active = true;
|
|
832
|
+
chart->draft_kind = 2;
|
|
833
|
+
chart->draft_guide = true;
|
|
834
|
+
chart->draft_has_b = false;
|
|
835
|
+
chart->draft_color = color;
|
|
836
|
+
chart->draft_width = width;
|
|
837
|
+
chart->draft_points.clear();
|
|
838
|
+
chart->mark_dirty();
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
extern "C" void vroom_chart_append_draft_point(VroomChart* chart, int64_t time_ms,
|
|
842
|
+
double price) {
|
|
843
|
+
if (!chart) return;
|
|
844
|
+
if (!chart->draft_active || chart->draft_kind != 2) return;
|
|
845
|
+
chart->draft_points.push_back(VroomDrawPoint{time_ms, price});
|
|
846
|
+
// draft_a doubles as the stroke's first point for the node-dot code path.
|
|
847
|
+
if (chart->draft_points.size() == 1) chart->draft_a = chart->draft_points.front();
|
|
731
848
|
chart->mark_dirty();
|
|
732
849
|
}
|
|
733
850
|
|
|
@@ -735,6 +852,7 @@ extern "C" void vroom_chart_clear_draft(VroomChart* chart) {
|
|
|
735
852
|
if (!chart) return;
|
|
736
853
|
if (!chart->draft_active) return;
|
|
737
854
|
chart->draft_active = false;
|
|
855
|
+
chart->draft_points.clear();
|
|
738
856
|
chart->mark_dirty();
|
|
739
857
|
}
|
|
740
858
|
|
|
@@ -760,6 +878,91 @@ extern "C" bool vroom_chart_coord_at(VroomChart* chart, float x_px, float y_px,
|
|
|
760
878
|
return true;
|
|
761
879
|
}
|
|
762
880
|
|
|
881
|
+
extern "C" bool vroom_chart_project(VroomChart* chart, int64_t time_ms,
|
|
882
|
+
double price, float* out_x, float* out_y) {
|
|
883
|
+
if (!chart || chart->candles.empty()) return false;
|
|
884
|
+
const int64_t window_ms = chart->visible_end_ms - chart->visible_start_ms;
|
|
885
|
+
if (window_ms <= 0) return false;
|
|
886
|
+
const auto lay = chart->layout();
|
|
887
|
+
// Same bounds as coord_at / draw_chart so the projection matches the render.
|
|
888
|
+
const auto range = vroom::visible_indices(
|
|
889
|
+
chart->candles.data(), chart->candles.size(),
|
|
890
|
+
chart->visible_start_ms, chart->visible_end_ms);
|
|
891
|
+
const size_t n = range.end - range.start;
|
|
892
|
+
const auto bounds =
|
|
893
|
+
chart->price_bounds_manual
|
|
894
|
+
? chart->price_bounds
|
|
895
|
+
: vroom::auto_price_bounds(chart->candles.data() + range.start, n);
|
|
896
|
+
if (out_x)
|
|
897
|
+
*out_x = vroom::x_at_time(lay, chart->visible_start_ms, window_ms, time_ms);
|
|
898
|
+
if (out_y) *out_y = vroom::price_to_y(lay, bounds, price);
|
|
899
|
+
return true;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
extern "C" bool vroom_chart_hit_test_drawing(VroomChart* chart, float x_px,
|
|
903
|
+
float y_px, int32_t* out_index,
|
|
904
|
+
int32_t* out_part, float* out_t) {
|
|
905
|
+
if (!chart || chart->candles.empty()) return false;
|
|
906
|
+
const int64_t window_ms = chart->visible_end_ms - chart->visible_start_ms;
|
|
907
|
+
if (window_ms <= 0) return false;
|
|
908
|
+
const auto lay = chart->layout();
|
|
909
|
+
// Same bounds as coord_at / draw_chart so screen geometry matches the render.
|
|
910
|
+
const auto range = vroom::visible_indices(
|
|
911
|
+
chart->candles.data(), chart->candles.size(),
|
|
912
|
+
chart->visible_start_ms, chart->visible_end_ms);
|
|
913
|
+
const size_t n = range.end - range.start;
|
|
914
|
+
const auto bounds =
|
|
915
|
+
chart->price_bounds_manual
|
|
916
|
+
? chart->price_bounds
|
|
917
|
+
: vroom::auto_price_bounds(chart->candles.data() + range.start, n);
|
|
918
|
+
const auto hit = vroom::drawings::hit_test(*chart, lay, bounds, window_ms, x_px, y_px);
|
|
919
|
+
if (hit.index < 0) return false;
|
|
920
|
+
if (out_index) *out_index = hit.index;
|
|
921
|
+
if (out_part) *out_part = hit.part;
|
|
922
|
+
if (out_t) *out_t = hit.t;
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
extern "C" void vroom_chart_set_selected_drawing(VroomChart* chart,
|
|
927
|
+
int32_t index,
|
|
928
|
+
int32_t grabbed_endpoint) {
|
|
929
|
+
if (!chart) return;
|
|
930
|
+
if (index < 0 || index >= static_cast<int32_t>(chart->drawings.size())) {
|
|
931
|
+
index = -1;
|
|
932
|
+
grabbed_endpoint = -1;
|
|
933
|
+
}
|
|
934
|
+
chart->selected_drawing = index;
|
|
935
|
+
chart->grabbed_endpoint = grabbed_endpoint;
|
|
936
|
+
chart->mark_dirty();
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
extern "C" void vroom_chart_move_drawing_endpoint(VroomChart* chart, int32_t index,
|
|
940
|
+
int32_t endpoint, int64_t time_ms,
|
|
941
|
+
double price) {
|
|
942
|
+
if (!chart) return;
|
|
943
|
+
if (index < 0 || index >= static_cast<int32_t>(chart->drawings.size())) return;
|
|
944
|
+
VroomDrawPoint& pt = endpoint == 0 ? chart->drawings[index].a
|
|
945
|
+
: chart->drawings[index].b;
|
|
946
|
+
pt.time_ms = time_ms;
|
|
947
|
+
pt.price = price;
|
|
948
|
+
chart->mark_dirty();
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
extern "C" void vroom_chart_translate_drawing(VroomChart* chart, int32_t index,
|
|
952
|
+
int64_t d_time_ms, double d_price) {
|
|
953
|
+
if (!chart) return;
|
|
954
|
+
if (index < 0 || index >= static_cast<int32_t>(chart->drawings.size())) return;
|
|
955
|
+
auto& d = chart->drawings[index];
|
|
956
|
+
const auto shift = [&](VroomDrawPoint& p) {
|
|
957
|
+
p.time_ms += d_time_ms;
|
|
958
|
+
p.price += d_price;
|
|
959
|
+
};
|
|
960
|
+
shift(d.a);
|
|
961
|
+
shift(d.b);
|
|
962
|
+
for (VroomDrawPoint& p : d.points) shift(p);
|
|
963
|
+
chart->mark_dirty();
|
|
964
|
+
}
|
|
965
|
+
|
|
763
966
|
// ---- Indicators -----------------------------------------------------------
|
|
764
967
|
|
|
765
968
|
extern "C" void vroom_chart_set_rsi(VroomChart* chart, bool enabled, int period,
|
|
@@ -5,10 +5,16 @@
|
|
|
5
5
|
#include "include/core/SkCanvas.h"
|
|
6
6
|
#include "include/core/SkColor.h"
|
|
7
7
|
#include "include/core/SkPaint.h"
|
|
8
|
+
#include "include/core/SkPath.h"
|
|
9
|
+
#include "include/core/SkPathBuilder.h"
|
|
8
10
|
#include "include/core/SkPoint.h"
|
|
9
11
|
#include "include/core/SkRect.h"
|
|
10
12
|
#pragma clang diagnostic pop
|
|
11
13
|
|
|
14
|
+
#include <algorithm>
|
|
15
|
+
#include <array>
|
|
16
|
+
#include <cmath>
|
|
17
|
+
|
|
12
18
|
#include "chart.h"
|
|
13
19
|
|
|
14
20
|
namespace vroom::drawings {
|
|
@@ -31,18 +37,176 @@ SkPoint to_px(const VroomChart& chart, const Layout& lay, const PriceBounds& bou
|
|
|
31
37
|
return SkPoint{x, y};
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
// The four corners of the box with opposite corners `a` and `b`, in the fixed
|
|
41
|
+
// order used by hit-test `part` indices: a, (b.x,a.y), b, (a.x,b.y). Corner k's
|
|
42
|
+
// diagonal is corner (k+2)%4.
|
|
43
|
+
std::array<SkPoint, 4> box_corners(SkPoint a, SkPoint b) {
|
|
44
|
+
return {SkPoint{a.fX, a.fY}, SkPoint{b.fX, a.fY}, SkPoint{b.fX, b.fY},
|
|
45
|
+
SkPoint{a.fX, b.fY}};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The normalized (L,T,R,B) rectangle spanning opposite corners `a` and `b`.
|
|
49
|
+
SkRect box_rect(SkPoint a, SkPoint b) {
|
|
50
|
+
return SkRect::MakeLTRB(std::min(a.fX, b.fX), std::min(a.fY, b.fY),
|
|
51
|
+
std::max(a.fX, b.fX), std::max(a.fY, b.fY));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Strokes the box outline and paints a faint fill (~10% of the border alpha) of
|
|
55
|
+
// the same color, spanning opposite corners `a` and `b`.
|
|
56
|
+
void draw_box(SkCanvas* canvas, SkPoint a, SkPoint b, SkColor color, float width) {
|
|
57
|
+
const SkRect r = box_rect(a, b);
|
|
58
|
+
|
|
59
|
+
SkPaint fill;
|
|
60
|
+
fill.setAntiAlias(true);
|
|
61
|
+
fill.setStyle(SkPaint::kFill_Style);
|
|
62
|
+
// Faint fill: 10% of the border's alpha, same RGB.
|
|
63
|
+
const U8CPU fill_alpha = static_cast<U8CPU>(SkColorGetA(color) * 0.1f);
|
|
64
|
+
fill.setColor(SkColorSetA(color, fill_alpha));
|
|
65
|
+
canvas->drawRect(r, fill);
|
|
66
|
+
|
|
67
|
+
SkPaint border;
|
|
68
|
+
border.setAntiAlias(true);
|
|
69
|
+
border.setColor(color);
|
|
70
|
+
border.setStyle(SkPaint::kStroke_Style);
|
|
71
|
+
border.setStrokeWidth(width > 0.f ? width : 2.f);
|
|
72
|
+
canvas->drawRect(r, border);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Strokes a freehand path through `pts` (data space). The polyline is smoothed
|
|
76
|
+
// with quadratic curves through segment midpoints — each captured point becomes
|
|
77
|
+
// a control point, so the stroke reads as one organic curve instead of a chain
|
|
78
|
+
// of visible corners. Round caps/joins give it a pen-like feel.
|
|
79
|
+
void draw_path(SkCanvas* canvas, const VroomChart& chart, const Layout& lay,
|
|
80
|
+
const PriceBounds& bounds, int64_t window_ms,
|
|
81
|
+
const std::vector<VroomDrawPoint>& pts, SkColor color,
|
|
82
|
+
float width) {
|
|
83
|
+
if (pts.empty()) return;
|
|
84
|
+
const float w = width > 0.f ? width : 2.f;
|
|
85
|
+
|
|
86
|
+
SkPaint paint;
|
|
87
|
+
paint.setAntiAlias(true);
|
|
88
|
+
paint.setColor(color);
|
|
89
|
+
|
|
90
|
+
// A stroke that's still a single sample renders as a dot, so pressing down
|
|
91
|
+
// gives immediate feedback before the pointer moves.
|
|
92
|
+
if (pts.size() == 1) {
|
|
93
|
+
const SkPoint p = to_px(chart, lay, bounds, window_ms, pts[0]);
|
|
94
|
+
paint.setStyle(SkPaint::kFill_Style);
|
|
95
|
+
canvas->drawCircle(p.fX, p.fY, w * 0.5f, paint);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
SkPathBuilder builder;
|
|
100
|
+
builder.moveTo(to_px(chart, lay, bounds, window_ms, pts[0]));
|
|
101
|
+
// Curve through every interior point, ending each quad at the midpoint of
|
|
102
|
+
// the next segment; finish with a straight run to the final sample.
|
|
103
|
+
for (size_t i = 1; i + 1 < pts.size(); ++i) {
|
|
104
|
+
const SkPoint cur = to_px(chart, lay, bounds, window_ms, pts[i]);
|
|
105
|
+
const SkPoint next = to_px(chart, lay, bounds, window_ms, pts[i + 1]);
|
|
106
|
+
builder.quadTo(cur, SkPoint{(cur.fX + next.fX) * 0.5f,
|
|
107
|
+
(cur.fY + next.fY) * 0.5f});
|
|
108
|
+
}
|
|
109
|
+
builder.lineTo(to_px(chart, lay, bounds, window_ms, pts.back()));
|
|
110
|
+
|
|
111
|
+
paint.setStyle(SkPaint::kStroke_Style);
|
|
112
|
+
paint.setStrokeWidth(w);
|
|
113
|
+
paint.setStrokeCap(SkPaint::kRound_Cap);
|
|
114
|
+
paint.setStrokeJoin(SkPaint::kRound_Join);
|
|
115
|
+
canvas->drawPath(builder.detach(), paint);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
void draw_node(SkCanvas* canvas, SkPoint pt, float scale = 1.f) {
|
|
35
119
|
SkPaint fill;
|
|
36
120
|
fill.setAntiAlias(true);
|
|
37
121
|
fill.setColor(kNodeFill);
|
|
38
|
-
canvas->drawCircle(pt.fX, pt.fY, kNodeFillRadius, fill);
|
|
122
|
+
canvas->drawCircle(pt.fX, pt.fY, kNodeFillRadius * scale, fill);
|
|
39
123
|
|
|
40
124
|
SkPaint border;
|
|
41
125
|
border.setAntiAlias(true);
|
|
42
126
|
border.setColor(kNodeBorder);
|
|
43
127
|
border.setStyle(SkPaint::kStroke_Style);
|
|
44
128
|
border.setStrokeWidth(kNodeBorderWidth);
|
|
45
|
-
canvas->drawCircle(pt.fX, pt.fY, kNodeRingRadius, border);
|
|
129
|
+
canvas->drawCircle(pt.fX, pt.fY, kNodeRingRadius * scale, border);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Clamped projection parameter (0..1) of point (px,py) onto segment
|
|
133
|
+
// (ax,ay)-(bx,by): 0 at A, 1 at B.
|
|
134
|
+
float segment_t(float px, float py, float ax, float ay, float bx, float by) {
|
|
135
|
+
const float dx = bx - ax;
|
|
136
|
+
const float dy = by - ay;
|
|
137
|
+
const float len2 = dx * dx + dy * dy;
|
|
138
|
+
const float t = len2 > 0.f ? ((px - ax) * dx + (py - ay) * dy) / len2 : 0.f;
|
|
139
|
+
return std::clamp(t, 0.f, 1.f);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Shortest distance from point (px,py) to segment (ax,ay)-(bx,by), in px.
|
|
143
|
+
float dist_to_segment(float px, float py, float ax, float ay, float bx, float by) {
|
|
144
|
+
const float t = segment_t(px, py, ax, ay, bx, by);
|
|
145
|
+
const float cx = ax + t * (bx - ax);
|
|
146
|
+
const float cy = ay + t * (by - ay);
|
|
147
|
+
const float ex = px - cx;
|
|
148
|
+
const float ey = py - cy;
|
|
149
|
+
return std::sqrt(ex * ex + ey * ey);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// True when (px,py) is inside the box (the grabbable fill area).
|
|
153
|
+
bool point_in_rect(float px, float py, SkPoint a, SkPoint b) {
|
|
154
|
+
const SkRect r = box_rect(a, b);
|
|
155
|
+
return px >= r.fLeft && px <= r.fRight && py >= r.fTop && py <= r.fBottom;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Shortest distance from (px,py) to the freehand polyline through `pts`, in px.
|
|
159
|
+
// Cheap bounding-box reject first (a stroke can hold many points and most
|
|
160
|
+
// hit-tests miss), then distance-to-segment across consecutive samples — the
|
|
161
|
+
// same two-stage approach Excalidraw uses for its freedraw elements. Returns a
|
|
162
|
+
// value greater than `tol` on a clear miss.
|
|
163
|
+
float dist_to_path(float px, float py, const VroomChart& chart, const Layout& lay,
|
|
164
|
+
const PriceBounds& bounds, int64_t window_ms,
|
|
165
|
+
const std::vector<VroomDrawPoint>& pts, float tol) {
|
|
166
|
+
const float kMiss = tol + 1.f;
|
|
167
|
+
if (pts.size() < 2) return kMiss;
|
|
168
|
+
|
|
169
|
+
float min_x = 0.f, max_x = 0.f, min_y = 0.f, max_y = 0.f;
|
|
170
|
+
for (size_t i = 0; i < pts.size(); ++i) {
|
|
171
|
+
const SkPoint p = to_px(chart, lay, bounds, window_ms, pts[i]);
|
|
172
|
+
if (i == 0) {
|
|
173
|
+
min_x = max_x = p.fX;
|
|
174
|
+
min_y = max_y = p.fY;
|
|
175
|
+
} else {
|
|
176
|
+
min_x = std::min(min_x, p.fX);
|
|
177
|
+
max_x = std::max(max_x, p.fX);
|
|
178
|
+
min_y = std::min(min_y, p.fY);
|
|
179
|
+
max_y = std::max(max_y, p.fY);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (px < min_x - tol || px > max_x + tol || py < min_y - tol ||
|
|
183
|
+
py > max_y + tol) {
|
|
184
|
+
return kMiss;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
float best = kMiss;
|
|
188
|
+
SkPoint prev = to_px(chart, lay, bounds, window_ms, pts[0]);
|
|
189
|
+
for (size_t i = 1; i < pts.size(); ++i) {
|
|
190
|
+
const SkPoint cur = to_px(chart, lay, bounds, window_ms, pts[i]);
|
|
191
|
+
best = std::min(best,
|
|
192
|
+
dist_to_segment(px, py, prev.fX, prev.fY, cur.fX, cur.fY));
|
|
193
|
+
prev = cur;
|
|
194
|
+
}
|
|
195
|
+
return best;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Shortest distance from (px,py) to the box's four edges, in px (0 when on an
|
|
199
|
+
// edge). Used so the border is grabbable even outside the fill's tolerance.
|
|
200
|
+
float dist_to_rect_edges(float px, float py, SkPoint a, SkPoint b) {
|
|
201
|
+
const auto c = box_corners(a, b);
|
|
202
|
+
float best = dist_to_segment(px, py, c[0].fX, c[0].fY, c[1].fX, c[1].fY);
|
|
203
|
+
for (int i = 1; i < 4; ++i) {
|
|
204
|
+
const SkPoint& p0 = c[i];
|
|
205
|
+
const SkPoint& p1 = c[(i + 1) % 4];
|
|
206
|
+
best = std::min(best,
|
|
207
|
+
dist_to_segment(px, py, p0.fX, p0.fY, p1.fX, p1.fY));
|
|
208
|
+
}
|
|
209
|
+
return best;
|
|
46
210
|
}
|
|
47
211
|
} // namespace
|
|
48
212
|
|
|
@@ -62,9 +226,18 @@ void draw(SkCanvas* canvas,
|
|
|
62
226
|
if (!chart.drawings.empty()) {
|
|
63
227
|
canvas->save();
|
|
64
228
|
canvas->clipRect(clip);
|
|
65
|
-
for (const
|
|
229
|
+
for (const auto& d : chart.drawings) {
|
|
230
|
+
if (d.kind == 2) {
|
|
231
|
+
draw_path(canvas, chart, lay, bounds, window_ms, d.points,
|
|
232
|
+
static_cast<SkColor>(d.color), d.width);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
66
235
|
const SkPoint a = to_px(chart, lay, bounds, window_ms, d.a);
|
|
67
236
|
const SkPoint b = to_px(chart, lay, bounds, window_ms, d.b);
|
|
237
|
+
if (d.kind == 1) {
|
|
238
|
+
draw_box(canvas, a, b, static_cast<SkColor>(d.color), d.width);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
68
241
|
SkPaint line;
|
|
69
242
|
line.setAntiAlias(true);
|
|
70
243
|
line.setColor(static_cast<SkColor>(d.color));
|
|
@@ -75,34 +248,159 @@ void draw(SkCanvas* canvas,
|
|
|
75
248
|
canvas->restore();
|
|
76
249
|
}
|
|
77
250
|
|
|
251
|
+
// 1b. Handles on the selected committed drawing (unclipped, like the draft's
|
|
252
|
+
// dots). The grabbed endpoint renders 50% larger.
|
|
253
|
+
if (chart.selected_drawing >= 0 &&
|
|
254
|
+
static_cast<size_t>(chart.selected_drawing) < chart.drawings.size()) {
|
|
255
|
+
const auto& d = chart.drawings[chart.selected_drawing];
|
|
256
|
+
const SkPoint sa = to_px(chart, lay, bounds, window_ms, d.a);
|
|
257
|
+
const SkPoint sb = to_px(chart, lay, bounds, window_ms, d.b);
|
|
258
|
+
if (d.kind == 2) {
|
|
259
|
+
// Pencil: anchors on the first/last point are a visual cue that the
|
|
260
|
+
// stroke is movable — they aren't grab handles, so never enlarged.
|
|
261
|
+
draw_node(canvas, sa);
|
|
262
|
+
draw_node(canvas, sb);
|
|
263
|
+
} else if (d.kind == 1) {
|
|
264
|
+
// Four corner handles. The gesture layer stores the grabbed corner
|
|
265
|
+
// as endpoint `a` (its diagonal as `b`), so corner 0 is the active
|
|
266
|
+
// one while dragging.
|
|
267
|
+
const auto corners = box_corners(sa, sb);
|
|
268
|
+
for (int i = 0; i < 4; ++i) {
|
|
269
|
+
draw_node(canvas, corners[i],
|
|
270
|
+
(chart.grabbed_endpoint == 0 && i == 0) ? 1.5f : 1.f);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
draw_node(canvas, sa, chart.grabbed_endpoint == 0 ? 1.5f : 1.f);
|
|
274
|
+
draw_node(canvas, sb, chart.grabbed_endpoint == 1 ? 1.5f : 1.f);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
78
278
|
if (!chart.draft_active) return;
|
|
79
279
|
|
|
280
|
+
// 1c. Freehand stroke in progress: draw the live path, clipped like the
|
|
281
|
+
// committed shapes. It grows a point at a time via append_draft_point.
|
|
282
|
+
if (chart.draft_kind == 2) {
|
|
283
|
+
if (chart.draft_points.empty()) return;
|
|
284
|
+
canvas->save();
|
|
285
|
+
canvas->clipRect(clip);
|
|
286
|
+
draw_path(canvas, chart, lay, bounds, window_ms, chart.draft_points,
|
|
287
|
+
static_cast<SkColor>(chart.draft_color), chart.draft_width);
|
|
288
|
+
canvas->restore();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
80
292
|
const SkPoint a = to_px(chart, lay, bounds, window_ms, chart.draft_a);
|
|
81
293
|
const bool has_b = chart.draft_has_b;
|
|
82
294
|
const SkPoint b =
|
|
83
295
|
has_b ? to_px(chart, lay, bounds, window_ms, chart.draft_b) : a;
|
|
84
296
|
|
|
85
|
-
// 2.
|
|
86
|
-
//
|
|
87
|
-
//
|
|
297
|
+
// 2. Live preview (A->B), clipped to the candle area. Only while the second
|
|
298
|
+
// point is still being placed (draft_guide); once committed, the solid
|
|
299
|
+
// shape comes from chart.drawings instead. A box previews as a rectangle;
|
|
300
|
+
// a line as a guideline.
|
|
88
301
|
if (chart.draft_guide && has_b) {
|
|
89
302
|
canvas->save();
|
|
90
303
|
canvas->clipRect(clip);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
304
|
+
if (chart.draft_kind == 1) {
|
|
305
|
+
draw_box(canvas, a, b, static_cast<SkColor>(chart.draft_color),
|
|
306
|
+
chart.draft_width);
|
|
307
|
+
} else {
|
|
308
|
+
SkPaint guide;
|
|
309
|
+
guide.setAntiAlias(true);
|
|
310
|
+
guide.setColor(static_cast<SkColor>(chart.draft_color));
|
|
311
|
+
guide.setStyle(SkPaint::kStroke_Style);
|
|
312
|
+
guide.setStrokeWidth(chart.draft_width > 0.f ? chart.draft_width : 2.f);
|
|
313
|
+
canvas->drawLine(a, b, guide);
|
|
314
|
+
}
|
|
97
315
|
canvas->restore();
|
|
98
316
|
}
|
|
99
317
|
|
|
100
318
|
// 3. Node dots on top (not clipped, so an edge dot still renders fully).
|
|
101
319
|
// While guiding (placing the second point) only the anchor dot shows; the
|
|
102
|
-
// moving end is conveyed by the
|
|
103
|
-
// endpoints show dots.
|
|
320
|
+
// moving end is conveyed by the preview shape. Once selected (committed)
|
|
321
|
+
// both endpoints show dots.
|
|
104
322
|
draw_node(canvas, a);
|
|
105
323
|
if (has_b && !chart.draft_guide) draw_node(canvas, b);
|
|
106
324
|
}
|
|
107
325
|
|
|
326
|
+
HitResult hit_test(const VroomChart& chart,
|
|
327
|
+
const Layout& lay,
|
|
328
|
+
const PriceBounds& bounds,
|
|
329
|
+
int64_t window_ms,
|
|
330
|
+
float x,
|
|
331
|
+
float y) {
|
|
332
|
+
HitResult miss{-1, -1, 0.f};
|
|
333
|
+
if (window_ms <= 0 || chart.drawings.empty()) return miss;
|
|
334
|
+
|
|
335
|
+
// Grab-priority: if a drawing is selected, its (visible) handles win first.
|
|
336
|
+
constexpr float kHandleHit = kNodeRingRadius + 6.f;
|
|
337
|
+
if (chart.selected_drawing >= 0 &&
|
|
338
|
+
static_cast<size_t>(chart.selected_drawing) < chart.drawings.size()) {
|
|
339
|
+
const auto& d = chart.drawings[chart.selected_drawing];
|
|
340
|
+
const SkPoint a = to_px(chart, lay, bounds, window_ms, d.a);
|
|
341
|
+
const SkPoint b = to_px(chart, lay, bounds, window_ms, d.b);
|
|
342
|
+
if (d.kind == 2) {
|
|
343
|
+
// Pencil has no grab handles — its anchors translate like any other
|
|
344
|
+
// part of the stroke, so fall through to the body pass.
|
|
345
|
+
} else if (d.kind == 1) {
|
|
346
|
+
// Box: four corner handles (part 0..3).
|
|
347
|
+
const auto corners = box_corners(a, b);
|
|
348
|
+
for (int c = 0; c < 4; ++c) {
|
|
349
|
+
if (std::hypot(x - corners[c].fX, y - corners[c].fY) <= kHandleHit)
|
|
350
|
+
return HitResult{chart.selected_drawing, c, 0.f};
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
if (std::hypot(x - a.fX, y - a.fY) <= kHandleHit)
|
|
354
|
+
return HitResult{chart.selected_drawing, 0, 0.f};
|
|
355
|
+
if (std::hypot(x - b.fX, y - b.fY) <= kHandleHit)
|
|
356
|
+
return HitResult{chart.selected_drawing, 1, 1.f};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Otherwise the nearest drawing body within tolerance (topmost = last drawn).
|
|
361
|
+
// Line body is distance-to-segment (part 2); box body is the interior (dist
|
|
362
|
+
// 0) or a nearby edge (part 4).
|
|
363
|
+
constexpr float kBodyHit = 6.f;
|
|
364
|
+
float best = kBodyHit;
|
|
365
|
+
int32_t best_i = -1;
|
|
366
|
+
int32_t best_part = 2;
|
|
367
|
+
float best_t = 0.f;
|
|
368
|
+
for (size_t i = 0; i < chart.drawings.size(); ++i) {
|
|
369
|
+
const auto& d = chart.drawings[i];
|
|
370
|
+
if (d.kind == 2) {
|
|
371
|
+
const float dist = dist_to_path(x, y, chart, lay, bounds, window_ms,
|
|
372
|
+
d.points, kBodyHit);
|
|
373
|
+
if (dist <= best) { // <= so later (topmost) drawings win ties
|
|
374
|
+
best = dist;
|
|
375
|
+
best_i = static_cast<int32_t>(i);
|
|
376
|
+
best_part = 5;
|
|
377
|
+
best_t = 0.f;
|
|
378
|
+
}
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const SkPoint a = to_px(chart, lay, bounds, window_ms, d.a);
|
|
382
|
+
const SkPoint b = to_px(chart, lay, bounds, window_ms, d.b);
|
|
383
|
+
if (d.kind == 1) {
|
|
384
|
+
const float dist = point_in_rect(x, y, a, b)
|
|
385
|
+
? 0.f
|
|
386
|
+
: dist_to_rect_edges(x, y, a, b);
|
|
387
|
+
if (dist <= best) { // <= so later (topmost) drawings win ties
|
|
388
|
+
best = dist;
|
|
389
|
+
best_i = static_cast<int32_t>(i);
|
|
390
|
+
best_part = 4;
|
|
391
|
+
best_t = 0.f;
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
const float dist = dist_to_segment(x, y, a.fX, a.fY, b.fX, b.fY);
|
|
395
|
+
if (dist <= best) { // <= so later (topmost) drawings win ties
|
|
396
|
+
best = dist;
|
|
397
|
+
best_i = static_cast<int32_t>(i);
|
|
398
|
+
best_part = 2;
|
|
399
|
+
best_t = segment_t(x, y, a.fX, a.fY, b.fX, b.fY);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return best_i >= 0 ? HitResult{best_i, best_part, best_t} : miss;
|
|
404
|
+
}
|
|
405
|
+
|
|
108
406
|
} // namespace vroom::drawings
|
package/cpp/_core_src/drawings.h
CHANGED
|
@@ -31,4 +31,27 @@ void draw(SkCanvas* canvas,
|
|
|
31
31
|
float candle_right,
|
|
32
32
|
float candle_area_h);
|
|
33
33
|
|
|
34
|
+
// Result of a hit-test against the committed drawings. `index` is the drawing
|
|
35
|
+
// index (or -1 for a miss). `part` depends on the drawing kind:
|
|
36
|
+
// * line — 0 (endpoint A), 1 (endpoint B), or 2 (line body).
|
|
37
|
+
// * box — 0..3 (the four corners, in the order returned by box_corners:
|
|
38
|
+
// a, (b.x,a.y), b, (a.x,b.y)) or 4 (box body: interior or edges).
|
|
39
|
+
// * pencil — always 5 (stroke body). A freehand stroke has no grab handles:
|
|
40
|
+
// its end anchors are a visual cue only and translate the whole
|
|
41
|
+
// path like any other part of it.
|
|
42
|
+
// Corner/endpoint hits are only reported for the currently selected drawing
|
|
43
|
+
// (whose handles are visible).
|
|
44
|
+
struct HitResult {
|
|
45
|
+
int32_t index;
|
|
46
|
+
int32_t part;
|
|
47
|
+
float t; // 0..1 grab position along a line segment (A→B); 0 for box/handle hits
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
HitResult hit_test(const VroomChart& chart,
|
|
51
|
+
const vroom::Layout& lay,
|
|
52
|
+
const vroom::PriceBounds& bounds,
|
|
53
|
+
int64_t window_ms,
|
|
54
|
+
float x,
|
|
55
|
+
float y);
|
|
56
|
+
|
|
34
57
|
} // namespace vroom::drawings
|