@luxalgo/vela 0.6.17 → 0.6.19
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/dist/{DataProvider-B-Jz59Rf.d.ts → DataProvider-Cb-2lC5O.d.ts} +1 -1
- package/dist/{DataProvider-OfR3fXg9.d.cts → DataProvider-UtWw2CgJ.d.cts} +1 -1
- package/dist/{chunk-YCD72KGK.js → chunk-22QJKGXX.js} +565 -151
- package/dist/{chunk-OSZDSH7F.js → chunk-627A5HEV.js} +252 -120
- package/dist/{contributions-C3b0Sx5P.d.cts → contributions-5nz3PLRe.d.ts} +9 -4
- package/dist/{contributions-Swzz10fG.d.ts → contributions-DEPRtkmG.d.cts} +9 -4
- package/dist/index.cjs +563 -149
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +1 -1
- package/dist/{options-CX1lSYWA.d.cts → options-BCRmYALw.d.cts} +12 -3
- package/dist/{options-CX1lSYWA.d.ts → options-BCRmYALw.d.ts} +12 -3
- package/dist/{plugin-mvddudqd.d.ts → plugin-CUtfLeqz.d.ts} +3 -3
- package/dist/{plugin-BjnEE1zA.d.cts → plugin-DVUaROPo.d.cts} +3 -3
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-model-IURCya0H.d.ts → statusline-model-hY9cK4BS.d.cts} +17 -7
- package/dist/{statusline-model-D91cU2fF.d.cts → statusline-model-wW3m4in1.d.ts} +17 -7
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +563 -149
- package/dist/vela.global.min.js +53 -51
- package/dist/widget.cjs +814 -268
- package/dist/widget.d.cts +39 -12
- package/dist/widget.d.ts +39 -12
- package/dist/widget.js +3 -3
- package/dist/workspace.cjs +814 -268
- package/dist/workspace.d.cts +14 -5
- package/dist/workspace.d.ts +14 -5
- package/dist/workspace.js +2 -2
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection,
|
|
1
|
+
import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, DEFAULT_DRAWING_COLOR, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection, MAGNIFIER_TIMEFRAME_OPTIONS, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS, stableSeriesId } from './chunk-IFJJPXSV.js';
|
|
2
2
|
import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, blendOver, splitColor, CalloutBubble, Menu, TextArea, Popover, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-T5Z5YUCF.js';
|
|
3
|
-
import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL,
|
|
3
|
+
import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, BULLISH, CATEGORICAL, SERIES_LINE, BEARISH, isDarkColor, INFO, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-CAFCLMPF.js';
|
|
4
4
|
|
|
5
5
|
// src/core/events/EventBus.ts
|
|
6
6
|
var TypedEventBus = class {
|
|
@@ -2161,15 +2161,18 @@ var DrawingController = class {
|
|
|
2161
2161
|
next = [...ids];
|
|
2162
2162
|
}
|
|
2163
2163
|
this.selectedIds = next.filter((id) => this.store.has(id));
|
|
2164
|
+
this.announceSelection();
|
|
2165
|
+
}
|
|
2166
|
+
/** Push the selection to the renderer and announce it (primary + the full list). */
|
|
2167
|
+
announceSelection() {
|
|
2164
2168
|
this.port?.setSelection(this.selectedIds);
|
|
2165
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
2169
|
+
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null, ids: [...this.selectedIds] });
|
|
2166
2170
|
}
|
|
2167
2171
|
/** Restore a history snapshot, reconciling selection against what survived. */
|
|
2168
2172
|
restoreSnapshot(doc) {
|
|
2169
2173
|
this.store.load(doc);
|
|
2170
2174
|
this.selectedIds = this.selectedIds.filter((id) => this.store.has(id));
|
|
2171
|
-
this.
|
|
2172
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
2175
|
+
this.announceSelection();
|
|
2173
2176
|
}
|
|
2174
2177
|
/** Delete drawings as one undo step; prune them from the selection. */
|
|
2175
2178
|
deleteMany(ids) {
|
|
@@ -2302,6 +2305,9 @@ var DrawingController = class {
|
|
|
2302
2305
|
case "duplicate":
|
|
2303
2306
|
this.duplicate(i.ids);
|
|
2304
2307
|
break;
|
|
2308
|
+
case "clone":
|
|
2309
|
+
if (this.enabled && i.docs.length) this.insertClones(i.docs);
|
|
2310
|
+
break;
|
|
2305
2311
|
case "copy":
|
|
2306
2312
|
this.copy(i.ids);
|
|
2307
2313
|
break;
|
|
@@ -5239,15 +5245,15 @@ function candleOverrideFor(style, bags) {
|
|
|
5239
5245
|
if (!hasOwnCandlePaint(style)) return null;
|
|
5240
5246
|
const bag = bags[style] ?? {};
|
|
5241
5247
|
const color = (v) => isColor(v) ? v : null;
|
|
5242
|
-
const
|
|
5248
|
+
const bool2 = (v) => isBool(v) ? v : null;
|
|
5243
5249
|
return {
|
|
5244
5250
|
upColor: color(bag.candleUpColor),
|
|
5245
5251
|
downColor: color(bag.candleDownColor),
|
|
5246
|
-
bodyVisible:
|
|
5247
|
-
borderVisible:
|
|
5252
|
+
bodyVisible: bool2(bag.candleBodyVisible),
|
|
5253
|
+
borderVisible: bool2(bag.candleBorderVisible),
|
|
5248
5254
|
borderUpColor: color(bag.candleBorderUpColor),
|
|
5249
5255
|
borderDownColor: color(bag.candleBorderDownColor),
|
|
5250
|
-
wickVisible:
|
|
5256
|
+
wickVisible: bool2(bag.candleWickVisible),
|
|
5251
5257
|
wickUpColor: color(bag.candleWickUpColor),
|
|
5252
5258
|
wickDownColor: color(bag.candleWickDownColor)
|
|
5253
5259
|
};
|
|
@@ -7038,7 +7044,7 @@ var InputController = class {
|
|
|
7038
7044
|
if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
|
|
7039
7045
|
if (Number.isNaN(this.cursorX)) return;
|
|
7040
7046
|
if (this.dragging && this.region !== "drawing") return;
|
|
7041
|
-
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
|
|
7047
|
+
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
7042
7048
|
};
|
|
7043
7049
|
this.onDown = (e) => {
|
|
7044
7050
|
if (e.button === 1) {
|
|
@@ -7070,7 +7076,7 @@ var InputController = class {
|
|
|
7070
7076
|
this.startY = y;
|
|
7071
7077
|
if (this.deps.drawingsClaim?.(x, y)) {
|
|
7072
7078
|
this.region = "drawing";
|
|
7073
|
-
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
|
|
7079
|
+
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
7074
7080
|
this.capture(e.pointerId);
|
|
7075
7081
|
return;
|
|
7076
7082
|
}
|
|
@@ -7079,6 +7085,11 @@ var InputController = class {
|
|
|
7079
7085
|
this.capture(e.pointerId);
|
|
7080
7086
|
return;
|
|
7081
7087
|
}
|
|
7088
|
+
if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
|
|
7089
|
+
this.region = "drawing";
|
|
7090
|
+
this.capture(e.pointerId);
|
|
7091
|
+
return;
|
|
7092
|
+
}
|
|
7082
7093
|
const vp = this.deps.getCoords().getViewport();
|
|
7083
7094
|
this.deps.apply(vp);
|
|
7084
7095
|
this.startRightOffset = vp.rightOffset;
|
|
@@ -7127,7 +7138,7 @@ var InputController = class {
|
|
|
7127
7138
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
7128
7139
|
} else if (this.region === "drawing") {
|
|
7129
7140
|
if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
|
|
7130
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
7141
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
7131
7142
|
} else {
|
|
7132
7143
|
const coords = this.deps.getCoords();
|
|
7133
7144
|
const vp = coords.getViewport();
|
|
@@ -7150,7 +7161,7 @@ var InputController = class {
|
|
|
7150
7161
|
const drawCursor = this.deps.drawingsCursor?.(x, y);
|
|
7151
7162
|
const r = this.regionAt(x, y);
|
|
7152
7163
|
this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
|
|
7153
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
7164
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
7154
7165
|
}
|
|
7155
7166
|
if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
|
|
7156
7167
|
};
|
|
@@ -7491,12 +7502,12 @@ function defaultTradeMarkersState() {
|
|
|
7491
7502
|
function mergeTradeMarkersState(base, patch) {
|
|
7492
7503
|
const p = patch && typeof patch === "object" ? patch : {};
|
|
7493
7504
|
const c = p.colors && typeof p.colors === "object" ? p.colors : {};
|
|
7494
|
-
const
|
|
7505
|
+
const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
|
|
7495
7506
|
const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
|
|
7496
7507
|
return {
|
|
7497
|
-
visible:
|
|
7498
|
-
labels:
|
|
7499
|
-
qty:
|
|
7508
|
+
visible: bool2(p.visible, base.visible),
|
|
7509
|
+
labels: bool2(p.labels, base.labels),
|
|
7510
|
+
qty: bool2(p.qty, base.qty),
|
|
7500
7511
|
colors: {
|
|
7501
7512
|
long: color(c.long, base.colors.long),
|
|
7502
7513
|
short: color(c.short, base.colors.short),
|
|
@@ -11368,6 +11379,18 @@ var DrawingPainter = class {
|
|
|
11368
11379
|
}
|
|
11369
11380
|
this.paintHandles(ctx, pts);
|
|
11370
11381
|
}
|
|
11382
|
+
/** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
|
|
11383
|
+
* accent under a thin dashed outline. */
|
|
11384
|
+
paintMarquee(ctx, rect) {
|
|
11385
|
+
ctx.save();
|
|
11386
|
+
ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
|
|
11387
|
+
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
11388
|
+
ctx.strokeStyle = HANDLE_BORDER;
|
|
11389
|
+
ctx.lineWidth = 1;
|
|
11390
|
+
ctx.setLineDash([4, 3]);
|
|
11391
|
+
ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
|
|
11392
|
+
ctx.restore();
|
|
11393
|
+
}
|
|
11371
11394
|
/** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
|
|
11372
11395
|
paintSnapRing(ctx, x, y, theme) {
|
|
11373
11396
|
ctx.setLineDash([]);
|
|
@@ -12836,6 +12859,15 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
|
|
|
12836
12859
|
}
|
|
12837
12860
|
return null;
|
|
12838
12861
|
}
|
|
12862
|
+
function deletableSelection(selected, drawings) {
|
|
12863
|
+
const ids = [...selected];
|
|
12864
|
+
if (ids.length < 2) return ids;
|
|
12865
|
+
return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
|
|
12866
|
+
}
|
|
12867
|
+
function deleteTargets(hit, selected, drawings, withSelection) {
|
|
12868
|
+
if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
|
|
12869
|
+
return hit.locked ? [] : [hit.id];
|
|
12870
|
+
}
|
|
12839
12871
|
|
|
12840
12872
|
// src/renderers/native/drawings/DrawingInteraction.ts
|
|
12841
12873
|
var DRAG_SLOP2 = 3;
|
|
@@ -12939,10 +12971,32 @@ var DrawingInteraction = class {
|
|
|
12939
12971
|
activeDragId() {
|
|
12940
12972
|
return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
|
|
12941
12973
|
}
|
|
12974
|
+
/** The drawing under a press that has not been released yet (drag or click undecided), or null. */
|
|
12975
|
+
pressedId() {
|
|
12976
|
+
return this.state.kind === "pressed" ? this.state.id : null;
|
|
12977
|
+
}
|
|
12978
|
+
/** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
|
|
12979
|
+
dragClones() {
|
|
12980
|
+
return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
|
|
12981
|
+
}
|
|
12982
|
+
/** The marquee box being swept, normalized (or null when none is in progress). */
|
|
12983
|
+
marqueeRect() {
|
|
12984
|
+
if (this.state.kind !== "marquee") return null;
|
|
12985
|
+
const { x0, y0, x1, y1 } = this.state;
|
|
12986
|
+
return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
|
|
12987
|
+
}
|
|
12988
|
+
/** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
|
|
12989
|
+
* armed or a gesture is in flight (that path owns the press). */
|
|
12990
|
+
beginMarquee(x, y) {
|
|
12991
|
+
if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
|
|
12992
|
+
this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
|
|
12993
|
+
return true;
|
|
12994
|
+
}
|
|
12942
12995
|
/** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
|
|
12943
12996
|
* `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
|
|
12944
|
-
* while placing a line tool it locks the segment angle to 45° steps.
|
|
12945
|
-
|
|
12997
|
+
* while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
|
|
12998
|
+
* drawing also toggles on a click, but a body drag then moves a COPY. */
|
|
12999
|
+
down(x, y, mode = "off", shift2 = false, mod = false) {
|
|
12946
13000
|
const proj = this.deps.projector();
|
|
12947
13001
|
if (this.state.kind === "placing") {
|
|
12948
13002
|
if (this.state.draft.placementMode() !== "click") return;
|
|
@@ -12970,7 +13024,50 @@ var DrawingInteraction = class {
|
|
|
12970
13024
|
return;
|
|
12971
13025
|
}
|
|
12972
13026
|
const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
|
|
12973
|
-
|
|
13027
|
+
const riders = [];
|
|
13028
|
+
if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
|
|
13029
|
+
for (const id of this.deps.selectedIds()) {
|
|
13030
|
+
const d = id === hit.id ? null : this.byId(id);
|
|
13031
|
+
if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
|
|
13032
|
+
}
|
|
13033
|
+
}
|
|
13034
|
+
this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
|
|
13035
|
+
}
|
|
13036
|
+
}
|
|
13037
|
+
/** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
|
|
13038
|
+
* pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
|
|
13039
|
+
bodyDelta(paneId, x, y) {
|
|
13040
|
+
if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
|
|
13041
|
+
const proj = this.deps.projector();
|
|
13042
|
+
const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
|
|
13043
|
+
const to = proj.pxToPoint(x, y, paneId);
|
|
13044
|
+
return { dt: to.time - from.time, dp: to.price - from.price };
|
|
13045
|
+
}
|
|
13046
|
+
/** The pressed drawing and its riders, each with its anchors at the press. */
|
|
13047
|
+
dragSources(d) {
|
|
13048
|
+
if (this.state.kind !== "pressed") return [];
|
|
13049
|
+
const out = [{ d, orig: this.state.orig }];
|
|
13050
|
+
for (const r of this.state.riders) {
|
|
13051
|
+
const rd = this.byId(r.id);
|
|
13052
|
+
if (rd) out.push({ d: rd, orig: r.orig });
|
|
13053
|
+
}
|
|
13054
|
+
return out;
|
|
13055
|
+
}
|
|
13056
|
+
/** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
|
|
13057
|
+
* cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
|
|
13058
|
+
* sources that have not moved yet, so their own anchors are the origin to translate from. */
|
|
13059
|
+
dragBody(d, x, y) {
|
|
13060
|
+
if (this.state.kind !== "pressed") return;
|
|
13061
|
+
this.snapAt = null;
|
|
13062
|
+
if (this.state.mod && !this.state.clones) {
|
|
13063
|
+
this.state.clones = this.dragSources(d).flatMap((s) => {
|
|
13064
|
+
const c = deserializeDrawing(s.d.serialize());
|
|
13065
|
+
return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
|
|
13066
|
+
});
|
|
13067
|
+
}
|
|
13068
|
+
for (const t of this.state.clones ?? this.dragSources(d)) {
|
|
13069
|
+
const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
|
|
13070
|
+
t.d.anchors = t.d.translateBody(dt, dp, t.orig);
|
|
12974
13071
|
}
|
|
12975
13072
|
}
|
|
12976
13073
|
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
@@ -12994,12 +13091,20 @@ var DrawingInteraction = class {
|
|
|
12994
13091
|
this.deps.changed();
|
|
12995
13092
|
return;
|
|
12996
13093
|
}
|
|
13094
|
+
if (this.state.kind === "marquee") {
|
|
13095
|
+
this.state.x1 = x;
|
|
13096
|
+
this.state.y1 = y;
|
|
13097
|
+
this.deps.changed();
|
|
13098
|
+
return;
|
|
13099
|
+
}
|
|
12997
13100
|
if (this.state.kind !== "pressed") return;
|
|
12998
13101
|
const d = this.byId(this.state.id);
|
|
12999
13102
|
if (!d || d.locked) return;
|
|
13000
13103
|
if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP2 && Math.abs(y - this.state.py) <= DRAG_SLOP2) return;
|
|
13001
13104
|
this.state.moved = true;
|
|
13002
|
-
if (this.state.handle
|
|
13105
|
+
if (this.state.handle < 0) {
|
|
13106
|
+
this.dragBody(d, x, y);
|
|
13107
|
+
} else {
|
|
13003
13108
|
const a = d.anchors[this.state.handle];
|
|
13004
13109
|
if (a) {
|
|
13005
13110
|
const pivot = shift2 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
|
|
@@ -13007,12 +13112,6 @@ var DrawingInteraction = class {
|
|
|
13007
13112
|
applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
|
|
13008
13113
|
}
|
|
13009
13114
|
d.constrainHandleDrag(this.state.handle);
|
|
13010
|
-
} else {
|
|
13011
|
-
const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
|
|
13012
|
-
this.snapAt = null;
|
|
13013
|
-
const dt = pt.time - this.state.grab.time;
|
|
13014
|
-
const dp = pt.price - this.state.grab.price;
|
|
13015
|
-
d.anchors = d.translateBody(dt, dp, this.state.orig);
|
|
13016
13115
|
}
|
|
13017
13116
|
this.deps.changed();
|
|
13018
13117
|
}
|
|
@@ -13032,16 +13131,46 @@ var DrawingInteraction = class {
|
|
|
13032
13131
|
}
|
|
13033
13132
|
return;
|
|
13034
13133
|
}
|
|
13134
|
+
if (this.state.kind === "marquee") {
|
|
13135
|
+
const rect = this.marqueeRect();
|
|
13136
|
+
this.state = { kind: "idle" };
|
|
13137
|
+
if (rect.w > DRAG_SLOP2 || rect.h > DRAG_SLOP2) this.selectWithin(rect);
|
|
13138
|
+
this.deps.changed();
|
|
13139
|
+
return;
|
|
13140
|
+
}
|
|
13035
13141
|
if (this.state.kind !== "pressed") return;
|
|
13036
|
-
const { id, moved } = this.state;
|
|
13142
|
+
const { id, moved, riders, mod, clones } = this.state;
|
|
13037
13143
|
this.state = { kind: "idle" };
|
|
13038
13144
|
const d = this.byId(id);
|
|
13039
|
-
if (moved) {
|
|
13040
|
-
if (
|
|
13041
|
-
this.deps.
|
|
13042
|
-
|
|
13043
|
-
|
|
13145
|
+
if (!moved) {
|
|
13146
|
+
if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
|
|
13147
|
+
else this.deps.openSettings(id, x, y);
|
|
13148
|
+
return;
|
|
13149
|
+
}
|
|
13150
|
+
if (clones) {
|
|
13151
|
+
this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
|
|
13152
|
+
} else if (d) {
|
|
13153
|
+
const docs = [d.serialize()];
|
|
13154
|
+
for (const r of riders) {
|
|
13155
|
+
const rd = this.byId(r.id);
|
|
13156
|
+
if (rd) docs.push(rd.serialize());
|
|
13157
|
+
}
|
|
13158
|
+
if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
|
|
13159
|
+
else this.deps.emit({ kind: "edit", doc: docs[0] });
|
|
13160
|
+
}
|
|
13161
|
+
this.deps.changed();
|
|
13162
|
+
}
|
|
13163
|
+
/** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
|
|
13164
|
+
* so successive sweeps accumulate). No change when the box touches nothing new. */
|
|
13165
|
+
selectWithin(rect) {
|
|
13166
|
+
const proj = this.deps.projector();
|
|
13167
|
+
const ids = [...this.deps.selectedIds()];
|
|
13168
|
+
for (const d of this.deps.drawings()) {
|
|
13169
|
+
if (!d.visible || ids.includes(d.id)) continue;
|
|
13170
|
+
const b = d.bounds(proj);
|
|
13171
|
+
if (b && rectsIntersect(rect, b)) ids.push(d.id);
|
|
13044
13172
|
}
|
|
13173
|
+
if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
|
|
13045
13174
|
}
|
|
13046
13175
|
/** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
|
|
13047
13176
|
cancel() {
|
|
@@ -13054,8 +13183,15 @@ var DrawingInteraction = class {
|
|
|
13054
13183
|
return true;
|
|
13055
13184
|
}
|
|
13056
13185
|
if (this.state.kind === "pressed") {
|
|
13057
|
-
|
|
13058
|
-
|
|
13186
|
+
if (this.state.moved && !this.state.clones) {
|
|
13187
|
+
const d = this.byId(this.state.id);
|
|
13188
|
+
if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
|
|
13189
|
+
}
|
|
13190
|
+
this.state = { kind: "idle" };
|
|
13191
|
+
this.deps.changed();
|
|
13192
|
+
return true;
|
|
13193
|
+
}
|
|
13194
|
+
if (this.state.kind === "marquee") {
|
|
13059
13195
|
this.state = { kind: "idle" };
|
|
13060
13196
|
this.deps.changed();
|
|
13061
13197
|
return true;
|
|
@@ -13170,6 +13306,9 @@ var DrawingInteraction = class {
|
|
|
13170
13306
|
function cloneAnchors(d) {
|
|
13171
13307
|
return d.anchors.map((a) => ({ time: a.time, price: a.price }));
|
|
13172
13308
|
}
|
|
13309
|
+
function rectsIntersect(a, b) {
|
|
13310
|
+
return a.x <= b.x + b.w && b.x <= a.x + a.w && a.y <= b.y + b.h && b.y <= a.y + a.h;
|
|
13311
|
+
}
|
|
13173
13312
|
function applyFree(anchor, pt, free) {
|
|
13174
13313
|
if (free === "both" || free === "x") anchor.time = pt.time;
|
|
13175
13314
|
if (free === "both" || free === "y") anchor.price = pt.price;
|
|
@@ -13555,10 +13694,27 @@ function btn(label, primary, onClick) {
|
|
|
13555
13694
|
}
|
|
13556
13695
|
|
|
13557
13696
|
// src/renderers/native/drawings/DrawingSettingsPopup.ts
|
|
13697
|
+
var MIXED = /* @__PURE__ */ Symbol("mixed");
|
|
13698
|
+
function sharedPaths(drawings) {
|
|
13699
|
+
const [first, ...rest] = drawings;
|
|
13700
|
+
const paths = new Set(first?.schema().fields.map((f) => f.path) ?? []);
|
|
13701
|
+
for (const d of rest) {
|
|
13702
|
+
const own = new Set(d.schema().fields.map((f) => f.path));
|
|
13703
|
+
for (const p of paths) if (!own.has(p)) paths.delete(p);
|
|
13704
|
+
}
|
|
13705
|
+
return paths;
|
|
13706
|
+
}
|
|
13707
|
+
function commonValue(drawings, read) {
|
|
13708
|
+
const first = read(drawings[0]);
|
|
13709
|
+
return drawings.every((d) => read(d) === first) ? first : MIXED;
|
|
13710
|
+
}
|
|
13711
|
+
function distinctValues(drawings, read) {
|
|
13712
|
+
return [...new Set(drawings.map(read))];
|
|
13713
|
+
}
|
|
13558
13714
|
var BTN = 30;
|
|
13559
13715
|
var ICON = 21;
|
|
13560
13716
|
var STYLE_ID4 = "vela-drawing-popup-styles";
|
|
13561
|
-
var STYLE_REV = "
|
|
13717
|
+
var STYLE_REV = "4";
|
|
13562
13718
|
function ensureStyles4() {
|
|
13563
13719
|
if (typeof document === "undefined") return;
|
|
13564
13720
|
const existing = document.getElementById(STYLE_ID4);
|
|
@@ -13570,6 +13726,8 @@ function ensureStyles4() {
|
|
|
13570
13726
|
.vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
13571
13727
|
.vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
13572
13728
|
.vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
13729
|
+
/* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
|
|
13730
|
+
.vela-dpop-btn[data-active='mixed']{background:var(--vela-hover-strong);color:var(--vela-fg-bright);outline:1px dashed var(--vela-fg-muted);outline-offset:-2px;}
|
|
13573
13731
|
.vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
13574
13732
|
.vela-dpop-item:hover{background:var(--vela-hover-strong);}
|
|
13575
13733
|
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
@@ -13601,7 +13759,7 @@ var DrawingSettingsPopup = class {
|
|
|
13601
13759
|
if (this.isOwnChrome(node)) return;
|
|
13602
13760
|
const cb = this.onClose;
|
|
13603
13761
|
this.close();
|
|
13604
|
-
cb?.();
|
|
13762
|
+
cb?.(e);
|
|
13605
13763
|
};
|
|
13606
13764
|
this.theme = theme;
|
|
13607
13765
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
@@ -13626,16 +13784,27 @@ var DrawingSettingsPopup = class {
|
|
|
13626
13784
|
contains(node) {
|
|
13627
13785
|
return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
|
|
13628
13786
|
}
|
|
13629
|
-
/** Open the quick toolbar for `
|
|
13630
|
-
* `
|
|
13631
|
-
|
|
13787
|
+
/** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
|
|
13788
|
+
* their `anchor` box. With several drawings the bar shows only the controls they ALL support,
|
|
13789
|
+
* a control whose values differ reads as mixed, and every edit applies to all of them —
|
|
13790
|
+
* per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
|
|
13791
|
+
* `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
|
|
13792
|
+
* programmatic close), with the dismissing press when there is one, so the caller can tell a
|
|
13793
|
+
* modifier press (multi-select) from a plain one. */
|
|
13794
|
+
open(drawings, anchor, actions, onClose) {
|
|
13632
13795
|
this.close();
|
|
13796
|
+
const drawing = drawings[0];
|
|
13797
|
+
if (!drawing) return;
|
|
13633
13798
|
ensureStyles4();
|
|
13634
13799
|
this.onClose = onClose ?? null;
|
|
13635
13800
|
const t = this.theme;
|
|
13801
|
+
const multi = drawings.length > 1;
|
|
13636
13802
|
const schema = drawing.schema();
|
|
13637
|
-
const paths =
|
|
13638
|
-
const
|
|
13803
|
+
const paths = sharedPaths(drawings);
|
|
13804
|
+
const common = (read) => commonValue(drawings, read);
|
|
13805
|
+
const every = (pred) => drawings.every(pred);
|
|
13806
|
+
const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
|
|
13807
|
+
const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
|
|
13639
13808
|
const el = document.createElement("div");
|
|
13640
13809
|
el.className = "vela-dpop";
|
|
13641
13810
|
el.style.cssText = `position:absolute;z-index:22;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);color:${t.textColor};font:var(--vela-font-size-md) ${t.fontFamily};display:flex;flex-direction:column;pointer-events:auto;overflow:hidden;`;
|
|
@@ -13649,75 +13818,84 @@ var DrawingSettingsPopup = class {
|
|
|
13649
13818
|
bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
|
|
13650
13819
|
bar.appendChild(this.dragHandle());
|
|
13651
13820
|
if (paths.has("glyph")) {
|
|
13652
|
-
const cur =
|
|
13821
|
+
const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
|
|
13653
13822
|
bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
|
|
13654
13823
|
}
|
|
13655
13824
|
if (paths.has("size")) {
|
|
13656
|
-
const sz =
|
|
13825
|
+
const sz = common((d) => d.size ?? "normal");
|
|
13657
13826
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
13658
13827
|
}
|
|
13659
|
-
if (paths.has("magnifier.timeframe") &&
|
|
13828
|
+
if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
|
|
13829
|
+
const mag = (d) => d;
|
|
13660
13830
|
const options = this.lowerTimeframeOptions();
|
|
13661
13831
|
if (options.length > 0) {
|
|
13662
13832
|
bar.appendChild(
|
|
13663
|
-
this.dropdown("Lower timeframe", options.map((o) => o.value),
|
|
13833
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
13664
13834
|
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
13665
13835
|
labelInTrigger: true
|
|
13666
13836
|
})
|
|
13667
13837
|
);
|
|
13668
13838
|
}
|
|
13669
|
-
bar.appendChild(
|
|
13670
|
-
bar.appendChild(
|
|
13839
|
+
bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
|
|
13840
|
+
bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
|
|
13671
13841
|
}
|
|
13672
|
-
if (paths.has("style.lineColor")) bar.appendChild(
|
|
13842
|
+
if (paths.has("style.lineColor")) bar.appendChild(swatch("Line color", BRUSH_ICON, (d) => d.style.lineColor || (d instanceof Magnifier ? contrastColor(this.theme.background) : DEFAULT_DRAWING_COLOR), "style.lineColor"));
|
|
13673
13843
|
if (paths.has("style.lineWidth")) {
|
|
13674
|
-
const
|
|
13675
|
-
|
|
13676
|
-
|
|
13844
|
+
const widthField = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
|
|
13845
|
+
const wf = widthField(drawing);
|
|
13846
|
+
const width = common((d) => d.style.lineWidth);
|
|
13847
|
+
const markers = every((d) => {
|
|
13848
|
+
const f = widthField(d);
|
|
13849
|
+
return f?.kind === "number" && (f.min ?? 1) > 1;
|
|
13850
|
+
});
|
|
13851
|
+
if (markers && wf?.kind === "number") {
|
|
13852
|
+
bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
|
|
13677
13853
|
} else {
|
|
13678
|
-
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4],
|
|
13854
|
+
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4], width, (w) => lineIcon(w, "solid"), (v) => actions.patch({ "style.lineWidth": v }), { label: (v) => `${v}px`, labelInTrigger: true }));
|
|
13679
13855
|
}
|
|
13680
13856
|
}
|
|
13681
|
-
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value),
|
|
13682
|
-
if (paths.has("style.fillColor")) bar.appendChild(
|
|
13857
|
+
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value), common((d) => d.style.lineStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "style.lineStyle": v }), { label: styleLabel2 }));
|
|
13858
|
+
if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
|
|
13683
13859
|
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
13684
|
-
const isPosition = paths.has("riskPercent") &&
|
|
13860
|
+
const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
|
|
13685
13861
|
if (isPosition) {
|
|
13686
|
-
const pos =
|
|
13687
|
-
bar.appendChild(
|
|
13688
|
-
bar.appendChild(
|
|
13862
|
+
const pos = (d) => d;
|
|
13863
|
+
bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
|
|
13864
|
+
bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
|
|
13689
13865
|
}
|
|
13690
|
-
const
|
|
13691
|
-
if (paths.has("reg.midColor") &&
|
|
13866
|
+
const regOf = (d) => d.reg;
|
|
13867
|
+
if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
|
|
13868
|
+
const reg = (d) => regOf(d);
|
|
13692
13869
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
13693
|
-
bar.appendChild(
|
|
13694
|
-
bar.appendChild(this.dropdown("Midline style", styles, reg.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
13695
|
-
bar.appendChild(
|
|
13696
|
-
bar.appendChild(this.dropdown("Upper line style", styles, reg.upperStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
|
|
13697
|
-
bar.appendChild(
|
|
13698
|
-
bar.appendChild(this.dropdown("Lower line style", styles, reg.lowerStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
|
|
13699
|
-
bar.appendChild(
|
|
13700
|
-
bar.appendChild(
|
|
13701
|
-
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, reg.showR2, (v) => actions.patch({ "reg.showR2": v })));
|
|
13702
|
-
}
|
|
13703
|
-
const
|
|
13704
|
-
if (paths.has("vwap.midColor") &&
|
|
13870
|
+
bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
|
|
13871
|
+
bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
13872
|
+
bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
|
|
13873
|
+
bar.appendChild(this.dropdown("Upper line style", styles, common((d) => reg(d).upperStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
|
|
13874
|
+
bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
|
|
13875
|
+
bar.appendChild(this.dropdown("Lower line style", styles, common((d) => reg(d).lowerStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
|
|
13876
|
+
bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
|
|
13877
|
+
bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
|
|
13878
|
+
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
|
|
13879
|
+
}
|
|
13880
|
+
const vwapOf = (d) => d.vwap;
|
|
13881
|
+
if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
|
|
13882
|
+
const vwap2 = (d) => vwapOf(d);
|
|
13705
13883
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
13706
13884
|
const MULTS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
|
|
13707
|
-
bar.appendChild(
|
|
13708
|
-
bar.appendChild(this.dropdown("VWAP style", styles, vwap2.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
|
|
13885
|
+
bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
|
|
13886
|
+
bar.appendChild(this.dropdown("VWAP style", styles, common((d) => vwap2(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
|
|
13709
13887
|
bar.appendChild(
|
|
13710
|
-
this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
13888
|
+
this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
13711
13889
|
label: (v) => `${v}\u03C3`,
|
|
13712
13890
|
labelInTrigger: true
|
|
13713
13891
|
})
|
|
13714
13892
|
);
|
|
13715
|
-
bar.appendChild(
|
|
13716
|
-
bar.appendChild(
|
|
13717
|
-
bar.appendChild(
|
|
13893
|
+
bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
|
|
13894
|
+
bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
|
|
13895
|
+
bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
|
|
13718
13896
|
}
|
|
13719
13897
|
if (paths.has("maxCurvature")) {
|
|
13720
|
-
const cur =
|
|
13898
|
+
const cur = common((d) => d.maxCurvature ?? 24);
|
|
13721
13899
|
bar.appendChild(
|
|
13722
13900
|
this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON, (v) => actions.patch({ maxCurvature: v }), {
|
|
13723
13901
|
label: (v) => `n=${v}`,
|
|
@@ -13726,7 +13904,7 @@ var DrawingSettingsPopup = class {
|
|
|
13726
13904
|
);
|
|
13727
13905
|
}
|
|
13728
13906
|
if (paths.has("mach")) {
|
|
13729
|
-
const cur =
|
|
13907
|
+
const cur = common((d) => d.mach ?? 2);
|
|
13730
13908
|
bar.appendChild(
|
|
13731
13909
|
this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON, (v) => actions.patch({ mach: v }), {
|
|
13732
13910
|
label: (v) => `M=${v}`,
|
|
@@ -13735,7 +13913,7 @@ var DrawingSettingsPopup = class {
|
|
|
13735
13913
|
);
|
|
13736
13914
|
}
|
|
13737
13915
|
if (paths.has("waveCount")) {
|
|
13738
|
-
const cur =
|
|
13916
|
+
const cur = common((d) => d.waveCount ?? 6);
|
|
13739
13917
|
bar.appendChild(
|
|
13740
13918
|
this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON, (v) => actions.patch({ waveCount: v }), {
|
|
13741
13919
|
label: (v) => `${v}`,
|
|
@@ -13743,26 +13921,27 @@ var DrawingSettingsPopup = class {
|
|
|
13743
13921
|
})
|
|
13744
13922
|
);
|
|
13745
13923
|
}
|
|
13746
|
-
const
|
|
13747
|
-
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON,
|
|
13748
|
-
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON,
|
|
13749
|
-
if (paths.has("text.color") && textOnBar) bar.appendChild(
|
|
13750
|
-
if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value),
|
|
13751
|
-
|
|
13752
|
-
if (paths.has("text.
|
|
13753
|
-
if (paths.has("text.
|
|
13924
|
+
const flags = (d) => d;
|
|
13925
|
+
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
|
|
13926
|
+
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
|
|
13927
|
+
if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
|
|
13928
|
+
if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), common((d) => d.text?.size ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ "text.size": v }), { label: sizeLabel }));
|
|
13929
|
+
const textField = !multi && paths.has("text.value");
|
|
13930
|
+
if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
|
|
13931
|
+
if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
|
|
13932
|
+
if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
|
|
13754
13933
|
const editableLevels = drawing.editableLevels();
|
|
13755
|
-
if (editableLevels && !(
|
|
13756
|
-
const fib =
|
|
13934
|
+
if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
|
|
13935
|
+
const fib = (d) => d;
|
|
13757
13936
|
const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
|
|
13758
|
-
bar.appendChild(this.dropdown("Numbers size", sizes, fib.numbersSize ?? "small", (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
13759
|
-
bar.appendChild(this.dropdown("Label size", sizes, fib.labelsSize ?? "normal", (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
13937
|
+
bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
13938
|
+
bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
13760
13939
|
}
|
|
13761
13940
|
bar.appendChild(this.divider());
|
|
13762
|
-
if (isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
13763
|
-
if (isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
13764
|
-
if (editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
13765
|
-
bar.appendChild(this.toggle("Lock", LOCK_ICON,
|
|
13941
|
+
if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
13942
|
+
if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
13943
|
+
if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
13944
|
+
bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
|
|
13766
13945
|
const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
|
|
13767
13946
|
del.style.color = "var(--vela-danger)";
|
|
13768
13947
|
bar.appendChild(del);
|
|
@@ -13948,7 +14127,7 @@ var DrawingSettingsPopup = class {
|
|
|
13948
14127
|
});
|
|
13949
14128
|
return b;
|
|
13950
14129
|
}
|
|
13951
|
-
/** The trailing kebab overflow — z-order actions
|
|
14130
|
+
/** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
|
|
13952
14131
|
kebabButton(actions) {
|
|
13953
14132
|
const b = this.base("More");
|
|
13954
14133
|
b.innerHTML = sized(KEBAB_ICON);
|
|
@@ -13960,6 +14139,7 @@ var DrawingSettingsPopup = class {
|
|
|
13960
14139
|
return;
|
|
13961
14140
|
}
|
|
13962
14141
|
this.openActionMenu(b, [
|
|
14142
|
+
{ icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
|
|
13963
14143
|
{ icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
|
|
13964
14144
|
{ icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
|
|
13965
14145
|
{ icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
|
|
@@ -14116,26 +14296,30 @@ var DrawingSettingsPopup = class {
|
|
|
14116
14296
|
this.tipEl?.remove();
|
|
14117
14297
|
this.tipEl = null;
|
|
14118
14298
|
}
|
|
14119
|
-
|
|
14299
|
+
/** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
|
|
14300
|
+
* the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
|
|
14301
|
+
* toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
|
|
14302
|
+
toggle(tip, icon2, active, onChange, off) {
|
|
14120
14303
|
const b = this.base(tip);
|
|
14121
|
-
b.innerHTML = sized(icon2);
|
|
14122
14304
|
const set = (on2) => {
|
|
14123
|
-
b.dataset.active = on2 ? "1" : "0";
|
|
14305
|
+
b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
|
|
14306
|
+
b.innerHTML = sized(off && on2 === false ? off : icon2);
|
|
14124
14307
|
};
|
|
14125
14308
|
set(active);
|
|
14126
14309
|
let on = active;
|
|
14127
14310
|
b.addEventListener("click", () => {
|
|
14128
|
-
on = !on;
|
|
14311
|
+
on = on === MIXED ? true : !on;
|
|
14129
14312
|
set(on);
|
|
14130
14313
|
onChange(on);
|
|
14131
14314
|
});
|
|
14132
14315
|
return b;
|
|
14133
14316
|
}
|
|
14134
14317
|
/** An inline numeric width field for tools whose stroke range outgrows the 1–4px
|
|
14135
|
-
* ladder — the value is clamped to the schema's declared min/max on commit.
|
|
14318
|
+
* ladder — the value is clamped to the schema's declared min/max on commit. A mixed
|
|
14319
|
+
* value shows an empty field with a dash until a number is typed for all. */
|
|
14136
14320
|
widthInput(tip, value, min, max, step, onChange) {
|
|
14137
14321
|
const ni = new NumberInput({
|
|
14138
|
-
value,
|
|
14322
|
+
value: value === MIXED ? min : value,
|
|
14139
14323
|
min,
|
|
14140
14324
|
max,
|
|
14141
14325
|
step,
|
|
@@ -14147,13 +14331,18 @@ var DrawingSettingsPopup = class {
|
|
|
14147
14331
|
title: tip,
|
|
14148
14332
|
onChange
|
|
14149
14333
|
});
|
|
14334
|
+
if (value === MIXED) {
|
|
14335
|
+
ni.input.value = "";
|
|
14336
|
+
ni.input.placeholder = "\u2014";
|
|
14337
|
+
}
|
|
14150
14338
|
ni.el.dataset.tip = tip;
|
|
14151
14339
|
trapChartKeys(ni.el);
|
|
14152
14340
|
return ni.el;
|
|
14153
14341
|
}
|
|
14154
14342
|
/** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
|
|
14155
14343
|
* text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
|
|
14156
|
-
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
14344
|
+
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
14345
|
+
* A mixed value shows a dash in the trigger and highlights no row. */
|
|
14157
14346
|
dropdown(tip, values, current, render, onChange, opts = {}) {
|
|
14158
14347
|
const b = this.base(tip);
|
|
14159
14348
|
b.style.width = "auto";
|
|
@@ -14163,14 +14352,20 @@ var DrawingSettingsPopup = class {
|
|
|
14163
14352
|
let cur = current;
|
|
14164
14353
|
const paint = (v) => {
|
|
14165
14354
|
b.replaceChildren();
|
|
14166
|
-
|
|
14355
|
+
if (v === MIXED) {
|
|
14356
|
+
const dash = document.createElement("span");
|
|
14357
|
+
dash.textContent = "\u2014";
|
|
14358
|
+
dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
|
|
14359
|
+
b.appendChild(dash);
|
|
14360
|
+
}
|
|
14361
|
+
const glyph = v === MIXED ? "" : render(v);
|
|
14167
14362
|
if (glyph) {
|
|
14168
14363
|
const ic = document.createElement("span");
|
|
14169
14364
|
ic.style.cssText = "display:flex;";
|
|
14170
14365
|
ic.innerHTML = sized(glyph);
|
|
14171
14366
|
b.appendChild(ic);
|
|
14172
14367
|
}
|
|
14173
|
-
if (opts.label && opts.labelInTrigger) {
|
|
14368
|
+
if (opts.label && opts.labelInTrigger && v !== MIXED) {
|
|
14174
14369
|
const tx = document.createElement("span");
|
|
14175
14370
|
tx.textContent = opts.label(v);
|
|
14176
14371
|
tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
|
|
@@ -14240,8 +14435,10 @@ var DrawingSettingsPopup = class {
|
|
|
14240
14435
|
}
|
|
14241
14436
|
/** A color control: an `icon` over a thin **colored underline** showing the current
|
|
14242
14437
|
* value (the common idiom), with an invisible native picker overlaid to edit it.
|
|
14243
|
-
* `iconSize` shrinks it for the floating text controls.
|
|
14244
|
-
|
|
14438
|
+
* `iconSize` shrinks it for the floating text controls. A mixed color paints the
|
|
14439
|
+
* underline striped with the colors `used` across the selection, and the picker then
|
|
14440
|
+
* leads with those colors so unifying onto one of them is a single click. */
|
|
14441
|
+
colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
|
|
14245
14442
|
const b = document.createElement("button");
|
|
14246
14443
|
b.type = "button";
|
|
14247
14444
|
b.dataset.tip = tip;
|
|
@@ -14251,13 +14448,14 @@ var DrawingSettingsPopup = class {
|
|
|
14251
14448
|
ic.style.cssText = "display:flex;";
|
|
14252
14449
|
ic.innerHTML = sized(icon2, iconSize);
|
|
14253
14450
|
const bar = document.createElement("span");
|
|
14254
|
-
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color};`;
|
|
14451
|
+
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
|
|
14255
14452
|
b.append(ic, bar);
|
|
14256
14453
|
let cur = color;
|
|
14257
14454
|
b.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
14258
14455
|
b.addEventListener("click", (e) => {
|
|
14259
14456
|
e.stopPropagation();
|
|
14260
|
-
|
|
14457
|
+
const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
|
|
14458
|
+
this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
|
|
14261
14459
|
cur = v;
|
|
14262
14460
|
bar.style.background = v;
|
|
14263
14461
|
onChange(v);
|
|
@@ -14266,20 +14464,42 @@ var DrawingSettingsPopup = class {
|
|
|
14266
14464
|
return b;
|
|
14267
14465
|
}
|
|
14268
14466
|
/** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
|
|
14269
|
-
toggleColorPopover(anchor, color, onChange) {
|
|
14467
|
+
toggleColorPopover(anchor, color, used, onChange) {
|
|
14270
14468
|
if (this.colorOwner === anchor) {
|
|
14271
14469
|
this.closeColorPopover();
|
|
14272
14470
|
return;
|
|
14273
14471
|
}
|
|
14274
|
-
this.openColorPopover(anchor, color, onChange);
|
|
14472
|
+
this.openColorPopover(anchor, color, used, onChange);
|
|
14275
14473
|
}
|
|
14276
|
-
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
14277
|
-
|
|
14474
|
+
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
14475
|
+
* `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
|
|
14476
|
+
openColorPopover(anchor, color, used, onChange) {
|
|
14278
14477
|
const t = this.theme;
|
|
14279
14478
|
this.colorPop = this.hostFloat(anchor, {
|
|
14280
14479
|
zIndex: 25,
|
|
14281
14480
|
padding: "10px",
|
|
14282
|
-
fill: (el) => {
|
|
14481
|
+
fill: (el, pop) => {
|
|
14482
|
+
if (used.length > 1) {
|
|
14483
|
+
const row = document.createElement("div");
|
|
14484
|
+
row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
|
|
14485
|
+
const lbl = document.createElement("span");
|
|
14486
|
+
lbl.textContent = "In use";
|
|
14487
|
+
lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
|
|
14488
|
+
row.appendChild(lbl);
|
|
14489
|
+
for (const c of used) {
|
|
14490
|
+
const sw = document.createElement("button");
|
|
14491
|
+
sw.type = "button";
|
|
14492
|
+
sw.dataset.tip = c;
|
|
14493
|
+
sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
|
|
14494
|
+
sw.addEventListener("click", (e) => {
|
|
14495
|
+
e.stopPropagation();
|
|
14496
|
+
pop.hide();
|
|
14497
|
+
onChange(c);
|
|
14498
|
+
});
|
|
14499
|
+
row.appendChild(sw);
|
|
14500
|
+
}
|
|
14501
|
+
el.appendChild(row);
|
|
14502
|
+
}
|
|
14283
14503
|
el.appendChild(buildColorPicker(color, t, onChange));
|
|
14284
14504
|
}
|
|
14285
14505
|
});
|
|
@@ -14300,6 +14520,7 @@ var TYPE_ICON = icon("type");
|
|
|
14300
14520
|
var PRICE_DELTA_ICON = icon("price-delta");
|
|
14301
14521
|
var DATE_DELTA_ICON = icon("date-delta");
|
|
14302
14522
|
var LOCK_ICON = icon("lock");
|
|
14523
|
+
var UNLOCK_ICON = icon("unlock");
|
|
14303
14524
|
var FRONT_ICON = icon("bring-front");
|
|
14304
14525
|
var BACK_ICON = icon("send-back");
|
|
14305
14526
|
var TRASH_ICON = icon("trash");
|
|
@@ -14307,6 +14528,7 @@ var BOLD_ICON = icon("bold");
|
|
|
14307
14528
|
var ITALIC_ICON = icon("italic");
|
|
14308
14529
|
var GRIP_ICON = icon("grip");
|
|
14309
14530
|
var KEBAB_ICON = icon("kebab");
|
|
14531
|
+
var CLONE_ICON = icon("clone");
|
|
14310
14532
|
var RESET_ICON = icon("reset");
|
|
14311
14533
|
var GEAR_ICON = icon("gear");
|
|
14312
14534
|
var CHEVRON_ICON = icon("chevron-down");
|
|
@@ -14315,6 +14537,12 @@ var BANDS_ICON = icon("bands");
|
|
|
14315
14537
|
var DEDEKIND_ICON = icon("dedekind");
|
|
14316
14538
|
var SONIC_ICON = icon("sonic");
|
|
14317
14539
|
var SUPERSONIC_ICON = icon("supersonic");
|
|
14540
|
+
function stripes(colors) {
|
|
14541
|
+
const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
|
|
14542
|
+
const step = 100 / bands.length;
|
|
14543
|
+
const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
|
|
14544
|
+
return `linear-gradient(90deg,${stops})`;
|
|
14545
|
+
}
|
|
14318
14546
|
function lineIcon(width, style) {
|
|
14319
14547
|
const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
|
|
14320
14548
|
return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
|
|
@@ -14598,6 +14826,20 @@ var UserDrawingController = class {
|
|
|
14598
14826
|
// reported exactly when it actually changes).
|
|
14599
14827
|
this.textEditor = null;
|
|
14600
14828
|
this.painter = new DrawingPainter();
|
|
14829
|
+
/** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
|
|
14830
|
+
* shrinking selection does with it. */
|
|
14831
|
+
this.popupMulti = false;
|
|
14832
|
+
/** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
|
|
14833
|
+
* selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
|
|
14834
|
+
* selection or sweep a marquee onto it), or it landed on a drawing that is already selected
|
|
14835
|
+
* (the canvas handler ran first and is holding it for a drag or a click), so the drawings
|
|
14836
|
+
* whose bar just closed must stay selected. */
|
|
14837
|
+
this.onPopupDismissed = (e) => {
|
|
14838
|
+
if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
|
|
14839
|
+
const pressed = this.interaction.pressedId();
|
|
14840
|
+
if (pressed && this.selectedIds.has(pressed)) return;
|
|
14841
|
+
this.clearSelection();
|
|
14842
|
+
};
|
|
14601
14843
|
this.ctx = canvas.getContext("2d");
|
|
14602
14844
|
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
14603
14845
|
this.toolbar = new DrawingToolbar(
|
|
@@ -14774,6 +15016,12 @@ var UserDrawingController = class {
|
|
|
14774
15016
|
}
|
|
14775
15017
|
setSelection(ids) {
|
|
14776
15018
|
this.selectedIds = new Set(ids);
|
|
15019
|
+
if (ids.length >= 2) this.openSettingsForSelection(ids);
|
|
15020
|
+
else if (this.popupMulti) {
|
|
15021
|
+
this.popupMulti = false;
|
|
15022
|
+
this.popup.close();
|
|
15023
|
+
if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
|
|
15024
|
+
}
|
|
14777
15025
|
this.render();
|
|
14778
15026
|
}
|
|
14779
15027
|
/** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
|
|
@@ -14819,12 +15067,16 @@ var UserDrawingController = class {
|
|
|
14819
15067
|
});
|
|
14820
15068
|
}
|
|
14821
15069
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
14822
|
-
* Shared by the eraser (click + drag) and the middle-click shortcut
|
|
14823
|
-
|
|
15070
|
+
* Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
|
|
15071
|
+
* `withSelection` so a hit on a member of a multi-selection removes the selection's
|
|
15072
|
+
* unlocked members — whichever member was hit, locked or not. */
|
|
15073
|
+
deleteAt(x, y, withSelection = false) {
|
|
14824
15074
|
const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
|
|
14825
|
-
if (!hit
|
|
15075
|
+
if (!hit) return false;
|
|
15076
|
+
const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
|
|
15077
|
+
if (ids.length === 0) return false;
|
|
14826
15078
|
this.popup.close();
|
|
14827
|
-
this.emit({ kind: "delete", ids
|
|
15079
|
+
this.emit({ kind: "delete", ids });
|
|
14828
15080
|
return true;
|
|
14829
15081
|
}
|
|
14830
15082
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
@@ -14850,7 +15102,14 @@ var UserDrawingController = class {
|
|
|
14850
15102
|
this.render();
|
|
14851
15103
|
}
|
|
14852
15104
|
}
|
|
14853
|
-
|
|
15105
|
+
/** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
|
|
15106
|
+
* the selection). Returns false when a mode/tool is active (the normal press path owns it). */
|
|
15107
|
+
beginMarqueeAt(x, y) {
|
|
15108
|
+
if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
|
|
15109
|
+
this.render();
|
|
15110
|
+
return true;
|
|
15111
|
+
}
|
|
15112
|
+
pointerDown(x, y, snap = "off", shift2 = false, mod = false) {
|
|
14854
15113
|
if (this.eraserMode) {
|
|
14855
15114
|
this.erasing = true;
|
|
14856
15115
|
this.deleteAt(x, y);
|
|
@@ -14870,9 +15129,9 @@ var UserDrawingController = class {
|
|
|
14870
15129
|
return;
|
|
14871
15130
|
}
|
|
14872
15131
|
}
|
|
14873
|
-
this.interaction.down(x, y, snap, shift2);
|
|
15132
|
+
this.interaction.down(x, y, snap, shift2, mod);
|
|
14874
15133
|
}
|
|
14875
|
-
pointerMove(x, y, snap = "off", shift2 = false) {
|
|
15134
|
+
pointerMove(x, y, snap = "off", shift2 = false, mod = false) {
|
|
14876
15135
|
if (this.eraserMode) {
|
|
14877
15136
|
if (this.erasing) this.deleteAt(x, y);
|
|
14878
15137
|
return;
|
|
@@ -14884,12 +15143,14 @@ var UserDrawingController = class {
|
|
|
14884
15143
|
return;
|
|
14885
15144
|
}
|
|
14886
15145
|
this.interaction.move(x, y, snap, shift2);
|
|
14887
|
-
this.updateHover(x, y);
|
|
15146
|
+
this.updateHover(x, y, mod);
|
|
14888
15147
|
}
|
|
14889
|
-
/** Track which drawing is hovered so its handles appear on hover (and only then).
|
|
14890
|
-
|
|
15148
|
+
/** Track which drawing is hovered so its handles appear on hover (and only then). While
|
|
15149
|
+
* Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
|
|
15150
|
+
* selected, so a hovered candidate doesn't already read as selected. */
|
|
15151
|
+
updateHover(x, y, mod = false) {
|
|
14891
15152
|
let id = null;
|
|
14892
|
-
if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
15153
|
+
if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
14893
15154
|
id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
|
|
14894
15155
|
}
|
|
14895
15156
|
if (id !== this.hoveredId) {
|
|
@@ -14909,7 +15170,12 @@ var UserDrawingController = class {
|
|
|
14909
15170
|
this.render();
|
|
14910
15171
|
return;
|
|
14911
15172
|
}
|
|
15173
|
+
const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
|
|
14912
15174
|
this.interaction.up(x, y);
|
|
15175
|
+
if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
|
|
15176
|
+
if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
|
|
15177
|
+
else this.openSettingsById(dragged, x, y);
|
|
15178
|
+
}
|
|
14913
15179
|
}
|
|
14914
15180
|
/** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
|
|
14915
15181
|
toggleMeasure() {
|
|
@@ -15174,7 +15440,7 @@ var UserDrawingController = class {
|
|
|
15174
15440
|
this.emit({ kind: "duplicate", ids: this.selectionIds() });
|
|
15175
15441
|
break;
|
|
15176
15442
|
case "delete": {
|
|
15177
|
-
const ids = this.selectedIds.size ? this.
|
|
15443
|
+
const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
|
|
15178
15444
|
if (ids.length) {
|
|
15179
15445
|
this.popup.close();
|
|
15180
15446
|
this.emit({ kind: "delete", ids });
|
|
@@ -15190,6 +15456,9 @@ var UserDrawingController = class {
|
|
|
15190
15456
|
selectionIds() {
|
|
15191
15457
|
return [...this.selectedIds];
|
|
15192
15458
|
}
|
|
15459
|
+
deletableSelection() {
|
|
15460
|
+
return deletableSelection(this.selectedIds, this.drawings);
|
|
15461
|
+
}
|
|
15193
15462
|
/** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
|
|
15194
15463
|
nudgeSelection(dx, dy) {
|
|
15195
15464
|
const proj = this.deps.projector();
|
|
@@ -15298,6 +15567,8 @@ var UserDrawingController = class {
|
|
|
15298
15567
|
this.painter.seriesLook = this.deps.seriesLook();
|
|
15299
15568
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
15300
15569
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
15570
|
+
const clones = this.interaction.dragClones();
|
|
15571
|
+
if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
|
|
15301
15572
|
this.layoutTextEditor();
|
|
15302
15573
|
const ghost = this.interaction.ghost();
|
|
15303
15574
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
@@ -15313,6 +15584,8 @@ var UserDrawingController = class {
|
|
|
15313
15584
|
const my = m ? proj.yOf(m.point.price, m.paneId) : null;
|
|
15314
15585
|
if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
|
|
15315
15586
|
if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
|
|
15587
|
+
const marquee = this.interaction.marqueeRect();
|
|
15588
|
+
if (marquee) this.painter.paintMarquee(ctx, marquee);
|
|
15316
15589
|
}
|
|
15317
15590
|
destroy() {
|
|
15318
15591
|
this.closeTextEditor();
|
|
@@ -15330,15 +15603,80 @@ var UserDrawingController = class {
|
|
|
15330
15603
|
if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
|
|
15331
15604
|
this.render();
|
|
15332
15605
|
}
|
|
15606
|
+
/** Report the live drawings' current state as one edit (one undo step, however many). */
|
|
15607
|
+
emitEdits(drawings) {
|
|
15608
|
+
const docs = drawings.map((d) => d.serialize());
|
|
15609
|
+
if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
|
|
15610
|
+
else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
|
|
15611
|
+
}
|
|
15612
|
+
/** The one bar for a multi-selection: it shows the controls all of its drawings share and
|
|
15613
|
+
* every action applies to all of them. Floats clear of their combined bounds. */
|
|
15614
|
+
openSettingsForSelection(ids) {
|
|
15615
|
+
const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
|
|
15616
|
+
const drawings = live();
|
|
15617
|
+
if (drawings.length < 2) return;
|
|
15618
|
+
const proj = this.deps.projector();
|
|
15619
|
+
let anchor = null;
|
|
15620
|
+
for (const d of drawings) {
|
|
15621
|
+
const b = d.bounds(proj);
|
|
15622
|
+
if (!b) continue;
|
|
15623
|
+
if (!anchor) anchor = { ...b };
|
|
15624
|
+
else {
|
|
15625
|
+
const right = Math.max(anchor.x + anchor.w, b.x + b.w);
|
|
15626
|
+
const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
|
|
15627
|
+
anchor.x = Math.min(anchor.x, b.x);
|
|
15628
|
+
anchor.y = Math.min(anchor.y, b.y);
|
|
15629
|
+
anchor.w = right - anchor.x;
|
|
15630
|
+
anchor.h = bottom - anchor.y;
|
|
15631
|
+
}
|
|
15632
|
+
}
|
|
15633
|
+
this.popupMulti = true;
|
|
15634
|
+
this.emit({ kind: "settings", id: drawings[0].id });
|
|
15635
|
+
this.popup.open(drawings, anchor, {
|
|
15636
|
+
resolve: () => live()[0] ?? null,
|
|
15637
|
+
patch: (p) => {
|
|
15638
|
+
const ds = live();
|
|
15639
|
+
for (const d of ds) d.applySettings(p);
|
|
15640
|
+
this.render();
|
|
15641
|
+
this.emitEdits(ds);
|
|
15642
|
+
},
|
|
15643
|
+
setLocked: (v) => {
|
|
15644
|
+
const ds = live();
|
|
15645
|
+
for (const d of ds) d.locked = v;
|
|
15646
|
+
this.emitEdits(ds);
|
|
15647
|
+
},
|
|
15648
|
+
reorder: (to) => {
|
|
15649
|
+
for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
|
|
15650
|
+
},
|
|
15651
|
+
duplicate: () => {
|
|
15652
|
+
this.popup.close();
|
|
15653
|
+
this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
|
|
15654
|
+
},
|
|
15655
|
+
resetSettings: () => {
|
|
15656
|
+
const ds = live();
|
|
15657
|
+
for (const d of ds) resetDrawingSettings(d);
|
|
15658
|
+
this.render();
|
|
15659
|
+
this.emitEdits(ds);
|
|
15660
|
+
this.openSettingsForSelection(ids);
|
|
15661
|
+
},
|
|
15662
|
+
remove: () => {
|
|
15663
|
+
const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
|
|
15664
|
+
if (ids2.length === 0) return;
|
|
15665
|
+
this.popup.close();
|
|
15666
|
+
this.emit({ kind: "delete", ids: ids2 });
|
|
15667
|
+
}
|
|
15668
|
+
}, this.onPopupDismissed);
|
|
15669
|
+
}
|
|
15333
15670
|
/** A click on a drawing opens its settings toolbar (text labels edit text here too). */
|
|
15334
15671
|
openSettingsById(id, _x, _y) {
|
|
15335
15672
|
const drawing = this.drawings.find((d) => d.id === id);
|
|
15336
15673
|
if (!drawing) return;
|
|
15337
15674
|
const live = () => this.drawings.find((d) => d.id === id);
|
|
15675
|
+
this.popupMulti = false;
|
|
15338
15676
|
this.emit({ kind: "select", ids: [id] });
|
|
15339
15677
|
this.emit({ kind: "settings", id });
|
|
15340
15678
|
const anchor = drawing.bounds(this.deps.projector());
|
|
15341
|
-
this.popup.open(drawing, anchor, {
|
|
15679
|
+
this.popup.open([drawing], anchor, {
|
|
15342
15680
|
// Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
|
|
15343
15681
|
// position tool's price fields, where one edit can flip another level) resolves fresh.
|
|
15344
15682
|
resolve: () => live() ?? null,
|
|
@@ -15356,6 +15694,11 @@ var UserDrawingController = class {
|
|
|
15356
15694
|
this.emit({ kind: "edit", doc: d.serialize() });
|
|
15357
15695
|
},
|
|
15358
15696
|
reorder: (to) => this.emit({ kind: "reorder", id, to }),
|
|
15697
|
+
duplicate: () => {
|
|
15698
|
+
this.finishTextEditor(true);
|
|
15699
|
+
this.popup.close();
|
|
15700
|
+
this.emit({ kind: "duplicate", ids: [id] });
|
|
15701
|
+
},
|
|
15359
15702
|
resetSettings: () => {
|
|
15360
15703
|
const d = live();
|
|
15361
15704
|
if (!d) return;
|
|
@@ -15378,7 +15721,13 @@ var UserDrawingController = class {
|
|
|
15378
15721
|
this.popup.close();
|
|
15379
15722
|
this.emit({ kind: "delete", ids: [id] });
|
|
15380
15723
|
}
|
|
15381
|
-
},
|
|
15724
|
+
}, this.onPopupDismissed);
|
|
15725
|
+
}
|
|
15726
|
+
/** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
|
|
15727
|
+
* does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
|
|
15728
|
+
* selection whose bar was dismissed by a modifier press) — this covers those. */
|
|
15729
|
+
deselect() {
|
|
15730
|
+
this.clearSelection();
|
|
15382
15731
|
}
|
|
15383
15732
|
/** Report placement progress upstream (`draft` intent) so the drawings sync can
|
|
15384
15733
|
* mirror the ghost live on linked charts. Called from every render; only actual
|
|
@@ -15404,6 +15753,13 @@ var UserDrawingController = class {
|
|
|
15404
15753
|
}, 0);
|
|
15405
15754
|
return;
|
|
15406
15755
|
}
|
|
15756
|
+
if (i.kind === "clone") {
|
|
15757
|
+
const before = new Set(this.drawings.map((d) => d.id));
|
|
15758
|
+
this.intentCb?.(i);
|
|
15759
|
+
const fresh = this.drawings.filter((d) => !before.has(d.id));
|
|
15760
|
+
if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
|
|
15761
|
+
return;
|
|
15762
|
+
}
|
|
15407
15763
|
this.intentCb?.(i);
|
|
15408
15764
|
}
|
|
15409
15765
|
};
|
|
@@ -17689,7 +18045,10 @@ var NativeRenderer = class {
|
|
|
17689
18045
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
17690
18046
|
fling: (v) => this.fling(v),
|
|
17691
18047
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
17692
|
-
onClick: (x) =>
|
|
18048
|
+
onClick: (x) => {
|
|
18049
|
+
this.userDrawings?.deselect();
|
|
18050
|
+
this.handleClick(x);
|
|
18051
|
+
},
|
|
17693
18052
|
onAxisLongPress: (axis, x, y) => {
|
|
17694
18053
|
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
17695
18054
|
},
|
|
@@ -17707,11 +18066,13 @@ var NativeRenderer = class {
|
|
|
17707
18066
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
17708
18067
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
17709
18068
|
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
17710
|
-
|
|
18069
|
+
drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
|
|
18070
|
+
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
|
|
18071
|
+
// middle-click: a selected hit takes the whole selection
|
|
17711
18072
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
17712
18073
|
drawingsSnapMode: () => this.snapMode,
|
|
17713
|
-
drawingsPointerDown: (x, y, snap, shift2) => this.userDrawings?.pointerDown(x, y, snap, shift2),
|
|
17714
|
-
drawingsPointerMove: (x, y, snap, shift2) => this.userDrawings?.pointerMove(x, y, snap, shift2),
|
|
18074
|
+
drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
|
|
18075
|
+
drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
|
|
17715
18076
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
17716
18077
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
17717
18078
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
@@ -21535,7 +21896,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21535
21896
|
if (!model) continue;
|
|
21536
21897
|
const paneId = model.paneId ?? "price";
|
|
21537
21898
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
21538
|
-
byPane.get(paneId).push({
|
|
21899
|
+
byPane.get(paneId).push({
|
|
21900
|
+
id: r.id,
|
|
21901
|
+
title: model.title || r.title,
|
|
21902
|
+
...model.shorttitle ? { shorttitle: model.shorttitle } : {},
|
|
21903
|
+
ownScale: model.ownScale === true
|
|
21904
|
+
});
|
|
21539
21905
|
}
|
|
21540
21906
|
const panes = [];
|
|
21541
21907
|
for (const paneId of this.paneOrder) {
|
|
@@ -22105,6 +22471,10 @@ function str3(inputs, key, fallback) {
|
|
|
22105
22471
|
const v = inputs[key];
|
|
22106
22472
|
return typeof v === "string" && v !== "" ? v : fallback;
|
|
22107
22473
|
}
|
|
22474
|
+
function bool(inputs, key, fallback) {
|
|
22475
|
+
const v = inputs[key];
|
|
22476
|
+
return typeof v === "boolean" ? v : fallback;
|
|
22477
|
+
}
|
|
22108
22478
|
function toPoints(bars, values, colors) {
|
|
22109
22479
|
const pts = new Array(bars.length);
|
|
22110
22480
|
for (let i = 0; i < bars.length; i++) {
|
|
@@ -23636,10 +24006,6 @@ var aroon = {
|
|
|
23636
24006
|
plots: [
|
|
23637
24007
|
{ key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
|
|
23638
24008
|
{ key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
|
|
23639
|
-
],
|
|
23640
|
-
levels: [
|
|
23641
|
-
{ key: "upper", price: 100, color: NEUTRAL, lineStyle: "dotted" },
|
|
23642
|
-
{ key: "lower", price: 0, color: NEUTRAL, lineStyle: "dotted" }
|
|
23643
24009
|
]
|
|
23644
24010
|
};
|
|
23645
24011
|
}
|
|
@@ -24137,8 +24503,10 @@ var volumeSpecs = [
|
|
|
24137
24503
|
var DAY_MS2 = 864e5;
|
|
24138
24504
|
function periodKey(anchor, time) {
|
|
24139
24505
|
if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
|
|
24140
|
-
if (anchor === "Month") {
|
|
24506
|
+
if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
|
|
24141
24507
|
const d = new Date(time);
|
|
24508
|
+
if (anchor === "Year") return d.getUTCFullYear();
|
|
24509
|
+
if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
|
|
24142
24510
|
return d.getUTCFullYear() * 12 + d.getUTCMonth();
|
|
24143
24511
|
}
|
|
24144
24512
|
return Math.floor(time / DAY_MS2);
|
|
@@ -24190,40 +24558,86 @@ var parabolicSar = {
|
|
|
24190
24558
|
return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
|
|
24191
24559
|
}
|
|
24192
24560
|
};
|
|
24561
|
+
var VWAP_BANDS_GROUP = "Bands";
|
|
24562
|
+
var VWAP_STYLE = "Style";
|
|
24563
|
+
var VWAP_BANDS = [
|
|
24564
|
+
{ on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
|
|
24565
|
+
{ on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
|
|
24566
|
+
{ on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
|
|
24567
|
+
];
|
|
24193
24568
|
var vwap = {
|
|
24194
24569
|
type: "vwap",
|
|
24195
24570
|
title: "Volume Weighted Average Price",
|
|
24196
24571
|
shortTitle: "VWAP",
|
|
24197
24572
|
overlay: true,
|
|
24198
24573
|
inputs: [
|
|
24199
|
-
{ key: "anchor", title: "
|
|
24200
|
-
|
|
24201
|
-
|
|
24574
|
+
{ key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
|
|
24575
|
+
sourceInput("HLC3"),
|
|
24576
|
+
// Each band is one row: its toggle leads, the multiplier follows unlabeled.
|
|
24577
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
24578
|
+
{ key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
|
|
24579
|
+
{ key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
|
|
24580
|
+
]),
|
|
24581
|
+
{ ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
|
|
24582
|
+
// One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
|
|
24583
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
24584
|
+
{ ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
|
|
24585
|
+
{ key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
|
|
24586
|
+
{ ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
|
|
24587
|
+
])
|
|
24202
24588
|
],
|
|
24203
24589
|
compute: (bars, inputs) => {
|
|
24204
24590
|
const anchor = str3(inputs, "anchor", "Day");
|
|
24205
|
-
const
|
|
24206
|
-
const
|
|
24591
|
+
const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
|
|
24592
|
+
const n = bars.length;
|
|
24593
|
+
const values = new Array(n).fill(Number.NaN);
|
|
24594
|
+
const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
|
|
24595
|
+
mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
|
|
24596
|
+
color: str3(inputs, b.color, b.defColor),
|
|
24597
|
+
fill: bool(inputs, b.fill, true),
|
|
24598
|
+
fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
|
|
24599
|
+
up: new Array(n).fill(Number.NaN),
|
|
24600
|
+
down: new Array(n).fill(Number.NaN)
|
|
24601
|
+
}));
|
|
24207
24602
|
let period = Number.NaN;
|
|
24208
24603
|
let cumPV = 0;
|
|
24604
|
+
let cumPV2 = 0;
|
|
24209
24605
|
let cumV = 0;
|
|
24210
|
-
for (let i = 0; i <
|
|
24606
|
+
for (let i = 0; i < n; i++) {
|
|
24211
24607
|
const b = bars[i];
|
|
24212
24608
|
const key = periodKey(anchor, b.time);
|
|
24213
24609
|
if (key !== period) {
|
|
24214
24610
|
period = key;
|
|
24215
24611
|
cumPV = 0;
|
|
24612
|
+
cumPV2 = 0;
|
|
24216
24613
|
cumV = 0;
|
|
24217
24614
|
}
|
|
24218
24615
|
const v = b.volume;
|
|
24219
24616
|
if (v != null && Number.isFinite(v) && v > 0) {
|
|
24220
|
-
const tp =
|
|
24617
|
+
const tp = src[i];
|
|
24221
24618
|
cumPV += tp * v;
|
|
24619
|
+
cumPV2 += tp * tp * v;
|
|
24222
24620
|
cumV += v;
|
|
24223
24621
|
}
|
|
24224
|
-
if (cumV
|
|
24225
|
-
|
|
24226
|
-
|
|
24622
|
+
if (cumV <= 0) continue;
|
|
24623
|
+
const mean = cumPV / cumV;
|
|
24624
|
+
const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
|
|
24625
|
+
values[i] = mean;
|
|
24626
|
+
for (const band of bands) {
|
|
24627
|
+
band.up[i] = mean + band.mult * sd;
|
|
24628
|
+
band.down[i] = mean - band.mult * sd;
|
|
24629
|
+
}
|
|
24630
|
+
}
|
|
24631
|
+
const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
|
|
24632
|
+
const fills = [];
|
|
24633
|
+
bands.forEach((band, i) => {
|
|
24634
|
+
plots.push(
|
|
24635
|
+
{ key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
|
|
24636
|
+
{ key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
|
|
24637
|
+
);
|
|
24638
|
+
if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
|
|
24639
|
+
});
|
|
24640
|
+
return { plots, bands: fills };
|
|
24227
24641
|
}
|
|
24228
24642
|
};
|
|
24229
24643
|
var pivotPoints = {
|