File size: 11,823 Bytes
494c9e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
/**
 * 高亮管理器:直方图 token 高亮(边框/下划线)+ chunk 字符区间下划线(独立 SVG 线段)
 */

import {HighlightStyle, RectCacheEntry} from "./types";
import {HIGHLIGHT_CONSTANTS} from "./constants";

/** 字符区间下划线线段(与 SVG overlay 坐标系一致) */
export type CharIntervalUnderlineSeg = { x1: number; x2: number; y: number };

export class HighlightManager {
    private rectCache: Map<string, RectCacheEntry>;
    private underlineCache: Map<string, SVGLineElement>;
    private svgOverlay: SVGSVGElement;
    private currentStyle: HighlightStyle = 'border';
    private readonly MAX_RETRY_ATTEMPTS = 10;
    /** rect 缓存未就绪时的重试计数(与直方图 / 区间下划线分开,避免互相打断) */
    private rectCacheWait = { indices: 0, interval: 0 };
    private intervalUnderlineLines: SVGLineElement[] = [];

    constructor(
        svgOverlay: SVGSVGElement,
        rectCache: Map<string, RectCacheEntry>,
        underlineCache: Map<string, SVGLineElement>
    ) {
        this.svgOverlay = svgOverlay;
        this.rectCache = rectCache;
        this.underlineCache = underlineCache;
    }

    /**
     * 设置需要高亮的token索引
     * @param indices 需要高亮的token索引集合
     * @param highlightStyle 高亮样式:'border' 使用边框,'underline' 使用下划线
     */
    setHighlightedIndices(indices: Set<number>, highlightStyle: HighlightStyle = 'border'): void {
        this.whenRectCacheReady('indices', 'HighlightManager: token 高亮缓存未就绪,已达最大重试', () => {
            this.removeIntervalSvgLines();

            if (this.currentStyle !== highlightStyle) {
                this.clearPreviousStyle(this.currentStyle);
            }
            this.currentStyle = highlightStyle;

            const highlightedRects: SVGRectElement[] = [];

            this.rectCache.forEach(({ rect, tokenIndex }, rectKey) => {
                const currentClass = rect.getAttribute('class') || '';
                const isHighlighted = indices.has(tokenIndex);

                if (highlightStyle === 'underline') {
                    this.applyUnderlineHighlight(rect, rectKey, isHighlighted, currentClass);
                } else {
                    this.applyBorderHighlight(rect, rectKey, isHighlighted, currentClass, highlightedRects);
                }
            });

            if (highlightStyle === 'border' && highlightedRects.length > 0) {
                highlightedRects.forEach(rect => {
                    if (rect.parentNode === this.svgOverlay) {
                        this.svgOverlay.appendChild(rect);
                    }
                });
            }
        });
    }

    /**
     * 应用下划线高亮样式
     */
    private applyUnderlineHighlight(
        rect: SVGRectElement,
        rectKey: string,
        isHighlighted: boolean,
        currentClass: string
    ): void {
        if (isHighlighted) {
            // 移除边框样式
            this.removeBorderStyle(rect);

            // 创建或更新下划线
            let underline = this.underlineCache.get(rectKey);
            if (!underline) {
                underline = this.createUnderline(rectKey);
            }

            // 更新下划线位置(rect底部)
            this.updateUnderlinePosition(rect, underline);

            // 更新class
            this.updateHighlightClass(rect, true);
        } else {
            // 移除下划线
            const underline = this.underlineCache.get(rectKey);
            if (underline) {
                underline.style.display = 'none';
            }

            // 确保没有边框
            this.removeBorderStyle(rect);

            // 移除class
            this.updateHighlightClass(rect, false);
        }
    }

    /**
     * 应用边框高亮样式
     */
    private applyBorderHighlight(
        rect: SVGRectElement,
        rectKey: string,
        isHighlighted: boolean,
        currentClass: string,
        highlightedRects: SVGRectElement[]
    ): void {
        if (isHighlighted) {
            // 先移除可能存在的下划线
            const underline = this.underlineCache.get(rectKey);
            if (underline) {
                underline.style.display = 'none';
            }

            // 添加高亮样式:统一色值的实线边框,无外发光
            rect.setAttribute('stroke', HIGHLIGHT_CONSTANTS.HIGHLIGHT_COLOR);
            rect.setAttribute('stroke-width', HIGHLIGHT_CONSTANTS.BORDER_WIDTH);
            rect.setAttribute('stroke-opacity', '1');
            rect.removeAttribute('stroke-dasharray');

            // 添加class
            this.updateHighlightClass(rect, true);

            // 收集需要移到末尾的rect
            highlightedRects.push(rect);
        } else {
            // 移除高亮样式
            this.removeBorderStyle(rect);

            // 确保移除下划线(如果存在)
            const underline = this.underlineCache.get(rectKey);
            if (underline) {
                underline.style.display = 'none';
            }

            // 移除class
            this.updateHighlightClass(rect, false);
        }
    }

    /**
     * 清除所有高亮(token 边框/下划线 + 字符区间下划线)
     */
    clearHighlight(): void {
        this.clearRectHighlightsOnly();
        this.removeIntervalSvgLines();
    }

    /** 仅清除 token 矩形上的高亮样式(保留字符区间线,供内部组合使用) */
    private clearRectHighlightsOnly(): void {
        this.rectCache.forEach(({ rect }, rectKey) => {
            this.removeBorderStyle(rect);
            this.updateHighlightClass(rect, false);
            const underline = this.underlineCache.get(rectKey);
            if (underline) {
                underline.style.display = 'none';
            }
        });
    }

    private removeIntervalSvgLines(): void {
        for (const line of this.intervalUnderlineLines) {
            line.remove();
        }
        this.intervalUnderlineLines = [];
    }

    /** 仅移除 chunk 字符区间下划线(不碰直方图 token 高亮) */
    clearCharIntervalUnderlines(): void {
        this.removeIntervalSvgLines();
    }

    /**
     * 按字符区间对应的 overlay 线段绘制下划线;并清除直方图 token 高亮。
     */
    setCharIntervalUnderlines(segments: CharIntervalUnderlineSeg[]): void {
        this.whenRectCacheReady('interval', 'HighlightManager: 区间下划线缓存未就绪,已达最大重试', () => {
            this.clearRectHighlightsOnly();
            this.removeIntervalSvgLines();
            this.appendIntervalUnderlineLines(segments);
        });
    }

    /** 布局变化时只刷新区间线几何 */
    updateCharIntervalUnderlines(segments: CharIntervalUnderlineSeg[]): void {
        this.removeIntervalSvgLines();
        this.appendIntervalUnderlineLines(segments);
    }

    /**
     * rect 缓存就绪后执行;未就绪则短暂重试(SVG 与缓存稍晚于组件构造)。
     */
    private whenRectCacheReady(
        slot: 'indices' | 'interval',
        warnMessage: string,
        run: () => void
    ): void {
        if (this.rectCache.size > 0) {
            this.rectCacheWait[slot] = 0;
            run();
            return;
        }
        if (this.rectCacheWait[slot] < this.MAX_RETRY_ATTEMPTS) {
            this.rectCacheWait[slot] += 1;
            setTimeout(() => this.whenRectCacheReady(slot, warnMessage, run), 50);
            return;
        }
        console.warn(warnMessage);
        this.rectCacheWait[slot] = 0;
    }

    private static underlineStrokeAttrs(line: SVGLineElement, cssClass: string): void {
        line.setAttribute('stroke', HIGHLIGHT_CONSTANTS.HIGHLIGHT_COLOR);
        line.setAttribute('stroke-width', HIGHLIGHT_CONSTANTS.UNDERLINE_WIDTH);
        line.setAttribute('stroke-opacity', '1');
        line.setAttribute('class', cssClass);
    }

    private appendIntervalUnderlineLines(segments: CharIntervalUnderlineSeg[]): void {
        const ns = 'http://www.w3.org/2000/svg';
        for (const s of segments) {
            const line = document.createElementNS(ns, 'line');
            HighlightManager.underlineStrokeAttrs(line, HIGHLIGHT_CONSTANTS.INTERVAL_UNDERLINE_CLASS);
            line.setAttribute('x1', String(s.x1));
            line.setAttribute('x2', String(s.x2));
            line.setAttribute('y1', String(s.y));
            line.setAttribute('y2', String(s.y));
            this.svgOverlay.appendChild(line);
            this.intervalUnderlineLines.push(line);
        }
    }

    /**
     * 清除之前的样式(切换样式时使用)
     */
    private clearPreviousStyle(previousStyle: HighlightStyle): void {
        if (previousStyle === 'underline') {
            // 从下划线模式切换到边框模式:清除所有下划线
            this.underlineCache.forEach((underline) => {
                underline.style.display = 'none';
            });
        } else if (previousStyle === 'border') {
            // 从边框模式切换到下划线模式:清除所有边框
            this.rectCache.forEach(({ rect }) => {
                this.removeBorderStyle(rect);
            });
        }
    }

    /**
     * 移除边框样式
     */
    private removeBorderStyle(rect: SVGRectElement): void {
        rect.removeAttribute('stroke');
        rect.removeAttribute('stroke-width');
        rect.removeAttribute('stroke-opacity');
        rect.removeAttribute('stroke-dasharray');
    }

    /**
     * 创建下划线元素
     */
    private createUnderline(rectKey: string): SVGLineElement {
        const underline = document.createElementNS('http://www.w3.org/2000/svg', 'line');
        HighlightManager.underlineStrokeAttrs(underline, HIGHLIGHT_CONSTANTS.UNDERLINE_CLASS);
        this.svgOverlay.appendChild(underline);
        this.underlineCache.set(rectKey, underline);
        return underline;
    }

    /**
     * 更新高亮class(工具方法)
     */
    private updateHighlightClass(rect: SVGRectElement, isHighlighted: boolean): void {
        const currentClass = rect.getAttribute('class') || '';
        if (isHighlighted) {
            if (!currentClass.includes(HIGHLIGHT_CONSTANTS.HIGHLIGHT_CLASS)) {
                rect.setAttribute('class', (currentClass + ' ' + HIGHLIGHT_CONSTANTS.HIGHLIGHT_CLASS).trim());
            }
        } else {
            const newClass = currentClass.replace(new RegExp(`\\b${HIGHLIGHT_CONSTANTS.HIGHLIGHT_CLASS}\\b`, 'g'), '').trim();
            rect.setAttribute('class', newClass);
        }
    }

    /**
     * 更新下划线位置
     */
    private updateUnderlinePosition(rect: SVGRectElement, underline: SVGLineElement): void {
        const x = parseFloat(rect.getAttribute('x') || '0');
        const y = parseFloat(rect.getAttribute('y') || '0');
        const width = parseFloat(rect.getAttribute('width') || '0');
        const height = parseFloat(rect.getAttribute('height') || '0');
        const bottomY = y + height;

        underline.setAttribute('x1', x.toString());
        underline.setAttribute('x2', (x + width).toString());
        underline.setAttribute('y1', bottomY.toString());
        underline.setAttribute('y2', bottomY.toString());
        underline.style.display = '';
    }

    /**
     * 更新下划线位置(当rect位置变化时调用)
     */
    updateUnderlinePositions(): void {
        this.rectCache.forEach(({ rect }, rectKey) => {
            const underline = this.underlineCache.get(rectKey);
            if (underline && underline.style.display !== 'none') {
                this.updateUnderlinePosition(rect, underline);
            }
        });
    }
}