@jspsych/plugin-tobii-user-position 0.1.1
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 +133 -0
- package/dist/index.browser.js +690 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.min.js +141 -0
- package/dist/index.browser.min.js.map +1 -0
- package/dist/index.cjs +689 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +282 -0
- package/dist/index.js +687 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
- package/src/index.d.ts +279 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +337 -0
- package/src/index.js.map +1 -0
- package/src/index.spec.d.ts +2 -0
- package/src/index.spec.d.ts.map +1 -0
- package/src/index.spec.js +102 -0
- package/src/index.spec.js.map +1 -0
- package/src/index.spec.ts +126 -0
- package/src/index.ts +368 -0
- package/src/position-display.d.ts +59 -0
- package/src/position-display.d.ts.map +1 -0
- package/src/position-display.js +421 -0
- package/src/position-display.js.map +1 -0
- package/src/position-display.ts +532 -0
- package/src/types.d.ts +18 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.js +2 -0
- package/src/types.js.map +1 -0
- package/src/types.ts +18 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Tobii User Position
|
|
3
|
+
* @description jsPsych plugin for Tobii eye tracker user position guide. Displays real-time
|
|
4
|
+
* head position feedback to help participants maintain optimal positioning for eye tracking.
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
* @author jsPsych Team
|
|
7
|
+
* @see {@link https://github.com/jspsych/jspsych-tobii/tree/main/packages/plugin-tobii-user-position#readme Documentation}
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from 'jspsych';
|
|
11
|
+
import { version } from '../package.json';
|
|
12
|
+
import type TobiiExtension from '@jspsych/extension-tobii';
|
|
13
|
+
import { PositionDisplay } from './position-display';
|
|
14
|
+
import type { PositionQuality } from './types';
|
|
15
|
+
|
|
16
|
+
const info = <const>{
|
|
17
|
+
name: 'tobii-user-position',
|
|
18
|
+
version: version,
|
|
19
|
+
parameters: {
|
|
20
|
+
/** Duration to show the position guide (ms), null for manual */
|
|
21
|
+
duration: {
|
|
22
|
+
type: ParameterType.INT,
|
|
23
|
+
default: null,
|
|
24
|
+
},
|
|
25
|
+
/** Message to display */
|
|
26
|
+
message: {
|
|
27
|
+
type: ParameterType.STRING,
|
|
28
|
+
default: 'Please position yourself so the indicators are green',
|
|
29
|
+
},
|
|
30
|
+
/** Update interval (ms) */
|
|
31
|
+
update_interval: {
|
|
32
|
+
type: ParameterType.INT,
|
|
33
|
+
default: 100,
|
|
34
|
+
},
|
|
35
|
+
/** Show distance feedback */
|
|
36
|
+
show_distance_feedback: {
|
|
37
|
+
type: ParameterType.BOOL,
|
|
38
|
+
default: true,
|
|
39
|
+
},
|
|
40
|
+
/** Show position feedback */
|
|
41
|
+
show_position_feedback: {
|
|
42
|
+
type: ParameterType.BOOL,
|
|
43
|
+
default: true,
|
|
44
|
+
},
|
|
45
|
+
/** Button text for manual continuation */
|
|
46
|
+
button_text: {
|
|
47
|
+
type: ParameterType.STRING,
|
|
48
|
+
default: 'Continue',
|
|
49
|
+
},
|
|
50
|
+
/** Only show button when position is good */
|
|
51
|
+
require_good_position: {
|
|
52
|
+
type: ParameterType.BOOL,
|
|
53
|
+
default: false,
|
|
54
|
+
},
|
|
55
|
+
/** Background color */
|
|
56
|
+
background_color: {
|
|
57
|
+
type: ParameterType.STRING,
|
|
58
|
+
default: '#f0f0f0',
|
|
59
|
+
},
|
|
60
|
+
/** Good position color */
|
|
61
|
+
good_color: {
|
|
62
|
+
type: ParameterType.STRING,
|
|
63
|
+
default: '#28a745',
|
|
64
|
+
},
|
|
65
|
+
/** Fair position color */
|
|
66
|
+
fair_color: {
|
|
67
|
+
type: ParameterType.STRING,
|
|
68
|
+
default: '#ffc107',
|
|
69
|
+
},
|
|
70
|
+
/** Poor position color */
|
|
71
|
+
poor_color: {
|
|
72
|
+
type: ParameterType.STRING,
|
|
73
|
+
default: '#dc3545',
|
|
74
|
+
},
|
|
75
|
+
/** Button color */
|
|
76
|
+
button_color: {
|
|
77
|
+
type: ParameterType.STRING,
|
|
78
|
+
default: '#007bff',
|
|
79
|
+
},
|
|
80
|
+
/** Button hover color */
|
|
81
|
+
button_hover_color: {
|
|
82
|
+
type: ParameterType.STRING,
|
|
83
|
+
default: '#0056b3',
|
|
84
|
+
},
|
|
85
|
+
/** Font size */
|
|
86
|
+
font_size: {
|
|
87
|
+
type: ParameterType.STRING,
|
|
88
|
+
default: '18px',
|
|
89
|
+
},
|
|
90
|
+
/** Position offset threshold for "good" status (normalized, default 0.15) */
|
|
91
|
+
position_threshold_good: {
|
|
92
|
+
type: ParameterType.FLOAT,
|
|
93
|
+
default: 0.15,
|
|
94
|
+
},
|
|
95
|
+
/** Position offset threshold for "fair" status (normalized, default 0.25) */
|
|
96
|
+
position_threshold_fair: {
|
|
97
|
+
type: ParameterType.FLOAT,
|
|
98
|
+
default: 0.25,
|
|
99
|
+
},
|
|
100
|
+
/** Distance offset threshold for "good" status (normalized, default 0.1) */
|
|
101
|
+
distance_threshold_good: {
|
|
102
|
+
type: ParameterType.FLOAT,
|
|
103
|
+
default: 0.1,
|
|
104
|
+
},
|
|
105
|
+
/** Distance offset threshold for "fair" status (normalized, default 0.2) */
|
|
106
|
+
distance_threshold_fair: {
|
|
107
|
+
type: ParameterType.FLOAT,
|
|
108
|
+
default: 0.2,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
data: {
|
|
112
|
+
/** Average X position during trial */
|
|
113
|
+
average_x: {
|
|
114
|
+
type: ParameterType.FLOAT,
|
|
115
|
+
},
|
|
116
|
+
/** Average Y position during trial */
|
|
117
|
+
average_y: {
|
|
118
|
+
type: ParameterType.FLOAT,
|
|
119
|
+
},
|
|
120
|
+
/** Average Z position (distance) during trial */
|
|
121
|
+
average_z: {
|
|
122
|
+
type: ParameterType.FLOAT,
|
|
123
|
+
},
|
|
124
|
+
/** Whether position was good at end */
|
|
125
|
+
position_good: {
|
|
126
|
+
type: ParameterType.BOOL,
|
|
127
|
+
},
|
|
128
|
+
/** Horizontal position status */
|
|
129
|
+
horizontal_status: {
|
|
130
|
+
type: ParameterType.STRING,
|
|
131
|
+
},
|
|
132
|
+
/** Vertical position status */
|
|
133
|
+
vertical_status: {
|
|
134
|
+
type: ParameterType.STRING,
|
|
135
|
+
},
|
|
136
|
+
/** Distance status */
|
|
137
|
+
distance_status: {
|
|
138
|
+
type: ParameterType.STRING,
|
|
139
|
+
},
|
|
140
|
+
/** Duration of trial */
|
|
141
|
+
rt: {
|
|
142
|
+
type: ParameterType.INT,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
type Info = typeof info;
|
|
148
|
+
|
|
149
|
+
class TobiiUserPositionPlugin implements JsPsychPlugin<Info> {
|
|
150
|
+
static info = info;
|
|
151
|
+
|
|
152
|
+
constructor(private jsPsych: JsPsych) {}
|
|
153
|
+
|
|
154
|
+
private static removeStyles(): void {
|
|
155
|
+
const el = document.getElementById('tobii-user-position-styles');
|
|
156
|
+
if (el) {
|
|
157
|
+
el.remove();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private injectStyles(trial: TrialType<Info>): void {
|
|
162
|
+
// Remove existing styles so each trial gets its own colors
|
|
163
|
+
TobiiUserPositionPlugin.removeStyles();
|
|
164
|
+
|
|
165
|
+
const css = `
|
|
166
|
+
.tobii-user-position-container {
|
|
167
|
+
display: flex;
|
|
168
|
+
flex-direction: column;
|
|
169
|
+
align-items: center;
|
|
170
|
+
justify-content: center;
|
|
171
|
+
width: 100%;
|
|
172
|
+
height: 100vh;
|
|
173
|
+
font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
174
|
+
font-size: ${trial.font_size};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.tobii-user-position-message {
|
|
178
|
+
margin-bottom: 40px;
|
|
179
|
+
text-align: center;
|
|
180
|
+
font-weight: 500;
|
|
181
|
+
color: #333;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.tobii-user-position-guide {
|
|
185
|
+
position: relative;
|
|
186
|
+
margin-bottom: 40px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.tobii-position-feedback {
|
|
190
|
+
text-align: center;
|
|
191
|
+
margin-bottom: 30px;
|
|
192
|
+
font-weight: 600;
|
|
193
|
+
font-size: 1.1em;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.tobii-user-position-button {
|
|
197
|
+
padding: 12px 32px;
|
|
198
|
+
font-size: 16px;
|
|
199
|
+
font-weight: 500;
|
|
200
|
+
border: none;
|
|
201
|
+
border-radius: 6px;
|
|
202
|
+
background-color: ${trial.button_color};
|
|
203
|
+
color: white;
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
transition: all 0.2s ease;
|
|
206
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.tobii-user-position-button:hover:not(:disabled) {
|
|
210
|
+
background-color: ${trial.button_hover_color};
|
|
211
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
|
212
|
+
transform: translateY(-1px);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.tobii-user-position-button:disabled {
|
|
216
|
+
background-color: #ccc;
|
|
217
|
+
cursor: not-allowed;
|
|
218
|
+
opacity: 0.6;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.tobii-center-marker {
|
|
222
|
+
position: absolute;
|
|
223
|
+
top: 50%;
|
|
224
|
+
left: 50%;
|
|
225
|
+
transform: translate(-50%, -50%);
|
|
226
|
+
width: 30px;
|
|
227
|
+
height: 30px;
|
|
228
|
+
border: 2px dashed #666;
|
|
229
|
+
border-radius: 50%;
|
|
230
|
+
opacity: 0.5;
|
|
231
|
+
}
|
|
232
|
+
`;
|
|
233
|
+
|
|
234
|
+
const styleElement = document.createElement('style');
|
|
235
|
+
styleElement.id = 'tobii-user-position-styles';
|
|
236
|
+
styleElement.textContent = css;
|
|
237
|
+
document.head.appendChild(styleElement);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
trial(display_element: HTMLElement, trial: TrialType<Info>) {
|
|
241
|
+
return new Promise<void>((resolve) => {
|
|
242
|
+
// Inject CSS
|
|
243
|
+
this.injectStyles(trial);
|
|
244
|
+
|
|
245
|
+
// Check for Tobii extension
|
|
246
|
+
const tobiiExtension = this.jsPsych.extensions.tobii as unknown as TobiiExtension;
|
|
247
|
+
if (!tobiiExtension) {
|
|
248
|
+
throw new Error('Tobii extension not loaded');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Create container
|
|
252
|
+
display_element.innerHTML = `
|
|
253
|
+
<div class="tobii-user-position-container">
|
|
254
|
+
</div>
|
|
255
|
+
`;
|
|
256
|
+
|
|
257
|
+
const container = display_element.querySelector(
|
|
258
|
+
'.tobii-user-position-container'
|
|
259
|
+
) as HTMLElement;
|
|
260
|
+
|
|
261
|
+
// Create position display
|
|
262
|
+
const positionDisplay = new PositionDisplay(container, {
|
|
263
|
+
message: trial.message!,
|
|
264
|
+
showDistanceFeedback: trial.show_distance_feedback!,
|
|
265
|
+
showPositionFeedback: trial.show_position_feedback!,
|
|
266
|
+
backgroundColor: trial.background_color!,
|
|
267
|
+
goodColor: trial.good_color!,
|
|
268
|
+
fairColor: trial.fair_color!,
|
|
269
|
+
poorColor: trial.poor_color!,
|
|
270
|
+
fontSize: trial.font_size!,
|
|
271
|
+
positionThresholdGood: trial.position_threshold_good!,
|
|
272
|
+
positionThresholdFair: trial.position_threshold_fair!,
|
|
273
|
+
distanceThresholdGood: trial.distance_threshold_good!,
|
|
274
|
+
distanceThresholdFair: trial.distance_threshold_fair!,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Add continue button if no duration specified
|
|
278
|
+
let continueButton: HTMLButtonElement | null = null;
|
|
279
|
+
if (trial.duration === null) {
|
|
280
|
+
continueButton = document.createElement('button');
|
|
281
|
+
continueButton.className = 'tobii-user-position-button';
|
|
282
|
+
continueButton.textContent = trial.button_text!;
|
|
283
|
+
if (trial.require_good_position) {
|
|
284
|
+
continueButton.disabled = true;
|
|
285
|
+
}
|
|
286
|
+
container.appendChild(continueButton);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Track position data
|
|
290
|
+
const positionSamples: PositionQuality[] = [];
|
|
291
|
+
const startTime = performance.now();
|
|
292
|
+
|
|
293
|
+
// Update position display periodically
|
|
294
|
+
const updateInterval = setInterval(async () => {
|
|
295
|
+
try {
|
|
296
|
+
const positionData = await tobiiExtension.getUserPosition();
|
|
297
|
+
positionDisplay.updatePosition(positionData);
|
|
298
|
+
|
|
299
|
+
// Track position quality
|
|
300
|
+
const quality = positionDisplay.getCurrentQuality(positionData);
|
|
301
|
+
positionSamples.push(quality);
|
|
302
|
+
|
|
303
|
+
// Update button state if required
|
|
304
|
+
if (continueButton && trial.require_good_position) {
|
|
305
|
+
continueButton.disabled = !quality.isGoodPosition;
|
|
306
|
+
}
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error('Error updating user position:', error);
|
|
309
|
+
}
|
|
310
|
+
}, trial.update_interval!);
|
|
311
|
+
|
|
312
|
+
// Cleanup helper to ensure DOM and styles are always cleaned up
|
|
313
|
+
const cleanup = () => {
|
|
314
|
+
clearInterval(updateInterval);
|
|
315
|
+
positionDisplay.destroy();
|
|
316
|
+
display_element.innerHTML = '';
|
|
317
|
+
TobiiUserPositionPlugin.removeStyles();
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Handle trial end
|
|
321
|
+
const endTrial = () => {
|
|
322
|
+
// Calculate average position
|
|
323
|
+
const validSamples = positionSamples.filter(
|
|
324
|
+
(s) => s.averageX !== null && s.averageY !== null && s.averageZ !== null
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
let averageX = null;
|
|
328
|
+
let averageY = null;
|
|
329
|
+
let averageZ = null;
|
|
330
|
+
let finalQuality: PositionQuality | null = null;
|
|
331
|
+
|
|
332
|
+
if (validSamples.length > 0) {
|
|
333
|
+
averageX = validSamples.reduce((sum, s) => sum + s.averageX!, 0) / validSamples.length;
|
|
334
|
+
averageY = validSamples.reduce((sum, s) => sum + s.averageY!, 0) / validSamples.length;
|
|
335
|
+
averageZ = validSamples.reduce((sum, s) => sum + s.averageZ!, 0) / validSamples.length;
|
|
336
|
+
finalQuality = positionSamples[positionSamples.length - 1];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const trialData = {
|
|
340
|
+
average_x: averageX,
|
|
341
|
+
average_y: averageY,
|
|
342
|
+
average_z: averageZ,
|
|
343
|
+
position_good: finalQuality?.isGoodPosition ?? false,
|
|
344
|
+
horizontal_status: finalQuality?.horizontalStatus ?? 'poor',
|
|
345
|
+
vertical_status: finalQuality?.verticalStatus ?? 'poor',
|
|
346
|
+
distance_status: finalQuality?.distanceStatus ?? 'poor',
|
|
347
|
+
rt: Math.round(performance.now() - startTime),
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
cleanup();
|
|
351
|
+
this.jsPsych.finishTrial(trialData);
|
|
352
|
+
resolve();
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// Set up continue button
|
|
356
|
+
if (continueButton) {
|
|
357
|
+
continueButton.addEventListener('click', endTrial);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Set up duration timeout
|
|
361
|
+
if (trial.duration != null) {
|
|
362
|
+
this.jsPsych.pluginAPI.setTimeout(endTrial, trial.duration);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export default TobiiUserPositionPlugin;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { UserPositionData, PositionQuality } from './types';
|
|
2
|
+
export interface PositionDisplayOptions {
|
|
3
|
+
message: string;
|
|
4
|
+
showDistanceFeedback: boolean;
|
|
5
|
+
showPositionFeedback: boolean;
|
|
6
|
+
backgroundColor: string;
|
|
7
|
+
goodColor: string;
|
|
8
|
+
fairColor: string;
|
|
9
|
+
poorColor: string;
|
|
10
|
+
fontSize: string;
|
|
11
|
+
positionThresholdGood: number;
|
|
12
|
+
positionThresholdFair: number;
|
|
13
|
+
distanceThresholdGood: number;
|
|
14
|
+
distanceThresholdFair: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Display component for user position guide
|
|
18
|
+
* Shows a face outline that scales with distance, similar to Tobii Eye Tracker Manager
|
|
19
|
+
*/
|
|
20
|
+
export declare class PositionDisplay {
|
|
21
|
+
private container;
|
|
22
|
+
private messageElement;
|
|
23
|
+
private trackingBoxElement;
|
|
24
|
+
private faceOutlineElement;
|
|
25
|
+
private leftEyeElement;
|
|
26
|
+
private rightEyeElement;
|
|
27
|
+
private distanceBarContainer;
|
|
28
|
+
private distanceBarFill;
|
|
29
|
+
private feedbackElement;
|
|
30
|
+
private options;
|
|
31
|
+
private readonly BOX_WIDTH;
|
|
32
|
+
private readonly BOX_HEIGHT;
|
|
33
|
+
private readonly MIN_FACE_SCALE;
|
|
34
|
+
private readonly MAX_FACE_SCALE;
|
|
35
|
+
private readonly OPTIMAL_FACE_SCALE;
|
|
36
|
+
constructor(container: HTMLElement, options: PositionDisplayOptions);
|
|
37
|
+
private createDisplay;
|
|
38
|
+
private createFaceSVG;
|
|
39
|
+
/**
|
|
40
|
+
* Update the display with new position data
|
|
41
|
+
*/
|
|
42
|
+
updatePosition(positionData: UserPositionData | null): void;
|
|
43
|
+
private getAveragePosition;
|
|
44
|
+
private updateFaceDisplay;
|
|
45
|
+
private updateDistanceBar;
|
|
46
|
+
private updateEyeIndicators;
|
|
47
|
+
private updateTextualFeedback;
|
|
48
|
+
private assessPositionQuality;
|
|
49
|
+
private showNoData;
|
|
50
|
+
/**
|
|
51
|
+
* Get current position quality
|
|
52
|
+
*/
|
|
53
|
+
getCurrentQuality(positionData: UserPositionData | null): PositionQuality;
|
|
54
|
+
/**
|
|
55
|
+
* Remove the display
|
|
56
|
+
*/
|
|
57
|
+
destroy(): void;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=position-display.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"position-display.d.ts","sourceRoot":"","sources":["position-display.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE5D,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED;;;GAGG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,cAAc,CAAe;IACrC,OAAO,CAAC,kBAAkB,CAAe;IACzC,OAAO,CAAC,kBAAkB,CAAe;IACzC,OAAO,CAAC,cAAc,CAAe;IACrC,OAAO,CAAC,eAAe,CAAe;IACtC,OAAO,CAAC,oBAAoB,CAAe;IAC3C,OAAO,CAAC,eAAe,CAAe;IACtC,OAAO,CAAC,eAAe,CAAe;IACtC,OAAO,CAAC,OAAO,CAAyB;IAGxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAO;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAO;IAClC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAO;IACtC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAO;IACtC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAO;gBAE9B,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,sBAAsB;IAMnE,OAAO,CAAC,aAAa;IA8IrB,OAAO,CAAC,aAAa;IA4DrB;;OAEG;IACI,cAAc,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,GAAG,IAAI;IAyClE,OAAO,CAAC,kBAAkB;IAgB1B,OAAO,CAAC,iBAAiB;IAgCzB,OAAO,CAAC,iBAAiB;IAwBzB,OAAO,CAAC,mBAAmB;IAc3B,OAAO,CAAC,qBAAqB;IAqD7B,OAAO,CAAC,qBAAqB;IAoC7B,OAAO,CAAC,UAAU;IAQlB;;OAEG;IACI,iBAAiB,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI,GAAG,eAAe;IA+ChF;;OAEG;IACI,OAAO,IAAI,IAAI;CAGvB"}
|