generated from city-unit/st-extension-example
-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathtl_style.js
291 lines (266 loc) · 13.2 KB
/
tl_style.js
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
import { extension_settings, getContext, loadExtensionSettings } from '../../../extensions.js';
import { characters, getRequestHeaders, openCharacterChat, saveSettingsDebounced, getThumbnailUrl } from '../../../../script.js';
import { power_user } from '../../../power-user.js';
/**
* Extracts the alpha (opacity) value from a given RGBA color string.
*
* @param {string} rgbaString - The RGBA color string in the format "rgba(r, g, b, a)".
* @returns {number|null} The extracted alpha value as a float or null if no match is found.
*/
function getAlphaFromRGBA(rgbaString) {
const match = rgbaString.match(/rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*(\d*(?:\.\d+)?)\s*\)/);
return match ? parseFloat(match[1]) : null;
}
/**
* Highlights the path from a specified checkpoint node to the root in a data structure representing a Cytoscape graph.
* The function iteratively traces and highlights edges and nodes, adjusting visual attributes like color, thickness, and zIndex.
*
* @param {Object} rawData - The data structure representing the Cytoscape graph with nodes and edges.
* @param {string|number} bookmarkNodeId - The ID of the checkpoint node to start highlighting from.
* @param {number} currentHighlightThickness - The starting thickness for highlighting edges (default is 4).
* @param {number} startingZIndex - The starting zIndex for nodes and edges to be highlighted (default is 1000).
*/
function highlightPathToRoot(rawData, bookmarkNodeId, currentHighlightThickness = 4, startingZIndex = 1000) {
let bookmarkNode = Object.values(rawData).find(entry =>
entry.group === 'nodes' && entry.data.id === bookmarkNodeId,
);
if (!bookmarkNode) {
console.error('Checkpoint node not found!');
return;
}
let currentNode = bookmarkNode;
let currentZIndex = startingZIndex;
while (currentNode) {
// If the current node has the isBookmark attribute and it's not the initial bookmarkNode, stop highlighting
if (currentNode !== bookmarkNode && currentNode.data.isBookmark) {
break; // exit from the while loop
}
let incomingEdge = Object.values(rawData).find(entry =>
entry.group === 'edges' && entry.data.target === currentNode.data.id,
);
if (incomingEdge) {
incomingEdge.data.isHighlight = true;
incomingEdge.data.color = bookmarkNode.data.color;
incomingEdge.data.bookmarkName = bookmarkNode.data.bookmarkName;
incomingEdge.data.highlightThickness = currentHighlightThickness;
currentHighlightThickness = Math.min(currentHighlightThickness + 0.1, 6);
currentNode.data.borderColor = incomingEdge.data.color;
// Set the zIndex of the incomingEdge
incomingEdge.data.zIndex = currentZIndex;
currentZIndex++; // Increase the zIndex for the next edge in the path
// Select the next node up
currentNode = Object.values(rawData).find(entry =>
entry.group === 'nodes' && entry.data.id === incomingEdge.data.source,
);
} else { // This was the topmost node
currentNode = null;
}
}
}
/**
* Sets up visual styles for nodes and edges based on provided node data and context settings.
* This function prepares styles that are to be used with Cytoscape to visually represent a graph.
* Depending on extension settings and context, different colors, shapes, and styles are applied to nodes and edges.
* Additionally, paths from checkpoint nodes to the root are highlighted.
*
* @param {Object} nodeData - Data structure representing the graph with nodes and edges.
* @returns {Array} An array of style definitions suitable for use with Cytoscape.
*/
export function setupStylesAndData(nodeData) {
const context = getContext();
let selected_group = context.groupId;
let group = context.groups.find(group => group.id === selected_group);
let this_chid = context.characterId;
const avatarImg = selected_group ? group?.avatar_url : getThumbnailUrl('avatar', characters[this_chid]['avatar']);
let theme = {};
if (extension_settings.timeline.useChatColors) {
theme.charNodeColor = power_user.main_text_color;
theme.edgeColor = power_user.italics_text_color;
theme.userNodeColor = power_user.quote_text_color;
theme.bookmarkColor = 'rgba(255, 215, 0, 1)'; // gold
// power_user.blur_tint_color;
// power_user.user_mes_blur_tint_color;
// power_user.bot_mes_blur_tint_color;
// power_user.shadow_color;
}
else {
theme.charNodeColor = extension_settings.timeline.charNodeColor;
theme.edgeColor = extension_settings.timeline.edgeColor;
theme.userNodeColor = extension_settings.timeline.userNodeColor;
theme.bookmarkColor = extension_settings.timeline.bookmarkColor;
}
Object.values(nodeData).forEach(entry => {
if (entry.group === 'nodes' && entry.data.isBookmark) {
highlightPathToRoot(nodeData, entry.data.id);
}
});
const cytoscapeStyles = [
{
selector: 'edge',
style: {
'curve-style': extension_settings.timeline.curveStyle,
'taxi-direction': 'rightward',
'segment-distances': [5, 5], // corner radius
'line-color': function (ele) {
return ele.data('isHighlight') ? ele.data('color') : theme.edgeColor;
},
'line-opacity': function (ele) {
return ele.data('isHighlight') ? 1 : getAlphaFromRGBA(theme.edgeColor);
},
'width': function (ele) {
return ele.data('highlightThickness') ? ele.data('highlightThickness') : 3;
},
'z-index': function (ele) {
return ele.data('zIndex') ? ele.data('zIndex') : 1;
},
},
},
{
selector: 'node',
style: {
'width': function (ele) {
let totalSwipes = Number(ele.data('totalSwipes'));
if (isNaN(totalSwipes)) {
totalSwipes = 0;
}
return extension_settings.timeline.swipeScale ? Math.abs(Math.log(totalSwipes + 1)) * 4 + Number(extension_settings.timeline.nodeWidth) : extension_settings.timeline.nodeWidth;
},
'height': function (ele) {
let totalSwipes = Number(ele.data('totalSwipes'));
if (isNaN(totalSwipes)) {
totalSwipes = 0;
}
return extension_settings.timeline.swipeScale ? Math.abs(Math.log(totalSwipes + 1)) * 4 + Number(extension_settings.timeline.nodeHeight) : extension_settings.timeline.nodeHeight;
},
'shape': extension_settings.timeline.nodeShape, // or 'circle'
'background-color': function (ele) {
return ele.data('is_user') ? theme.userNodeColor : theme.charNodeColor;
},
'background-opacity': function (ele) {
return ele.data('is_user') ? getAlphaFromRGBA(theme.userNodeColor) : getAlphaFromRGBA(theme.charNodeColor);
},
'border-color': function (ele) {
// NOTE: We highlight the checkpoint node itself based on the color theme, ignoring its 'borderColor' property.
// All the other nodes along the checkpoint path get highlighted with the checkpoint's random color.
return ele.data('isBookmark') ? theme.bookmarkColor : ele.data('borderColor') ? ele.data('borderColor') : ele.data('totalSwipes') ? (ele.data('is_user') ? theme.userNodeColor : theme.charNodeColor) : 'black';
},
'border-width': function (ele) {
return ele.data('isBookmark') || ele.data('totalSwipes') ? 5 : ele.data('borderColor') ? 3 : 0;
},
'border-opacity': function (ele) {
// The remark of 'border-color' applies here, too.
return ele.data('isBookmark') ? getAlphaFromRGBA(theme.bookmarkColor) : ele.data('borderColor') ? 1 : ele.data('totalSwipes') > 0 ? 1 : 0;
},
'border-style': function (ele) {
return ele.data('totalSwipes') > 0 ? 'double' : 'solid'; // Halo around node with swipes
},
},
},
{
selector: 'node[label="root"]',
style: {
'background-image': extension_settings.timeline.avatarAsRoot ? avatarImg : 'none',
'background-fit': extension_settings.timeline.avatarAsRoot ? 'cover' : 'none',
// TODO: Ideally, we should determine the aspect ratio from the avatar image.
'width': extension_settings.timeline.avatarAsRoot ? '40px' : extension_settings.timeline.nodeWidth,
'height': extension_settings.timeline.avatarAsRoot ? '50px' : extension_settings.timeline.nodeHeight,
'shape': extension_settings.timeline.avatarAsRoot ? 'rectangle' : extension_settings.timeline.nodeShape,
},
},
{
selector: 'node[?is_system]', // Select nodes with is_system property set to true
style: {
'background-color': 'grey',
'border-style': 'dashed',
'border-width': 3,
'border-color': function (ele) {
return ele.data('isBookmark') ? extension_settings.timeline.bookmarkColor : ele.data('borderColor') ? ele.data('borderColor') : 'black';
},
},
},
{
selector: 'node[?isSwipe]', // Select nodes with isSwipe property set to true
style: {
'background-opacity': .5,
'border-width': 3,
'border-color': function (ele) {
return ele.data('isBookmark') ? extension_settings.timeline.bookmarkColor : ele.data('borderColor') ? ele.data('borderColor') : 'grey';
},
'border-style': 'dashed',
'border-opacity': 1,
},
},
{
selector: 'edge[?isSwipe]',
style: {
'line-style': 'dashed',
'line-opacity': .5,
},
},
{
selector: '.NoticeMe', // This gets flashed on and off upon zooming to the current chat node
style: {
'background-opacity': 0.5,
}
}
];
return cytoscapeStyles;
}
/**
* Highlights specific elements (nodes or edges) in a Cytoscape graph based on a given selector.
* Used e.g. in the text search functionality.
*
* Initially, all elements in the graph are dimmed. Based on the provided selector, matching nodes or edges are then
* highlighted with a white underlay. If the selector pertains to an edge with a specific color, nodes with the same
* border color are also highlighted.
*
* @param {Object} cy - The Cytoscape instance containing the graph elements.
* @param {string|function} selector - A Cytoscape-compatible selector string used to determine which elements to highlight.
* Alternatively, a callable selector; in that case, it is assumed to select nodes.
* This is safer when selecting by node text content, which may include special characters.
*/
export function highlightElements(cy, selector) {
cy.elements().style({ 'opacity': 0.2 }); // Dim all nodes and edges
// Defaults for when `selector` selects nodes.
let underlayPadding = '5px';
let underlayShape = 'ellipse';
// If it's an edge selector (i.e. for a checkpoint path)
if (((typeof selector === "string") || (selector instanceof String)) && selector.startsWith('edge')) {
let colorValue = selector.match(/color="([^"]+)"/)[1]; // Extract the color from the selector
let nodeSelector = `node[borderColor="${colorValue}"]`; // Construct the corresponding node selector
// Style the associated nodes
cy.elements(nodeSelector).style({
'opacity': 1,
'underlay-color': 'white',
'underlay-padding': '2px',
'underlay-opacity': 0.5,
'underlay-shape': 'ellipse',
});
// For edges.
underlayPadding = '2px';
underlayShape = '';
}
// Style the originally selected elements (any kind)
cy.elements(selector).style({
'opacity': 1,
'underlay-color': 'white',
'underlay-padding': underlayPadding,
'underlay-opacity': 0.5,
'underlay-shape': underlayShape,
});
}
/**
* Restores all elements in a Cytoscape graph to their default visual state.
* The opacity of all elements is set back to 1, and any applied underlays are removed.
*
* @param {Object} cy - The Cytoscape instance containing the graph elements.
*/
export function restoreElements(cy) {
cy.elements().style({
'opacity': 1,
'underlay-color': '',
'underlay-padding': '',
'underlay-opacity': '',
'underlay-shape': '',
});
}