@simon_he/vue-tui 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -26
- package/dist/TMermaidText-frVr8Ysb.js +885 -0
- package/dist/{TSelect-DR0FomSu.js → TSelect-tJ87rKPM.js} +59 -242
- package/dist/{TTree-kzhlB5P0.js → TTree-CGdE-CYu.js} +138 -102
- package/dist/agent/mermaid.cjs +2 -0
- package/dist/agent/mermaid.d.cts +2 -0
- package/dist/agent/mermaid.d.ts +2 -0
- package/dist/agent/mermaid.js +1 -0
- package/dist/agent.cjs +31188 -24907
- package/dist/agent.d.cts +11 -1
- package/dist/agent.d.ts +11 -1
- package/dist/agent.js +516 -11
- package/dist/{block-source-CKUOnkgT.js → block-source-CrlmaBM5.js} +1039 -66
- package/dist/cli/input.d.ts +8 -0
- package/dist/cli/stdin-buffer.d.ts +1 -1
- package/dist/cli/video/ffmpeg-frame-source.d.ts +10 -0
- package/dist/cli/video/png-frame-parser.d.ts +4 -0
- package/dist/cli/video/raw-frame-parser.d.ts +1 -0
- package/dist/cli/video/yt-dlp-frame-source.d.ts +11 -0
- package/dist/cli.cjs +5146 -6767
- package/dist/cli.d.cts +5 -1
- package/dist/cli.d.ts +5 -1
- package/dist/cli.js +2869 -154
- package/dist/core/buffer/eaw-ranges-unicode-17.d.ts +14 -0
- package/dist/core/perf/instrumentation-noop.d.ts +48 -0
- package/dist/core/perf/instrumentation.d.ts +136 -0
- package/dist/core/terminal/create-terminal.d.ts +1 -0
- package/dist/core.cjs +843 -1469
- package/dist/core.js +4 -5
- package/dist/create-terminal-app.d.ts +24 -1
- package/dist/debug-logger-CmsGI4of.js +74 -0
- package/dist/{dom-renderer-BniJSQyX.js → dom-renderer-DXx8fWzD.js} +2 -3
- package/dist/experimental/3d/bun/contributor-avatar-atlas.d.ts +12 -0
- package/dist/experimental/3d/bun/png.d.ts +1 -0
- package/dist/experimental/3d/bun/terminal-badge-scene.d.ts +11 -0
- package/dist/experimental/3d/bun/terminal-badge.wgsl.d.ts +1 -0
- package/dist/experimental/3d/bun/webgpu-renderer.d.ts +26 -0
- package/dist/experimental/3d/bun.cjs +7271 -0
- package/dist/experimental/3d/bun.d.cts +4 -0
- package/dist/experimental/3d/bun.d.ts +4 -0
- package/dist/experimental/3d/bun.js +7390 -0
- package/dist/experimental/video/node.cjs +497 -0
- package/dist/experimental/video/node.d.cts +4 -0
- package/dist/experimental/video/node.d.ts +4 -0
- package/dist/experimental/video/node.js +610 -0
- package/dist/experimental.cjs +6876 -6192
- package/dist/experimental.d.cts +7 -0
- package/dist/experimental.d.ts +7 -0
- package/dist/experimental.js +3005 -9
- package/dist/frame-perf-CHpWhbC4.js +111 -0
- package/dist/{create-terminal-DBFNYr8f.js → hyperlink-_1FAj7Wq.js} +440 -13
- package/dist/index.cjs +5017 -10339
- package/dist/index.js +14 -13
- package/dist/{linkify-D_gzn9Xq.js → linkify-D68T7rdW.js} +193 -3
- package/dist/markdown.cjs +21295 -12691
- package/dist/markdown.d.cts +1 -1
- package/dist/markdown.d.ts +1 -1
- package/dist/markdown.js +211 -51
- package/dist/mermaid-BNhM1jAZ.js +123 -0
- package/dist/mermaid.cjs +1707 -0
- package/dist/mermaid.d.cts +3 -0
- package/dist/mermaid.d.ts +3 -0
- package/dist/mermaid.js +6 -0
- package/dist/observability/frame-perf-store.d.ts +2 -1
- package/dist/observability/frame-perf.d.ts +33 -0
- package/dist/observability.cjs +106 -107
- package/dist/observability.d.cts +2 -2
- package/dist/observability.d.ts +2 -2
- package/dist/observability.js +3 -3
- package/dist/{wheel-scroll-vBRaQ7p-.js → rect-DUpeDfBg.js} +253 -117
- package/dist/renderer/cli/stdout-metrics.d.ts +10 -0
- package/dist/renderer/cli/stdout-renderer.d.ts +3 -0
- package/dist/renderer/terminal-graphic-render-queue.d.ts +50 -0
- package/dist/renderer/terminal-graphics-trace.d.ts +53 -0
- package/dist/renderer/terminal-graphics.d.ts +147 -0
- package/dist/renderer-dom.cjs +364 -903
- package/dist/renderer-dom.js +4 -5
- package/dist/runtime/index.d.ts +2 -0
- package/dist/{runtime-rfcZCyJq.js → runtime-Byf8XXOT.js} +28 -15
- package/dist/runtime.cjs +427 -889
- package/dist/runtime.js +2 -2
- package/dist/terminal-graphics-Bbe83wSr.js +597 -0
- package/dist/{terminal-selection-DBinO6p5.js → terminal-selection-4W1SJj4c.js} +4 -1
- package/dist/{tlog-theme-CvLFNMwk.js → tlog-theme-Bul2Na3u.js} +1237 -12
- package/dist/{trace-CjzYeIxN.js → trace-Bh8WnoMo.js} +5 -0
- package/dist/vue/agent/create-png-terminal-graphic-renderer.d.ts +19 -0
- package/dist/vue/components/T3DViewport.d.ts +473 -0
- package/dist/vue/components/TAgentTerminalGraphic.d.ts +494 -0
- package/dist/vue/components/TCharts.d.ts +1121 -0
- package/dist/vue/components/TFeedback.d.ts +1 -1
- package/dist/vue/components/TFlex.d.ts +389 -0
- package/dist/vue/components/TForm.d.ts +4 -4
- package/dist/vue/components/TLogSearchBar.d.ts +3 -3
- package/dist/vue/components/TLogSearchPager.d.ts +2 -2
- package/dist/vue/components/TMarkdownText.d.ts +162 -4
- package/dist/vue/components/TMermaidText.d.ts +900 -0
- package/dist/vue/components/TVideo.d.ts +291 -0
- package/dist/vue/components/TVirtualList.d.ts +9 -0
- package/dist/vue/components/TVirtualMarkdown.d.ts +170 -9
- package/dist/vue/components/TVirtualRows.d.ts +9 -0
- package/dist/vue/components/TerminalProvider.d.ts +2 -0
- package/dist/vue/context.d.ts +24 -0
- package/dist/vue/index.d.ts +5 -0
- package/dist/vue/layout/flex.d.ts +67 -0
- package/dist/vue/markdown/ast.d.ts +5 -2
- package/dist/vue/markdown/document.d.ts +5 -0
- package/dist/vue/markdown/image-actions.d.ts +14 -0
- package/dist/vue/markdown/image.d.ts +11 -0
- package/dist/vue/markdown/link-actions.d.ts +14 -0
- package/dist/vue/markdown/math-actions.d.ts +14 -0
- package/dist/vue/markdown/math.d.ts +6 -0
- package/dist/vue/markdown/render.d.ts +28 -0
- package/dist/vue/markdown/types.d.ts +78 -0
- package/dist/vue/mermaid/beautiful-mermaid.d.ts +1436 -0
- package/dist/vue/render/render-manager.d.ts +7 -0
- package/dist/vue/video/types.d.ts +39 -0
- package/dist/vue.cjs +7207 -12437
- package/dist/vue.js +704 -38
- package/dist/wheel-scroll-DASkEZr-.js +106 -0
- package/dist/{hyperlink-Bn02UXJb.js → width-GqllnV8C.js} +166 -111
- package/package.json +119 -14
- package/dist/frame-perf-C6Xfg85O.js +0 -24
- package/dist/sgr-BmzxEjAO.js +0 -304
- /package/dist/{ansi-palette-CNXqFciC.js → ansi-palette-CH6jc2Z7.js} +0 -0
- /package/dist/{path-suggest-BNqcuE3-.js → path-suggest-Dndu4dTi.js} +0 -0
package/dist/experimental.js
CHANGED
|
@@ -1,12 +1,3008 @@
|
|
|
1
|
-
import { sanitizeTerminalHref } from "./hyperlink-
|
|
2
|
-
import "./
|
|
3
|
-
import "./
|
|
4
|
-
import "./linkify-
|
|
5
|
-
import "./
|
|
6
|
-
import "./
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
1
|
+
import { sanitizeTerminalHref } from "./hyperlink-_1FAj7Wq.js";
|
|
2
|
+
import "./width-GqllnV8C.js";
|
|
3
|
+
import { EventZIndexContextKey, TerminalGraphicsActivityKey, forEachTextCellSegment, intersectRect, sanitizeInlineText, sliceByCells, spaces, textCellWidth$1 as textCellWidth, translateRect, useLayout, useRenderNode, useTerminal, useTerminalNode, useVisibility } from "./rect-DUpeDfBg.js";
|
|
4
|
+
import { TText, TView, createFrameMailbox, mergeStyle } from "./linkify-D68T7rdW.js";
|
|
5
|
+
import "./frame-perf-CHpWhbC4.js";
|
|
6
|
+
import "./terminal-selection-4W1SJj4c.js";
|
|
7
|
+
import "./wheel-scroll-DASkEZr-.js";
|
|
8
|
+
import { createIterm2InlineImageSequence, createKittyDeleteGraphicsSequence, createKittyGraphicsSequence, createKittyPlacementSequence, getTerminalGraphicsOutput, getTerminalGraphicsOutputVersion, subscribeTerminalGraphicsOutput } from "./terminal-graphics-Bbe83wSr.js";
|
|
9
|
+
import { TAgentTerminalGraphic, TLogLinksPanel, TLogMinimap, TLogScrollbar, TLogSearchBar, TLogSearchPager, TLogSearchResults, TLogView, TLogVirtualLinksPanel, TLogVirtualSearchResults, TTranscriptView, TVirtualList, createAppendOnlyLogStore, createTLogDensityBucketsFromMarkers, createTLogLevelPlugin, createTLogLineMatcherPlugin, createTLogLinkActionPlugin, createTLogOsc8LinkPlugin, createTLogUrlPlugin, detectTLogUrls, dispatchTLogPluginLinkAction, getTLogPluginMetadata, handleTLogKeymapEvent, matchesTLogKeyBinding, parseTLogAnnotatedText, resolveTLogLinksPanelTheme, resolveTLogMinimapTheme, resolveTLogScrollbarTheme, resolveTLogSearchBarTheme, resolveTLogSearchPagerTheme, resolveTLogSearchResultsTheme, resolveTLogViewTheme, stripTLogAnsiText, tlogDarkPreset, tlogDarkTheme, tlogDefaultKeymap, tlogDefaultPreset, tlogDefaultTheme, tlogHighContrastKeymap, tlogHighContrastPreset, tlogHighContrastTheme, toTLogExternalLinkFromVisibleLink, useTLogLinkController, useTLogRetainedIndex, useTLogSearchController, useTLogSearchResultsPage, useTLogVirtualSearchResults } from "./tlog-theme-Bul2Na3u.js";
|
|
10
|
+
import { computed, defineComponent, getCurrentInstance, h, inject, markRaw, onBeforeUnmount, onUnmounted, ref, shallowRef, watch } from "vue";
|
|
9
11
|
|
|
12
|
+
//#region src/vue/components/TVideo.ts
|
|
13
|
+
const DEFAULT_MAX_FPS$1 = 12;
|
|
14
|
+
const DEFAULT_MAX_PIXEL_WIDTH = 640;
|
|
15
|
+
const DEFAULT_MAX_PIXEL_HEIGHT = 360;
|
|
16
|
+
const MAX_PIXEL_DIMENSION = 4096;
|
|
17
|
+
const MAX_FRAME_BYTES = 8 * 1024 * 1024;
|
|
18
|
+
const MAX_GRAY8_FPS = 10;
|
|
19
|
+
const MAX_GRAY8_WIDTH = 512;
|
|
20
|
+
const MAX_GRAY8_HEIGHT = 256;
|
|
21
|
+
const ASCII_RAMP = " .:-=+*#%@";
|
|
22
|
+
const CONTROL_REFRESH_MS = 250;
|
|
23
|
+
const PLAYBACK_RATES = [
|
|
24
|
+
1,
|
|
25
|
+
2,
|
|
26
|
+
3
|
|
27
|
+
];
|
|
28
|
+
function positiveInt(value, fallback, max) {
|
|
29
|
+
const parsed = Math.floor(Number(value));
|
|
30
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return Math.min(max, fallback);
|
|
31
|
+
return Math.min(max, parsed);
|
|
32
|
+
}
|
|
33
|
+
function positiveNumber(value, fallback, max) {
|
|
34
|
+
const parsed = Number(value);
|
|
35
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
36
|
+
return Math.min(max, parsed);
|
|
37
|
+
}
|
|
38
|
+
function playbackRate(value) {
|
|
39
|
+
return value === 2 || value === 3 ? value : 1;
|
|
40
|
+
}
|
|
41
|
+
function optionalDuration(value) {
|
|
42
|
+
const parsed = Number(value);
|
|
43
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
44
|
+
}
|
|
45
|
+
function formatPlaybackTime(valueMs, hourDigits = 0) {
|
|
46
|
+
if (valueMs == null) return "--:--";
|
|
47
|
+
const seconds = Math.max(0, Math.floor(valueMs / 1e3));
|
|
48
|
+
const hours = Math.floor(seconds / 3600);
|
|
49
|
+
const minutes = Math.floor(seconds % 3600 / 60);
|
|
50
|
+
const remainder = seconds % 60;
|
|
51
|
+
if (hours > 0 || hourDigits > 0) return `${String(hours).padStart(Math.max(1, hourDigits), "0")}:${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`;
|
|
52
|
+
return `${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`;
|
|
53
|
+
}
|
|
54
|
+
function readUint32(bytes, offset) {
|
|
55
|
+
return bytes[offset] * 16777216 + (bytes[offset + 1] << 16) + (bytes[offset + 2] << 8) + bytes[offset + 3] >>> 0;
|
|
56
|
+
}
|
|
57
|
+
function normalizeFrame(frame) {
|
|
58
|
+
if (frame.format === "gray8") {
|
|
59
|
+
const pixelWidth$1 = Math.floor(Number(frame.pixelWidth));
|
|
60
|
+
const pixelHeight$1 = Math.floor(Number(frame.pixelHeight));
|
|
61
|
+
const pixels = frame.pixels;
|
|
62
|
+
if (!Number.isFinite(pixelWidth$1) || !Number.isFinite(pixelHeight$1) || pixelWidth$1 <= 0 || pixelHeight$1 <= 0 || pixelWidth$1 > MAX_GRAY8_WIDTH || pixelHeight$1 > MAX_GRAY8_HEIGHT || !(pixels instanceof Uint8Array) || pixels.length !== pixelWidth$1 * pixelHeight$1 || pixels.length > MAX_FRAME_BYTES) throw new Error("TVideo gray8 frame must match its bounded pixel dimensions");
|
|
63
|
+
return {
|
|
64
|
+
format: "gray8",
|
|
65
|
+
pixels,
|
|
66
|
+
timestampMs: Math.max(0, Number(frame.timestampMs) || 0),
|
|
67
|
+
pixelWidth: pixelWidth$1,
|
|
68
|
+
pixelHeight: pixelHeight$1,
|
|
69
|
+
fingerprint: frame.fingerprint,
|
|
70
|
+
durationMs: optionalDuration(frame.durationMs)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const png = frame.png;
|
|
74
|
+
if (!(png instanceof Uint8Array) || png.length < 24 || png.length > MAX_FRAME_BYTES) throw new Error("TVideo frame must contain a bounded PNG Uint8Array");
|
|
75
|
+
if (png[0] !== 137 || png[1] !== 80 || png[2] !== 78 || png[3] !== 71 || png[4] !== 13 || png[5] !== 10 || png[6] !== 26 || png[7] !== 10) throw new Error("TVideo frame source emitted invalid PNG data");
|
|
76
|
+
const pixelWidth = readUint32(png, 16);
|
|
77
|
+
const pixelHeight = readUint32(png, 20);
|
|
78
|
+
if (pixelWidth <= 0 || pixelHeight <= 0 || pixelWidth > MAX_PIXEL_DIMENSION || pixelHeight > MAX_PIXEL_DIMENSION) throw new Error(`TVideo frame has an unsupported size: ${pixelWidth}x${pixelHeight}`);
|
|
79
|
+
return {
|
|
80
|
+
format: "png",
|
|
81
|
+
png,
|
|
82
|
+
timestampMs: Math.max(0, Number(frame.timestampMs) || 0),
|
|
83
|
+
pixelWidth,
|
|
84
|
+
pixelHeight,
|
|
85
|
+
fingerprint: frame.fingerprint,
|
|
86
|
+
durationMs: optionalDuration(frame.durationMs)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function frameBytes(frame) {
|
|
90
|
+
return frame.format === "gray8" ? frame.pixels : frame.png;
|
|
91
|
+
}
|
|
92
|
+
function gray8ToAscii(frame, cellWidth) {
|
|
93
|
+
const lines = [];
|
|
94
|
+
const width = Math.max(1, Math.floor(cellWidth));
|
|
95
|
+
for (let y = 0; y < frame.pixelHeight; y++) {
|
|
96
|
+
let line = "";
|
|
97
|
+
const rowStart = y * frame.pixelWidth;
|
|
98
|
+
for (let x = 0; x < frame.pixelWidth; x++) {
|
|
99
|
+
const value = frame.pixels[rowStart + x];
|
|
100
|
+
const glyph = ASCII_RAMP[Math.min(ASCII_RAMP.length - 1, value * ASCII_RAMP.length >> 8)];
|
|
101
|
+
line += `${glyph}${glyph}`;
|
|
102
|
+
}
|
|
103
|
+
lines.push(line.slice(0, width));
|
|
104
|
+
}
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
function sameBytes(left, right) {
|
|
108
|
+
if (left.length !== right.length) return false;
|
|
109
|
+
for (let index = 0; index < left.length; index++) if (left[index] !== right[index]) return false;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
function bytesToBase64(bytes) {
|
|
113
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
114
|
+
const chunks = [];
|
|
115
|
+
let chunk = "";
|
|
116
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
117
|
+
const a = bytes[index];
|
|
118
|
+
const hasB = index + 1 < bytes.length;
|
|
119
|
+
const hasC = index + 2 < bytes.length;
|
|
120
|
+
const b = hasB ? bytes[index + 1] : 0;
|
|
121
|
+
const c = hasC ? bytes[index + 2] : 0;
|
|
122
|
+
chunk += alphabet[a >> 2] + alphabet[(a & 3) << 4 | b >> 4] + (hasB ? alphabet[(b & 15) << 2 | c >> 6] : "=") + (hasC ? alphabet[c & 63] : "=");
|
|
123
|
+
if (chunk.length >= 16384) {
|
|
124
|
+
chunks.push(chunk);
|
|
125
|
+
chunk = "";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (chunk) chunks.push(chunk);
|
|
129
|
+
return chunks.join("");
|
|
130
|
+
}
|
|
131
|
+
function resolveKittyPlacement(frame, context) {
|
|
132
|
+
const rect = context.viewport.rect;
|
|
133
|
+
const full = context.viewport.fullRect;
|
|
134
|
+
const columns = Math.max(1, Math.floor(rect.w));
|
|
135
|
+
const rows = Math.max(1, Math.floor(rect.h));
|
|
136
|
+
if (full.w <= 0 || full.h <= 0) return {
|
|
137
|
+
columns,
|
|
138
|
+
rows
|
|
139
|
+
};
|
|
140
|
+
const offsetX = Math.max(0, rect.x - full.x);
|
|
141
|
+
const offsetY = Math.max(0, rect.y - full.y);
|
|
142
|
+
const visibleW = Math.max(0, Math.min(rect.w, full.w - offsetX));
|
|
143
|
+
const visibleH = Math.max(0, Math.min(rect.h, full.h - offsetY));
|
|
144
|
+
if (offsetX <= 0 && offsetY <= 0 && visibleW >= full.w && visibleH >= full.h) return {
|
|
145
|
+
columns,
|
|
146
|
+
rows
|
|
147
|
+
};
|
|
148
|
+
const sourceX = Math.max(0, Math.min(frame.pixelWidth - 1, Math.floor(offsetX * frame.pixelWidth / full.w)));
|
|
149
|
+
const sourceY = Math.max(0, Math.min(frame.pixelHeight - 1, Math.floor(offsetY * frame.pixelHeight / full.h)));
|
|
150
|
+
const sourceRight = Math.max(sourceX + 1, Math.min(frame.pixelWidth, Math.ceil((offsetX + visibleW) * frame.pixelWidth / full.w)));
|
|
151
|
+
const sourceBottom = Math.max(sourceY + 1, Math.min(frame.pixelHeight, Math.ceil((offsetY + visibleH) * frame.pixelHeight / full.h)));
|
|
152
|
+
return {
|
|
153
|
+
columns,
|
|
154
|
+
rows,
|
|
155
|
+
sourceX,
|
|
156
|
+
sourceY,
|
|
157
|
+
sourceWidth: sourceRight - sourceX,
|
|
158
|
+
sourceHeight: sourceBottom - sourceY
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function createFrameRenderer(frame, cellWidth) {
|
|
162
|
+
if (frame.format === "gray8") {
|
|
163
|
+
const text = gray8ToAscii(frame, cellWidth);
|
|
164
|
+
return () => ({
|
|
165
|
+
type: "text",
|
|
166
|
+
text
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
let base64;
|
|
170
|
+
return (_content, context) => {
|
|
171
|
+
if (!context.visible || !context.rawVisible) return null;
|
|
172
|
+
base64 ?? (base64 = bytesToBase64(frame.png));
|
|
173
|
+
if (context.protocol === "kitty") {
|
|
174
|
+
const placement = resolveKittyPlacement(frame, context);
|
|
175
|
+
return {
|
|
176
|
+
type: "sequence",
|
|
177
|
+
protocol: "kitty",
|
|
178
|
+
sequence: createKittyGraphicsSequence(base64, {
|
|
179
|
+
imageId: context.imageId,
|
|
180
|
+
placementId: context.placementId,
|
|
181
|
+
...placement
|
|
182
|
+
}),
|
|
183
|
+
resizeSequence: createKittyPlacementSequence({
|
|
184
|
+
imageId: context.imageId,
|
|
185
|
+
placementId: context.placementId,
|
|
186
|
+
...placement
|
|
187
|
+
}),
|
|
188
|
+
clearSequence: createKittyDeleteGraphicsSequence({
|
|
189
|
+
imageId: context.imageId,
|
|
190
|
+
placementId: context.placementId,
|
|
191
|
+
freeImageData: true
|
|
192
|
+
}),
|
|
193
|
+
cols: context.width,
|
|
194
|
+
rows: context.height,
|
|
195
|
+
sourceWidth: frame.pixelWidth,
|
|
196
|
+
sourceHeight: frame.pixelHeight
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (context.protocol === "iterm2") return {
|
|
200
|
+
type: "sequence",
|
|
201
|
+
protocol: "iterm2",
|
|
202
|
+
sequence: createIterm2InlineImageSequence(base64, {
|
|
203
|
+
width: context.width,
|
|
204
|
+
height: context.height,
|
|
205
|
+
preserveAspectRatio: true,
|
|
206
|
+
doNotMoveCursor: true
|
|
207
|
+
}),
|
|
208
|
+
cols: context.width,
|
|
209
|
+
rows: context.height,
|
|
210
|
+
sourceWidth: frame.pixelWidth,
|
|
211
|
+
sourceHeight: frame.pixelHeight
|
|
212
|
+
};
|
|
213
|
+
return null;
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function errorMessage(error) {
|
|
217
|
+
return error instanceof Error ? error.message : String(error);
|
|
218
|
+
}
|
|
219
|
+
const tVideoProps = {
|
|
220
|
+
x: {
|
|
221
|
+
type: Number,
|
|
222
|
+
required: true
|
|
223
|
+
},
|
|
224
|
+
y: {
|
|
225
|
+
type: Number,
|
|
226
|
+
required: true
|
|
227
|
+
},
|
|
228
|
+
w: {
|
|
229
|
+
type: Number,
|
|
230
|
+
required: true
|
|
231
|
+
},
|
|
232
|
+
h: {
|
|
233
|
+
type: Number,
|
|
234
|
+
required: true
|
|
235
|
+
},
|
|
236
|
+
zIndex: {
|
|
237
|
+
type: Number,
|
|
238
|
+
default: 0
|
|
239
|
+
},
|
|
240
|
+
src: {
|
|
241
|
+
type: String,
|
|
242
|
+
required: true
|
|
243
|
+
},
|
|
244
|
+
frameSource: {
|
|
245
|
+
type: Function,
|
|
246
|
+
required: true
|
|
247
|
+
},
|
|
248
|
+
paused: {
|
|
249
|
+
type: Boolean,
|
|
250
|
+
default: void 0
|
|
251
|
+
},
|
|
252
|
+
playbackRate: {
|
|
253
|
+
type: Number,
|
|
254
|
+
default: void 0
|
|
255
|
+
},
|
|
256
|
+
controls: {
|
|
257
|
+
type: Boolean,
|
|
258
|
+
default: false
|
|
259
|
+
},
|
|
260
|
+
controlsLayout: {
|
|
261
|
+
type: String,
|
|
262
|
+
default: "compact"
|
|
263
|
+
},
|
|
264
|
+
durationMs: {
|
|
265
|
+
type: Number,
|
|
266
|
+
default: void 0
|
|
267
|
+
},
|
|
268
|
+
loop: {
|
|
269
|
+
type: Boolean,
|
|
270
|
+
default: false
|
|
271
|
+
},
|
|
272
|
+
maxFps: {
|
|
273
|
+
type: Number,
|
|
274
|
+
default: DEFAULT_MAX_FPS$1
|
|
275
|
+
},
|
|
276
|
+
pixelWidth: {
|
|
277
|
+
type: Number,
|
|
278
|
+
default: void 0
|
|
279
|
+
},
|
|
280
|
+
pixelHeight: {
|
|
281
|
+
type: Number,
|
|
282
|
+
default: void 0
|
|
283
|
+
},
|
|
284
|
+
fallback: {
|
|
285
|
+
type: String,
|
|
286
|
+
default: "[video]"
|
|
287
|
+
},
|
|
288
|
+
style: {
|
|
289
|
+
type: Object,
|
|
290
|
+
default: void 0
|
|
291
|
+
},
|
|
292
|
+
clear: {
|
|
293
|
+
type: Boolean,
|
|
294
|
+
default: true
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const TVideo = defineComponent({
|
|
298
|
+
name: "TVideo",
|
|
299
|
+
props: tVideoProps,
|
|
300
|
+
emits: {
|
|
301
|
+
frame: (_event) => true,
|
|
302
|
+
ended: () => true,
|
|
303
|
+
error: (_error) => true,
|
|
304
|
+
seek: (_event) => true,
|
|
305
|
+
"update:paused": (_paused) => true,
|
|
306
|
+
"update:playbackRate": (_rate) => true
|
|
307
|
+
},
|
|
308
|
+
setup(props, { emit }) {
|
|
309
|
+
const instance = getCurrentInstance();
|
|
310
|
+
const { terminal, scheduler } = useTerminal();
|
|
311
|
+
const layout = useLayout();
|
|
312
|
+
const { visible, rootProps } = useVisibility({ provide: true });
|
|
313
|
+
const graphicsActivity = inject(TerminalGraphicsActivityKey, null);
|
|
314
|
+
const frameRenderer = shallowRef();
|
|
315
|
+
const playbackError = shallowRef("");
|
|
316
|
+
const internalPaused = shallowRef(props.paused ?? false);
|
|
317
|
+
const internalPlaybackRate = shallowRef(playbackRate(props.playbackRate));
|
|
318
|
+
const discoveredDurationMs = shallowRef();
|
|
319
|
+
const displayedTimestampMs = shallowRef(0);
|
|
320
|
+
const seekPreviewMs = shallowRef();
|
|
321
|
+
const seeking = shallowRef(false);
|
|
322
|
+
const terminalSizeVersion = shallowRef(0);
|
|
323
|
+
const graphicsOutputVersion = shallowRef(getTerminalGraphicsOutputVersion(terminal));
|
|
324
|
+
const placementKey = `TVideo:${instance?.uid ?? "unknown"}`;
|
|
325
|
+
const mailboxId = `${placementKey}:frame`;
|
|
326
|
+
let alive = true;
|
|
327
|
+
let generation = 0;
|
|
328
|
+
let activeAbort = null;
|
|
329
|
+
let lastSourceBytes = null;
|
|
330
|
+
let lastQueuedBytes = null;
|
|
331
|
+
let lastQueuedFormat = null;
|
|
332
|
+
let lastQueuedFingerprint;
|
|
333
|
+
let lastTimestampMs = 0;
|
|
334
|
+
let droppedFrames = 0;
|
|
335
|
+
let pendingEndedGeneration = null;
|
|
336
|
+
let suppressNextVideoClick = false;
|
|
337
|
+
let controlPointerCapture = null;
|
|
338
|
+
const unsubscribeTerminalResize = terminal.on("resize", () => {
|
|
339
|
+
terminalSizeVersion.value++;
|
|
340
|
+
});
|
|
341
|
+
const unsubscribeGraphicsOutput = subscribeTerminalGraphicsOutput(terminal, () => {
|
|
342
|
+
graphicsOutputVersion.value = getTerminalGraphicsOutputVersion(terminal);
|
|
343
|
+
});
|
|
344
|
+
const showControls = computed(() => props.controls && Math.floor(props.w) >= 14 && Math.floor(props.h) >= 2);
|
|
345
|
+
const resolvedControlsLayout = computed(() => props.controlsLayout === "cinema" && Math.floor(props.h) >= 3 ? "cinema" : "compact");
|
|
346
|
+
const controlRows = computed(() => showControls.value ? resolvedControlsLayout.value === "cinema" ? 2 : 1 : 0);
|
|
347
|
+
const videoHeight = computed(() => Math.max(1, Math.floor(props.h) - controlRows.value));
|
|
348
|
+
const resolvedMaxFps = computed(() => positiveNumber(props.maxFps, DEFAULT_MAX_FPS$1, 60));
|
|
349
|
+
const resolvedPixelSize = computed(() => {
|
|
350
|
+
const desiredWidth = Math.max(2, Math.floor(props.w) * 8);
|
|
351
|
+
const desiredHeight = Math.max(2, videoHeight.value * 16);
|
|
352
|
+
if (props.pixelWidth == null && props.pixelHeight == null) {
|
|
353
|
+
const scale = Math.min(1, DEFAULT_MAX_PIXEL_WIDTH / desiredWidth, DEFAULT_MAX_PIXEL_HEIGHT / desiredHeight);
|
|
354
|
+
return {
|
|
355
|
+
width: Math.max(2, Math.floor(desiredWidth * scale)),
|
|
356
|
+
height: Math.max(2, Math.floor(desiredHeight * scale))
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
if (props.pixelWidth != null && props.pixelHeight == null) {
|
|
360
|
+
const width = positiveInt(props.pixelWidth, desiredWidth, MAX_PIXEL_DIMENSION);
|
|
361
|
+
const inferredHeight = width * desiredHeight / desiredWidth;
|
|
362
|
+
const scale = Math.min(1, MAX_PIXEL_DIMENSION / inferredHeight);
|
|
363
|
+
return {
|
|
364
|
+
width: Math.max(2, Math.floor(width * scale)),
|
|
365
|
+
height: Math.max(2, Math.floor(inferredHeight * scale))
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
if (props.pixelWidth == null && props.pixelHeight != null) {
|
|
369
|
+
const height = positiveInt(props.pixelHeight, desiredHeight, MAX_PIXEL_DIMENSION);
|
|
370
|
+
const inferredWidth = height * desiredWidth / desiredHeight;
|
|
371
|
+
const scale = Math.min(1, MAX_PIXEL_DIMENSION / inferredWidth);
|
|
372
|
+
return {
|
|
373
|
+
width: Math.max(2, Math.floor(inferredWidth * scale)),
|
|
374
|
+
height: Math.max(2, Math.floor(height * scale))
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
width: positiveInt(props.pixelWidth, desiredWidth, MAX_PIXEL_DIMENSION),
|
|
379
|
+
height: positiveInt(props.pixelHeight, desiredHeight, MAX_PIXEL_DIMENSION)
|
|
380
|
+
};
|
|
381
|
+
});
|
|
382
|
+
const resolvedPixelWidth = computed(() => resolvedPixelSize.value.width);
|
|
383
|
+
const resolvedPixelHeight = computed(() => resolvedPixelSize.value.height);
|
|
384
|
+
const paintable = computed(() => {
|
|
385
|
+
terminalSizeVersion.value;
|
|
386
|
+
if (!visible.value) return false;
|
|
387
|
+
const size = terminal.size();
|
|
388
|
+
const terminalRect = {
|
|
389
|
+
x: 0,
|
|
390
|
+
y: 0,
|
|
391
|
+
w: Math.max(0, Math.floor(size.cols)),
|
|
392
|
+
h: Math.max(0, Math.floor(size.rows))
|
|
393
|
+
};
|
|
394
|
+
const clip = layout.clipRect ? intersectRect(layout.clipRect, terminalRect) : terminalRect;
|
|
395
|
+
if (!clip) return false;
|
|
396
|
+
const rect = translateRect({
|
|
397
|
+
x: props.x,
|
|
398
|
+
y: props.y,
|
|
399
|
+
w: props.w,
|
|
400
|
+
h: videoHeight.value
|
|
401
|
+
}, layout.originX, layout.originY);
|
|
402
|
+
const visibleRect = intersectRect(rect, clip);
|
|
403
|
+
return Boolean(visibleRect && visibleRect.w > 0 && visibleRect.h > 0);
|
|
404
|
+
});
|
|
405
|
+
const graphicsProtocol = computed(() => {
|
|
406
|
+
graphicsOutputVersion.value;
|
|
407
|
+
return getTerminalGraphicsOutput(terminal)?.capabilities.preferredProtocol ?? null;
|
|
408
|
+
});
|
|
409
|
+
const preferredFormat = computed(() => graphicsProtocol.value === "kitty" || graphicsProtocol.value === "iterm2" ? "png" : "gray8");
|
|
410
|
+
const resolvedDecodeFps = computed(() => preferredFormat.value === "gray8" ? Math.min(MAX_GRAY8_FPS, resolvedMaxFps.value) : resolvedMaxFps.value);
|
|
411
|
+
const resolvedDecodeWidth = computed(() => preferredFormat.value === "gray8" ? positiveInt(Math.ceil(props.w / 2), 1, MAX_GRAY8_WIDTH) : resolvedPixelWidth.value);
|
|
412
|
+
const resolvedDecodeHeight = computed(() => preferredFormat.value === "gray8" ? positiveInt(videoHeight.value, 1, MAX_GRAY8_HEIGHT) : resolvedPixelHeight.value);
|
|
413
|
+
const resolvedDurationMs = computed(() => optionalDuration(props.durationMs) ?? discoveredDurationMs.value);
|
|
414
|
+
const effectivePaused = computed(() => props.paused ?? internalPaused.value);
|
|
415
|
+
const effectivePlaybackRate = computed(() => props.playbackRate == null ? internalPlaybackRate.value : playbackRate(props.playbackRate));
|
|
416
|
+
const controlTimestampMs = computed(() => seekPreviewMs.value ?? displayedTimestampMs.value);
|
|
417
|
+
const timeHourDigits = computed(() => {
|
|
418
|
+
const duration = resolvedDurationMs.value;
|
|
419
|
+
if (!duration || duration < 36e5) return 0;
|
|
420
|
+
return String(Math.floor(duration / 36e5)).length;
|
|
421
|
+
});
|
|
422
|
+
const currentTimeText = computed(() => formatPlaybackTime(controlTimestampMs.value, timeHourDigits.value));
|
|
423
|
+
const durationTimeText = computed(() => formatPlaybackTime(resolvedDurationMs.value, timeHourDigits.value));
|
|
424
|
+
const controlLayout = computed(() => {
|
|
425
|
+
const width = Math.max(1, Math.floor(props.w));
|
|
426
|
+
const progressY = videoHeight.value;
|
|
427
|
+
const current = currentTimeText.value;
|
|
428
|
+
const duration = durationTimeText.value;
|
|
429
|
+
const timeWidth = Math.max(textCellWidth(current), textCellWidth(duration));
|
|
430
|
+
const ratesWidth = 8;
|
|
431
|
+
if (resolvedControlsLayout.value === "cinema") {
|
|
432
|
+
const ratesX$1 = Math.max(3, width - ratesWidth);
|
|
433
|
+
const timeText = `${current} / ${duration}`;
|
|
434
|
+
return {
|
|
435
|
+
width,
|
|
436
|
+
kind: "cinema",
|
|
437
|
+
progressX: 0,
|
|
438
|
+
progressY,
|
|
439
|
+
progressWidth: width,
|
|
440
|
+
actionY: progressY + 1,
|
|
441
|
+
ratesX: ratesX$1,
|
|
442
|
+
showAllRates: true,
|
|
443
|
+
showTimes: 3 + textCellWidth(timeText) < ratesX$1,
|
|
444
|
+
current,
|
|
445
|
+
duration,
|
|
446
|
+
timeText,
|
|
447
|
+
timeWidth,
|
|
448
|
+
currentX: 3,
|
|
449
|
+
totalX: 0
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
const ratesX = width - ratesWidth;
|
|
453
|
+
const progressX = timeWidth + 4;
|
|
454
|
+
const totalX = ratesX - timeWidth - 1;
|
|
455
|
+
const progressWidth = totalX - progressX - 1;
|
|
456
|
+
if (progressWidth >= 4) return {
|
|
457
|
+
width,
|
|
458
|
+
kind: "compact",
|
|
459
|
+
progressX,
|
|
460
|
+
progressY,
|
|
461
|
+
progressWidth,
|
|
462
|
+
actionY: progressY,
|
|
463
|
+
ratesX,
|
|
464
|
+
showAllRates: true,
|
|
465
|
+
showTimes: true,
|
|
466
|
+
current,
|
|
467
|
+
duration,
|
|
468
|
+
timeText: "",
|
|
469
|
+
timeWidth,
|
|
470
|
+
currentX: 3,
|
|
471
|
+
totalX
|
|
472
|
+
};
|
|
473
|
+
const compactRatesX = Math.max(4, width - 2);
|
|
474
|
+
return {
|
|
475
|
+
width,
|
|
476
|
+
kind: "compact",
|
|
477
|
+
progressX: 3,
|
|
478
|
+
progressY,
|
|
479
|
+
progressWidth: Math.max(1, compactRatesX - 4),
|
|
480
|
+
actionY: progressY,
|
|
481
|
+
ratesX: compactRatesX,
|
|
482
|
+
showAllRates: false,
|
|
483
|
+
showTimes: false,
|
|
484
|
+
current,
|
|
485
|
+
duration,
|
|
486
|
+
timeText: "",
|
|
487
|
+
timeWidth,
|
|
488
|
+
currentX: 0,
|
|
489
|
+
totalX: 0
|
|
490
|
+
};
|
|
491
|
+
});
|
|
492
|
+
const rateStyles = computed(() => PLAYBACK_RATES.map((rate) => rate === effectivePlaybackRate.value ? {
|
|
493
|
+
...props.style,
|
|
494
|
+
inverse: true
|
|
495
|
+
} : props.style));
|
|
496
|
+
const remainingProgressStyle = computed(() => ({
|
|
497
|
+
...props.style,
|
|
498
|
+
dim: true
|
|
499
|
+
}));
|
|
500
|
+
const progressThumbStyle = computed(() => ({
|
|
501
|
+
...props.style,
|
|
502
|
+
inverse: true
|
|
503
|
+
}));
|
|
504
|
+
const progressSegments = computed(() => {
|
|
505
|
+
const { progressWidth } = controlLayout.value;
|
|
506
|
+
const duration = resolvedDurationMs.value;
|
|
507
|
+
if (!duration) return {
|
|
508
|
+
playedWidth: 0,
|
|
509
|
+
thumbWidth: 0,
|
|
510
|
+
remainingWidth: progressWidth
|
|
511
|
+
};
|
|
512
|
+
const ratio = Math.max(0, Math.min(1, controlTimestampMs.value / duration));
|
|
513
|
+
const marker = Math.round(ratio * Math.max(0, progressWidth - 1));
|
|
514
|
+
return {
|
|
515
|
+
playedWidth: marker,
|
|
516
|
+
thumbWidth: 1,
|
|
517
|
+
remainingWidth: progressWidth - marker - 1
|
|
518
|
+
};
|
|
519
|
+
});
|
|
520
|
+
const controlFill = computed(() => " ".repeat(controlLayout.value.width));
|
|
521
|
+
const progressGlyphs = computed(() => ({
|
|
522
|
+
played: "━".repeat(progressSegments.value.playedWidth),
|
|
523
|
+
remaining: "─".repeat(progressSegments.value.remainingWidth)
|
|
524
|
+
}));
|
|
525
|
+
const playbackEnabled = computed(() => alive && !effectivePaused.value && !graphicsActivity?.scrolling.value && props.src.trim().length > 0 && paintable.value);
|
|
526
|
+
const shouldPlay = computed(() => playbackEnabled.value && !seeking.value);
|
|
527
|
+
const visibleFallback = computed(() => playbackError.value ? `${props.fallback}: ${playbackError.value}` : props.fallback);
|
|
528
|
+
function timelineTimestamp(timestampMs) {
|
|
529
|
+
const duration = resolvedDurationMs.value;
|
|
530
|
+
if (!duration) return Math.max(0, timestampMs);
|
|
531
|
+
if (props.loop) return Math.max(0, timestampMs) % duration;
|
|
532
|
+
return Math.max(0, Math.min(duration, timestampMs));
|
|
533
|
+
}
|
|
534
|
+
function seekTimestamp(timestampMs) {
|
|
535
|
+
const duration = resolvedDurationMs.value;
|
|
536
|
+
if (!duration) return Math.max(0, timestampMs);
|
|
537
|
+
const end = props.loop ? Math.max(0, duration - 1) : duration;
|
|
538
|
+
return Math.max(0, Math.min(end, timestampMs));
|
|
539
|
+
}
|
|
540
|
+
function updateTimestamp(timestampMs, force = false) {
|
|
541
|
+
const next = timelineTimestamp(timestampMs);
|
|
542
|
+
lastTimestampMs = next;
|
|
543
|
+
if (!showControls.value) return;
|
|
544
|
+
const threshold = CONTROL_REFRESH_MS * effectivePlaybackRate.value;
|
|
545
|
+
if (force || next < displayedTimestampMs.value || Math.abs(next - displayedTimestampMs.value) >= threshold) displayedTimestampMs.value = next;
|
|
546
|
+
}
|
|
547
|
+
function commitFrame(frame, dropped) {
|
|
548
|
+
if (!alive) return;
|
|
549
|
+
droppedFrames += dropped;
|
|
550
|
+
frameRenderer.value = markRaw(createFrameRenderer(frame, props.w));
|
|
551
|
+
updateTimestamp(frame.timestampMs);
|
|
552
|
+
const durationMs = resolvedDurationMs.value;
|
|
553
|
+
emit("frame", {
|
|
554
|
+
timestampMs: frame.timestampMs,
|
|
555
|
+
pixelWidth: frame.pixelWidth,
|
|
556
|
+
pixelHeight: frame.pixelHeight,
|
|
557
|
+
droppedFrames,
|
|
558
|
+
...durationMs ? { durationMs } : {},
|
|
559
|
+
playbackRate: effectivePlaybackRate.value
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
const mailbox = createFrameMailbox({
|
|
563
|
+
scheduler,
|
|
564
|
+
id: mailboxId,
|
|
565
|
+
reason: "stream",
|
|
566
|
+
priority: "low",
|
|
567
|
+
apply(frame, _context, meta) {
|
|
568
|
+
commitFrame(frame, meta.dropped);
|
|
569
|
+
if (pendingEndedGeneration === generation) {
|
|
570
|
+
pendingEndedGeneration = null;
|
|
571
|
+
emit("ended");
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
function stopPlayback() {
|
|
576
|
+
generation++;
|
|
577
|
+
pendingEndedGeneration = null;
|
|
578
|
+
const discardedPendingFrame = mailbox.hasPending();
|
|
579
|
+
mailbox.cancel();
|
|
580
|
+
if (discardedPendingFrame) {
|
|
581
|
+
lastQueuedBytes = null;
|
|
582
|
+
lastQueuedFormat = null;
|
|
583
|
+
lastQueuedFingerprint = void 0;
|
|
584
|
+
}
|
|
585
|
+
activeAbort?.abort();
|
|
586
|
+
activeAbort = null;
|
|
587
|
+
}
|
|
588
|
+
function resetQueuedFrameIdentity() {
|
|
589
|
+
lastSourceBytes = null;
|
|
590
|
+
lastQueuedBytes = null;
|
|
591
|
+
lastQueuedFormat = null;
|
|
592
|
+
lastQueuedFingerprint = void 0;
|
|
593
|
+
}
|
|
594
|
+
function setPaused(paused) {
|
|
595
|
+
if (effectivePaused.value === paused) return;
|
|
596
|
+
if (props.paused == null) internalPaused.value = paused;
|
|
597
|
+
emit("update:paused", paused);
|
|
598
|
+
}
|
|
599
|
+
function setPlaybackRate(rate) {
|
|
600
|
+
const next = playbackRate(rate);
|
|
601
|
+
if (effectivePlaybackRate.value === next) return;
|
|
602
|
+
if (props.playbackRate == null) internalPlaybackRate.value = next;
|
|
603
|
+
emit("update:playbackRate", next);
|
|
604
|
+
}
|
|
605
|
+
function applySeek(timestampMs, restart) {
|
|
606
|
+
const parsed = Number(timestampMs);
|
|
607
|
+
if (!Number.isFinite(parsed)) return;
|
|
608
|
+
stopPlayback();
|
|
609
|
+
resetQueuedFrameIdentity();
|
|
610
|
+
const next = seekTimestamp(parsed);
|
|
611
|
+
updateTimestamp(next, true);
|
|
612
|
+
seekPreviewMs.value = void 0;
|
|
613
|
+
const durationMs = resolvedDurationMs.value;
|
|
614
|
+
emit("seek", {
|
|
615
|
+
timestampMs: next,
|
|
616
|
+
...durationMs ? { durationMs } : {}
|
|
617
|
+
});
|
|
618
|
+
if (restart) startPlayback(effectivePaused.value);
|
|
619
|
+
}
|
|
620
|
+
function controlCell(event) {
|
|
621
|
+
return {
|
|
622
|
+
x: Math.floor(event.cellX - layout.originX - props.x),
|
|
623
|
+
y: Math.floor(event.cellY - layout.originY - props.y)
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function previewSeekAt(cellX) {
|
|
627
|
+
const duration = resolvedDurationMs.value;
|
|
628
|
+
if (!duration) return;
|
|
629
|
+
const { progressX, progressWidth } = controlLayout.value;
|
|
630
|
+
const offset = Math.max(0, Math.min(progressWidth - 1, cellX - progressX));
|
|
631
|
+
seekPreviewMs.value = progressWidth <= 1 ? 0 : offset / Math.max(1, progressWidth - 1) * duration;
|
|
632
|
+
}
|
|
633
|
+
function captureControlPointer(event) {
|
|
634
|
+
const native = event.nativeEvent;
|
|
635
|
+
const pointerId = native?.pointerId;
|
|
636
|
+
const target = native?.target;
|
|
637
|
+
if (pointerId == null || !target?.setPointerCapture) return;
|
|
638
|
+
target.setPointerCapture(pointerId);
|
|
639
|
+
controlPointerCapture = {
|
|
640
|
+
pointerId,
|
|
641
|
+
target
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function releaseControlPointer() {
|
|
645
|
+
if (!controlPointerCapture) return;
|
|
646
|
+
controlPointerCapture.target.releasePointerCapture?.(controlPointerCapture.pointerId);
|
|
647
|
+
controlPointerCapture = null;
|
|
648
|
+
}
|
|
649
|
+
function rateAt(cellX) {
|
|
650
|
+
const { ratesX, showAllRates } = controlLayout.value;
|
|
651
|
+
const offset = cellX - ratesX;
|
|
652
|
+
if (!showAllRates) {
|
|
653
|
+
if (offset < 0 || offset >= 2) return void 0;
|
|
654
|
+
const index = PLAYBACK_RATES.indexOf(effectivePlaybackRate.value);
|
|
655
|
+
return PLAYBACK_RATES[(index + 1) % PLAYBACK_RATES.length];
|
|
656
|
+
}
|
|
657
|
+
if (offset < 0 || offset % 3 >= 2) return void 0;
|
|
658
|
+
return PLAYBACK_RATES[Math.floor(offset / 3)];
|
|
659
|
+
}
|
|
660
|
+
function onInteractivePointerDown(event) {
|
|
661
|
+
if (event.button != null && event.button !== 0) return;
|
|
662
|
+
const { x: cellX, y: cellY } = controlCell(event);
|
|
663
|
+
if (cellY < videoHeight.value) {
|
|
664
|
+
suppressNextVideoClick = false;
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (!showControls.value) return;
|
|
668
|
+
const { actionY, progressY, progressX, progressWidth } = controlLayout.value;
|
|
669
|
+
const rate = rateAt(cellX);
|
|
670
|
+
if (cellY === actionY && cellX <= 1) {
|
|
671
|
+
event.preventDefault();
|
|
672
|
+
event.stopPropagation();
|
|
673
|
+
suppressNextVideoClick = true;
|
|
674
|
+
setPaused(!effectivePaused.value);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (cellY === actionY && rate) {
|
|
678
|
+
event.preventDefault();
|
|
679
|
+
event.stopPropagation();
|
|
680
|
+
suppressNextVideoClick = true;
|
|
681
|
+
setPlaybackRate(rate);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (cellY !== progressY || !resolvedDurationMs.value || cellX < progressX || cellX >= progressX + progressWidth) return;
|
|
685
|
+
event.preventDefault();
|
|
686
|
+
event.stopPropagation();
|
|
687
|
+
suppressNextVideoClick = true;
|
|
688
|
+
seeking.value = true;
|
|
689
|
+
stopPlayback();
|
|
690
|
+
previewSeekAt(cellX);
|
|
691
|
+
captureControlPointer(event);
|
|
692
|
+
}
|
|
693
|
+
function onInteractivePointerMove(event) {
|
|
694
|
+
if (!seeking.value) return;
|
|
695
|
+
if (event.buttons === 0) {
|
|
696
|
+
onInteractivePointerUp(event);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
event.preventDefault();
|
|
700
|
+
event.stopPropagation();
|
|
701
|
+
previewSeekAt(controlCell(event).x);
|
|
702
|
+
}
|
|
703
|
+
function onInteractivePointerUp(event) {
|
|
704
|
+
if (!seeking.value) return;
|
|
705
|
+
event.preventDefault();
|
|
706
|
+
event.stopPropagation();
|
|
707
|
+
previewSeekAt(controlCell(event).x);
|
|
708
|
+
const timestampMs = seekPreviewMs.value ?? lastTimestampMs;
|
|
709
|
+
applySeek(timestampMs, false);
|
|
710
|
+
seeking.value = false;
|
|
711
|
+
releaseControlPointer();
|
|
712
|
+
startPlayback(effectivePaused.value);
|
|
713
|
+
}
|
|
714
|
+
function onVideoClick(event) {
|
|
715
|
+
if (event.button != null && event.button !== 0) return;
|
|
716
|
+
if (suppressNextVideoClick) {
|
|
717
|
+
suppressNextVideoClick = false;
|
|
718
|
+
event.preventDefault();
|
|
719
|
+
event.stopPropagation();
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (controlCell(event).y >= videoHeight.value) return;
|
|
723
|
+
event.preventDefault();
|
|
724
|
+
event.stopPropagation();
|
|
725
|
+
setPaused(!effectivePaused.value);
|
|
726
|
+
}
|
|
727
|
+
function finishControlSeek() {
|
|
728
|
+
if (!seeking.value) return;
|
|
729
|
+
const timestampMs = seekPreviewMs.value ?? lastTimestampMs;
|
|
730
|
+
applySeek(timestampMs, false);
|
|
731
|
+
seeking.value = false;
|
|
732
|
+
releaseControlPointer();
|
|
733
|
+
startPlayback(effectivePaused.value);
|
|
734
|
+
}
|
|
735
|
+
function onControlKeydown(event) {
|
|
736
|
+
if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
|
|
737
|
+
event.preventDefault();
|
|
738
|
+
event.stopPropagation();
|
|
739
|
+
setPaused(!effectivePaused.value);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
743
|
+
event.preventDefault();
|
|
744
|
+
event.stopPropagation();
|
|
745
|
+
const index = PLAYBACK_RATES.indexOf(effectivePlaybackRate.value);
|
|
746
|
+
const offset = event.key === "ArrowUp" ? 1 : -1;
|
|
747
|
+
setPlaybackRate(PLAYBACK_RATES[Math.max(0, Math.min(2, index + offset))]);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (!event.repeat && (event.key === "Home" || event.key === "End") && resolvedDurationMs.value) {
|
|
751
|
+
event.preventDefault();
|
|
752
|
+
event.stopPropagation();
|
|
753
|
+
applySeek(event.key === "Home" ? 0 : resolvedDurationMs.value, true);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (event.key === "1" || event.key === "2" || event.key === "3") {
|
|
757
|
+
event.preventDefault();
|
|
758
|
+
event.stopPropagation();
|
|
759
|
+
setPlaybackRate(Number(event.key));
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function queueFrame(frame, expectedFormat) {
|
|
763
|
+
let normalized = normalizeFrame(frame);
|
|
764
|
+
if (normalized.format !== expectedFormat) throw new Error(`TVideo frame source did not provide requested ${expectedFormat} frames`);
|
|
765
|
+
if (normalized.durationMs != null && normalized.durationMs !== discoveredDurationMs.value) discoveredDurationMs.value = normalized.durationMs;
|
|
766
|
+
normalized = {
|
|
767
|
+
...normalized,
|
|
768
|
+
timestampMs: timelineTimestamp(normalized.timestampMs)
|
|
769
|
+
};
|
|
770
|
+
const sourceBytes = frameBytes(normalized);
|
|
771
|
+
const reusesSourceBuffer = sourceBytes === lastSourceBytes;
|
|
772
|
+
lastSourceBytes = sourceBytes;
|
|
773
|
+
const stableFrame = reusesSourceBuffer ? normalized.format === "gray8" ? {
|
|
774
|
+
...normalized,
|
|
775
|
+
pixels: new Uint8Array(normalized.pixels)
|
|
776
|
+
} : {
|
|
777
|
+
...normalized,
|
|
778
|
+
png: new Uint8Array(normalized.png)
|
|
779
|
+
} : normalized;
|
|
780
|
+
const bytes = frameBytes(stableFrame);
|
|
781
|
+
const sameFingerprint = normalized.format === lastQueuedFormat && normalized.fingerprint != null && lastQueuedFingerprint != null && normalized.fingerprint === lastQueuedFingerprint;
|
|
782
|
+
const canCompareBytes = normalized.fingerprint == null && lastQueuedFingerprint == null && !reusesSourceBuffer;
|
|
783
|
+
if (sameFingerprint || canCompareBytes && normalized.format === lastQueuedFormat && lastQueuedBytes && sameBytes(lastQueuedBytes, sourceBytes)) {
|
|
784
|
+
lastQueuedBytes = bytes;
|
|
785
|
+
lastQueuedFormat = normalized.format;
|
|
786
|
+
lastQueuedFingerprint = normalized.fingerprint;
|
|
787
|
+
updateTimestamp(normalized.timestampMs);
|
|
788
|
+
if (mailbox.hasPending()) mailbox.replacePending(stableFrame);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
lastQueuedBytes = bytes;
|
|
792
|
+
lastQueuedFormat = normalized.format;
|
|
793
|
+
lastQueuedFingerprint = normalized.fingerprint;
|
|
794
|
+
if (!mailbox.queue(stableFrame)) {
|
|
795
|
+
commitFrame(stableFrame, 0);
|
|
796
|
+
scheduler.invalidate({
|
|
797
|
+
priority: "low",
|
|
798
|
+
reason: "stream"
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function startPlayback(oneFrame = false) {
|
|
803
|
+
stopPlayback();
|
|
804
|
+
if (!shouldPlay.value && !(oneFrame && alive && props.src.trim().length > 0 && paintable.value && !graphicsActivity?.scrolling.value)) return;
|
|
805
|
+
const currentGeneration = generation;
|
|
806
|
+
const abort = new AbortController();
|
|
807
|
+
activeAbort = abort;
|
|
808
|
+
playbackError.value = "";
|
|
809
|
+
const source = props.frameSource;
|
|
810
|
+
const format = preferredFormat.value;
|
|
811
|
+
const context = {
|
|
812
|
+
src: props.src,
|
|
813
|
+
signal: abort.signal,
|
|
814
|
+
maxFps: resolvedDecodeFps.value,
|
|
815
|
+
pixelWidth: resolvedDecodeWidth.value,
|
|
816
|
+
pixelHeight: resolvedDecodeHeight.value,
|
|
817
|
+
startAtMs: lastTimestampMs,
|
|
818
|
+
preferredFormat: format,
|
|
819
|
+
playbackRate: effectivePlaybackRate.value,
|
|
820
|
+
loop: props.loop
|
|
821
|
+
};
|
|
822
|
+
(async () => {
|
|
823
|
+
try {
|
|
824
|
+
const frames = await source(context);
|
|
825
|
+
for await (const frame of frames) {
|
|
826
|
+
if (!alive || abort.signal.aborted || currentGeneration !== generation) return;
|
|
827
|
+
queueFrame(frame, format);
|
|
828
|
+
if (oneFrame) return;
|
|
829
|
+
}
|
|
830
|
+
if (!alive || abort.signal.aborted || currentGeneration !== generation) return;
|
|
831
|
+
if (mailbox.hasPending()) pendingEndedGeneration = currentGeneration;
|
|
832
|
+
else emit("ended");
|
|
833
|
+
} catch (error) {
|
|
834
|
+
if (!alive || abort.signal.aborted || currentGeneration !== generation) return;
|
|
835
|
+
playbackError.value = errorMessage(error);
|
|
836
|
+
emit("error", error);
|
|
837
|
+
scheduler.invalidate({
|
|
838
|
+
priority: "low",
|
|
839
|
+
reason: "data"
|
|
840
|
+
});
|
|
841
|
+
} finally {
|
|
842
|
+
if (activeAbort === abort) activeAbort = null;
|
|
843
|
+
}
|
|
844
|
+
})();
|
|
845
|
+
}
|
|
846
|
+
watch(() => props.paused, (paused) => {
|
|
847
|
+
if (paused != null) internalPaused.value = paused;
|
|
848
|
+
});
|
|
849
|
+
watch(() => props.playbackRate, (rate) => {
|
|
850
|
+
if (rate != null) internalPlaybackRate.value = playbackRate(rate);
|
|
851
|
+
});
|
|
852
|
+
watch([
|
|
853
|
+
() => props.src,
|
|
854
|
+
() => props.frameSource,
|
|
855
|
+
playbackEnabled,
|
|
856
|
+
resolvedDecodeFps,
|
|
857
|
+
resolvedDecodeWidth,
|
|
858
|
+
resolvedDecodeHeight,
|
|
859
|
+
preferredFormat,
|
|
860
|
+
effectivePlaybackRate,
|
|
861
|
+
() => props.loop
|
|
862
|
+
], (next, previous) => {
|
|
863
|
+
const sourceChanged = !previous || next[0] !== previous[0] || next[1] !== previous[1];
|
|
864
|
+
const formatChanged = !previous || next[6] !== previous[6];
|
|
865
|
+
if (sourceChanged || formatChanged) {
|
|
866
|
+
resetQueuedFrameIdentity();
|
|
867
|
+
if (sourceChanged) {
|
|
868
|
+
lastTimestampMs = 0;
|
|
869
|
+
displayedTimestampMs.value = 0;
|
|
870
|
+
discoveredDurationMs.value = void 0;
|
|
871
|
+
seekPreviewMs.value = void 0;
|
|
872
|
+
seeking.value = false;
|
|
873
|
+
suppressNextVideoClick = false;
|
|
874
|
+
releaseControlPointer();
|
|
875
|
+
droppedFrames = 0;
|
|
876
|
+
}
|
|
877
|
+
playbackError.value = "";
|
|
878
|
+
frameRenderer.value = void 0;
|
|
879
|
+
}
|
|
880
|
+
startPlayback();
|
|
881
|
+
}, {
|
|
882
|
+
immediate: true,
|
|
883
|
+
flush: "post"
|
|
884
|
+
});
|
|
885
|
+
onBeforeUnmount(() => {
|
|
886
|
+
alive = false;
|
|
887
|
+
stopPlayback();
|
|
888
|
+
releaseControlPointer();
|
|
889
|
+
mailbox.dispose();
|
|
890
|
+
unsubscribeTerminalResize();
|
|
891
|
+
unsubscribeGraphicsOutput();
|
|
892
|
+
});
|
|
893
|
+
return () => {
|
|
894
|
+
const children = [h(TAgentTerminalGraphic, {
|
|
895
|
+
x: props.x,
|
|
896
|
+
y: props.y,
|
|
897
|
+
w: props.w,
|
|
898
|
+
h: videoHeight.value,
|
|
899
|
+
zIndex: props.zIndex,
|
|
900
|
+
content: props.src.trim() ? props.src : "video",
|
|
901
|
+
fallback: visibleFallback.value,
|
|
902
|
+
style: props.style,
|
|
903
|
+
clear: props.clear,
|
|
904
|
+
final: false,
|
|
905
|
+
streaming: true,
|
|
906
|
+
renderer: frameRenderer.value,
|
|
907
|
+
placementKey,
|
|
908
|
+
preserveRawWhileRendering: true,
|
|
909
|
+
ignoreSamePlaneRawCoverage: true,
|
|
910
|
+
suspended: !paintable.value
|
|
911
|
+
})];
|
|
912
|
+
if (props.controls) children.push(h(TView, {
|
|
913
|
+
x: props.x,
|
|
914
|
+
y: props.y,
|
|
915
|
+
w: Math.max(0, Math.floor(props.w)),
|
|
916
|
+
h: Math.max(0, Math.floor(props.h)),
|
|
917
|
+
zIndex: props.zIndex,
|
|
918
|
+
focusable: true,
|
|
919
|
+
selectable: false,
|
|
920
|
+
onClick: onVideoClick,
|
|
921
|
+
onPointerdown: onInteractivePointerDown,
|
|
922
|
+
onPointermove: onInteractivePointerMove,
|
|
923
|
+
onPointerup: onInteractivePointerUp,
|
|
924
|
+
onKeydown: onControlKeydown,
|
|
925
|
+
onBlur: finishControlSeek
|
|
926
|
+
}, () => {
|
|
927
|
+
if (!showControls.value) return [];
|
|
928
|
+
const layout$1 = controlLayout.value;
|
|
929
|
+
const segments = progressSegments.value;
|
|
930
|
+
const controls = Array.from({ length: controlRows.value }, (_, index) => h(TText, {
|
|
931
|
+
x: 0,
|
|
932
|
+
y: videoHeight.value + index,
|
|
933
|
+
w: layout$1.width,
|
|
934
|
+
value: controlFill.value,
|
|
935
|
+
style: props.style
|
|
936
|
+
}));
|
|
937
|
+
controls.push(h(TText, {
|
|
938
|
+
x: 0,
|
|
939
|
+
y: layout$1.actionY,
|
|
940
|
+
w: 2,
|
|
941
|
+
value: effectivePaused.value ? "> " : "||",
|
|
942
|
+
style: props.style
|
|
943
|
+
}));
|
|
944
|
+
if (layout$1.showTimes) if (layout$1.kind === "cinema") controls.push(h(TText, {
|
|
945
|
+
x: layout$1.currentX,
|
|
946
|
+
y: layout$1.actionY,
|
|
947
|
+
w: textCellWidth(layout$1.timeText),
|
|
948
|
+
value: layout$1.timeText,
|
|
949
|
+
style: props.style
|
|
950
|
+
}));
|
|
951
|
+
else controls.push(h(TText, {
|
|
952
|
+
x: layout$1.currentX,
|
|
953
|
+
y: layout$1.actionY,
|
|
954
|
+
w: layout$1.timeWidth,
|
|
955
|
+
value: layout$1.current,
|
|
956
|
+
style: props.style
|
|
957
|
+
}), h(TText, {
|
|
958
|
+
x: layout$1.totalX,
|
|
959
|
+
y: layout$1.actionY,
|
|
960
|
+
w: layout$1.timeWidth,
|
|
961
|
+
value: layout$1.duration,
|
|
962
|
+
style: props.style
|
|
963
|
+
}));
|
|
964
|
+
if (segments.playedWidth > 0) controls.push(h(TText, {
|
|
965
|
+
x: layout$1.progressX,
|
|
966
|
+
y: layout$1.progressY,
|
|
967
|
+
w: segments.playedWidth,
|
|
968
|
+
value: progressGlyphs.value.played,
|
|
969
|
+
style: props.style
|
|
970
|
+
}));
|
|
971
|
+
if (segments.thumbWidth > 0) controls.push(h(TText, {
|
|
972
|
+
x: layout$1.progressX + segments.playedWidth,
|
|
973
|
+
y: layout$1.progressY,
|
|
974
|
+
w: 1,
|
|
975
|
+
value: "╋",
|
|
976
|
+
style: progressThumbStyle.value
|
|
977
|
+
}));
|
|
978
|
+
if (segments.remainingWidth > 0) controls.push(h(TText, {
|
|
979
|
+
x: layout$1.progressX + segments.playedWidth + segments.thumbWidth,
|
|
980
|
+
y: layout$1.progressY,
|
|
981
|
+
w: segments.remainingWidth,
|
|
982
|
+
value: progressGlyphs.value.remaining,
|
|
983
|
+
style: remainingProgressStyle.value
|
|
984
|
+
}));
|
|
985
|
+
if (layout$1.showAllRates) controls.push(...PLAYBACK_RATES.map((rate, index) => h(TText, {
|
|
986
|
+
x: layout$1.ratesX + index * 3,
|
|
987
|
+
y: layout$1.actionY,
|
|
988
|
+
w: 2,
|
|
989
|
+
value: `${rate}x`,
|
|
990
|
+
style: rateStyles.value[index]
|
|
991
|
+
})));
|
|
992
|
+
else controls.push(h(TText, {
|
|
993
|
+
x: layout$1.ratesX,
|
|
994
|
+
y: layout$1.actionY,
|
|
995
|
+
w: 2,
|
|
996
|
+
value: `${effectivePlaybackRate.value}x`,
|
|
997
|
+
style: rateStyles.value[PLAYBACK_RATES.indexOf(effectivePlaybackRate.value)]
|
|
998
|
+
}));
|
|
999
|
+
return controls;
|
|
1000
|
+
}));
|
|
1001
|
+
return h("span", rootProps, children);
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
//#endregion
|
|
1007
|
+
//#region src/vue/components/T3DViewport.ts
|
|
1008
|
+
const DEFAULT_MAX_FPS = 24;
|
|
1009
|
+
const DEFAULT_AUTO_ROTATE_SPEED = .35;
|
|
1010
|
+
const DEFAULT_POINTER_SENSITIVITY = .055;
|
|
1011
|
+
const VELOCITY_DAMPING_PER_SECOND = 5.5;
|
|
1012
|
+
const ZOOM_DAMPING_PER_SECOND = 7;
|
|
1013
|
+
const MAX_ZOOM_VELOCITY = 3;
|
|
1014
|
+
const DEFAULT_MIN_ZOOM = .65;
|
|
1015
|
+
const DEFAULT_MAX_ZOOM = 1.65;
|
|
1016
|
+
const DEFAULT_ZOOM_SENSITIVITY = .1;
|
|
1017
|
+
const MAX_PITCH = Math.PI * .42;
|
|
1018
|
+
const t3DViewportProps = {
|
|
1019
|
+
x: {
|
|
1020
|
+
type: Number,
|
|
1021
|
+
required: true
|
|
1022
|
+
},
|
|
1023
|
+
y: {
|
|
1024
|
+
type: Number,
|
|
1025
|
+
required: true
|
|
1026
|
+
},
|
|
1027
|
+
w: {
|
|
1028
|
+
type: Number,
|
|
1029
|
+
required: true
|
|
1030
|
+
},
|
|
1031
|
+
h: {
|
|
1032
|
+
type: Number,
|
|
1033
|
+
required: true
|
|
1034
|
+
},
|
|
1035
|
+
zIndex: {
|
|
1036
|
+
type: Number,
|
|
1037
|
+
default: 0
|
|
1038
|
+
},
|
|
1039
|
+
renderer: {
|
|
1040
|
+
type: Object,
|
|
1041
|
+
required: true
|
|
1042
|
+
},
|
|
1043
|
+
paused: {
|
|
1044
|
+
type: Boolean,
|
|
1045
|
+
default: false
|
|
1046
|
+
},
|
|
1047
|
+
maxFps: {
|
|
1048
|
+
type: Number,
|
|
1049
|
+
default: DEFAULT_MAX_FPS
|
|
1050
|
+
},
|
|
1051
|
+
pixelWidth: {
|
|
1052
|
+
type: Number,
|
|
1053
|
+
default: void 0
|
|
1054
|
+
},
|
|
1055
|
+
pixelHeight: {
|
|
1056
|
+
type: Number,
|
|
1057
|
+
default: void 0
|
|
1058
|
+
},
|
|
1059
|
+
fallback: {
|
|
1060
|
+
type: String,
|
|
1061
|
+
default: "[3D viewport]"
|
|
1062
|
+
},
|
|
1063
|
+
style: {
|
|
1064
|
+
type: Object,
|
|
1065
|
+
default: void 0
|
|
1066
|
+
},
|
|
1067
|
+
clear: {
|
|
1068
|
+
type: Boolean,
|
|
1069
|
+
default: true
|
|
1070
|
+
},
|
|
1071
|
+
interactive: {
|
|
1072
|
+
type: Boolean,
|
|
1073
|
+
default: true
|
|
1074
|
+
},
|
|
1075
|
+
initialYaw: {
|
|
1076
|
+
type: Number,
|
|
1077
|
+
default: 0
|
|
1078
|
+
},
|
|
1079
|
+
initialPitch: {
|
|
1080
|
+
type: Number,
|
|
1081
|
+
default: 0
|
|
1082
|
+
},
|
|
1083
|
+
autoRotate: {
|
|
1084
|
+
type: Boolean,
|
|
1085
|
+
default: true
|
|
1086
|
+
},
|
|
1087
|
+
autoRotateSpeed: {
|
|
1088
|
+
type: Number,
|
|
1089
|
+
default: DEFAULT_AUTO_ROTATE_SPEED
|
|
1090
|
+
},
|
|
1091
|
+
pointerSensitivity: {
|
|
1092
|
+
type: Number,
|
|
1093
|
+
default: DEFAULT_POINTER_SENSITIVITY
|
|
1094
|
+
},
|
|
1095
|
+
initialZoom: {
|
|
1096
|
+
type: Number,
|
|
1097
|
+
default: 1
|
|
1098
|
+
},
|
|
1099
|
+
minZoom: {
|
|
1100
|
+
type: Number,
|
|
1101
|
+
default: DEFAULT_MIN_ZOOM
|
|
1102
|
+
},
|
|
1103
|
+
maxZoom: {
|
|
1104
|
+
type: Number,
|
|
1105
|
+
default: DEFAULT_MAX_ZOOM
|
|
1106
|
+
},
|
|
1107
|
+
zoomSensitivity: {
|
|
1108
|
+
type: Number,
|
|
1109
|
+
default: DEFAULT_ZOOM_SENSITIVITY
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
function finite(value, fallback) {
|
|
1113
|
+
return Number.isFinite(value) ? value : fallback;
|
|
1114
|
+
}
|
|
1115
|
+
function clampPitch(value) {
|
|
1116
|
+
return Math.max(-MAX_PITCH, Math.min(MAX_PITCH, value));
|
|
1117
|
+
}
|
|
1118
|
+
function zoomBounds(minZoom, maxZoom) {
|
|
1119
|
+
const first = Math.max(.01, finite(minZoom, DEFAULT_MIN_ZOOM));
|
|
1120
|
+
const second = Math.max(.01, finite(maxZoom, DEFAULT_MAX_ZOOM));
|
|
1121
|
+
return first <= second ? {
|
|
1122
|
+
min: first,
|
|
1123
|
+
max: second
|
|
1124
|
+
} : {
|
|
1125
|
+
min: second,
|
|
1126
|
+
max: first
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function clampZoom(value, minZoom, maxZoom) {
|
|
1130
|
+
const { min, max } = zoomBounds(minZoom, maxZoom);
|
|
1131
|
+
return Math.max(min, Math.min(max, finite(value, 1)));
|
|
1132
|
+
}
|
|
1133
|
+
function normalizedWheelDelta(event) {
|
|
1134
|
+
const deltaY = Number(event.deltaY ?? 0);
|
|
1135
|
+
if (!Number.isFinite(deltaY) || deltaY === 0) return 0;
|
|
1136
|
+
if (event.deltaMode === 1) return deltaY;
|
|
1137
|
+
if (event.deltaMode === 2) return Math.max(-3, Math.min(3, deltaY * 3));
|
|
1138
|
+
if (event.deltaMode === 0) return deltaY / 100;
|
|
1139
|
+
return Number.isInteger(deltaY) && Math.abs(deltaY) <= 3 ? deltaY : deltaY / 100;
|
|
1140
|
+
}
|
|
1141
|
+
function nowMs() {
|
|
1142
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
1143
|
+
}
|
|
1144
|
+
function abortError() {
|
|
1145
|
+
const error = new Error("T3D viewport render aborted");
|
|
1146
|
+
error.name = "AbortError";
|
|
1147
|
+
return error;
|
|
1148
|
+
}
|
|
1149
|
+
function warnDev(message) {
|
|
1150
|
+
const nodeEnv = globalThis.process?.env?.NODE_ENV;
|
|
1151
|
+
if (nodeEnv !== "production") console.warn(message);
|
|
1152
|
+
}
|
|
1153
|
+
function waitFor(ms, signal) {
|
|
1154
|
+
if (signal.aborted || ms <= 0) return Promise.resolve();
|
|
1155
|
+
return new Promise((resolve) => {
|
|
1156
|
+
const timer = setTimeout(done, ms);
|
|
1157
|
+
function done() {
|
|
1158
|
+
clearTimeout(timer);
|
|
1159
|
+
signal.removeEventListener("abort", done);
|
|
1160
|
+
resolve();
|
|
1161
|
+
}
|
|
1162
|
+
signal.addEventListener("abort", done, { once: true });
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Browser-safe interactive 3D viewport. Frames are pulled from a renderer and
|
|
1167
|
+
* delegated directly to TVideo, which owns frame coalescing and terminal output.
|
|
1168
|
+
*
|
|
1169
|
+
* @event frame Emitted after TVideo commits a rendered frame.
|
|
1170
|
+
* @event error Emitted when the renderer or TVideo frame handling fails.
|
|
1171
|
+
*/
|
|
1172
|
+
const T3DViewport = defineComponent({
|
|
1173
|
+
name: "T3DViewport",
|
|
1174
|
+
props: t3DViewportProps,
|
|
1175
|
+
emits: {
|
|
1176
|
+
frame: (_event) => true,
|
|
1177
|
+
error: (_error) => true,
|
|
1178
|
+
objecthover: (_hit) => true,
|
|
1179
|
+
objectselect: (_hit) => true
|
|
1180
|
+
},
|
|
1181
|
+
setup(props, { emit, expose }) {
|
|
1182
|
+
const layout = useLayout();
|
|
1183
|
+
const renderer = props.renderer;
|
|
1184
|
+
let alive = true;
|
|
1185
|
+
let yaw = finite(props.initialYaw, 0);
|
|
1186
|
+
let pitch = clampPitch(finite(props.initialPitch, 0));
|
|
1187
|
+
let yawVelocity = 0;
|
|
1188
|
+
let pitchVelocity = 0;
|
|
1189
|
+
let pointerX = 0;
|
|
1190
|
+
let pointerY = 0;
|
|
1191
|
+
let pointerSpeed = 0;
|
|
1192
|
+
let hovering = false;
|
|
1193
|
+
let zoom = clampZoom(props.initialZoom, props.minZoom, props.maxZoom);
|
|
1194
|
+
let zoomVelocity = 0;
|
|
1195
|
+
let hoveredObject = null;
|
|
1196
|
+
let selectedObject = null;
|
|
1197
|
+
let actualPixelWidth = Math.max(1, Math.floor(props.pixelWidth ?? props.w));
|
|
1198
|
+
let actualPixelHeight = Math.max(1, Math.floor(props.pixelHeight ?? props.h));
|
|
1199
|
+
let pointer = null;
|
|
1200
|
+
let drag = null;
|
|
1201
|
+
let renderTail = Promise.resolve();
|
|
1202
|
+
function motionSnapshot() {
|
|
1203
|
+
return {
|
|
1204
|
+
yaw,
|
|
1205
|
+
pitch,
|
|
1206
|
+
yawVelocity,
|
|
1207
|
+
pitchVelocity,
|
|
1208
|
+
pointerX,
|
|
1209
|
+
pointerY,
|
|
1210
|
+
pointerSpeed,
|
|
1211
|
+
hovering,
|
|
1212
|
+
zoom,
|
|
1213
|
+
zoomVelocity,
|
|
1214
|
+
hoveredObjectId: hoveredObject?.objectId ?? null,
|
|
1215
|
+
selectedObjectId: selectedObject?.objectId ?? null
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
async function renderFrame(context) {
|
|
1219
|
+
const previous = renderTail;
|
|
1220
|
+
let release = () => {};
|
|
1221
|
+
renderTail = new Promise((resolve) => {
|
|
1222
|
+
release = resolve;
|
|
1223
|
+
});
|
|
1224
|
+
await previous;
|
|
1225
|
+
try {
|
|
1226
|
+
if (!alive || context.signal.aborted) throw abortError();
|
|
1227
|
+
return await renderer.render(context);
|
|
1228
|
+
} finally {
|
|
1229
|
+
release();
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
function sameHit(left, right) {
|
|
1233
|
+
return left?.objectId === right?.objectId;
|
|
1234
|
+
}
|
|
1235
|
+
function setHoveredObject(hit) {
|
|
1236
|
+
if (sameHit(hoveredObject, hit)) return;
|
|
1237
|
+
hoveredObject = hit;
|
|
1238
|
+
emit("objecthover", hit);
|
|
1239
|
+
}
|
|
1240
|
+
function setSelectedObject(hit) {
|
|
1241
|
+
if (sameHit(selectedObject, hit)) return;
|
|
1242
|
+
selectedObject = hit;
|
|
1243
|
+
emit("objectselect", hit);
|
|
1244
|
+
}
|
|
1245
|
+
function resetMotion() {
|
|
1246
|
+
yaw = finite(props.initialYaw, 0);
|
|
1247
|
+
pitch = clampPitch(finite(props.initialPitch, 0));
|
|
1248
|
+
yawVelocity = 0;
|
|
1249
|
+
pitchVelocity = 0;
|
|
1250
|
+
pointerX = 0;
|
|
1251
|
+
pointerY = 0;
|
|
1252
|
+
pointerSpeed = 0;
|
|
1253
|
+
zoom = clampZoom(props.initialZoom, props.minZoom, props.maxZoom);
|
|
1254
|
+
zoomVelocity = 0;
|
|
1255
|
+
setHoveredObject(null);
|
|
1256
|
+
setSelectedObject(null);
|
|
1257
|
+
pointer = null;
|
|
1258
|
+
drag = null;
|
|
1259
|
+
}
|
|
1260
|
+
expose({ resetMotion });
|
|
1261
|
+
function localCell(event) {
|
|
1262
|
+
return {
|
|
1263
|
+
x: Math.floor(event.cellX - layout.originX - props.x),
|
|
1264
|
+
y: Math.floor(event.cellY - layout.originY - props.y)
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
function eventTime(event) {
|
|
1268
|
+
return Number.isFinite(event.timeStamp) ? event.timeStamp : nowMs();
|
|
1269
|
+
}
|
|
1270
|
+
function updatePointerPosition(point) {
|
|
1271
|
+
const width = Math.max(1, Math.floor(props.w));
|
|
1272
|
+
const height = Math.max(1, Math.floor(props.h));
|
|
1273
|
+
pointerX = Math.max(-1, Math.min(1, (point.x + .5) / width * 2 - 1));
|
|
1274
|
+
pointerY = Math.max(-1, Math.min(1, (point.y + .5) / height * 2 - 1));
|
|
1275
|
+
}
|
|
1276
|
+
function hitTest() {
|
|
1277
|
+
if (!renderer.hitTest) return null;
|
|
1278
|
+
try {
|
|
1279
|
+
return renderer.hitTest({
|
|
1280
|
+
pointerX,
|
|
1281
|
+
pointerY,
|
|
1282
|
+
pixelWidth: actualPixelWidth,
|
|
1283
|
+
pixelHeight: actualPixelHeight,
|
|
1284
|
+
cellWidth: Math.max(1, Math.floor(props.w)),
|
|
1285
|
+
cellHeight: Math.max(1, Math.floor(props.h)),
|
|
1286
|
+
motion: motionSnapshot()
|
|
1287
|
+
});
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
emit("error", error);
|
|
1290
|
+
return null;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
function updateHoveredObject() {
|
|
1294
|
+
if (renderer.hitTest) setHoveredObject(hitTest());
|
|
1295
|
+
}
|
|
1296
|
+
function onPointerEnter(event) {
|
|
1297
|
+
if (!props.interactive) return;
|
|
1298
|
+
const point = localCell(event);
|
|
1299
|
+
hovering = true;
|
|
1300
|
+
updatePointerPosition(point);
|
|
1301
|
+
pointer = {
|
|
1302
|
+
...point,
|
|
1303
|
+
timeMs: eventTime(event)
|
|
1304
|
+
};
|
|
1305
|
+
updateHoveredObject();
|
|
1306
|
+
}
|
|
1307
|
+
function onPointerDown(event) {
|
|
1308
|
+
if (!props.interactive || event.button != null && event.button !== 0) return;
|
|
1309
|
+
event.preventDefault();
|
|
1310
|
+
event.stopPropagation();
|
|
1311
|
+
const point = localCell(event);
|
|
1312
|
+
const timeMs = eventTime(event);
|
|
1313
|
+
hovering = true;
|
|
1314
|
+
updatePointerPosition(point);
|
|
1315
|
+
yawVelocity = 0;
|
|
1316
|
+
pitchVelocity = 0;
|
|
1317
|
+
pointerSpeed = 0;
|
|
1318
|
+
pointer = {
|
|
1319
|
+
...point,
|
|
1320
|
+
timeMs
|
|
1321
|
+
};
|
|
1322
|
+
drag = {
|
|
1323
|
+
...point,
|
|
1324
|
+
timeMs,
|
|
1325
|
+
moved: false
|
|
1326
|
+
};
|
|
1327
|
+
updateHoveredObject();
|
|
1328
|
+
}
|
|
1329
|
+
function onPointerMove(event) {
|
|
1330
|
+
if (!props.interactive) return;
|
|
1331
|
+
hovering = true;
|
|
1332
|
+
const point = localCell(event);
|
|
1333
|
+
updatePointerPosition(point);
|
|
1334
|
+
const timeMs = eventTime(event);
|
|
1335
|
+
const previous = drag ?? pointer;
|
|
1336
|
+
pointer = {
|
|
1337
|
+
...point,
|
|
1338
|
+
timeMs
|
|
1339
|
+
};
|
|
1340
|
+
if (!previous) return;
|
|
1341
|
+
const dtMs = Math.max(1, timeMs - previous.timeMs);
|
|
1342
|
+
const dx = point.x - previous.x;
|
|
1343
|
+
const dy = point.y - previous.y;
|
|
1344
|
+
const sensitivity = finite(props.pointerSensitivity, DEFAULT_POINTER_SENSITIVITY);
|
|
1345
|
+
const deltaYaw = dx * sensitivity;
|
|
1346
|
+
const deltaPitch = dy * sensitivity;
|
|
1347
|
+
pointerSpeed = Math.hypot(dx, dy) * 1e3 / dtMs;
|
|
1348
|
+
if (!drag) {
|
|
1349
|
+
if (renderer.hitTest) {
|
|
1350
|
+
yawVelocity = 0;
|
|
1351
|
+
pitchVelocity = 0;
|
|
1352
|
+
updateHoveredObject();
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
yawVelocity = deltaYaw * 650 / dtMs;
|
|
1356
|
+
pitchVelocity = deltaPitch * 450 / dtMs;
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
if (event.buttons === 0) {
|
|
1360
|
+
drag = null;
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
event.preventDefault();
|
|
1364
|
+
event.stopPropagation();
|
|
1365
|
+
yaw += deltaYaw;
|
|
1366
|
+
pitch = clampPitch(pitch + deltaPitch);
|
|
1367
|
+
yawVelocity = deltaYaw * 1e3 / dtMs;
|
|
1368
|
+
pitchVelocity = deltaPitch * 1e3 / dtMs;
|
|
1369
|
+
setHoveredObject(null);
|
|
1370
|
+
drag = {
|
|
1371
|
+
...point,
|
|
1372
|
+
timeMs,
|
|
1373
|
+
moved: drag.moved || dx !== 0 || dy !== 0
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
function onPointerUp(event) {
|
|
1377
|
+
if (!drag) return;
|
|
1378
|
+
event.preventDefault();
|
|
1379
|
+
event.stopPropagation();
|
|
1380
|
+
const moved = drag.moved;
|
|
1381
|
+
drag = null;
|
|
1382
|
+
if (!moved && renderer.hitTest) {
|
|
1383
|
+
const hit = hitTest();
|
|
1384
|
+
setHoveredObject(hit);
|
|
1385
|
+
setSelectedObject(hit);
|
|
1386
|
+
} else updateHoveredObject();
|
|
1387
|
+
}
|
|
1388
|
+
function onPointerLeave(event) {
|
|
1389
|
+
if (drag) {
|
|
1390
|
+
event.preventDefault();
|
|
1391
|
+
event.stopPropagation();
|
|
1392
|
+
drag = null;
|
|
1393
|
+
}
|
|
1394
|
+
hovering = false;
|
|
1395
|
+
pointer = null;
|
|
1396
|
+
setHoveredObject(null);
|
|
1397
|
+
}
|
|
1398
|
+
function onWheel(event) {
|
|
1399
|
+
if (!props.interactive) return;
|
|
1400
|
+
const delta = normalizedWheelDelta(event);
|
|
1401
|
+
const sensitivity = Math.max(0, finite(props.zoomSensitivity, DEFAULT_ZOOM_SENSITIVITY));
|
|
1402
|
+
if (delta === 0 || sensitivity === 0) return;
|
|
1403
|
+
event.preventDefault();
|
|
1404
|
+
event.stopPropagation();
|
|
1405
|
+
zoomVelocity = Math.max(-MAX_ZOOM_VELOCITY, Math.min(MAX_ZOOM_VELOCITY, zoomVelocity - delta * sensitivity * 6));
|
|
1406
|
+
}
|
|
1407
|
+
function advanceZoom(deltaMs) {
|
|
1408
|
+
if (deltaMs <= 0 || zoomVelocity === 0) return;
|
|
1409
|
+
const dt = Math.min(deltaMs, 100) / 1e3;
|
|
1410
|
+
const next = zoom + zoomVelocity * dt;
|
|
1411
|
+
zoom = clampZoom(next, props.minZoom, props.maxZoom);
|
|
1412
|
+
if (zoom !== next) {
|
|
1413
|
+
zoomVelocity = 0;
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
zoomVelocity *= Math.exp(-ZOOM_DAMPING_PER_SECOND * dt);
|
|
1417
|
+
if (Math.abs(zoomVelocity) < .001) zoomVelocity = 0;
|
|
1418
|
+
}
|
|
1419
|
+
function advanceMotion(deltaMs) {
|
|
1420
|
+
if (deltaMs <= 0) return;
|
|
1421
|
+
advanceZoom(deltaMs);
|
|
1422
|
+
if (drag) return;
|
|
1423
|
+
const dt = Math.min(deltaMs, 100) / 1e3;
|
|
1424
|
+
if (hovering && !renderer.hitTest) {
|
|
1425
|
+
const targetPitch = clampPitch(-pointerY * .36);
|
|
1426
|
+
pitchVelocity += (targetPitch - pitch) * 4.5 * dt;
|
|
1427
|
+
} else if (!hovering) {
|
|
1428
|
+
pointerX *= Math.exp(-7 * dt);
|
|
1429
|
+
pointerY *= Math.exp(-7 * dt);
|
|
1430
|
+
}
|
|
1431
|
+
yaw += yawVelocity * dt;
|
|
1432
|
+
pitch = clampPitch(pitch + pitchVelocity * dt);
|
|
1433
|
+
const inspectingObject = Boolean(renderer.hitTest && (hoveredObject || selectedObject));
|
|
1434
|
+
if (props.autoRotate && !inspectingObject) {
|
|
1435
|
+
const baseSpeed = finite(props.autoRotateSpeed, DEFAULT_AUTO_ROTATE_SPEED);
|
|
1436
|
+
const steering = hovering ? .85 + pointerX * 1.75 : 1;
|
|
1437
|
+
yaw += baseSpeed * steering * dt;
|
|
1438
|
+
}
|
|
1439
|
+
const decay = Math.exp(-VELOCITY_DAMPING_PER_SECOND * dt);
|
|
1440
|
+
yawVelocity *= decay;
|
|
1441
|
+
pitchVelocity *= decay;
|
|
1442
|
+
pointerSpeed *= decay;
|
|
1443
|
+
}
|
|
1444
|
+
const frameSource = async function* (videoContext) {
|
|
1445
|
+
const startedAt = nowMs();
|
|
1446
|
+
let previousAt = startedAt;
|
|
1447
|
+
let frame = 0;
|
|
1448
|
+
const frameIntervalMs = 1e3 / Math.max(1, finite(videoContext.maxFps, DEFAULT_MAX_FPS));
|
|
1449
|
+
while (alive && !videoContext.signal.aborted) {
|
|
1450
|
+
const pulledAt = nowMs();
|
|
1451
|
+
const deltaMs = frame === 0 ? 0 : Math.max(0, pulledAt - previousAt);
|
|
1452
|
+
advanceMotion(deltaMs);
|
|
1453
|
+
actualPixelWidth = videoContext.pixelWidth;
|
|
1454
|
+
actualPixelHeight = videoContext.pixelHeight;
|
|
1455
|
+
const result = await renderFrame({
|
|
1456
|
+
timeMs: Math.max(0, pulledAt - startedAt),
|
|
1457
|
+
deltaMs,
|
|
1458
|
+
frame,
|
|
1459
|
+
pixelWidth: videoContext.pixelWidth,
|
|
1460
|
+
pixelHeight: videoContext.pixelHeight,
|
|
1461
|
+
format: videoContext.preferredFormat,
|
|
1462
|
+
signal: videoContext.signal,
|
|
1463
|
+
motion: motionSnapshot()
|
|
1464
|
+
});
|
|
1465
|
+
if (!alive || videoContext.signal.aborted) return;
|
|
1466
|
+
yield result;
|
|
1467
|
+
frame++;
|
|
1468
|
+
previousAt = pulledAt;
|
|
1469
|
+
await waitFor(frameIntervalMs - (nowMs() - pulledAt), videoContext.signal);
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
watch(() => props.renderer, (next) => {
|
|
1473
|
+
if (next === renderer) return;
|
|
1474
|
+
warnDev("[vue-tui] T3DViewport renderer is init-only. Remount T3DViewport to replace it.");
|
|
1475
|
+
});
|
|
1476
|
+
watch(() => props.interactive, (interactive) => {
|
|
1477
|
+
if (!interactive) {
|
|
1478
|
+
hovering = false;
|
|
1479
|
+
pointer = null;
|
|
1480
|
+
drag = null;
|
|
1481
|
+
setHoveredObject(null);
|
|
1482
|
+
setSelectedObject(null);
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
onBeforeUnmount(() => {
|
|
1486
|
+
alive = false;
|
|
1487
|
+
pointer = null;
|
|
1488
|
+
drag = null;
|
|
1489
|
+
});
|
|
1490
|
+
onUnmounted(() => renderer.dispose?.());
|
|
1491
|
+
return () => h("span", [h(TVideo, {
|
|
1492
|
+
x: props.x,
|
|
1493
|
+
y: props.y,
|
|
1494
|
+
w: props.w,
|
|
1495
|
+
h: props.h,
|
|
1496
|
+
zIndex: props.zIndex,
|
|
1497
|
+
src: "t3d://viewport",
|
|
1498
|
+
frameSource,
|
|
1499
|
+
paused: props.paused,
|
|
1500
|
+
maxFps: props.maxFps,
|
|
1501
|
+
pixelWidth: props.pixelWidth,
|
|
1502
|
+
pixelHeight: props.pixelHeight,
|
|
1503
|
+
fallback: props.fallback,
|
|
1504
|
+
style: props.style,
|
|
1505
|
+
clear: props.clear,
|
|
1506
|
+
onFrame: (event) => emit("frame", event),
|
|
1507
|
+
onError: (error) => emit("error", error)
|
|
1508
|
+
}), props.interactive ? h(TView, {
|
|
1509
|
+
x: props.x,
|
|
1510
|
+
y: props.y,
|
|
1511
|
+
w: Math.max(0, Math.floor(props.w)),
|
|
1512
|
+
h: Math.max(0, Math.floor(props.h)),
|
|
1513
|
+
zIndex: props.zIndex + 1,
|
|
1514
|
+
selectable: false,
|
|
1515
|
+
onPointerenter: onPointerEnter,
|
|
1516
|
+
onPointermove: onPointerMove,
|
|
1517
|
+
onPointerleave: onPointerLeave,
|
|
1518
|
+
onPointerdown: onPointerDown,
|
|
1519
|
+
onPointerup: onPointerUp,
|
|
1520
|
+
onWheel
|
|
1521
|
+
}) : null]);
|
|
1522
|
+
}
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
//#endregion
|
|
1526
|
+
//#region src/vue/components/TCharts.ts
|
|
1527
|
+
const DEFAULT_HEATMAP_LEVEL_STYLES = Object.freeze([
|
|
1528
|
+
Object.freeze({ fg: "#9be9a8" }),
|
|
1529
|
+
Object.freeze({ fg: "#40c463" }),
|
|
1530
|
+
Object.freeze({ fg: "#30a14e" }),
|
|
1531
|
+
Object.freeze({ fg: "#216e39" })
|
|
1532
|
+
]);
|
|
1533
|
+
const DEFAULT_PIE_SEGMENT_STYLES = Object.freeze([
|
|
1534
|
+
Object.freeze({ fg: "cyanBright" }),
|
|
1535
|
+
Object.freeze({ fg: "magentaBright" }),
|
|
1536
|
+
Object.freeze({ fg: "yellowBright" }),
|
|
1537
|
+
Object.freeze({ fg: "greenBright" }),
|
|
1538
|
+
Object.freeze({ fg: "blueBright" }),
|
|
1539
|
+
Object.freeze({ fg: "redBright" })
|
|
1540
|
+
]);
|
|
1541
|
+
const TERMINAL_CELL_HEIGHT_TO_WIDTH = 1.8;
|
|
1542
|
+
const PIE_QUADRANT_BLOCKS = Object.freeze([
|
|
1543
|
+
" ",
|
|
1544
|
+
"▘",
|
|
1545
|
+
"▝",
|
|
1546
|
+
"▀",
|
|
1547
|
+
"▖",
|
|
1548
|
+
"▌",
|
|
1549
|
+
"▞",
|
|
1550
|
+
"▛",
|
|
1551
|
+
"▗",
|
|
1552
|
+
"▚",
|
|
1553
|
+
"▐",
|
|
1554
|
+
"▜",
|
|
1555
|
+
"▄",
|
|
1556
|
+
"▙",
|
|
1557
|
+
"▟",
|
|
1558
|
+
"█"
|
|
1559
|
+
]);
|
|
1560
|
+
const PIE_CELL_SAMPLES = Object.freeze([
|
|
1561
|
+
Object.freeze({
|
|
1562
|
+
dx: .25,
|
|
1563
|
+
dy: .25,
|
|
1564
|
+
bit: 1
|
|
1565
|
+
}),
|
|
1566
|
+
Object.freeze({
|
|
1567
|
+
dx: .75,
|
|
1568
|
+
dy: .25,
|
|
1569
|
+
bit: 2
|
|
1570
|
+
}),
|
|
1571
|
+
Object.freeze({
|
|
1572
|
+
dx: .25,
|
|
1573
|
+
dy: .75,
|
|
1574
|
+
bit: 4
|
|
1575
|
+
}),
|
|
1576
|
+
Object.freeze({
|
|
1577
|
+
dx: .75,
|
|
1578
|
+
dy: .75,
|
|
1579
|
+
bit: 8
|
|
1580
|
+
})
|
|
1581
|
+
]);
|
|
1582
|
+
function cellCount(value, fallback) {
|
|
1583
|
+
const n = Math.floor(Number(value ?? fallback));
|
|
1584
|
+
return Number.isFinite(n) ? Math.max(0, n) : Math.max(0, Math.floor(fallback));
|
|
1585
|
+
}
|
|
1586
|
+
function finiteRange(values) {
|
|
1587
|
+
let min = Number.POSITIVE_INFINITY;
|
|
1588
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
1589
|
+
for (const raw of values) {
|
|
1590
|
+
const value = Number(raw);
|
|
1591
|
+
if (!Number.isFinite(value)) continue;
|
|
1592
|
+
if (value < min) min = value;
|
|
1593
|
+
if (value > max) max = value;
|
|
1594
|
+
}
|
|
1595
|
+
return min === Number.POSITIVE_INFINITY ? null : {
|
|
1596
|
+
min,
|
|
1597
|
+
max
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
function maxPositive(values) {
|
|
1601
|
+
let max = 0;
|
|
1602
|
+
for (const raw of values) {
|
|
1603
|
+
const value = Number(raw);
|
|
1604
|
+
if (Number.isFinite(value) && value > max) max = value;
|
|
1605
|
+
}
|
|
1606
|
+
return max;
|
|
1607
|
+
}
|
|
1608
|
+
function domainFromValues(values, minValue, maxValue) {
|
|
1609
|
+
const range = finiteRange(values);
|
|
1610
|
+
const fallbackMin = range?.min ?? 0;
|
|
1611
|
+
const fallbackMax = range?.max ?? 0;
|
|
1612
|
+
const hasMin = Number.isFinite(Number(minValue));
|
|
1613
|
+
const hasMax = Number.isFinite(Number(maxValue));
|
|
1614
|
+
const min = hasMin ? Number(minValue) : fallbackMin;
|
|
1615
|
+
const max = hasMax ? Number(maxValue) : fallbackMax;
|
|
1616
|
+
if (max < min) {
|
|
1617
|
+
if (hasMin && !hasMax) return {
|
|
1618
|
+
min,
|
|
1619
|
+
max: min
|
|
1620
|
+
};
|
|
1621
|
+
if (!hasMin && hasMax) return {
|
|
1622
|
+
min: max,
|
|
1623
|
+
max
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
return max >= min ? {
|
|
1627
|
+
min,
|
|
1628
|
+
max
|
|
1629
|
+
} : {
|
|
1630
|
+
min: max,
|
|
1631
|
+
max: min
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
function normalizeCandlestick(candle) {
|
|
1635
|
+
const open = Number(candle.open);
|
|
1636
|
+
const high = Number(candle.high);
|
|
1637
|
+
const low = Number(candle.low);
|
|
1638
|
+
const close = Number(candle.close);
|
|
1639
|
+
if (!Number.isFinite(open) || !Number.isFinite(high) || !Number.isFinite(low) || !Number.isFinite(close)) return null;
|
|
1640
|
+
return {
|
|
1641
|
+
open,
|
|
1642
|
+
high,
|
|
1643
|
+
low,
|
|
1644
|
+
close
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
function candlestickDomainValues(candles) {
|
|
1648
|
+
return candles.flatMap((candle) => {
|
|
1649
|
+
const values = normalizeCandlestick(candle);
|
|
1650
|
+
return values ? [
|
|
1651
|
+
values.open,
|
|
1652
|
+
values.low,
|
|
1653
|
+
values.high,
|
|
1654
|
+
values.close
|
|
1655
|
+
] : [];
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
function finiteLineRuns(values) {
|
|
1659
|
+
const runs = [];
|
|
1660
|
+
let points = [];
|
|
1661
|
+
for (let originalIndex = 0; originalIndex < values.length; originalIndex++) {
|
|
1662
|
+
const raw = values[originalIndex];
|
|
1663
|
+
const value = Number(raw);
|
|
1664
|
+
if (Number.isFinite(value)) {
|
|
1665
|
+
points.push({
|
|
1666
|
+
value,
|
|
1667
|
+
originalIndex
|
|
1668
|
+
});
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
if (points.length > 0) {
|
|
1672
|
+
runs.push({ points });
|
|
1673
|
+
points = [];
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
if (points.length > 0) runs.push({ points });
|
|
1677
|
+
return runs;
|
|
1678
|
+
}
|
|
1679
|
+
function valueToY(value, min, max, height) {
|
|
1680
|
+
if (height <= 1) return 0;
|
|
1681
|
+
if (max === min) {
|
|
1682
|
+
if (value < min) return height - 1;
|
|
1683
|
+
if (value > max) return 0;
|
|
1684
|
+
return Math.floor((height - 1) / 2);
|
|
1685
|
+
}
|
|
1686
|
+
const scale = Math.max(Math.abs(min), Math.abs(max), Math.abs(value), 1);
|
|
1687
|
+
const scaledMin = min / scale;
|
|
1688
|
+
const scaledMax = max / scale;
|
|
1689
|
+
const ratio = (value / scale - scaledMin) / (scaledMax - scaledMin);
|
|
1690
|
+
return Math.max(0, Math.min(height - 1, Math.round((1 - ratio) * (height - 1))));
|
|
1691
|
+
}
|
|
1692
|
+
function yToValue(y, min, max, height) {
|
|
1693
|
+
if (height <= 1 || max === min) return max;
|
|
1694
|
+
const ratio = 1 - Math.max(0, Math.min(height - 1, y)) / (height - 1);
|
|
1695
|
+
const scale = Math.max(Math.abs(min), Math.abs(max), 1);
|
|
1696
|
+
const scaledMin = min / scale;
|
|
1697
|
+
const scaledMax = max / scale;
|
|
1698
|
+
return (scaledMin + ratio * (scaledMax - scaledMin)) * scale;
|
|
1699
|
+
}
|
|
1700
|
+
function emptyChartRows(height) {
|
|
1701
|
+
return Array.from({ length: height }, () => []);
|
|
1702
|
+
}
|
|
1703
|
+
function samplePositionAtX(x, width, sourceLength) {
|
|
1704
|
+
if (width <= 1 || sourceLength <= 1) return 0;
|
|
1705
|
+
return x / (width - 1) * (sourceLength - 1);
|
|
1706
|
+
}
|
|
1707
|
+
function lineXForOriginalIndex(originalIndex, plotX, plotW, sourceLength) {
|
|
1708
|
+
if (plotW <= 1 || sourceLength <= 1) return plotX;
|
|
1709
|
+
return plotX + Math.round(originalIndex / (sourceLength - 1) * (plotW - 1));
|
|
1710
|
+
}
|
|
1711
|
+
function lineBucketIndexRange(plotX, plotW, sourceLength) {
|
|
1712
|
+
if (sourceLength <= 0) return {
|
|
1713
|
+
startIndex: 0,
|
|
1714
|
+
endIndex: -1
|
|
1715
|
+
};
|
|
1716
|
+
if (plotW <= 1 || sourceLength <= 1) return {
|
|
1717
|
+
startIndex: 0,
|
|
1718
|
+
endIndex: sourceLength - 1
|
|
1719
|
+
};
|
|
1720
|
+
let startIndex = 0;
|
|
1721
|
+
let high = sourceLength;
|
|
1722
|
+
while (startIndex < high) {
|
|
1723
|
+
const mid = Math.floor((startIndex + high) / 2);
|
|
1724
|
+
if (lineXForOriginalIndex(mid, 0, plotW, sourceLength) < plotX) startIndex = mid + 1;
|
|
1725
|
+
else high = mid;
|
|
1726
|
+
}
|
|
1727
|
+
let endExclusive = startIndex;
|
|
1728
|
+
high = sourceLength;
|
|
1729
|
+
while (endExclusive < high) {
|
|
1730
|
+
const mid = Math.floor((endExclusive + high) / 2);
|
|
1731
|
+
if (lineXForOriginalIndex(mid, 0, plotW, sourceLength) <= plotX) endExclusive = mid + 1;
|
|
1732
|
+
else high = mid;
|
|
1733
|
+
}
|
|
1734
|
+
return endExclusive <= startIndex ? {
|
|
1735
|
+
startIndex: 0,
|
|
1736
|
+
endIndex: -1
|
|
1737
|
+
} : {
|
|
1738
|
+
startIndex,
|
|
1739
|
+
endIndex: endExclusive - 1
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
function lowerBoundLinePoint(points, position) {
|
|
1743
|
+
let low = 0;
|
|
1744
|
+
let high = points.length;
|
|
1745
|
+
while (low < high) {
|
|
1746
|
+
const mid = Math.floor((low + high) / 2);
|
|
1747
|
+
if (points[mid].originalIndex < position) low = mid + 1;
|
|
1748
|
+
else high = mid;
|
|
1749
|
+
}
|
|
1750
|
+
return low;
|
|
1751
|
+
}
|
|
1752
|
+
function lineValueAtPosition(points, position) {
|
|
1753
|
+
if (!points.length) return null;
|
|
1754
|
+
const first = points[0];
|
|
1755
|
+
const last = points[points.length - 1];
|
|
1756
|
+
if (position < first.originalIndex || position > last.originalIndex) return null;
|
|
1757
|
+
if (points.length === 1) return first.value;
|
|
1758
|
+
const index = Math.min(Math.max(1, lowerBoundLinePoint(points, position)), points.length - 1);
|
|
1759
|
+
const previous = points[index - 1];
|
|
1760
|
+
const current = points[index];
|
|
1761
|
+
const span = current.originalIndex - previous.originalIndex;
|
|
1762
|
+
if (span <= 0) return current.value;
|
|
1763
|
+
const ratio = (position - previous.originalIndex) / span;
|
|
1764
|
+
return previous.value * (1 - ratio) + current.value * ratio;
|
|
1765
|
+
}
|
|
1766
|
+
function lowerBoundLineRun(runs, position) {
|
|
1767
|
+
let low = 0;
|
|
1768
|
+
let high = runs.length;
|
|
1769
|
+
while (low < high) {
|
|
1770
|
+
const mid = Math.floor((low + high) / 2);
|
|
1771
|
+
const points = runs[mid].points;
|
|
1772
|
+
if (points[points.length - 1].originalIndex < position) low = mid + 1;
|
|
1773
|
+
else high = mid;
|
|
1774
|
+
}
|
|
1775
|
+
return low;
|
|
1776
|
+
}
|
|
1777
|
+
function lineRunAtPosition(runs, position) {
|
|
1778
|
+
const run = runs[lowerBoundLineRun(runs, position)];
|
|
1779
|
+
if (!run) return null;
|
|
1780
|
+
const first = run.points[0];
|
|
1781
|
+
const last = run.points[run.points.length - 1];
|
|
1782
|
+
return position >= first.originalIndex && position <= last.originalIndex ? run : null;
|
|
1783
|
+
}
|
|
1784
|
+
function nearestLinePoint(points, position) {
|
|
1785
|
+
if (!points.length) return null;
|
|
1786
|
+
const index = lowerBoundLinePoint(points, position);
|
|
1787
|
+
if (index <= 0) return points[0];
|
|
1788
|
+
if (index >= points.length) return points[points.length - 1];
|
|
1789
|
+
const previous = points[index - 1];
|
|
1790
|
+
const current = points[index];
|
|
1791
|
+
return position - previous.originalIndex <= current.originalIndex - position ? previous : current;
|
|
1792
|
+
}
|
|
1793
|
+
function nearestLinePointAtPlotX(runs, plotX, plotW, sourceLength, position) {
|
|
1794
|
+
const { startIndex, endIndex } = lineBucketIndexRange(plotX, plotW, sourceLength);
|
|
1795
|
+
if (endIndex < startIndex) return null;
|
|
1796
|
+
let best = null;
|
|
1797
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
1798
|
+
for (let runIndex = lowerBoundLineRun(runs, startIndex); runIndex < runs.length; runIndex++) {
|
|
1799
|
+
const points = runs[runIndex].points;
|
|
1800
|
+
const first = points[0];
|
|
1801
|
+
const last = points[points.length - 1];
|
|
1802
|
+
if (first.originalIndex > endIndex) break;
|
|
1803
|
+
if (last.originalIndex < startIndex) continue;
|
|
1804
|
+
for (let pointIndex = lowerBoundLinePoint(points, startIndex); pointIndex < points.length; pointIndex++) {
|
|
1805
|
+
const point = points[pointIndex];
|
|
1806
|
+
if (point.originalIndex > endIndex) break;
|
|
1807
|
+
if (lineXForOriginalIndex(point.originalIndex, 0, plotW, sourceLength) !== plotX) continue;
|
|
1808
|
+
const distance = Math.abs(point.originalIndex - position);
|
|
1809
|
+
if (distance < bestDistance) {
|
|
1810
|
+
best = point;
|
|
1811
|
+
bestDistance = distance;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
return best;
|
|
1816
|
+
}
|
|
1817
|
+
function nearestLinePointInRuns(runs, position, plotX, plotW, sourceLength) {
|
|
1818
|
+
const run = lineRunAtPosition(runs, position);
|
|
1819
|
+
if (run && run.points.length > 1) return nearestLinePoint(run.points, position);
|
|
1820
|
+
return nearestLinePointAtPlotX(runs, plotX, plotW, sourceLength, position);
|
|
1821
|
+
}
|
|
1822
|
+
function lineBucketsForRun(points, plotW, sourceLength) {
|
|
1823
|
+
const buckets = [];
|
|
1824
|
+
let current = null;
|
|
1825
|
+
for (const point of points) {
|
|
1826
|
+
const x = lineXForOriginalIndex(point.originalIndex, 0, plotW, sourceLength);
|
|
1827
|
+
if (x < 0 || x >= plotW) continue;
|
|
1828
|
+
if (!current || current.x !== x) {
|
|
1829
|
+
if (current) buckets.push(current);
|
|
1830
|
+
current = {
|
|
1831
|
+
x,
|
|
1832
|
+
minValue: point.value,
|
|
1833
|
+
maxValue: point.value,
|
|
1834
|
+
firstIndex: point.originalIndex,
|
|
1835
|
+
firstValue: point.value,
|
|
1836
|
+
lastIndex: point.originalIndex,
|
|
1837
|
+
lastValue: point.value
|
|
1838
|
+
};
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
if (point.value < current.minValue) current.minValue = point.value;
|
|
1842
|
+
if (point.value > current.maxValue) current.maxValue = point.value;
|
|
1843
|
+
current.lastIndex = point.originalIndex;
|
|
1844
|
+
current.lastValue = point.value;
|
|
1845
|
+
}
|
|
1846
|
+
if (current) buckets.push(current);
|
|
1847
|
+
return buckets;
|
|
1848
|
+
}
|
|
1849
|
+
function lineBucketRepresentativeValue(points, bucket, plotW, sourceLength) {
|
|
1850
|
+
const position = samplePositionAtX(bucket.x, plotW, sourceLength);
|
|
1851
|
+
if (position <= bucket.firstIndex) return bucket.firstValue;
|
|
1852
|
+
if (position >= bucket.lastIndex) return bucket.lastValue;
|
|
1853
|
+
return lineValueAtPosition(points, position) ?? bucket.lastValue;
|
|
1854
|
+
}
|
|
1855
|
+
function lineSegmentPointsForRun(points, buckets, plotW, sourceLength) {
|
|
1856
|
+
const valuesByX = new Map();
|
|
1857
|
+
const first = points[0];
|
|
1858
|
+
const last = points[points.length - 1];
|
|
1859
|
+
if (first && last) if (plotW <= 1 || sourceLength <= 1) {
|
|
1860
|
+
const value = lineValueAtPosition(points, 0);
|
|
1861
|
+
if (value != null) valuesByX.set(0, value);
|
|
1862
|
+
} else {
|
|
1863
|
+
const scale = (sourceLength - 1) / (plotW - 1);
|
|
1864
|
+
const startX = Math.max(0, Math.ceil(first.originalIndex / scale));
|
|
1865
|
+
const endX = Math.min(plotW - 1, Math.floor(last.originalIndex / scale));
|
|
1866
|
+
for (let x = startX; x <= endX; x++) {
|
|
1867
|
+
const value = lineValueAtPosition(points, samplePositionAtX(x, plotW, sourceLength));
|
|
1868
|
+
if (value != null) valuesByX.set(x, value);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
for (const bucket of buckets) if (!valuesByX.has(bucket.x)) valuesByX.set(bucket.x, lineBucketRepresentativeValue(points, bucket, plotW, sourceLength));
|
|
1872
|
+
return Array.from(valuesByX, ([x, value]) => ({
|
|
1873
|
+
x,
|
|
1874
|
+
value
|
|
1875
|
+
})).sort((a, b) => a.x - b.x);
|
|
1876
|
+
}
|
|
1877
|
+
function putLineGlyph(cells, x, y, ch, style, width, height, occupied) {
|
|
1878
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
|
|
1879
|
+
if (x < 0 || x >= width || y < 0 || y >= height) return;
|
|
1880
|
+
cells.push({
|
|
1881
|
+
x,
|
|
1882
|
+
y,
|
|
1883
|
+
ch,
|
|
1884
|
+
style
|
|
1885
|
+
});
|
|
1886
|
+
occupied?.add(y * width + x);
|
|
1887
|
+
}
|
|
1888
|
+
function pushLineBucketExtrema(cells, bucket, layout, min, max, style, width, height, occupied) {
|
|
1889
|
+
const x = layout.plotX + bucket.x;
|
|
1890
|
+
const minY = valueToY(bucket.minValue, min, max, layout.plotH);
|
|
1891
|
+
const maxY = valueToY(bucket.maxValue, min, max, layout.plotH);
|
|
1892
|
+
const top = Math.min(minY, maxY);
|
|
1893
|
+
const bottom = Math.max(minY, maxY);
|
|
1894
|
+
if (top === bottom) {
|
|
1895
|
+
const y = layout.plotY + top;
|
|
1896
|
+
if (!occupied.has(y * width + x)) putLineGlyph(cells, x, y, "●", style, width, height, occupied);
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
for (let y = top; y <= bottom; y++) {
|
|
1900
|
+
const cellY = layout.plotY + y;
|
|
1901
|
+
if (!occupied.has(cellY * width + x)) putLineGlyph(cells, x, cellY, "│", style, width, height, occupied);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
function drawSteppedLineSegment(cells, fromX, fromY, toX, toY, style, width, height, occupied) {
|
|
1905
|
+
if (toY === fromY) {
|
|
1906
|
+
for (let x = fromX + 1; x <= toX; x++) putLineGlyph(cells, x, toY, "─", style, width, height, occupied);
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
const rising = toY < fromY;
|
|
1910
|
+
for (let x = fromX + 1; x < toX; x++) putLineGlyph(cells, x, fromY, "─", style, width, height, occupied);
|
|
1911
|
+
putLineGlyph(cells, toX, fromY, rising ? "╯" : "╮", style, width, height, occupied);
|
|
1912
|
+
const top = Math.min(fromY, toY);
|
|
1913
|
+
const bottom = Math.max(fromY, toY);
|
|
1914
|
+
for (let y = top + 1; y < bottom; y++) putLineGlyph(cells, toX, y, "│", style, width, height, occupied);
|
|
1915
|
+
putLineGlyph(cells, toX, toY, rising ? "╭" : "╰", style, width, height, occupied);
|
|
1916
|
+
}
|
|
1917
|
+
function chartGlyph(value, fallback) {
|
|
1918
|
+
const text = sliceByCells(sanitizeInlineText(value || fallback), 1);
|
|
1919
|
+
return text && textCellWidth(text) === 1 ? text : fallback;
|
|
1920
|
+
}
|
|
1921
|
+
function formatChartValue(value) {
|
|
1922
|
+
if (!Number.isFinite(value)) return "0";
|
|
1923
|
+
const abs = Math.abs(value);
|
|
1924
|
+
if (abs >= 1e6) return `${(value / 1e6).toFixed(1)}m`;
|
|
1925
|
+
if (abs >= 1e4) return `${Math.round(value / 1e3)}k`;
|
|
1926
|
+
if (abs >= 1e3) return `${(value / 1e3).toFixed(1)}k`;
|
|
1927
|
+
if (Number.isInteger(value)) return String(value);
|
|
1928
|
+
if (abs >= 100) return value.toFixed(0);
|
|
1929
|
+
if (abs >= 10) return value.toFixed(1);
|
|
1930
|
+
if (abs > 0 && abs < .01) return Number(value.toPrecision(2)).toString();
|
|
1931
|
+
return Number(value.toFixed(2)).toString();
|
|
1932
|
+
}
|
|
1933
|
+
function fitLabel(value, width) {
|
|
1934
|
+
return sliceByCells(sanitizeInlineText(value), Math.max(0, width));
|
|
1935
|
+
}
|
|
1936
|
+
function pushTextCells(cells, x, y, text, style, maxWidth = Number.POSITIVE_INFINITY) {
|
|
1937
|
+
const clipped = fitLabel(text, maxWidth);
|
|
1938
|
+
let cursor = x;
|
|
1939
|
+
forEachTextCellSegment(clipped, (segment) => {
|
|
1940
|
+
if (segment.cells <= 0) return;
|
|
1941
|
+
cells.push({
|
|
1942
|
+
x: cursor,
|
|
1943
|
+
y,
|
|
1944
|
+
ch: segment.text,
|
|
1945
|
+
style
|
|
1946
|
+
});
|
|
1947
|
+
cursor += segment.cells;
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
function chooseTooltipPlacement(cells, width, height, anchorX, anchorY, textWidth, preferredY, bounds) {
|
|
1951
|
+
const boundsX = Math.max(0, Math.min(width, Math.floor(bounds?.x ?? 0)));
|
|
1952
|
+
const boundsY = Math.max(0, Math.min(height, Math.floor(bounds?.y ?? 0)));
|
|
1953
|
+
const boundsRight = Math.max(boundsX, Math.min(width, Math.floor((bounds?.x ?? 0) + (bounds?.w ?? width))));
|
|
1954
|
+
const boundsBottom = Math.max(boundsY, Math.min(height, Math.floor((bounds?.y ?? 0) + (bounds?.h ?? height))));
|
|
1955
|
+
const availableW = boundsRight - boundsX;
|
|
1956
|
+
if (availableW <= 0 || boundsBottom <= boundsY) return {
|
|
1957
|
+
x: 0,
|
|
1958
|
+
y: 0
|
|
1959
|
+
};
|
|
1960
|
+
const tooltipWidth = Math.min(availableW, Math.max(1, textWidth));
|
|
1961
|
+
const clampX = (x) => Math.max(boundsX, Math.min(x, Math.max(boundsX, boundsRight - tooltipWidth)));
|
|
1962
|
+
const xCandidates = [
|
|
1963
|
+
anchorX + 2,
|
|
1964
|
+
anchorX - tooltipWidth - 2,
|
|
1965
|
+
boundsX,
|
|
1966
|
+
boundsRight - tooltipWidth
|
|
1967
|
+
].map(clampX).filter((x, index, values) => values.indexOf(x) === index);
|
|
1968
|
+
const yCandidates = [
|
|
1969
|
+
preferredY,
|
|
1970
|
+
anchorY > 0 ? anchorY - 1 : anchorY + 1,
|
|
1971
|
+
anchorY + 1,
|
|
1972
|
+
anchorY - 2,
|
|
1973
|
+
anchorY + 2,
|
|
1974
|
+
boundsY,
|
|
1975
|
+
boundsBottom - 1
|
|
1976
|
+
].filter((y) => y != null && y >= boundsY && y < boundsBottom);
|
|
1977
|
+
let best = {
|
|
1978
|
+
x: xCandidates[0] ?? 0,
|
|
1979
|
+
y: yCandidates[0] ?? 0,
|
|
1980
|
+
score: Number.POSITIVE_INFINITY
|
|
1981
|
+
};
|
|
1982
|
+
for (const y of yCandidates) for (const x of xCandidates) {
|
|
1983
|
+
let overlap = 0;
|
|
1984
|
+
for (const cell of cells) {
|
|
1985
|
+
if (cell.y !== y) continue;
|
|
1986
|
+
const cellW = Math.max(1, textCellWidth(cell.ch));
|
|
1987
|
+
if (cell.x < x + tooltipWidth && cell.x + cellW > x) overlap++;
|
|
1988
|
+
}
|
|
1989
|
+
const score = overlap * 1e3 + Math.abs(y - anchorY) + Math.abs(x - anchorX) / Math.max(1, availableW);
|
|
1990
|
+
if (score < best.score) best = {
|
|
1991
|
+
x,
|
|
1992
|
+
y,
|
|
1993
|
+
score
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
return {
|
|
1997
|
+
x: best.x,
|
|
1998
|
+
y: best.y
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
function useChartVisibleRect(props, size) {
|
|
2002
|
+
const layout = useLayout();
|
|
2003
|
+
return computed(() => {
|
|
2004
|
+
const full = translateRect({
|
|
2005
|
+
x: Math.floor(props.x),
|
|
2006
|
+
y: Math.floor(props.y),
|
|
2007
|
+
w: size.value.width,
|
|
2008
|
+
h: size.value.height
|
|
2009
|
+
}, layout.originX, layout.originY);
|
|
2010
|
+
const abs = layout.clipRect ? intersectRect(full, layout.clipRect) : full;
|
|
2011
|
+
if (!abs) return {
|
|
2012
|
+
x: 0,
|
|
2013
|
+
y: 0,
|
|
2014
|
+
w: 0,
|
|
2015
|
+
h: 0
|
|
2016
|
+
};
|
|
2017
|
+
return {
|
|
2018
|
+
x: abs.x - full.x,
|
|
2019
|
+
y: abs.y - full.y,
|
|
2020
|
+
w: abs.w,
|
|
2021
|
+
h: abs.h
|
|
2022
|
+
};
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
function resolveAxisLayout(width, height, min, max, showAxes) {
|
|
2026
|
+
if (!showAxes || width < 12 || height < 5) return {
|
|
2027
|
+
enabled: false,
|
|
2028
|
+
plotX: 0,
|
|
2029
|
+
plotY: 0,
|
|
2030
|
+
plotW: width,
|
|
2031
|
+
plotH: height,
|
|
2032
|
+
axisX: -1,
|
|
2033
|
+
axisY: height,
|
|
2034
|
+
minLabel: formatChartValue(min),
|
|
2035
|
+
maxLabel: formatChartValue(max)
|
|
2036
|
+
};
|
|
2037
|
+
const minLabel = fitLabel(formatChartValue(min), 7);
|
|
2038
|
+
const maxLabel = fitLabel(formatChartValue(max), 7);
|
|
2039
|
+
const labelW = Math.max(2, textCellWidth(minLabel), textCellWidth(maxLabel));
|
|
2040
|
+
const plotX = labelW + 1;
|
|
2041
|
+
const plotY = 0;
|
|
2042
|
+
const plotW = Math.max(0, width - plotX);
|
|
2043
|
+
const plotH = Math.max(0, height - 2);
|
|
2044
|
+
if (plotW < 2 || plotH < 2) return {
|
|
2045
|
+
enabled: false,
|
|
2046
|
+
plotX: 0,
|
|
2047
|
+
plotY: 0,
|
|
2048
|
+
plotW: width,
|
|
2049
|
+
plotH: height,
|
|
2050
|
+
axisX: -1,
|
|
2051
|
+
axisY: height,
|
|
2052
|
+
minLabel,
|
|
2053
|
+
maxLabel
|
|
2054
|
+
};
|
|
2055
|
+
return {
|
|
2056
|
+
enabled: true,
|
|
2057
|
+
plotX,
|
|
2058
|
+
plotY,
|
|
2059
|
+
plotW,
|
|
2060
|
+
plotH,
|
|
2061
|
+
axisX: plotX - 1,
|
|
2062
|
+
axisY: plotY + plotH,
|
|
2063
|
+
minLabel,
|
|
2064
|
+
maxLabel
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
function pushAxes(cells, layout, style, labelStyle, xLabel, yLabel, startLabel, endLabel) {
|
|
2068
|
+
if (!layout.enabled) return;
|
|
2069
|
+
pushTextCells(cells, 0, layout.plotY, layout.maxLabel, labelStyle, layout.axisX);
|
|
2070
|
+
pushTextCells(cells, 0, Math.max(layout.plotY, layout.axisY - 1), layout.minLabel, labelStyle, layout.axisX);
|
|
2071
|
+
for (let y = layout.plotY; y < layout.axisY; y++) cells.push({
|
|
2072
|
+
x: layout.axisX,
|
|
2073
|
+
y,
|
|
2074
|
+
ch: "│",
|
|
2075
|
+
style
|
|
2076
|
+
});
|
|
2077
|
+
cells.push({
|
|
2078
|
+
x: layout.axisX,
|
|
2079
|
+
y: layout.axisY,
|
|
2080
|
+
ch: "└",
|
|
2081
|
+
style
|
|
2082
|
+
});
|
|
2083
|
+
for (let x = layout.plotX; x < layout.plotX + layout.plotW; x++) cells.push({
|
|
2084
|
+
x,
|
|
2085
|
+
y: layout.axisY,
|
|
2086
|
+
ch: "─",
|
|
2087
|
+
style
|
|
2088
|
+
});
|
|
2089
|
+
if (yLabel) pushTextCells(cells, layout.plotX, layout.plotY, yLabel, labelStyle, layout.plotW);
|
|
2090
|
+
if (xLabel) {
|
|
2091
|
+
const xLabelW = textCellWidth(sanitizeInlineText(xLabel));
|
|
2092
|
+
const x = layout.plotX + Math.max(0, Math.floor((layout.plotW - xLabelW) / 2));
|
|
2093
|
+
pushTextCells(cells, x, layout.axisY + 1, xLabel, labelStyle, layout.plotW);
|
|
2094
|
+
} else {
|
|
2095
|
+
pushTextCells(cells, layout.plotX, layout.axisY + 1, startLabel, labelStyle, layout.plotW);
|
|
2096
|
+
const endW = textCellWidth(sanitizeInlineText(endLabel));
|
|
2097
|
+
const endX = layout.plotX + Math.max(0, layout.plotW - endW);
|
|
2098
|
+
pushTextCells(cells, endX, layout.axisY + 1, endLabel, labelStyle, layout.plotW);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
function createSurface(width, height, clearStyle, cells) {
|
|
2102
|
+
const rows = emptyChartRows(height);
|
|
2103
|
+
for (const cell of cells) {
|
|
2104
|
+
if (!Number.isFinite(cell.x) || !Number.isFinite(cell.y)) continue;
|
|
2105
|
+
if (cell.y < 0 || cell.y >= height || cell.x < 0 || cell.x >= width) continue;
|
|
2106
|
+
rows[cell.y].push(cell);
|
|
2107
|
+
}
|
|
2108
|
+
return {
|
|
2109
|
+
width,
|
|
2110
|
+
height,
|
|
2111
|
+
clearStyle,
|
|
2112
|
+
rows
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
function useChartRender(props, surface, options) {
|
|
2116
|
+
const { terminal } = useTerminal();
|
|
2117
|
+
const layout = useLayout();
|
|
2118
|
+
const { visible, rootProps } = useVisibility();
|
|
2119
|
+
const parentEventZ = inject(EventZIndexContextKey, computed(() => 0));
|
|
2120
|
+
const eventZ = computed(() => (parentEventZ.value ?? 0) + (props.zIndex ?? 0));
|
|
2121
|
+
const fullRect = computed(() => translateRect({
|
|
2122
|
+
x: Math.floor(props.x),
|
|
2123
|
+
y: Math.floor(props.y),
|
|
2124
|
+
w: surface.value.width,
|
|
2125
|
+
h: surface.value.height
|
|
2126
|
+
}, layout.originX, layout.originY));
|
|
2127
|
+
const absRect = computed(() => {
|
|
2128
|
+
const translated = fullRect.value;
|
|
2129
|
+
if (!layout.clipRect) return translated;
|
|
2130
|
+
return intersectRect(translated, layout.clipRect) ?? {
|
|
2131
|
+
x: 0,
|
|
2132
|
+
y: 0,
|
|
2133
|
+
w: 0,
|
|
2134
|
+
h: 0
|
|
2135
|
+
};
|
|
2136
|
+
});
|
|
2137
|
+
const dirtyRowsHint = computed(() => {
|
|
2138
|
+
if (!visible.value) return [];
|
|
2139
|
+
const r = absRect.value;
|
|
2140
|
+
if (r.w <= 0 || r.h <= 0) return [];
|
|
2141
|
+
return Array.from({ length: r.h }, (_, index) => r.y + index);
|
|
2142
|
+
});
|
|
2143
|
+
if (options?.handlers) {
|
|
2144
|
+
const handlers = options.handlers({
|
|
2145
|
+
fullRect,
|
|
2146
|
+
absRect
|
|
2147
|
+
});
|
|
2148
|
+
useTerminalNode(() => ({
|
|
2149
|
+
rect: visible.value ? absRect.value : {
|
|
2150
|
+
x: 0,
|
|
2151
|
+
y: 0,
|
|
2152
|
+
w: 0,
|
|
2153
|
+
h: 0
|
|
2154
|
+
},
|
|
2155
|
+
zIndex: eventZ.value,
|
|
2156
|
+
visible: visible.value,
|
|
2157
|
+
selectable: options.selectable ?? false,
|
|
2158
|
+
handlers
|
|
2159
|
+
}));
|
|
2160
|
+
}
|
|
2161
|
+
useRenderNode(() => ({
|
|
2162
|
+
zIndex: props.zIndex,
|
|
2163
|
+
rect: visible.value ? absRect.value : {
|
|
2164
|
+
x: 0,
|
|
2165
|
+
y: 0,
|
|
2166
|
+
w: 0,
|
|
2167
|
+
h: 0
|
|
2168
|
+
},
|
|
2169
|
+
dirtyRowsHint: dirtyRowsHint.value,
|
|
2170
|
+
deps: [
|
|
2171
|
+
visible.value,
|
|
2172
|
+
absRect.value,
|
|
2173
|
+
fullRect.value,
|
|
2174
|
+
surface.value
|
|
2175
|
+
],
|
|
2176
|
+
paint: (dirtyRows) => {
|
|
2177
|
+
if (!visible.value) return;
|
|
2178
|
+
const r = absRect.value;
|
|
2179
|
+
if (r.w <= 0 || r.h <= 0) return;
|
|
2180
|
+
const full = fullRect.value;
|
|
2181
|
+
const current = surface.value;
|
|
2182
|
+
const blank = spaces(r.w);
|
|
2183
|
+
const paintRow = (y) => {
|
|
2184
|
+
if (y < r.y || y >= r.y + r.h) return;
|
|
2185
|
+
const localY = y - full.y;
|
|
2186
|
+
if (localY < 0 || localY >= current.height) return;
|
|
2187
|
+
terminal.write(blank, {
|
|
2188
|
+
x: r.x,
|
|
2189
|
+
y,
|
|
2190
|
+
style: current.clearStyle
|
|
2191
|
+
});
|
|
2192
|
+
for (const cell of current.rows[localY] ?? []) {
|
|
2193
|
+
const x = full.x + cell.x;
|
|
2194
|
+
const cellWidth = Math.max(1, textCellWidth(cell.ch));
|
|
2195
|
+
if (x < r.x || x + cellWidth > r.x + r.w) continue;
|
|
2196
|
+
terminal.put(x, y, cell.ch, cell.style);
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
if (!dirtyRows) {
|
|
2200
|
+
for (let y = r.y; y < r.y + r.h; y++) paintRow(y);
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
for (const y of dirtyRows) paintRow(y);
|
|
2204
|
+
}
|
|
2205
|
+
}));
|
|
2206
|
+
return () => h("span", rootProps);
|
|
2207
|
+
}
|
|
2208
|
+
function positiveHeatmapStyle(value, max, styles) {
|
|
2209
|
+
if (!styles.length) return {};
|
|
2210
|
+
if (max <= 0) return styles[0] ?? {};
|
|
2211
|
+
const ratio = Math.max(0, Math.min(1, value / max));
|
|
2212
|
+
const index = Math.max(0, Math.min(styles.length - 1, Math.ceil(ratio * styles.length) - 1));
|
|
2213
|
+
return styles[index] ?? {};
|
|
2214
|
+
}
|
|
2215
|
+
const TContributionGraph = defineComponent({
|
|
2216
|
+
name: "TContributionGraph",
|
|
2217
|
+
props: {
|
|
2218
|
+
x: {
|
|
2219
|
+
type: Number,
|
|
2220
|
+
required: true
|
|
2221
|
+
},
|
|
2222
|
+
y: {
|
|
2223
|
+
type: Number,
|
|
2224
|
+
required: true
|
|
2225
|
+
},
|
|
2226
|
+
w: {
|
|
2227
|
+
type: Number,
|
|
2228
|
+
default: void 0
|
|
2229
|
+
},
|
|
2230
|
+
h: {
|
|
2231
|
+
type: Number,
|
|
2232
|
+
default: void 0
|
|
2233
|
+
},
|
|
2234
|
+
zIndex: {
|
|
2235
|
+
type: Number,
|
|
2236
|
+
default: 0
|
|
2237
|
+
},
|
|
2238
|
+
values: {
|
|
2239
|
+
type: Array,
|
|
2240
|
+
required: true
|
|
2241
|
+
},
|
|
2242
|
+
rows: {
|
|
2243
|
+
type: Number,
|
|
2244
|
+
default: 7
|
|
2245
|
+
},
|
|
2246
|
+
columns: {
|
|
2247
|
+
type: Number,
|
|
2248
|
+
default: void 0
|
|
2249
|
+
},
|
|
2250
|
+
max: {
|
|
2251
|
+
type: Number,
|
|
2252
|
+
default: void 0
|
|
2253
|
+
},
|
|
2254
|
+
labels: {
|
|
2255
|
+
type: Array,
|
|
2256
|
+
default: void 0
|
|
2257
|
+
},
|
|
2258
|
+
unit: {
|
|
2259
|
+
type: String,
|
|
2260
|
+
default: ""
|
|
2261
|
+
},
|
|
2262
|
+
showTooltip: {
|
|
2263
|
+
type: Boolean,
|
|
2264
|
+
default: true
|
|
2265
|
+
},
|
|
2266
|
+
emptyStyle: {
|
|
2267
|
+
type: Object,
|
|
2268
|
+
default: () => ({
|
|
2269
|
+
fg: "blackBright",
|
|
2270
|
+
dim: true
|
|
2271
|
+
})
|
|
2272
|
+
},
|
|
2273
|
+
levelStyles: {
|
|
2274
|
+
type: Array,
|
|
2275
|
+
default: () => DEFAULT_HEATMAP_LEVEL_STYLES
|
|
2276
|
+
},
|
|
2277
|
+
cell: {
|
|
2278
|
+
type: String,
|
|
2279
|
+
default: "■"
|
|
2280
|
+
},
|
|
2281
|
+
gap: {
|
|
2282
|
+
type: Number,
|
|
2283
|
+
default: 1
|
|
2284
|
+
},
|
|
2285
|
+
hoverStyle: {
|
|
2286
|
+
type: Object,
|
|
2287
|
+
default: () => ({})
|
|
2288
|
+
},
|
|
2289
|
+
tooltipStyle: {
|
|
2290
|
+
type: Object,
|
|
2291
|
+
default: () => ({ fg: "whiteBright" })
|
|
2292
|
+
},
|
|
2293
|
+
style: {
|
|
2294
|
+
type: Object,
|
|
2295
|
+
default: void 0
|
|
2296
|
+
}
|
|
2297
|
+
},
|
|
2298
|
+
setup(props) {
|
|
2299
|
+
const { defaultStyle } = useTerminal();
|
|
2300
|
+
const baseStyle = computed(() => mergeStyle(defaultStyle.value, props.style));
|
|
2301
|
+
const hoverPointer = ref(null);
|
|
2302
|
+
const graphLayout = computed(() => {
|
|
2303
|
+
const rowCount = cellCount(props.rows, 7);
|
|
2304
|
+
if (rowCount <= 0) return {
|
|
2305
|
+
rowCount,
|
|
2306
|
+
gap: 0,
|
|
2307
|
+
columnWidth: 1,
|
|
2308
|
+
columns: 0,
|
|
2309
|
+
width: 0,
|
|
2310
|
+
height: 0,
|
|
2311
|
+
startIndex: 0,
|
|
2312
|
+
maxValue: 0
|
|
2313
|
+
};
|
|
2314
|
+
const gap = cellCount(props.gap, 1);
|
|
2315
|
+
const dataColumns = Math.ceil(props.values.length / rowCount);
|
|
2316
|
+
const requestedColumns = props.columns == null ? dataColumns : cellCount(props.columns, 0);
|
|
2317
|
+
const columnWidth = 1 + gap;
|
|
2318
|
+
const maxColumnsFromWidth = props.w == null ? requestedColumns : Math.floor((cellCount(props.w, 0) + gap) / columnWidth);
|
|
2319
|
+
const columns = Math.max(0, Math.min(requestedColumns, maxColumnsFromWidth));
|
|
2320
|
+
const width = props.w == null ? Math.max(0, columns * columnWidth - gap) : cellCount(props.w, 0);
|
|
2321
|
+
const height = props.h == null ? rowCount + (props.showTooltip ? 1 : 0) : cellCount(props.h, rowCount);
|
|
2322
|
+
const maxValue = props.max == null ? maxPositive(props.values) : Math.max(0, Number(props.max) || 0);
|
|
2323
|
+
const startIndex = Math.max(0, (dataColumns - columns) * rowCount);
|
|
2324
|
+
return {
|
|
2325
|
+
rowCount,
|
|
2326
|
+
gap,
|
|
2327
|
+
columnWidth,
|
|
2328
|
+
columns,
|
|
2329
|
+
width,
|
|
2330
|
+
height,
|
|
2331
|
+
startIndex,
|
|
2332
|
+
maxValue
|
|
2333
|
+
};
|
|
2334
|
+
});
|
|
2335
|
+
const visibleRect = useChartVisibleRect(props, graphLayout);
|
|
2336
|
+
function hitCell(localX, localY) {
|
|
2337
|
+
const current = graphLayout.value;
|
|
2338
|
+
if (current.width <= 0 || current.height <= 0 || current.columns <= 0) return null;
|
|
2339
|
+
const graphHeight = Math.min(current.rowCount, current.height);
|
|
2340
|
+
if (localX < 0 || localX >= current.width || localY < 0 || localY >= graphHeight) return null;
|
|
2341
|
+
const col = Math.floor(localX / current.columnWidth);
|
|
2342
|
+
const cellX = col * current.columnWidth;
|
|
2343
|
+
if (col < 0 || col >= current.columns) return null;
|
|
2344
|
+
if (localX !== cellX) return null;
|
|
2345
|
+
const index = current.startIndex + col * current.rowCount + localY;
|
|
2346
|
+
if (index < 0 || index >= props.values.length) return null;
|
|
2347
|
+
const value = Number(props.values[index]);
|
|
2348
|
+
if (!Number.isFinite(value)) return null;
|
|
2349
|
+
const label = props.labels?.[index] ?? `#${index + 1}`;
|
|
2350
|
+
return {
|
|
2351
|
+
x: cellX,
|
|
2352
|
+
y: localY,
|
|
2353
|
+
index,
|
|
2354
|
+
value,
|
|
2355
|
+
label
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
const surface = computed(() => {
|
|
2359
|
+
const current = graphLayout.value;
|
|
2360
|
+
if (current.rowCount <= 0) return createSurface(0, 0, baseStyle.value, []);
|
|
2361
|
+
const clearStyle = mergeStyle(baseStyle.value, props.emptyStyle);
|
|
2362
|
+
const levelStyles = props.levelStyles.map((style) => mergeStyle(baseStyle.value, style));
|
|
2363
|
+
const cells = [];
|
|
2364
|
+
const glyph = chartGlyph(props.cell, "■");
|
|
2365
|
+
const pointer = hoverPointer.value;
|
|
2366
|
+
const hovered = pointer ? hitCell(pointer.x, pointer.y) : null;
|
|
2367
|
+
for (let col = 0; col < current.columns; col++) {
|
|
2368
|
+
const cellX = col * current.columnWidth;
|
|
2369
|
+
for (let row = 0; row < current.rowCount && row < current.height; row++) {
|
|
2370
|
+
const index = current.startIndex + col * current.rowCount + row;
|
|
2371
|
+
const value = Number(props.values[index] ?? 0);
|
|
2372
|
+
const positive = Number.isFinite(value) && value > 0;
|
|
2373
|
+
const baseCellStyle = positive ? levelStyles.length ? positiveHeatmapStyle(value, current.maxValue, levelStyles) : baseStyle.value : clearStyle;
|
|
2374
|
+
const style = hovered?.index === index ? mergeStyle(baseCellStyle, props.hoverStyle) : baseCellStyle;
|
|
2375
|
+
cells.push({
|
|
2376
|
+
x: cellX,
|
|
2377
|
+
y: row,
|
|
2378
|
+
ch: glyph,
|
|
2379
|
+
style
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
const visible = visibleRect.value;
|
|
2384
|
+
if (props.showTooltip && hovered && current.width > 0 && current.height > 0 && visible.w > 0 && visible.h > 0) {
|
|
2385
|
+
const unit = sanitizeInlineText(props.unit);
|
|
2386
|
+
const value = unit ? `${formatChartValue(hovered.value)} ${unit}` : formatChartValue(hovered.value);
|
|
2387
|
+
const text = `${sanitizeInlineText(hovered.label)} ${value}`;
|
|
2388
|
+
const textWidth = textCellWidth(text);
|
|
2389
|
+
const graphHeight = Math.min(current.rowCount, current.height);
|
|
2390
|
+
const placement = chooseTooltipPlacement(cells, current.width, current.height, hovered.x, hovered.y, textWidth, current.height > graphHeight ? graphHeight : void 0, visible);
|
|
2391
|
+
pushTextCells(cells, placement.x, placement.y, text, mergeStyle(baseStyle.value, props.tooltipStyle), visible.x + visible.w - placement.x);
|
|
2392
|
+
}
|
|
2393
|
+
return createSurface(current.width, current.height, clearStyle, cells);
|
|
2394
|
+
});
|
|
2395
|
+
return useChartRender(props, surface, { handlers: ({ fullRect }) => ({
|
|
2396
|
+
pointermove: (event) => {
|
|
2397
|
+
const next = {
|
|
2398
|
+
x: Math.floor(event.cellX - fullRect.value.x),
|
|
2399
|
+
y: Math.floor(event.cellY - fullRect.value.y)
|
|
2400
|
+
};
|
|
2401
|
+
const current = hoverPointer.value;
|
|
2402
|
+
if (!current || current.x !== next.x || current.y !== next.y) hoverPointer.value = next;
|
|
2403
|
+
},
|
|
2404
|
+
pointerleave: () => {
|
|
2405
|
+
if (hoverPointer.value != null) hoverPointer.value = null;
|
|
2406
|
+
}
|
|
2407
|
+
}) });
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
2410
|
+
const TLineChart = defineComponent({
|
|
2411
|
+
name: "TLineChart",
|
|
2412
|
+
props: {
|
|
2413
|
+
x: {
|
|
2414
|
+
type: Number,
|
|
2415
|
+
required: true
|
|
2416
|
+
},
|
|
2417
|
+
y: {
|
|
2418
|
+
type: Number,
|
|
2419
|
+
required: true
|
|
2420
|
+
},
|
|
2421
|
+
w: {
|
|
2422
|
+
type: Number,
|
|
2423
|
+
required: true
|
|
2424
|
+
},
|
|
2425
|
+
h: {
|
|
2426
|
+
type: Number,
|
|
2427
|
+
required: true
|
|
2428
|
+
},
|
|
2429
|
+
zIndex: {
|
|
2430
|
+
type: Number,
|
|
2431
|
+
default: 0
|
|
2432
|
+
},
|
|
2433
|
+
values: {
|
|
2434
|
+
type: Array,
|
|
2435
|
+
required: true
|
|
2436
|
+
},
|
|
2437
|
+
labels: {
|
|
2438
|
+
type: Array,
|
|
2439
|
+
default: void 0
|
|
2440
|
+
},
|
|
2441
|
+
unit: {
|
|
2442
|
+
type: String,
|
|
2443
|
+
default: ""
|
|
2444
|
+
},
|
|
2445
|
+
min: {
|
|
2446
|
+
type: Number,
|
|
2447
|
+
default: void 0
|
|
2448
|
+
},
|
|
2449
|
+
max: {
|
|
2450
|
+
type: Number,
|
|
2451
|
+
default: void 0
|
|
2452
|
+
},
|
|
2453
|
+
style: {
|
|
2454
|
+
type: Object,
|
|
2455
|
+
default: void 0
|
|
2456
|
+
},
|
|
2457
|
+
lineStyle: {
|
|
2458
|
+
type: Object,
|
|
2459
|
+
default: () => ({ fg: "cyanBright" })
|
|
2460
|
+
},
|
|
2461
|
+
showAxes: {
|
|
2462
|
+
type: Boolean,
|
|
2463
|
+
default: true
|
|
2464
|
+
},
|
|
2465
|
+
axisStyle: {
|
|
2466
|
+
type: Object,
|
|
2467
|
+
default: () => ({
|
|
2468
|
+
fg: "white",
|
|
2469
|
+
dim: true
|
|
2470
|
+
})
|
|
2471
|
+
},
|
|
2472
|
+
labelStyle: {
|
|
2473
|
+
type: Object,
|
|
2474
|
+
default: () => ({ fg: "whiteBright" })
|
|
2475
|
+
},
|
|
2476
|
+
xLabel: {
|
|
2477
|
+
type: String,
|
|
2478
|
+
default: ""
|
|
2479
|
+
},
|
|
2480
|
+
yLabel: {
|
|
2481
|
+
type: String,
|
|
2482
|
+
default: ""
|
|
2483
|
+
},
|
|
2484
|
+
startLabel: {
|
|
2485
|
+
type: String,
|
|
2486
|
+
default: ""
|
|
2487
|
+
},
|
|
2488
|
+
endLabel: {
|
|
2489
|
+
type: String,
|
|
2490
|
+
default: ""
|
|
2491
|
+
},
|
|
2492
|
+
showTooltip: {
|
|
2493
|
+
type: Boolean,
|
|
2494
|
+
default: true
|
|
2495
|
+
},
|
|
2496
|
+
hoverStyle: {
|
|
2497
|
+
type: Object,
|
|
2498
|
+
default: () => ({
|
|
2499
|
+
fg: "whiteBright",
|
|
2500
|
+
bold: true
|
|
2501
|
+
})
|
|
2502
|
+
},
|
|
2503
|
+
tooltipStyle: {
|
|
2504
|
+
type: Object,
|
|
2505
|
+
default: () => ({ fg: "whiteBright" })
|
|
2506
|
+
}
|
|
2507
|
+
},
|
|
2508
|
+
setup(props) {
|
|
2509
|
+
const { defaultStyle } = useTerminal();
|
|
2510
|
+
const clearStyle = computed(() => mergeStyle(defaultStyle.value, props.style));
|
|
2511
|
+
const lineStyle = computed(() => mergeStyle(clearStyle.value, props.lineStyle));
|
|
2512
|
+
const axisStyle = computed(() => mergeStyle(clearStyle.value, props.axisStyle));
|
|
2513
|
+
const labelStyle = computed(() => mergeStyle(clearStyle.value, props.labelStyle));
|
|
2514
|
+
const hoverPointer = ref(null);
|
|
2515
|
+
const lineLayout = computed(() => {
|
|
2516
|
+
const width = cellCount(props.w, 0);
|
|
2517
|
+
const height = cellCount(props.h, 0);
|
|
2518
|
+
const runs = finiteLineRuns(props.values);
|
|
2519
|
+
const sourceLength = props.values.length;
|
|
2520
|
+
const { min, max } = domainFromValues(props.values, props.min, props.max);
|
|
2521
|
+
const layout = resolveAxisLayout(width, height, min, max, props.showAxes);
|
|
2522
|
+
return {
|
|
2523
|
+
width,
|
|
2524
|
+
height,
|
|
2525
|
+
layout,
|
|
2526
|
+
runs,
|
|
2527
|
+
sourceLength,
|
|
2528
|
+
min,
|
|
2529
|
+
max
|
|
2530
|
+
};
|
|
2531
|
+
});
|
|
2532
|
+
const visibleRect = useChartVisibleRect(props, lineLayout);
|
|
2533
|
+
function hitLine(localX, localY) {
|
|
2534
|
+
const current = lineLayout.value;
|
|
2535
|
+
const { layout, runs, sourceLength, min, max } = current;
|
|
2536
|
+
if (!runs.length || layout.plotW <= 0 || layout.plotH <= 0) return null;
|
|
2537
|
+
const plotX = localX - layout.plotX;
|
|
2538
|
+
const plotY = localY - layout.plotY;
|
|
2539
|
+
if (plotX < 0 || plotX >= layout.plotW || plotY < 0 || plotY >= layout.plotH) return null;
|
|
2540
|
+
const position = samplePositionAtX(plotX, layout.plotW, sourceLength);
|
|
2541
|
+
const point = nearestLinePointInRuns(runs, position, plotX, layout.plotW, sourceLength);
|
|
2542
|
+
if (!point) return null;
|
|
2543
|
+
const value = point.value;
|
|
2544
|
+
const x = lineXForOriginalIndex(point.originalIndex, layout.plotX, layout.plotW, sourceLength);
|
|
2545
|
+
const y = layout.plotY + valueToY(value, min, max, layout.plotH);
|
|
2546
|
+
const label = props.labels?.[point.originalIndex] ?? `#${point.originalIndex + 1}`;
|
|
2547
|
+
return {
|
|
2548
|
+
x,
|
|
2549
|
+
y,
|
|
2550
|
+
index: point.originalIndex,
|
|
2551
|
+
label,
|
|
2552
|
+
value
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
const baseCells = computed(() => {
|
|
2556
|
+
const { width, height, layout, runs, sourceLength, min, max } = lineLayout.value;
|
|
2557
|
+
const cells = [];
|
|
2558
|
+
if (layout.plotW > 0 && layout.plotH > 0 && runs.length > 0) {
|
|
2559
|
+
const occupiedLineCells = new Set();
|
|
2560
|
+
for (const run of runs) {
|
|
2561
|
+
const buckets = lineBucketsForRun(run.points, layout.plotW, sourceLength);
|
|
2562
|
+
let prevX = null;
|
|
2563
|
+
let prevY = null;
|
|
2564
|
+
for (const point of lineSegmentPointsForRun(run.points, buckets, layout.plotW, sourceLength)) {
|
|
2565
|
+
const x = layout.plotX + point.x;
|
|
2566
|
+
const y = layout.plotY + valueToY(point.value, min, max, layout.plotH);
|
|
2567
|
+
if (prevX == null || prevY == null) putLineGlyph(cells, x, y, "●", lineStyle.value, width, height, occupiedLineCells);
|
|
2568
|
+
else drawSteppedLineSegment(cells, prevX, prevY, x, y, lineStyle.value, width, height, occupiedLineCells);
|
|
2569
|
+
prevX = x;
|
|
2570
|
+
prevY = y;
|
|
2571
|
+
}
|
|
2572
|
+
for (const bucket of buckets) pushLineBucketExtrema(cells, bucket, layout, min, max, lineStyle.value, width, height, occupiedLineCells);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
pushAxes(cells, layout, axisStyle.value, labelStyle.value, props.xLabel, props.yLabel, props.startLabel || (sourceLength > 0 ? "1" : ""), props.endLabel || (sourceLength > 0 ? String(sourceLength) : ""));
|
|
2576
|
+
return cells;
|
|
2577
|
+
});
|
|
2578
|
+
const surface = computed(() => {
|
|
2579
|
+
const { width, height, layout } = lineLayout.value;
|
|
2580
|
+
const cells = baseCells.value.slice();
|
|
2581
|
+
const pointer = hoverPointer.value;
|
|
2582
|
+
const hovered = pointer ? hitLine(pointer.x, pointer.y) : null;
|
|
2583
|
+
if (hovered) cells.push({
|
|
2584
|
+
x: hovered.x,
|
|
2585
|
+
y: hovered.y,
|
|
2586
|
+
ch: "●",
|
|
2587
|
+
style: mergeStyle(lineStyle.value, props.hoverStyle)
|
|
2588
|
+
});
|
|
2589
|
+
const visible = visibleRect.value;
|
|
2590
|
+
if (props.showTooltip && hovered && width > 0 && height > 0 && visible.w > 0 && visible.h > 0) {
|
|
2591
|
+
const unit = sanitizeInlineText(props.unit);
|
|
2592
|
+
const yValue = unit ? `${formatChartValue(hovered.value)} ${unit}` : formatChartValue(hovered.value);
|
|
2593
|
+
const text = `${sanitizeInlineText(hovered.label)} x=${hovered.index + 1} y=${yValue}`;
|
|
2594
|
+
const textWidth = textCellWidth(text);
|
|
2595
|
+
const placement = chooseTooltipPlacement(cells, width, height, hovered.x, hovered.y, textWidth, layout.enabled ? layout.axisY + 1 : void 0, visible);
|
|
2596
|
+
pushTextCells(cells, placement.x, placement.y, text, mergeStyle(clearStyle.value, props.tooltipStyle), visible.x + visible.w - placement.x);
|
|
2597
|
+
}
|
|
2598
|
+
return createSurface(width, height, clearStyle.value, cells);
|
|
2599
|
+
});
|
|
2600
|
+
return useChartRender(props, surface, { handlers: ({ fullRect }) => ({
|
|
2601
|
+
pointermove: (event) => {
|
|
2602
|
+
const next = {
|
|
2603
|
+
x: Math.floor(event.cellX - fullRect.value.x),
|
|
2604
|
+
y: Math.floor(event.cellY - fullRect.value.y)
|
|
2605
|
+
};
|
|
2606
|
+
const current = hoverPointer.value;
|
|
2607
|
+
if (!current || current.x !== next.x || current.y !== next.y) hoverPointer.value = next;
|
|
2608
|
+
},
|
|
2609
|
+
pointerleave: () => {
|
|
2610
|
+
if (hoverPointer.value != null) hoverPointer.value = null;
|
|
2611
|
+
}
|
|
2612
|
+
}) });
|
|
2613
|
+
}
|
|
2614
|
+
});
|
|
2615
|
+
const TCandlestickChart = defineComponent({
|
|
2616
|
+
name: "TCandlestickChart",
|
|
2617
|
+
props: {
|
|
2618
|
+
x: {
|
|
2619
|
+
type: Number,
|
|
2620
|
+
required: true
|
|
2621
|
+
},
|
|
2622
|
+
y: {
|
|
2623
|
+
type: Number,
|
|
2624
|
+
required: true
|
|
2625
|
+
},
|
|
2626
|
+
w: {
|
|
2627
|
+
type: Number,
|
|
2628
|
+
required: true
|
|
2629
|
+
},
|
|
2630
|
+
h: {
|
|
2631
|
+
type: Number,
|
|
2632
|
+
required: true
|
|
2633
|
+
},
|
|
2634
|
+
zIndex: {
|
|
2635
|
+
type: Number,
|
|
2636
|
+
default: 0
|
|
2637
|
+
},
|
|
2638
|
+
candles: {
|
|
2639
|
+
type: Array,
|
|
2640
|
+
required: true
|
|
2641
|
+
},
|
|
2642
|
+
labels: {
|
|
2643
|
+
type: Array,
|
|
2644
|
+
default: void 0
|
|
2645
|
+
},
|
|
2646
|
+
min: {
|
|
2647
|
+
type: Number,
|
|
2648
|
+
default: void 0
|
|
2649
|
+
},
|
|
2650
|
+
max: {
|
|
2651
|
+
type: Number,
|
|
2652
|
+
default: void 0
|
|
2653
|
+
},
|
|
2654
|
+
style: {
|
|
2655
|
+
type: Object,
|
|
2656
|
+
default: void 0
|
|
2657
|
+
},
|
|
2658
|
+
upStyle: {
|
|
2659
|
+
type: Object,
|
|
2660
|
+
default: () => ({ fg: "greenBright" })
|
|
2661
|
+
},
|
|
2662
|
+
downStyle: {
|
|
2663
|
+
type: Object,
|
|
2664
|
+
default: () => ({ fg: "redBright" })
|
|
2665
|
+
},
|
|
2666
|
+
wickStyle: {
|
|
2667
|
+
type: Object,
|
|
2668
|
+
default: void 0
|
|
2669
|
+
},
|
|
2670
|
+
showAxes: {
|
|
2671
|
+
type: Boolean,
|
|
2672
|
+
default: true
|
|
2673
|
+
},
|
|
2674
|
+
axisStyle: {
|
|
2675
|
+
type: Object,
|
|
2676
|
+
default: () => ({
|
|
2677
|
+
fg: "white",
|
|
2678
|
+
dim: true
|
|
2679
|
+
})
|
|
2680
|
+
},
|
|
2681
|
+
labelStyle: {
|
|
2682
|
+
type: Object,
|
|
2683
|
+
default: () => ({ fg: "whiteBright" })
|
|
2684
|
+
},
|
|
2685
|
+
xLabel: {
|
|
2686
|
+
type: String,
|
|
2687
|
+
default: ""
|
|
2688
|
+
},
|
|
2689
|
+
yLabel: {
|
|
2690
|
+
type: String,
|
|
2691
|
+
default: ""
|
|
2692
|
+
},
|
|
2693
|
+
startLabel: {
|
|
2694
|
+
type: String,
|
|
2695
|
+
default: ""
|
|
2696
|
+
},
|
|
2697
|
+
endLabel: {
|
|
2698
|
+
type: String,
|
|
2699
|
+
default: ""
|
|
2700
|
+
},
|
|
2701
|
+
showTooltip: {
|
|
2702
|
+
type: Boolean,
|
|
2703
|
+
default: true
|
|
2704
|
+
},
|
|
2705
|
+
hoverStyle: {
|
|
2706
|
+
type: Object,
|
|
2707
|
+
default: () => ({})
|
|
2708
|
+
},
|
|
2709
|
+
tooltipStyle: {
|
|
2710
|
+
type: Object,
|
|
2711
|
+
default: () => ({ fg: "whiteBright" })
|
|
2712
|
+
}
|
|
2713
|
+
},
|
|
2714
|
+
setup(props) {
|
|
2715
|
+
const { defaultStyle } = useTerminal();
|
|
2716
|
+
const baseStyle = computed(() => mergeStyle(defaultStyle.value, props.style));
|
|
2717
|
+
const axisStyle = computed(() => mergeStyle(baseStyle.value, props.axisStyle));
|
|
2718
|
+
const labelStyle = computed(() => mergeStyle(baseStyle.value, props.labelStyle));
|
|
2719
|
+
const hoverPointer = ref(null);
|
|
2720
|
+
const candleLayout = computed(() => {
|
|
2721
|
+
const width = cellCount(props.w, 0);
|
|
2722
|
+
const height = cellCount(props.h, 0);
|
|
2723
|
+
let min = 0;
|
|
2724
|
+
let max = 0;
|
|
2725
|
+
let layout = resolveAxisLayout(width, height, min, max, props.showAxes);
|
|
2726
|
+
let startIndex = props.candles.length;
|
|
2727
|
+
let visibleCandles = [];
|
|
2728
|
+
for (let capacity = Math.min(props.candles.length, width); capacity >= 0; capacity--) {
|
|
2729
|
+
startIndex = Math.max(0, props.candles.length - capacity);
|
|
2730
|
+
visibleCandles = props.candles.slice(startIndex);
|
|
2731
|
+
({min, max} = domainFromValues(candlestickDomainValues(visibleCandles), props.min, props.max));
|
|
2732
|
+
layout = resolveAxisLayout(width, height, min, max, props.showAxes);
|
|
2733
|
+
if (visibleCandles.length <= layout.plotW) break;
|
|
2734
|
+
}
|
|
2735
|
+
const candleOffsetX = Math.max(0, layout.plotW - visibleCandles.length);
|
|
2736
|
+
return {
|
|
2737
|
+
width,
|
|
2738
|
+
height,
|
|
2739
|
+
layout,
|
|
2740
|
+
visibleCandles,
|
|
2741
|
+
startIndex,
|
|
2742
|
+
candleOffsetX,
|
|
2743
|
+
min,
|
|
2744
|
+
max
|
|
2745
|
+
};
|
|
2746
|
+
});
|
|
2747
|
+
const visibleRect = useChartVisibleRect(props, candleLayout);
|
|
2748
|
+
function hitCandle(localX, localY) {
|
|
2749
|
+
const current = candleLayout.value;
|
|
2750
|
+
const { layout } = current;
|
|
2751
|
+
if (current.width <= 0 || current.height <= 0 || layout.plotW <= 0 || layout.plotH <= 0) return null;
|
|
2752
|
+
const plotX = localX - layout.plotX - current.candleOffsetX;
|
|
2753
|
+
const plotY = localY - layout.plotY;
|
|
2754
|
+
if (plotX < 0 || plotX >= current.visibleCandles.length || current.visibleCandles.length <= 0) return null;
|
|
2755
|
+
if (plotY < 0 || plotY >= layout.plotH) return null;
|
|
2756
|
+
const candleX = plotX;
|
|
2757
|
+
const candle = current.visibleCandles[candleX];
|
|
2758
|
+
const values = normalizeCandlestick(candle);
|
|
2759
|
+
if (!values) return null;
|
|
2760
|
+
const { open, high, low, close } = values;
|
|
2761
|
+
const index = current.startIndex + candleX;
|
|
2762
|
+
const label = props.labels?.[index] ?? `#${index + 1}`;
|
|
2763
|
+
return {
|
|
2764
|
+
x: layout.plotX + current.candleOffsetX + candleX,
|
|
2765
|
+
y: localY,
|
|
2766
|
+
index,
|
|
2767
|
+
label,
|
|
2768
|
+
value: yToValue(plotY, current.min, current.max, layout.plotH),
|
|
2769
|
+
open,
|
|
2770
|
+
high,
|
|
2771
|
+
low,
|
|
2772
|
+
close
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
const surface = computed(() => {
|
|
2776
|
+
const current = candleLayout.value;
|
|
2777
|
+
const { width, height, layout, visibleCandles, candleOffsetX, min, max } = current;
|
|
2778
|
+
const cells = [];
|
|
2779
|
+
const upStyle = mergeStyle(baseStyle.value, props.upStyle);
|
|
2780
|
+
const downStyle = mergeStyle(baseStyle.value, props.downStyle);
|
|
2781
|
+
const upWickStyle = mergeStyle(baseStyle.value, props.upStyle, props.wickStyle);
|
|
2782
|
+
const downWickStyle = mergeStyle(baseStyle.value, props.downStyle, props.wickStyle);
|
|
2783
|
+
const pointer = hoverPointer.value;
|
|
2784
|
+
const hovered = pointer ? hitCandle(pointer.x, pointer.y) : null;
|
|
2785
|
+
for (let x = 0; x < visibleCandles.length; x++) {
|
|
2786
|
+
const candle = visibleCandles[x];
|
|
2787
|
+
const values = normalizeCandlestick(candle);
|
|
2788
|
+
if (!values) continue;
|
|
2789
|
+
const { open, high, low, close } = values;
|
|
2790
|
+
const up = close >= open;
|
|
2791
|
+
const baseBodyStyle = up ? upStyle : downStyle;
|
|
2792
|
+
const baseWickStyle = up ? upWickStyle : downWickStyle;
|
|
2793
|
+
const hoveredCandle = hovered?.index === current.startIndex + x;
|
|
2794
|
+
const bodyStyle = hoveredCandle ? mergeStyle(baseBodyStyle, props.hoverStyle) : baseBodyStyle;
|
|
2795
|
+
const wickStyle = hoveredCandle ? mergeStyle(baseWickStyle, props.hoverStyle) : baseWickStyle;
|
|
2796
|
+
const highY = valueToY(Math.max(high, low, open, close), min, max, layout.plotH);
|
|
2797
|
+
const lowY = valueToY(Math.min(high, low, open, close), min, max, layout.plotH);
|
|
2798
|
+
const openY = valueToY(open, min, max, layout.plotH);
|
|
2799
|
+
const closeY = valueToY(close, min, max, layout.plotH);
|
|
2800
|
+
const bodyTop = Math.min(openY, closeY);
|
|
2801
|
+
const bodyBottom = Math.max(openY, closeY);
|
|
2802
|
+
for (let y = highY; y <= lowY; y++) {
|
|
2803
|
+
const inBody = y >= bodyTop && y <= bodyBottom;
|
|
2804
|
+
cells.push({
|
|
2805
|
+
x: layout.plotX + candleOffsetX + x,
|
|
2806
|
+
y: layout.plotY + y,
|
|
2807
|
+
ch: inBody ? "█" : "│",
|
|
2808
|
+
style: inBody ? bodyStyle : wickStyle
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
const firstVisible = props.candles.length - visibleCandles.length + 1;
|
|
2813
|
+
pushAxes(cells, layout, axisStyle.value, labelStyle.value, props.xLabel, props.yLabel, props.startLabel || (visibleCandles.length > 0 ? String(firstVisible) : ""), props.endLabel || (visibleCandles.length > 0 ? String(props.candles.length) : ""));
|
|
2814
|
+
const visible = visibleRect.value;
|
|
2815
|
+
if (props.showTooltip && hovered && width > 0 && height > 0 && visible.w > 0 && visible.h > 0) {
|
|
2816
|
+
const text = `${sanitizeInlineText(hovered.label)} x=${hovered.index + 1} y=${formatChartValue(hovered.value)} O:${formatChartValue(hovered.open)} H:${formatChartValue(hovered.high)} L:${formatChartValue(hovered.low)} C:${formatChartValue(hovered.close)}`;
|
|
2817
|
+
const textWidth = textCellWidth(text);
|
|
2818
|
+
const placement = chooseTooltipPlacement(cells, width, height, hovered.x, hovered.y, textWidth, void 0, visible);
|
|
2819
|
+
pushTextCells(cells, placement.x, placement.y, text, mergeStyle(baseStyle.value, props.tooltipStyle), visible.x + visible.w - placement.x);
|
|
2820
|
+
}
|
|
2821
|
+
return createSurface(width, height, baseStyle.value, cells);
|
|
2822
|
+
});
|
|
2823
|
+
return useChartRender(props, surface, { handlers: ({ fullRect }) => ({
|
|
2824
|
+
pointermove: (event) => {
|
|
2825
|
+
const next = {
|
|
2826
|
+
x: Math.floor(event.cellX - fullRect.value.x),
|
|
2827
|
+
y: Math.floor(event.cellY - fullRect.value.y)
|
|
2828
|
+
};
|
|
2829
|
+
const current = hoverPointer.value;
|
|
2830
|
+
if (!current || current.x !== next.x || current.y !== next.y) hoverPointer.value = next;
|
|
2831
|
+
},
|
|
2832
|
+
pointerleave: () => {
|
|
2833
|
+
if (hoverPointer.value != null) hoverPointer.value = null;
|
|
2834
|
+
}
|
|
2835
|
+
}) });
|
|
2836
|
+
}
|
|
2837
|
+
});
|
|
2838
|
+
const TPieChart = defineComponent({
|
|
2839
|
+
name: "TPieChart",
|
|
2840
|
+
props: {
|
|
2841
|
+
x: {
|
|
2842
|
+
type: Number,
|
|
2843
|
+
required: true
|
|
2844
|
+
},
|
|
2845
|
+
y: {
|
|
2846
|
+
type: Number,
|
|
2847
|
+
required: true
|
|
2848
|
+
},
|
|
2849
|
+
w: {
|
|
2850
|
+
type: Number,
|
|
2851
|
+
required: true
|
|
2852
|
+
},
|
|
2853
|
+
h: {
|
|
2854
|
+
type: Number,
|
|
2855
|
+
required: true
|
|
2856
|
+
},
|
|
2857
|
+
zIndex: {
|
|
2858
|
+
type: Number,
|
|
2859
|
+
default: 0
|
|
2860
|
+
},
|
|
2861
|
+
values: {
|
|
2862
|
+
type: Array,
|
|
2863
|
+
required: true
|
|
2864
|
+
},
|
|
2865
|
+
labels: {
|
|
2866
|
+
type: Array,
|
|
2867
|
+
default: void 0
|
|
2868
|
+
},
|
|
2869
|
+
style: {
|
|
2870
|
+
type: Object,
|
|
2871
|
+
default: void 0
|
|
2872
|
+
},
|
|
2873
|
+
segmentStyles: {
|
|
2874
|
+
type: Array,
|
|
2875
|
+
default: () => DEFAULT_PIE_SEGMENT_STYLES
|
|
2876
|
+
},
|
|
2877
|
+
cell: {
|
|
2878
|
+
type: String,
|
|
2879
|
+
default: "█"
|
|
2880
|
+
},
|
|
2881
|
+
showLegend: {
|
|
2882
|
+
type: Boolean,
|
|
2883
|
+
default: true
|
|
2884
|
+
},
|
|
2885
|
+
legendStyle: {
|
|
2886
|
+
type: Object,
|
|
2887
|
+
default: () => ({ fg: "whiteBright" })
|
|
2888
|
+
}
|
|
2889
|
+
},
|
|
2890
|
+
setup(props) {
|
|
2891
|
+
const { defaultStyle } = useTerminal();
|
|
2892
|
+
const baseStyle = computed(() => mergeStyle(defaultStyle.value, props.style));
|
|
2893
|
+
const legendStyle = computed(() => mergeStyle(baseStyle.value, props.legendStyle));
|
|
2894
|
+
const surface = computed(() => {
|
|
2895
|
+
const width = cellCount(props.w, 0);
|
|
2896
|
+
const height = cellCount(props.h, 0);
|
|
2897
|
+
const values = props.values.map((value) => {
|
|
2898
|
+
const n = Number(value);
|
|
2899
|
+
return Number.isFinite(n) ? Math.max(0, n) : 0;
|
|
2900
|
+
});
|
|
2901
|
+
const scale = maxPositive(values);
|
|
2902
|
+
const normalizedValues = scale > 0 ? values.map((value) => value / scale) : values;
|
|
2903
|
+
const total = normalizedValues.reduce((sum, value) => sum + value, 0);
|
|
2904
|
+
const cells = [];
|
|
2905
|
+
const glyph = chartGlyph(props.cell, "█");
|
|
2906
|
+
const segmentStyles = props.segmentStyles.map((style) => mergeStyle(baseStyle.value, style));
|
|
2907
|
+
const visibleSegments = values.map((value, index) => ({
|
|
2908
|
+
value,
|
|
2909
|
+
normalizedValue: normalizedValues[index] ?? 0,
|
|
2910
|
+
index
|
|
2911
|
+
})).filter((segment) => segment.value > 0);
|
|
2912
|
+
const legendItems = visibleSegments.map((segment) => {
|
|
2913
|
+
const label = props.labels?.[segment.index] ?? `S${segment.index + 1}`;
|
|
2914
|
+
const percent = total > 0 ? Math.round(segment.normalizedValue / total * 100) : 0;
|
|
2915
|
+
const text = `${label} ${formatChartValue(segment.value)} ${percent}%`;
|
|
2916
|
+
return {
|
|
2917
|
+
...segment,
|
|
2918
|
+
text
|
|
2919
|
+
};
|
|
2920
|
+
});
|
|
2921
|
+
const showLegend = props.showLegend && visibleSegments.length > 0 && width >= 18 && height >= 3;
|
|
2922
|
+
const desiredLegendWidth = legendItems.reduce((maxWidth, item) => Math.max(maxWidth, textCellWidth(sanitizeInlineText(item.text)) + 2), 0) || 12;
|
|
2923
|
+
const rightLegendWidth = showLegend ? Math.min(Math.max(12, desiredLegendWidth), Math.max(0, width - 6)) : 0;
|
|
2924
|
+
const rightPlotWidth = Math.max(0, width - rightLegendWidth - 1);
|
|
2925
|
+
const legendRows = Math.min(legendItems.length, height);
|
|
2926
|
+
const rightLegend = showLegend && rightPlotWidth >= 8;
|
|
2927
|
+
const bottomLegend = showLegend && !rightLegend && height >= legendRows + 4;
|
|
2928
|
+
const legendPlacement = rightLegend ? "right" : bottomLegend ? "bottom" : "none";
|
|
2929
|
+
const plotWidth = legendPlacement === "right" ? rightPlotWidth : width;
|
|
2930
|
+
const plotHeight = legendPlacement === "bottom" ? Math.max(0, height - legendRows - 1) : height;
|
|
2931
|
+
if (plotWidth > 0 && plotHeight > 0 && total > 0) {
|
|
2932
|
+
const segments = [];
|
|
2933
|
+
let cursor = 0;
|
|
2934
|
+
for (let i = 0; i < normalizedValues.length; i++) {
|
|
2935
|
+
const value = normalizedValues[i];
|
|
2936
|
+
if (value <= 0) continue;
|
|
2937
|
+
cursor += value / total * Math.PI * 2;
|
|
2938
|
+
segments.push({
|
|
2939
|
+
end: cursor,
|
|
2940
|
+
index: i,
|
|
2941
|
+
style: segmentStyles[i % Math.max(1, segmentStyles.length)] ?? baseStyle.value
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
const cx = plotWidth / 2;
|
|
2945
|
+
const cy = plotHeight / 2;
|
|
2946
|
+
const maxRx = plotWidth / 2;
|
|
2947
|
+
const maxRy = plotHeight / 2;
|
|
2948
|
+
const ry = Math.max(.5, Math.min(maxRy, maxRx / TERMINAL_CELL_HEIGHT_TO_WIDTH));
|
|
2949
|
+
const rx = Math.max(.5, Math.min(maxRx, ry * TERMINAL_CELL_HEIGHT_TO_WIDTH));
|
|
2950
|
+
const smoothEdge = glyph === "█";
|
|
2951
|
+
const segmentAt = (nx, ny) => {
|
|
2952
|
+
let angle = Math.atan2(ny, nx) + Math.PI / 2;
|
|
2953
|
+
if (angle < 0) angle += Math.PI * 2;
|
|
2954
|
+
return segments.find((candidate) => angle <= candidate.end) ?? segments.at(-1);
|
|
2955
|
+
};
|
|
2956
|
+
for (let y = 0; y < plotHeight; y++) for (let x = 0; x < plotWidth; x++) {
|
|
2957
|
+
let mask = 0;
|
|
2958
|
+
const hits = new Map();
|
|
2959
|
+
for (const sample of PIE_CELL_SAMPLES) {
|
|
2960
|
+
const nx = (x + sample.dx - cx) / rx;
|
|
2961
|
+
const ny = (y + sample.dy - cy) / ry;
|
|
2962
|
+
if (nx * nx + ny * ny > 1) continue;
|
|
2963
|
+
mask |= sample.bit;
|
|
2964
|
+
const segment = segmentAt(nx, ny);
|
|
2965
|
+
if (!segment) continue;
|
|
2966
|
+
const previous = hits.get(segment.index);
|
|
2967
|
+
hits.set(segment.index, {
|
|
2968
|
+
segment,
|
|
2969
|
+
count: (previous?.count ?? 0) + 1
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
if (!mask) continue;
|
|
2973
|
+
const selected = Array.from(hits.values()).sort((a, b) => b.count - a.count)[0];
|
|
2974
|
+
const ch = smoothEdge ? PIE_QUADRANT_BLOCKS[mask] ?? glyph : glyph;
|
|
2975
|
+
cells.push({
|
|
2976
|
+
x,
|
|
2977
|
+
y,
|
|
2978
|
+
ch,
|
|
2979
|
+
style: selected?.segment.style ?? baseStyle.value
|
|
2980
|
+
});
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
if (legendPlacement !== "none") {
|
|
2984
|
+
const legendX = legendPlacement === "right" ? plotWidth + 1 : 0;
|
|
2985
|
+
const legendY = legendPlacement === "right" ? 0 : plotHeight + 1;
|
|
2986
|
+
const maxTextWidth = legendPlacement === "right" ? Math.max(0, width - legendX - 2) : Math.max(0, width - 2);
|
|
2987
|
+
for (let row = 0; row < legendItems.length && legendY + row < height; row++) {
|
|
2988
|
+
const segment = legendItems[row];
|
|
2989
|
+
const style = segmentStyles[segment.index % Math.max(1, segmentStyles.length)] ?? baseStyle.value;
|
|
2990
|
+
cells.push({
|
|
2991
|
+
x: legendX,
|
|
2992
|
+
y: legendY + row,
|
|
2993
|
+
ch: glyph,
|
|
2994
|
+
style
|
|
2995
|
+
});
|
|
2996
|
+
pushTextCells(cells, legendX + 2, legendY + row, segment.text, legendStyle.value, maxTextWidth);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
return createSurface(width, height, baseStyle.value, cells);
|
|
3000
|
+
});
|
|
3001
|
+
return useChartRender(props, surface);
|
|
3002
|
+
}
|
|
3003
|
+
});
|
|
3004
|
+
|
|
3005
|
+
//#endregion
|
|
10
3006
|
//#region src/vue/log/tlog-session.ts
|
|
11
3007
|
function clamp(n, min, max) {
|
|
12
3008
|
return Math.max(min, Math.min(max, n));
|
|
@@ -111,4 +3107,4 @@ function restoreTLogViewSessionState(bindings, state) {
|
|
|
111
3107
|
}
|
|
112
3108
|
|
|
113
3109
|
//#endregion
|
|
114
|
-
export { TLogLinksPanel, TLogMinimap, TLogScrollbar, TLogSearchBar, TLogSearchPager, TLogSearchResults, TLogView, TLogVirtualLinksPanel, TLogVirtualSearchResults, TTranscriptView, TVirtualList, captureTLogViewSessionState, createAppendOnlyLogStore, createTLogDensityBucketsFromMarkers, createTLogLevelPlugin, createTLogLineMatcherPlugin, createTLogLinkActionPlugin, createTLogOsc8LinkPlugin, createTLogUrlPlugin, createTLogViewSessionStore, deserializeTLogViewSessionState, detectTLogUrls, dispatchTLogPluginLinkAction, getTLogPluginMetadata, handleTLogKeymapEvent, matchesTLogKeyBinding, parseTLogAnnotatedText, resolveTLogLinksPanelTheme, resolveTLogMinimapTheme, resolveTLogScrollbarTheme, resolveTLogSearchBarTheme, resolveTLogSearchPagerTheme, resolveTLogSearchResultsTheme, resolveTLogViewTheme, restoreTLogViewSessionState, sanitizeTerminalHref, serializeTLogViewSessionState, stripTLogAnsiText, tlogDarkPreset, tlogDarkTheme, tlogDefaultKeymap, tlogDefaultPreset, tlogDefaultTheme, tlogHighContrastKeymap, tlogHighContrastPreset, tlogHighContrastTheme, toTLogExternalLinkFromVisibleLink, useTLogLinkController, useTLogRetainedIndex, useTLogSearchController, useTLogSearchResultsPage, useTLogVirtualSearchResults };
|
|
3110
|
+
export { T3DViewport, TCandlestickChart, TContributionGraph, TLineChart, TLogLinksPanel, TLogMinimap, TLogScrollbar, TLogSearchBar, TLogSearchPager, TLogSearchResults, TLogView, TLogVirtualLinksPanel, TLogVirtualSearchResults, TPieChart, TTranscriptView, TVideo, TVirtualList, captureTLogViewSessionState, createAppendOnlyLogStore, createTLogDensityBucketsFromMarkers, createTLogLevelPlugin, createTLogLineMatcherPlugin, createTLogLinkActionPlugin, createTLogOsc8LinkPlugin, createTLogUrlPlugin, createTLogViewSessionStore, deserializeTLogViewSessionState, detectTLogUrls, dispatchTLogPluginLinkAction, getTLogPluginMetadata, handleTLogKeymapEvent, matchesTLogKeyBinding, parseTLogAnnotatedText, resolveTLogLinksPanelTheme, resolveTLogMinimapTheme, resolveTLogScrollbarTheme, resolveTLogSearchBarTheme, resolveTLogSearchPagerTheme, resolveTLogSearchResultsTheme, resolveTLogViewTheme, restoreTLogViewSessionState, sanitizeTerminalHref, serializeTLogViewSessionState, stripTLogAnsiText, tlogDarkPreset, tlogDarkTheme, tlogDefaultKeymap, tlogDefaultPreset, tlogDefaultTheme, tlogHighContrastKeymap, tlogHighContrastPreset, tlogHighContrastTheme, toTLogExternalLinkFromVisibleLink, useTLogLinkController, useTLogRetainedIndex, useTLogSearchController, useTLogSearchResultsPage, useTLogVirtualSearchResults };
|