react-native-vroom-chart 0.2.0 → 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.
@@ -27,13 +27,15 @@ ChartHostObject::~ChartHostObject() {
27
27
  std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
28
28
  jsi::Runtime& rt) {
29
29
  std::vector<jsi::PropNameID> out;
30
- out.reserve(22);
30
+ out.reserve(24);
31
31
  out.push_back(jsi::PropNameID::forAscii(rt, "setCandles"));
32
32
  out.push_back(jsi::PropNameID::forAscii(rt, "setSize"));
33
33
  out.push_back(jsi::PropNameID::forAscii(rt, "setColor"));
34
34
  out.push_back(jsi::PropNameID::forAscii(rt, "setFloat"));
35
35
  out.push_back(jsi::PropNameID::forAscii(rt, "setVisibleRange"));
36
36
  out.push_back(jsi::PropNameID::forAscii(rt, "setDefaultCandleWidth"));
37
+ out.push_back(jsi::PropNameID::forAscii(rt, "setChartType"));
38
+ out.push_back(jsi::PropNameID::forAscii(rt, "setMorph"));
37
39
  out.push_back(jsi::PropNameID::forAscii(rt, "pan"));
38
40
  out.push_back(jsi::PropNameID::forAscii(rt, "translate"));
39
41
  out.push_back(jsi::PropNameID::forAscii(rt, "zoom"));
@@ -188,6 +190,41 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
188
190
  });
189
191
  }
190
192
 
193
+ if (name == "setChartType") {
194
+ // setChartType(mode) — 0 = candlesticks (default), 1 = line chart.
195
+ return jsi::Function::createFromHostFunction(
196
+ rt,
197
+ jsi::PropNameID::forAscii(rt, "setChartType"),
198
+ 1,
199
+ [this](jsi::Runtime& /*rt2*/,
200
+ const jsi::Value& /*thisVal*/,
201
+ const jsi::Value* args,
202
+ size_t count) -> jsi::Value {
203
+ if (count < 1) return jsi::Value::undefined();
204
+ vroom_chart_set_chart_type(
205
+ chart_, static_cast<int32_t>(args[0].asNumber()));
206
+ return jsi::Value::undefined();
207
+ });
208
+ }
209
+
210
+ if (name == "setMorph") {
211
+ // setMorph(collapse, fade) — candle↔line blend for animated transitions.
212
+ return jsi::Function::createFromHostFunction(
213
+ rt,
214
+ jsi::PropNameID::forAscii(rt, "setMorph"),
215
+ 2,
216
+ [this](jsi::Runtime& /*rt2*/,
217
+ const jsi::Value& /*thisVal*/,
218
+ const jsi::Value* args,
219
+ size_t count) -> jsi::Value {
220
+ if (count < 2) return jsi::Value::undefined();
221
+ vroom_chart_set_morph(chart_,
222
+ static_cast<float>(args[0].asNumber()),
223
+ static_cast<float>(args[1].asNumber()));
224
+ return jsi::Value::undefined();
225
+ });
226
+ }
227
+
191
228
  if (name == "pan") {
192
229
  // pan(dx, dy) -> JsiSkPicture
193
230
  // Mutates the visible range and renders in one JSI call so gesture
@@ -55,12 +55,24 @@ typedef struct VroomDrawPoint {
55
55
  double price;
56
56
  } VroomDrawPoint;
57
57
 
58
- // A committed line drawing: a two-point trendline on the price pane.
58
+ // A committed drawing on the price pane. `kind` selects the geometry:
59
+ // 0 = line — a two-point trendline from `a` to `b`.
60
+ // 1 = box — an axis-aligned rectangle whose two opposite corners are `a`
61
+ // and `b`; the other two corners (a.x,b.y) and (b.x,a.y) derive.
62
+ // 2 = pencil — a freehand path through `points` (in draw order). `a`/`b` mirror
63
+ // the first/last point so bounds and handle code stay uniform.
64
+ //
65
+ // `points`/`point_count` are only read for kind 2; line and box leave them
66
+ // null/0. Like the rest of this API the points are copied internally, so the
67
+ // caller may free them as soon as the call returns.
59
68
  typedef struct VroomDrawing {
60
- VroomDrawPoint a;
61
- VroomDrawPoint b;
62
- uint32_t color; // 0xAARRGGBB
63
- float width; // stroke width in px
69
+ VroomDrawPoint a;
70
+ VroomDrawPoint b;
71
+ uint32_t color; // 0xAARRGGBB
72
+ float width; // stroke width in px
73
+ int32_t kind; // 0 = line, 1 = box, 2 = pencil
74
+ const VroomDrawPoint* points; // pencil path (kind 2), else null
75
+ int32_t point_count; // number of `points`, else 0
64
76
  } VroomDrawing;
65
77
 
66
78
  // A resting-liquidity band: a price interval carrying a total order size on one
@@ -112,6 +124,7 @@ typedef enum {
112
124
  VROOM_COLOR_WICK_BEAR, // bear wick; 0 alpha => inherit BEAR fill
113
125
  VROOM_COLOR_ACCENT_BULL, // generic up color: price indicator, volume, MACD
114
126
  VROOM_COLOR_ACCENT_BEAR, // generic down color
127
+ VROOM_COLOR_LINE, // line-chart-mode close-price polyline
115
128
  VROOM_COLOR_COUNT_
116
129
  } VroomColorKey;
117
130
 
@@ -127,6 +140,7 @@ typedef enum {
127
140
  VROOM_FLOAT_CANDLE_RADIUS_PX, // candle body corner radius px (0 = square)
128
141
  VROOM_FLOAT_WICK_ROUND_CAP, // 0/1: round the wick end caps
129
142
  VROOM_FLOAT_VOLUME_RADIUS_PX, // volume bar top-corner radius px (0 = square)
143
+ VROOM_FLOAT_LINE_WIDTH_PX, // line-chart-mode polyline stroke width px
130
144
  VROOM_FLOAT_COUNT_
131
145
  } VroomFloatKey;
132
146
 
@@ -163,6 +177,17 @@ void vroom_chart_set_visible_range(VroomChart* chart, int64_t start_ms, int64_t
163
177
  // set_visible_range still overrides it.
164
178
  void vroom_chart_set_default_candle_width(VroomChart* chart, float px);
165
179
 
180
+ // Chart render mode: 0 = candlesticks (default), 1 = line chart (a polyline
181
+ // through each candle's close). Other layers (volume, indicators, overlays,
182
+ // crosshair, drawings) are unaffected.
183
+ void vroom_chart_set_chart_type(VroomChart* chart, int32_t mode);
184
+
185
+ // Candle↔line morph blend for animated transitions. `collapse` folds candles
186
+ // toward their close price; `fade` crossfades candles→line. Both 0 = candles,
187
+ // both 1 = line. Driven per-frame by the host animation loop; set_chart_type
188
+ // snaps both to the target.
189
+ void vroom_chart_set_morph(VroomChart* chart, float collapse, float fade);
190
+
166
191
  // Reads the current visible time window. Either out pointer may be null.
167
192
  // Both are 0 when the window is still uninitialized.
168
193
  void vroom_chart_get_visible_range(VroomChart* chart,
@@ -304,6 +329,34 @@ void vroom_chart_set_vwap(VroomChart* chart, bool enabled, int reset_offset_min,
304
329
  void vroom_chart_set_drawings(VroomChart* chart, const VroomDrawing* drawings,
305
330
  size_t count);
306
331
 
332
+ // Hit-tests pixel (x_px, y_px) against the committed drawings. On a hit, fills
333
+ // *out_index with the drawing index, *out_part with 0 (endpoint A), 1
334
+ // (endpoint B), or 2 (line body), *out_t with the 0..1 grab position along the
335
+ // segment (A→B; 0/1 for handle hits), and returns true. Endpoint hits are only
336
+ // reported for the currently selected drawing (whose handles are visible).
337
+ // Returns false on a miss (out params untouched). Any out pointer may be null.
338
+ bool vroom_chart_hit_test_drawing(VroomChart* chart, float x_px, float y_px,
339
+ int32_t* out_index, int32_t* out_part,
340
+ float* out_t);
341
+
342
+ // Selects a committed drawing (renders its endpoint handles). `index` -1 clears
343
+ // the selection. `grabbed_endpoint` 0/1 renders that handle 50% larger while it's
344
+ // being dragged; -1 for none. An out-of-range index clears the selection.
345
+ void vroom_chart_set_selected_drawing(VroomChart* chart, int32_t index,
346
+ int32_t grabbed_endpoint);
347
+
348
+ // Moves one endpoint of a committed drawing to a new data-space anchor (for live
349
+ // handle dragging). `endpoint` is 0 (A) or 1 (B). No-op for an out-of-range index.
350
+ void vroom_chart_move_drawing_endpoint(VroomChart* chart, int32_t index,
351
+ int32_t endpoint, int64_t time_ms,
352
+ double price);
353
+
354
+ // Shifts a whole committed drawing by a *relative* data-space delta — `a`, `b`,
355
+ // and (for a pencil) every path point. Used for live body dragging of shapes
356
+ // whose points can't be restated cheaply. No-op for an out-of-range index.
357
+ void vroom_chart_translate_drawing(VroomChart* chart, int32_t index,
358
+ int64_t d_time_ms, double d_price);
359
+
307
360
  // ---- Liquidity bands (order-book depth overlay) ---------------------------
308
361
 
309
362
  // Replaces the full set of resting-liquidity bands and their shared style.
@@ -314,14 +367,29 @@ void vroom_chart_set_liquidity(VroomChart* chart, const VroomBand* bands,
314
367
 
315
368
  // Sets the transient in-progress "draft" the drawing tool shows while the user
316
369
  // places points. Node A is always shown; when `has_b`, node B is shown too.
317
- // `guide != 0` also draws the guideline A->B (the live line preview); `guide == 0`
318
- // draws node dots only (the committed segment already renders via set_drawings).
319
- // `color`/`width` style the guideline to match the eventual line.
370
+ // `guide != 0` also draws the live preview (a guideline for a line, or a preview
371
+ // rectangle for a box); `guide == 0` draws node dots only (the committed shape
372
+ // already renders via set_drawings). `kind` matches VroomDrawing (0 = line,
373
+ // 1 = box) so the preview geometry matches the eventual shape. `color`/`width`
374
+ // style the preview to match the eventual drawing.
320
375
  void vroom_chart_set_draft(VroomChart* chart, int64_t a_time, double a_price,
321
376
  bool has_b, int64_t b_time, double b_price,
322
- bool guide, uint32_t color, float width);
323
-
324
- // Clears the draft (hides the in-progress node dots / guideline).
377
+ bool guide, uint32_t color, float width,
378
+ int32_t kind);
379
+
380
+ // Begins a freehand (pencil) draft stroke, clearing any previous draft points.
381
+ // Follow with vroom_chart_append_draft_point per captured sample; the growing
382
+ // path renders live. `color`/`width` style it to match the eventual stroke.
383
+ void vroom_chart_start_draft_stroke(VroomChart* chart, uint32_t color,
384
+ float width);
385
+
386
+ // Appends one point to the in-progress freehand draft. Cheap (O(1) amortized) so
387
+ // it can be called on every pointer move without restating the whole path.
388
+ // No-op unless a draft stroke was started.
389
+ void vroom_chart_append_draft_point(VroomChart* chart, int64_t time_ms,
390
+ double price);
391
+
392
+ // Clears the draft (hides the in-progress node dots / guideline / stroke).
325
393
  void vroom_chart_clear_draft(VroomChart* chart);
326
394
 
327
395
  // Fills *out with the continuous data coordinate (time_ms, price) at pixel
@@ -331,6 +399,13 @@ void vroom_chart_clear_draft(VroomChart* chart);
331
399
  bool vroom_chart_coord_at(VroomChart* chart, float x_px, float y_px,
332
400
  VroomCoord* out);
333
401
 
402
+ // Projects a data coordinate (time_ms, price) to its pixel position, filling
403
+ // *out_x / *out_y (either may be null) with the free (non-snapped) mapping — the
404
+ // inverse of vroom_chart_coord_at, matching the rendered drawing geometry.
405
+ // Returns false (out params untouched) when there are no candles / degenerate.
406
+ bool vroom_chart_project(VroomChart* chart, int64_t time_ms, double price,
407
+ float* out_x, float* out_y);
408
+
334
409
  // ---- Rendering ------------------------------------------------------------
335
410
 
336
411
  void vroom_chart_draw(VroomChart* chart, SkCanvas* canvas);
@@ -30,11 +30,28 @@ void draw(SkCanvas* canvas,
30
30
  const PriceBounds& bounds,
31
31
  int64_t window_ms,
32
32
  int64_t visible_start_ms,
33
- int64_t candle_duration_ms) {
33
+ int64_t candle_duration_ms,
34
+ float collapse,
35
+ float opacity) {
34
36
  if (!canvas || n == 0) return;
35
37
 
36
- const float body_w = vroom::candle_body_width(
38
+ collapse = std::clamp(collapse, 0.f, 1.f);
39
+ opacity = std::clamp(opacity, 0.f, 1.f);
40
+ if (opacity <= 0.f) return;
41
+
42
+ // During the candle→line morph, fade the entire candle layer as one unit so
43
+ // overlapping bars/wicks composite cleanly instead of stacking alpha.
44
+ const bool fade_layer = opacity < 0.999f;
45
+ if (fade_layer) {
46
+ canvas->saveLayerAlpha(nullptr,
47
+ static_cast<U8CPU>(opacity * 255.f + 0.5f));
48
+ }
49
+
50
+ const float full_body_w = vroom::candle_body_width(
37
51
  lay, window_ms, candle_duration_ms);
52
+ // Thin the body toward the line stroke width as candles collapse to the line.
53
+ const float line_w = theme.floats[VROOM_FLOAT_LINE_WIDTH_PX];
54
+ const float body_w = full_body_w + (line_w - full_body_w) * collapse;
38
55
 
39
56
  const uint32_t fill_bull = theme.colors[VROOM_COLOR_BULL];
40
57
  const uint32_t fill_bear = theme.colors[VROOM_COLOR_BEAR];
@@ -94,10 +111,18 @@ void draw(SkCanvas* canvas,
94
111
  const float cx = vroom::candle_center_x(
95
112
  lay, c.time_ms, candle_duration_ms,
96
113
  visible_start_ms, window_ms);
97
- const float y_high = vroom::price_to_y(lay, bounds, c.high);
98
- const float y_low = vroom::price_to_y(lay, bounds, c.low);
99
- const float y_open = vroom::price_to_y(lay, bounds, c.open);
100
114
  const float y_close = vroom::price_to_y(lay, bounds, c.close);
115
+ // Collapse high/low/open toward the close so the candle folds into its
116
+ // close point (the line vertex) as `collapse` → 1.
117
+ const float y_high =
118
+ vroom::price_to_y(lay, bounds, c.high) * (1.f - collapse) +
119
+ y_close * collapse;
120
+ const float y_low =
121
+ vroom::price_to_y(lay, bounds, c.low) * (1.f - collapse) +
122
+ y_close * collapse;
123
+ const float y_open =
124
+ vroom::price_to_y(lay, bounds, c.open) * (1.f - collapse) +
125
+ y_close * collapse;
101
126
 
102
127
  canvas->drawLine(cx, y_high, cx, y_low,
103
128
  bull ? wick_bull : wick_bear);
@@ -133,6 +158,8 @@ void draw(SkCanvas* canvas,
133
158
  }
134
159
  }
135
160
  }
161
+
162
+ if (fade_layer) canvas->restore();
136
163
  }
137
164
 
138
165
  } // namespace vroom::candles
@@ -21,6 +21,11 @@ namespace vroom::candles {
21
21
 
22
22
  // Draws every candle in [visible, visible + n). The visible slice should
23
23
  // already be filtered by `visible_indices` in viewport.h.
24
+ //
25
+ // `collapse` (0..1) morphs each candle vertically toward its close price for the
26
+ // candle→line transition: 0 = normal candle, 1 = a flat point at the close (the
27
+ // body/wick heights and width shrink to the line). `opacity` (0..1) fades the
28
+ // whole candle layer out as the line fades in. Both default to a no-op.
24
29
  void draw(SkCanvas* canvas,
25
30
  const ::VroomCandle* visible,
26
31
  std::size_t n,
@@ -29,6 +34,8 @@ void draw(SkCanvas* canvas,
29
34
  const PriceBounds& bounds,
30
35
  int64_t window_ms,
31
36
  int64_t visible_start_ms,
32
- int64_t candle_duration_ms);
37
+ int64_t candle_duration_ms,
38
+ float collapse = 0.f,
39
+ float opacity = 1.f);
33
40
 
34
41
  } // namespace vroom::candles
@@ -149,9 +149,27 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
149
149
  vroom::liquidity::draw(canvas, *this, lay, bounds, candle_right,
150
150
  candle_area_h);
151
151
 
152
- // 5. Candles (wicks + bodies)
153
- vroom::candles::draw(canvas, visible, n, lay, theme, bounds,
154
- window_ms, visible_start_ms, candle_duration_ms);
152
+ // 5. Price series candles, a close-price line, or a blend of the two during
153
+ // the candle↔line morph. `morph_fade` crossfades candles→line and
154
+ // `morph_collapse` folds each candle toward its close (the line vertex).
155
+ // fade 0 = pure candles, fade 1 = pure line. The line reuses the MA-overlay
156
+ // polyline routine, fed the visible closes and styled by theme.LINE.
157
+ const float fade = morph_fade;
158
+ const float collapse = morph_collapse;
159
+ if (fade < 1.f) {
160
+ vroom::candles::draw(canvas, visible, n, lay, theme, bounds, window_ms,
161
+ visible_start_ms, candle_duration_ms, collapse,
162
+ 1.f - fade);
163
+ }
164
+ if (fade > 0.f) {
165
+ std::vector<double> closes(n);
166
+ for (std::size_t i = 0; i < n; ++i) closes[i] = visible[i].close;
167
+ vroom::ma_overlay::draw(
168
+ canvas, lay, bounds, visible, n, closes.data(), window_ms,
169
+ visible_start_ms, candle_duration_ms, candle_right, candle_area_h,
170
+ theme.colors[VROOM_COLOR_LINE], theme.floats[VROOM_FLOAT_LINE_WIDTH_PX],
171
+ nullptr, fade);
172
+ }
155
173
 
156
174
  // 5.5. Moving-average overlay lines (SMA/EMA) on the price pane, over the
157
175
  // candles. They share the candle price scale and don't reserve a pane.
@@ -54,6 +54,15 @@ struct VroomChart {
54
54
  // initial zoom). 0 = legacy "last ~80 candles" behavior.
55
55
  float default_candle_px = 0.f;
56
56
 
57
+ // Render mode: 0 = candlesticks (default), 1 = line chart (close polyline).
58
+ int chart_type = 0;
59
+
60
+ // Candle↔line morph blend, driven per-frame by the JS animation loop.
61
+ // `morph_collapse` folds candles toward their close; `morph_fade` crossfades
62
+ // candles→line. Both 0 = candles, both 1 = line. set_chart_type snaps them.
63
+ float morph_collapse = 0.f;
64
+ float morph_fade = 0.f;
65
+
57
66
  // Cached y-axis width in pixels, sized to fit the widest formatted price
58
67
  // label. 0 = uncomputed; layout() falls back to a width ratio.
59
68
  float axis_width_px = 0.f;
@@ -126,10 +135,22 @@ struct VroomChart {
126
135
  std::vector<unsigned char> vwap_breaks;
127
136
  bool vwap_dirty = true;
128
137
 
129
- // --- drawings (line annotations) ---------------------------------------
130
- // Committed two-point lines, anchored in data space so they track the
131
- // candles on pan/zoom. Drawn on the price pane above candles/overlays.
132
- std::vector<VroomDrawing> drawings;
138
+ // --- drawings (annotations) --------------------------------------------
139
+ // Committed drawings, anchored in data space so they track the candles on
140
+ // pan/zoom. Drawn on the price pane above candles/overlays.
141
+ //
142
+ // Mirrors the public VroomDrawing but *owns* its points, so a pencil path
143
+ // (kind 2) can carry a variable number of them. For line/box `points` is
144
+ // empty and only a/b are used; for pencil a/b mirror the first/last point.
145
+ struct StoredDrawing {
146
+ VroomDrawPoint a{};
147
+ VroomDrawPoint b{};
148
+ uint32_t color = 0xff2962ff;
149
+ float width = 2.f;
150
+ int32_t kind = 0;
151
+ std::vector<VroomDrawPoint> points; // pencil path (kind 2)
152
+ };
153
+ std::vector<StoredDrawing> drawings;
133
154
 
134
155
  // Transient in-progress "draft" the drawing tool shows while placing points.
135
156
  // draft_a is always drawn (node dot); draft_b is drawn when draft_has_b.
@@ -143,6 +164,16 @@ struct VroomChart {
143
164
  bool draft_guide = false;
144
165
  uint32_t draft_color = 0xff2962ff;
145
166
  float draft_width = 2.f;
167
+ int32_t draft_kind = 0; // 0 = line, 1 = box, 2 = pencil (VroomDrawing)
168
+ // Freehand stroke in progress (draft_kind 2), grown one point at a time.
169
+ std::vector<VroomDrawPoint> draft_points;
170
+
171
+ // Selection/editing state for committed drawings. selected_drawing indexes
172
+ // `drawings` (or -1); its endpoints render as handles. grabbed_endpoint is
173
+ // 0 (A) or 1 (B) while that handle is being dragged (rendered 50% larger),
174
+ // else -1.
175
+ int32_t selected_drawing = -1;
176
+ int32_t grabbed_endpoint = -1;
146
177
 
147
178
  // --- liquidity bands (order-book depth overlay) ------------------------
148
179
  // Resting-order bands anchored in price space, drawn behind the candles and
@@ -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
 
@@ -204,6 +205,26 @@ extern "C" void vroom_chart_set_default_candle_width(VroomChart* chart, float px
204
205
  chart->mark_dirty();
205
206
  }
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
+
207
228
  extern "C" void vroom_chart_get_visible_range(VroomChart* chart,
208
229
  int64_t* out_start_ms,
209
230
  int64_t* out_end_ms) {
@@ -749,7 +770,31 @@ extern "C" void vroom_chart_set_drawings(VroomChart* chart,
749
770
  const VroomDrawing* drawings,
750
771
  size_t count) {
751
772
  if (!chart) return;
752
- chart->drawings.assign(drawings, drawings + count);
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
+ }
753
798
  chart->mark_dirty();
754
799
  }
755
800
 
@@ -767,7 +812,7 @@ extern "C" void vroom_chart_set_liquidity(VroomChart* chart,
767
812
  extern "C" void vroom_chart_set_draft(VroomChart* chart, int64_t a_time,
768
813
  double a_price, bool has_b, int64_t b_time,
769
814
  double b_price, bool guide, uint32_t color,
770
- float width) {
815
+ float width, int32_t kind) {
771
816
  if (!chart) return;
772
817
  chart->draft_active = true;
773
818
  chart->draft_a = VroomDrawPoint{a_time, a_price};
@@ -776,6 +821,30 @@ extern "C" void vroom_chart_set_draft(VroomChart* chart, int64_t a_time,
776
821
  chart->draft_guide = guide;
777
822
  chart->draft_color = color;
778
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();
779
848
  chart->mark_dirty();
780
849
  }
781
850
 
@@ -783,6 +852,7 @@ extern "C" void vroom_chart_clear_draft(VroomChart* chart) {
783
852
  if (!chart) return;
784
853
  if (!chart->draft_active) return;
785
854
  chart->draft_active = false;
855
+ chart->draft_points.clear();
786
856
  chart->mark_dirty();
787
857
  }
788
858
 
@@ -808,6 +878,91 @@ extern "C" bool vroom_chart_coord_at(VroomChart* chart, float x_px, float y_px,
808
878
  return true;
809
879
  }
810
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
+
811
966
  // ---- Indicators -----------------------------------------------------------
812
967
 
813
968
  extern "C" void vroom_chart_set_rsi(VroomChart* chart, bool enabled, int period,