@teachinglab/omd 0.2.6 → 0.2.7
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { omdEquationNode } from '../nodes/omdEquationNode.js';
|
|
2
2
|
import { omdColor } from '../../src/omdColor.js';
|
|
3
|
-
import { jsvgLine } from '@teachinglab/jsvg';
|
|
3
|
+
import { jsvgLine, jsvgEllipse } from '@teachinglab/jsvg';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Handles visual layout, positioning, and visibility management for step visualizations
|
|
@@ -8,6 +8,21 @@ import { jsvgLine } from '@teachinglab/jsvg';
|
|
|
8
8
|
export class omdStepVisualizerLayout {
|
|
9
9
|
constructor(stepVisualizer) {
|
|
10
10
|
this.stepVisualizer = stepVisualizer;
|
|
11
|
+
this.expansionDots = []; // Small dots that show/hide hidden steps
|
|
12
|
+
this.fixedVisualizerPosition = 250; // Fixed position for the step visualizer from left edge
|
|
13
|
+
this.allowEquationRepositioning = true; // Flag to control when equations can be repositioned
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sets the fixed position for the step visualizer
|
|
18
|
+
* @param {number} position - The x position from the left edge where the visualizer should be positioned
|
|
19
|
+
*/
|
|
20
|
+
setFixedVisualizerPosition(position) {
|
|
21
|
+
this.fixedVisualizerPosition = position;
|
|
22
|
+
// Trigger a layout update if the visualizer is already initialized
|
|
23
|
+
if (this.stepVisualizer && this.stepVisualizer.stepDots.length > 0) {
|
|
24
|
+
this.updateVisualLayout();
|
|
25
|
+
}
|
|
11
26
|
}
|
|
12
27
|
|
|
13
28
|
/**
|
|
@@ -16,14 +31,31 @@ export class omdStepVisualizerLayout {
|
|
|
16
31
|
updateVisualLayout() {
|
|
17
32
|
if (this.stepVisualizer.stepDots.length === 0) return;
|
|
18
33
|
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
const baseRight = (this.stepVisualizer.sequenceWidth || this.stepVisualizer.width);
|
|
22
|
-
// Use EFFECTIVE padding (after pill clamping) to avoid overlap when pills are wider
|
|
34
|
+
// Calculate the total width needed for equations (including any padding)
|
|
35
|
+
const baseEquationWidth = (this.stepVisualizer.sequenceWidth || this.stepVisualizer.width);
|
|
23
36
|
const extraPaddingX = this._getMaxEquationEffectivePaddingX();
|
|
24
|
-
const
|
|
37
|
+
const totalEquationWidth = baseEquationWidth + extraPaddingX;
|
|
38
|
+
|
|
39
|
+
// Position visual container at a fixed position
|
|
40
|
+
const visualX = this.fixedVisualizerPosition;
|
|
25
41
|
this.stepVisualizer.visualContainer.setPosition(visualX, 0);
|
|
26
42
|
|
|
43
|
+
// Only reposition equations if explicitly allowed (not during simple dot clicks)
|
|
44
|
+
if (this.allowEquationRepositioning) {
|
|
45
|
+
// Calculate how much space is available for equations before the visualizer
|
|
46
|
+
const availableEquationSpace = this.fixedVisualizerPosition - this.stepVisualizer.visualSpacing;
|
|
47
|
+
|
|
48
|
+
// If equations are too wide, shift them left to fit
|
|
49
|
+
let equationOffsetX = 0;
|
|
50
|
+
if (totalEquationWidth > availableEquationSpace) {
|
|
51
|
+
equationOffsetX = availableEquationSpace - totalEquationWidth;
|
|
52
|
+
console.debug('Equations too wide, shifting left by:', -equationOffsetX);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Apply the offset to equation positioning
|
|
56
|
+
this._adjustEquationPositions(equationOffsetX);
|
|
57
|
+
}
|
|
58
|
+
|
|
27
59
|
// Position dots based on visible equations
|
|
28
60
|
const visibleSteps = this.stepVisualizer.steps.filter(s => s.visible !== false);
|
|
29
61
|
let currentY = 0;
|
|
@@ -88,6 +120,37 @@ export class omdStepVisualizerLayout {
|
|
|
88
120
|
|
|
89
121
|
this.stepVisualizer.visualContainer.setWidthAndHeight(containerWidth, containerHeight);
|
|
90
122
|
this.updateVisualZOrder();
|
|
123
|
+
|
|
124
|
+
// Position expansion dots after main dots are positioned
|
|
125
|
+
this._positionExpansionDots();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Adjusts the horizontal position of all equations by the specified offset
|
|
130
|
+
* @private
|
|
131
|
+
*/
|
|
132
|
+
_adjustEquationPositions(offsetX) {
|
|
133
|
+
if (offsetX === 0) return; // No adjustment needed
|
|
134
|
+
|
|
135
|
+
const sv = this.stepVisualizer;
|
|
136
|
+
|
|
137
|
+
// Adjust position of all steps (equations and operation display nodes)
|
|
138
|
+
sv.steps.forEach(step => {
|
|
139
|
+
if (step && step.setPosition && typeof step.setPosition === 'function') {
|
|
140
|
+
const currentX = step.xpos || 0;
|
|
141
|
+
const currentY = step.ypos || 0;
|
|
142
|
+
step.setPosition(currentX + offsetX, currentY);
|
|
143
|
+
|
|
144
|
+
// Also adjust operation display nodes if they exist
|
|
145
|
+
if (step.operationDisplayNode && step.operationDisplayNode.setPosition) {
|
|
146
|
+
const opCurrentX = step.operationDisplayNode.xpos || 0;
|
|
147
|
+
const opCurrentY = step.operationDisplayNode.ypos || 0;
|
|
148
|
+
step.operationDisplayNode.setPosition(opCurrentX + offsetX, opCurrentY);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
console.debug('Adjusted equation positions by offsetX:', offsetX);
|
|
91
154
|
}
|
|
92
155
|
|
|
93
156
|
/**
|
|
@@ -157,6 +220,16 @@ export class omdStepVisualizerLayout {
|
|
|
157
220
|
}
|
|
158
221
|
}
|
|
159
222
|
});
|
|
223
|
+
|
|
224
|
+
// Expansion dots on top of regular dots (z-index 4)
|
|
225
|
+
this.expansionDots.forEach(dot => {
|
|
226
|
+
if (dot && dot.svgObject) {
|
|
227
|
+
dot.svgObject.style.zIndex = '4';
|
|
228
|
+
if (dot.parentNode !== this.stepVisualizer.visualContainer) {
|
|
229
|
+
this.stepVisualizer.visualContainer.addChild(dot);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
});
|
|
160
233
|
}
|
|
161
234
|
|
|
162
235
|
/**
|
|
@@ -177,20 +250,33 @@ export class omdStepVisualizerLayout {
|
|
|
177
250
|
* Updates visibility of visual elements based on equation visibility
|
|
178
251
|
*/
|
|
179
252
|
updateVisualVisibility() {
|
|
253
|
+
console.debug('\n=== updateVisualVisibility START ===');
|
|
180
254
|
const sv = this.stepVisualizer;
|
|
181
255
|
|
|
182
|
-
// Update dot visibility first, which is the source of truth
|
|
183
|
-
sv.
|
|
256
|
+
// Update dot visibility and color first, which is the source of truth
|
|
257
|
+
const dotColor = sv.styling?.dotColor || omdColor.stepColor;
|
|
258
|
+
console.debug('Updating dot visibility and color...');
|
|
259
|
+
|
|
260
|
+
sv.stepDots.forEach((dot, index) => {
|
|
184
261
|
if (dot.equationRef && dot.equationRef.visible !== false) {
|
|
262
|
+
dot.setFillColor(dotColor);
|
|
263
|
+
dot.setStrokeColor(dotColor);
|
|
185
264
|
dot.show();
|
|
186
265
|
dot.visible = true; // Use the dot's own visibility property
|
|
266
|
+
console.debug(`Dot ${index}: VISIBLE (equation.visible=${dot.equationRef.visible})`);
|
|
187
267
|
} else {
|
|
188
268
|
dot.hide();
|
|
189
269
|
dot.visible = false;
|
|
270
|
+
console.debug(`Dot ${index}: HIDDEN (equation.visible=${dot.equationRef ? dot.equationRef.visible : 'no equation'})`);
|
|
190
271
|
}
|
|
191
272
|
});
|
|
192
273
|
|
|
274
|
+
// Clear existing expansion dots
|
|
275
|
+
console.debug('Clearing existing expansion dots...');
|
|
276
|
+
this._clearExpansionDots();
|
|
277
|
+
|
|
193
278
|
// Remove all old lines from the container and the array
|
|
279
|
+
console.debug('Removing old lines...');
|
|
194
280
|
sv.stepLines.forEach(line => {
|
|
195
281
|
// Remove the line if it is currently a child of the visualContainer
|
|
196
282
|
if (line.parent === sv.visualContainer) {
|
|
@@ -201,14 +287,17 @@ export class omdStepVisualizerLayout {
|
|
|
201
287
|
|
|
202
288
|
// Get the dots that are currently visible
|
|
203
289
|
const visibleDots = sv.stepDots.filter(dot => dot.visible);
|
|
290
|
+
console.debug(`Found ${visibleDots.length} visible dots out of ${sv.stepDots.length} total dots`);
|
|
204
291
|
|
|
205
292
|
// Re-create connecting lines only between the visible dots
|
|
293
|
+
console.debug('Creating lines between visible dots...');
|
|
206
294
|
for (let i = 0; i < visibleDots.length - 1; i++) {
|
|
207
295
|
const fromDot = visibleDots[i];
|
|
208
296
|
const toDot = visibleDots[i + 1];
|
|
209
297
|
|
|
210
298
|
const line = new jsvgLine();
|
|
211
|
-
|
|
299
|
+
const lineColor = sv.styling?.lineColor || omdColor.stepColor;
|
|
300
|
+
line.setStrokeColor(lineColor);
|
|
212
301
|
line.setStrokeWidth(sv.lineWidth);
|
|
213
302
|
line.fromDotIndex = sv.stepDots.indexOf(fromDot);
|
|
214
303
|
line.toDotIndex = sv.stepDots.indexOf(toDot);
|
|
@@ -216,9 +305,19 @@ export class omdStepVisualizerLayout {
|
|
|
216
305
|
sv.visualContainer.addChild(line);
|
|
217
306
|
sv.stepLines.push(line);
|
|
218
307
|
}
|
|
308
|
+
console.debug(`Created ${sv.stepLines.length} lines`);
|
|
219
309
|
|
|
220
310
|
// After creating the lines, update their positions
|
|
221
311
|
this.updateAllLinePositions();
|
|
312
|
+
|
|
313
|
+
// Create expansion dots for dots that have hidden steps before them
|
|
314
|
+
console.debug('\n>>> CALLING _createExpansionDots <<<');
|
|
315
|
+
this._createExpansionDots();
|
|
316
|
+
|
|
317
|
+
console.debug('\n>>> CALLING _positionExpansionDots <<<');
|
|
318
|
+
this._positionExpansionDots();
|
|
319
|
+
|
|
320
|
+
console.debug('=== updateVisualVisibility END ===\n');
|
|
222
321
|
}
|
|
223
322
|
|
|
224
323
|
/**
|
|
@@ -242,4 +341,536 @@ export class omdStepVisualizerLayout {
|
|
|
242
341
|
dot.svgObject.onclick = null;
|
|
243
342
|
}
|
|
244
343
|
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Clears all expansion dots
|
|
347
|
+
* @private
|
|
348
|
+
*/
|
|
349
|
+
_clearExpansionDots() {
|
|
350
|
+
this.expansionDots.forEach(dot => {
|
|
351
|
+
if (dot.parentNode === this.stepVisualizer.visualContainer) {
|
|
352
|
+
this.stepVisualizer.visualContainer.removeChild(dot);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
this.expansionDots = [];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Creates expansion dots for visible dots that have hidden steps before them
|
|
360
|
+
* @private
|
|
361
|
+
*/
|
|
362
|
+
_createExpansionDots() {
|
|
363
|
+
console.debug('=== _createExpansionDots START ===');
|
|
364
|
+
const sv = this.stepVisualizer;
|
|
365
|
+
const allDots = sv.stepDots;
|
|
366
|
+
const visibleDots = sv.stepDots.filter(dot => dot.visible);
|
|
367
|
+
|
|
368
|
+
console.debug('Total dots:', allDots.length, 'Visible dots:', visibleDots.length);
|
|
369
|
+
|
|
370
|
+
// Debug all steps and their properties
|
|
371
|
+
console.debug('=== ALL STEPS DEBUG ===');
|
|
372
|
+
sv.steps.forEach((step, i) => {
|
|
373
|
+
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
374
|
+
console.debug(`Step ${i}: type=${step.constructor.name}, stepMark=${step.stepMark}, visible=${step.visible}`);
|
|
375
|
+
} else {
|
|
376
|
+
console.debug(`Step ${i}: type=${step ? step.constructor.name : 'null'}, not an equation`);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Debug all dots and their properties
|
|
381
|
+
console.debug('=== ALL DOTS DEBUG ===');
|
|
382
|
+
allDots.forEach((dot, i) => {
|
|
383
|
+
if (dot && dot.equationRef) {
|
|
384
|
+
console.debug(`Dot ${i}: visible=${dot.visible}, equation.stepMark=${dot.equationRef.stepMark}, equation.visible=${dot.equationRef.visible}`);
|
|
385
|
+
} else {
|
|
386
|
+
console.debug(`Dot ${i}: no equation reference`);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
console.debug('=== CHECKING VISIBLE DOTS FOR EXPANSION OPPORTUNITIES ===');
|
|
391
|
+
|
|
392
|
+
// Check for hidden intermediate steps between consecutive visible major steps (stepMark = 0)
|
|
393
|
+
const visibleMajorSteps = [];
|
|
394
|
+
sv.steps.forEach((step, stepIndex) => {
|
|
395
|
+
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
396
|
+
if (step.stepMark === 0 && step.visible === true) {
|
|
397
|
+
visibleMajorSteps.push(stepIndex);
|
|
398
|
+
console.debug(`Found visible major step at index ${stepIndex}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
console.debug(`Visible major steps: [${visibleMajorSteps.join(', ')}]`);
|
|
404
|
+
|
|
405
|
+
// Check between consecutive visible major steps for hidden intermediate steps
|
|
406
|
+
for (let i = 1; i < visibleMajorSteps.length; i++) {
|
|
407
|
+
const previousMajorStepIndex = visibleMajorSteps[i - 1];
|
|
408
|
+
const currentMajorStepIndex = visibleMajorSteps[i];
|
|
409
|
+
|
|
410
|
+
console.debug(`\n--- Checking between major steps ${previousMajorStepIndex} and ${currentMajorStepIndex} ---`);
|
|
411
|
+
|
|
412
|
+
// Count hidden intermediate steps between these major steps
|
|
413
|
+
let hiddenIntermediateCount = 0;
|
|
414
|
+
for (let j = previousMajorStepIndex + 1; j < currentMajorStepIndex; j++) {
|
|
415
|
+
const step = sv.steps[j];
|
|
416
|
+
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
417
|
+
if (step.stepMark > 0 && step.visible === false) {
|
|
418
|
+
hiddenIntermediateCount++;
|
|
419
|
+
console.debug(` Found hidden intermediate step at ${j}: stepMark=${step.stepMark}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
console.debug(`Hidden intermediate steps between ${previousMajorStepIndex} and ${currentMajorStepIndex}: ${hiddenIntermediateCount}`);
|
|
425
|
+
|
|
426
|
+
if (hiddenIntermediateCount > 0) {
|
|
427
|
+
console.debug('>>> CREATING EXPANSION DOT <<<');
|
|
428
|
+
// Find the dot for the current major step to position the expansion dot above it
|
|
429
|
+
const currentMajorStep = sv.steps[currentMajorStepIndex];
|
|
430
|
+
const currentDotIndex = sv.stepDots.findIndex(dot => dot.equationRef === currentMajorStep);
|
|
431
|
+
|
|
432
|
+
if (currentDotIndex >= 0) {
|
|
433
|
+
// Find the position in the visible dots array
|
|
434
|
+
const visibleDotIndex = i; // i is the position in visibleMajorSteps array
|
|
435
|
+
console.debug(`Found dot at stepDots index ${currentDotIndex} for major step ${currentMajorStepIndex}, visible dot index: ${visibleDotIndex}`);
|
|
436
|
+
const expansionDot = this._createSingleExpansionDot(visibleDotIndex, previousMajorStepIndex, hiddenIntermediateCount);
|
|
437
|
+
expansionDot.majorStepIndex = currentMajorStepIndex; // Store for reference
|
|
438
|
+
this.expansionDots.push(expansionDot);
|
|
439
|
+
sv.visualContainer.addChild(expansionDot);
|
|
440
|
+
console.debug(`Expansion dot created at visible index ${visibleDotIndex} and added to container`);
|
|
441
|
+
} else {
|
|
442
|
+
console.debug(`WARNING: Could not find dot for major step ${currentMajorStepIndex}`);
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
console.debug('No expansion dot needed - no hidden intermediate steps');
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
console.debug('=== CHECKING FOR COLLAPSE DOTS ===');
|
|
450
|
+
// Also create collapse dots for expanded sequences
|
|
451
|
+
this._createCollapseDots();
|
|
452
|
+
|
|
453
|
+
console.debug(`Total expansion dots created: ${this.expansionDots.length}`);
|
|
454
|
+
console.debug('=== _createExpansionDots END ===\n');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Counts intermediate steps (stepMark > 0) between two visible dots
|
|
459
|
+
* @private
|
|
460
|
+
*/
|
|
461
|
+
_countIntermediateStepsBetween(fromDotIndex, toDotIndex) {
|
|
462
|
+
const sv = this.stepVisualizer;
|
|
463
|
+
let count = 0;
|
|
464
|
+
|
|
465
|
+
// Get the equation references for the from and to dots
|
|
466
|
+
const fromEquation = sv.stepDots[fromDotIndex]?.equationRef;
|
|
467
|
+
const toEquation = sv.stepDots[toDotIndex]?.equationRef;
|
|
468
|
+
|
|
469
|
+
if (!fromEquation || !toEquation) {
|
|
470
|
+
console.debug('Missing equation references for dots', fromDotIndex, toDotIndex);
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Find the step indices in the main steps array
|
|
475
|
+
const fromStepIndex = sv.steps.indexOf(fromEquation);
|
|
476
|
+
const toStepIndex = sv.steps.indexOf(toEquation);
|
|
477
|
+
|
|
478
|
+
console.debug('Checking steps between step indices:', fromStepIndex, 'and', toStepIndex);
|
|
479
|
+
|
|
480
|
+
// Count intermediate steps between these two major steps
|
|
481
|
+
for (let i = fromStepIndex + 1; i < toStepIndex; i++) {
|
|
482
|
+
const step = sv.steps[i];
|
|
483
|
+
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
484
|
+
// Count intermediate steps (stepMark > 0) that are currently hidden
|
|
485
|
+
if (step.stepMark !== undefined && step.stepMark > 0 && step.visible === false) {
|
|
486
|
+
console.debug(` Found intermediate step at ${i}: stepMark=${step.stepMark}, visible=${step.visible}`);
|
|
487
|
+
count++;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
console.debug('Intermediate steps between dots', fromDotIndex, 'and', toDotIndex, '(step indices', fromStepIndex, 'to', toStepIndex, '):', count);
|
|
493
|
+
return count;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Counts hidden steps between two step indices (legacy method for backward compatibility)
|
|
498
|
+
* @private
|
|
499
|
+
*/
|
|
500
|
+
_countHiddenStepsBetween(fromIndex, toIndex) {
|
|
501
|
+
return this._countIntermediateStepsBetween(fromIndex, toIndex);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Creates a single expansion dot
|
|
506
|
+
* @private
|
|
507
|
+
*/
|
|
508
|
+
_createSingleExpansionDot(currentStepIndex, previousStepIndex, hiddenCount) {
|
|
509
|
+
console.debug(`\n=== _createSingleExpansionDot(${currentStepIndex}, ${previousStepIndex}, ${hiddenCount}) ===`);
|
|
510
|
+
const sv = this.stepVisualizer;
|
|
511
|
+
const expansionRadius = Math.max(3, sv.dotRadius * 0.4);
|
|
512
|
+
|
|
513
|
+
console.debug(`Creating expansion dot with radius: ${expansionRadius}`);
|
|
514
|
+
|
|
515
|
+
const expansionDot = new jsvgEllipse();
|
|
516
|
+
expansionDot.setWidthAndHeight(expansionRadius * 2, expansionRadius * 2);
|
|
517
|
+
|
|
518
|
+
// Use same color as regular dots
|
|
519
|
+
const dotColor = sv.styling?.dotColor || omdColor.stepColor;
|
|
520
|
+
console.debug(`Setting expansion dot color: ${dotColor}`);
|
|
521
|
+
expansionDot.setFillColor(dotColor);
|
|
522
|
+
expansionDot.setStrokeColor(dotColor);
|
|
523
|
+
expansionDot.setStrokeWidth(1);
|
|
524
|
+
|
|
525
|
+
// Store metadata
|
|
526
|
+
expansionDot.isExpansionDot = true;
|
|
527
|
+
expansionDot.currentStepIndex = currentStepIndex;
|
|
528
|
+
expansionDot.previousStepIndex = previousStepIndex;
|
|
529
|
+
expansionDot.hiddenCount = hiddenCount;
|
|
530
|
+
expansionDot.radius = expansionRadius;
|
|
531
|
+
|
|
532
|
+
console.debug('Expansion dot metadata:', {
|
|
533
|
+
isExpansionDot: expansionDot.isExpansionDot,
|
|
534
|
+
currentStepIndex: expansionDot.currentStepIndex,
|
|
535
|
+
previousStepIndex: expansionDot.previousStepIndex,
|
|
536
|
+
hiddenCount: expansionDot.hiddenCount,
|
|
537
|
+
radius: expansionDot.radius
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// Make it clickable
|
|
541
|
+
expansionDot.svgObject.style.cursor = "pointer";
|
|
542
|
+
expansionDot.svgObject.onclick = (event) => {
|
|
543
|
+
try {
|
|
544
|
+
console.debug('Expansion dot clicked!');
|
|
545
|
+
this._handleExpansionDotClick(expansionDot);
|
|
546
|
+
event.stopPropagation();
|
|
547
|
+
} catch (error) {
|
|
548
|
+
console.error('Error in expansion dot click handler:', error);
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
console.debug('Expansion dot created successfully');
|
|
553
|
+
console.debug('=== _createSingleExpansionDot END ===\n');
|
|
554
|
+
|
|
555
|
+
return expansionDot;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Positions expansion dots above their corresponding main dots
|
|
560
|
+
* @private
|
|
561
|
+
*/
|
|
562
|
+
_positionExpansionDots() {
|
|
563
|
+
console.debug('\n=== _positionExpansionDots START ===');
|
|
564
|
+
const sv = this.stepVisualizer;
|
|
565
|
+
|
|
566
|
+
console.debug(`Positioning ${this.expansionDots.length} expansion dots`);
|
|
567
|
+
|
|
568
|
+
this.expansionDots.forEach((expansionDot, index) => {
|
|
569
|
+
console.debug(`\n--- Positioning expansion dot ${index} ---`);
|
|
570
|
+
let targetDot;
|
|
571
|
+
|
|
572
|
+
if (expansionDot.isCollapseDot) {
|
|
573
|
+
console.debug('This is a collapse dot');
|
|
574
|
+
// For collapse dots, use the currentStepIndex which points to the dot index
|
|
575
|
+
const dotIndex = expansionDot.currentStepIndex;
|
|
576
|
+
targetDot = sv.stepDots[dotIndex];
|
|
577
|
+
console.debug(`Using dot index ${dotIndex} for collapse dot positioning`);
|
|
578
|
+
} else {
|
|
579
|
+
console.debug('This is an expansion dot');
|
|
580
|
+
// For expansion dots, we need to find the actual visible dot that corresponds to the major step
|
|
581
|
+
const majorStepIndex = expansionDot.majorStepIndex;
|
|
582
|
+
const majorStep = sv.steps[majorStepIndex];
|
|
583
|
+
|
|
584
|
+
if (majorStep) {
|
|
585
|
+
// Find the dot that corresponds to this major step
|
|
586
|
+
const dotIndex = sv.stepDots.findIndex(dot => dot.equationRef === majorStep);
|
|
587
|
+
targetDot = sv.stepDots[dotIndex];
|
|
588
|
+
console.debug(`Found target dot at index ${dotIndex} for major step ${majorStepIndex}`);
|
|
589
|
+
} else {
|
|
590
|
+
console.debug(`WARNING: Major step at index ${majorStepIndex} not found`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (targetDot && targetDot.visible) {
|
|
595
|
+
const offsetY = -(expansionDot.radius * 2 + 8); // Position above main dot
|
|
596
|
+
const newX = targetDot.xpos;
|
|
597
|
+
const newY = targetDot.ypos + offsetY;
|
|
598
|
+
console.debug(`Positioning at: x=${newX}, y=${newY} (target: x=${targetDot.xpos}, y=${targetDot.ypos}, offset=${offsetY})`);
|
|
599
|
+
expansionDot.setPosition(newX, newY);
|
|
600
|
+
console.debug('Expansion dot positioned successfully');
|
|
601
|
+
} else {
|
|
602
|
+
console.debug(`WARNING: No visible target dot found for expansion dot ${index}`);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
console.debug('=== _positionExpansionDots END ===\n');
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Creates collapse dots for expanded sequences
|
|
611
|
+
* @private
|
|
612
|
+
*/
|
|
613
|
+
_createCollapseDots() {
|
|
614
|
+
console.debug('\n=== _createCollapseDots START ===');
|
|
615
|
+
const sv = this.stepVisualizer;
|
|
616
|
+
const allDots = sv.stepDots;
|
|
617
|
+
|
|
618
|
+
console.debug('Checking for visible intermediate steps to create collapse dots');
|
|
619
|
+
|
|
620
|
+
// Group visible intermediate steps by their consecutive sequences
|
|
621
|
+
const intermediateGroups = [];
|
|
622
|
+
let currentGroup = [];
|
|
623
|
+
|
|
624
|
+
allDots.forEach((dot, index) => {
|
|
625
|
+
if (dot && dot.visible && dot.equationRef) {
|
|
626
|
+
const stepMark = dot.equationRef.stepMark;
|
|
627
|
+
console.debug(`Dot ${index}: visible=${dot.visible}, stepMark=${stepMark}`);
|
|
628
|
+
|
|
629
|
+
if (stepMark !== undefined && stepMark > 0) {
|
|
630
|
+
currentGroup.push(index);
|
|
631
|
+
console.debug(`>>> Found visible intermediate step at index ${index} (stepMark=${stepMark})`);
|
|
632
|
+
} else if (currentGroup.length > 0) {
|
|
633
|
+
// We hit a major step, so end the current group
|
|
634
|
+
intermediateGroups.push([...currentGroup]);
|
|
635
|
+
console.debug(`Ended intermediate group:`, currentGroup);
|
|
636
|
+
currentGroup = [];
|
|
637
|
+
}
|
|
638
|
+
} else if (currentGroup.length > 0) {
|
|
639
|
+
// We hit a non-visible dot, so end the current group
|
|
640
|
+
intermediateGroups.push([...currentGroup]);
|
|
641
|
+
console.debug(`Ended intermediate group at non-visible dot:`, currentGroup);
|
|
642
|
+
currentGroup = [];
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// Don't forget the last group if it exists
|
|
647
|
+
if (currentGroup.length > 0) {
|
|
648
|
+
intermediateGroups.push([...currentGroup]);
|
|
649
|
+
console.debug(`Final intermediate group:`, currentGroup);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
console.debug(`Found ${intermediateGroups.length} groups of visible intermediate steps:`, intermediateGroups);
|
|
653
|
+
|
|
654
|
+
// Create a collapse dot for each group
|
|
655
|
+
intermediateGroups.forEach((group, groupIndex) => {
|
|
656
|
+
if (group.length > 0) {
|
|
657
|
+
console.debug(`\n>>> CREATING COLLAPSE DOT FOR GROUP ${groupIndex} <<<`);
|
|
658
|
+
|
|
659
|
+
// Find the major step that comes after the last intermediate step in this group
|
|
660
|
+
const lastIntermediateIndex = group[group.length - 1];
|
|
661
|
+
const lastIntermediateDot = sv.stepDots[lastIntermediateIndex];
|
|
662
|
+
const lastIntermediateStep = lastIntermediateDot.equationRef;
|
|
663
|
+
const lastIntermediateStepIndex = sv.steps.indexOf(lastIntermediateStep);
|
|
664
|
+
|
|
665
|
+
console.debug(`Last intermediate step in group ${groupIndex} is at step index ${lastIntermediateStepIndex}`);
|
|
666
|
+
|
|
667
|
+
// Find the next major step (stepMark = 0) after the intermediate steps
|
|
668
|
+
let majorStepAfterIndex = -1;
|
|
669
|
+
for (let i = lastIntermediateStepIndex + 1; i < sv.steps.length; i++) {
|
|
670
|
+
const step = sv.steps[i];
|
|
671
|
+
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
672
|
+
if (step.stepMark === 0 && step.visible === true) {
|
|
673
|
+
majorStepAfterIndex = i;
|
|
674
|
+
console.debug(`Found major step after intermediates at step index ${i}`);
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (majorStepAfterIndex >= 0) {
|
|
681
|
+
// Find the dot index for this major step
|
|
682
|
+
const majorStepAfter = sv.steps[majorStepAfterIndex];
|
|
683
|
+
const majorDotIndex = sv.stepDots.findIndex(dot => dot.equationRef === majorStepAfter);
|
|
684
|
+
|
|
685
|
+
if (majorDotIndex >= 0) {
|
|
686
|
+
console.debug(`Positioning collapse dot for group ${groupIndex} above major step dot at index ${majorDotIndex}`);
|
|
687
|
+
|
|
688
|
+
const collapseDot = this._createSingleExpansionDot(majorDotIndex, -1, group.length);
|
|
689
|
+
collapseDot.isCollapseDot = true;
|
|
690
|
+
collapseDot.intermediateSteps = group;
|
|
691
|
+
collapseDot.groupIndex = groupIndex; // Store group reference
|
|
692
|
+
this.expansionDots.push(collapseDot);
|
|
693
|
+
sv.visualContainer.addChild(collapseDot);
|
|
694
|
+
console.debug(`Collapse dot created and added to container for group ${groupIndex} at major step dot index: ${majorDotIndex}`);
|
|
695
|
+
} else {
|
|
696
|
+
console.debug(`WARNING: Could not find dot for major step after intermediates in group ${groupIndex}`);
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
console.debug(`WARNING: Could not find major step after intermediate steps in group ${groupIndex}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
console.debug('=== _createCollapseDots END ===\n');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Handles clicking on an expansion dot to toggle hidden steps
|
|
709
|
+
* @private
|
|
710
|
+
*/
|
|
711
|
+
_handleExpansionDotClick(expansionDot) {
|
|
712
|
+
const sv = this.stepVisualizer;
|
|
713
|
+
|
|
714
|
+
if (expansionDot.isCollapseDot) {
|
|
715
|
+
// Handle collapse dot click - hide only the specific group of intermediate steps
|
|
716
|
+
console.debug(`Collapse dot clicked: collapsing group ${expansionDot.groupIndex || 'unknown'}`);
|
|
717
|
+
|
|
718
|
+
// Hide only the intermediate steps in this specific group
|
|
719
|
+
const intermediateSteps = expansionDot.intermediateSteps || [];
|
|
720
|
+
console.debug(`Hiding ${intermediateSteps.length} intermediate steps in this group:`, intermediateSteps);
|
|
721
|
+
|
|
722
|
+
intermediateSteps.forEach(dotIndex => {
|
|
723
|
+
const dot = sv.stepDots[dotIndex];
|
|
724
|
+
if (dot && dot.equationRef) {
|
|
725
|
+
console.debug(`Hiding intermediate step with dot index ${dotIndex}, stepMark=${dot.equationRef.stepMark}`);
|
|
726
|
+
this._hideStep(dot.equationRef);
|
|
727
|
+
|
|
728
|
+
// Also hide the corresponding dot
|
|
729
|
+
dot.hide();
|
|
730
|
+
dot.visible = false;
|
|
731
|
+
console.debug(`Hidden dot ${dotIndex}`);
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// Remove any lines that connect to the hidden dots
|
|
736
|
+
console.debug('Cleaning up lines connected to hidden dots...');
|
|
737
|
+
this._removeLinesToHiddenDots();
|
|
738
|
+
|
|
739
|
+
console.debug(`Collapsed group ${expansionDot.groupIndex || 'unknown'}`);
|
|
740
|
+
} else {
|
|
741
|
+
// Handle expansion dot click - show steps between the major steps
|
|
742
|
+
const { majorStepIndex, previousStepIndex } = expansionDot;
|
|
743
|
+
|
|
744
|
+
console.debug('Expansion dot clicked: showing steps between major step indices', previousStepIndex, 'and', majorStepIndex);
|
|
745
|
+
|
|
746
|
+
// Remove this expansion dot immediately since we're expanding
|
|
747
|
+
console.debug('Removing clicked expansion dot from container and array');
|
|
748
|
+
if (expansionDot.parentNode === sv.visualContainer) {
|
|
749
|
+
sv.visualContainer.removeChild(expansionDot);
|
|
750
|
+
}
|
|
751
|
+
const dotIndex = this.expansionDots.indexOf(expansionDot);
|
|
752
|
+
if (dotIndex >= 0) {
|
|
753
|
+
this.expansionDots.splice(dotIndex, 1);
|
|
754
|
+
console.debug(`Removed expansion dot at index ${dotIndex}`);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Show all intermediate steps between the previous and current major steps
|
|
758
|
+
for (let i = previousStepIndex + 1; i < majorStepIndex; i++) {
|
|
759
|
+
const step = sv.steps[i];
|
|
760
|
+
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
761
|
+
if (step.stepMark > 0) {
|
|
762
|
+
console.debug('Showing intermediate step at index', i, 'with stepMark', step.stepMark);
|
|
763
|
+
this._showStep(step);
|
|
764
|
+
|
|
765
|
+
// Also show the corresponding dot
|
|
766
|
+
const stepDotIndex = sv.stepDots.findIndex(dot => dot.equationRef === step);
|
|
767
|
+
if (stepDotIndex >= 0) {
|
|
768
|
+
const stepDot = sv.stepDots[stepDotIndex];
|
|
769
|
+
stepDot.show();
|
|
770
|
+
stepDot.visible = true;
|
|
771
|
+
console.debug(`Shown dot ${stepDotIndex}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
console.debug('Showed intermediate steps');
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Force a complete refresh of the visualizer to clean up artifacts and rebuild lines
|
|
781
|
+
sv.rebuildVisualizer();
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Properly hides a step and all its child elements
|
|
786
|
+
* @private
|
|
787
|
+
*/
|
|
788
|
+
_hideStep(step) {
|
|
789
|
+
step.visible = false;
|
|
790
|
+
if (step.svgObject) {
|
|
791
|
+
step.svgObject.style.display = 'none';
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Also hide operation display nodes if they exist
|
|
795
|
+
if (step.operationDisplayNode) {
|
|
796
|
+
step.operationDisplayNode.visible = false;
|
|
797
|
+
if (step.operationDisplayNode.svgObject) {
|
|
798
|
+
step.operationDisplayNode.svgObject.style.display = 'none';
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Hide any child nodes recursively
|
|
803
|
+
if (step.children && Array.isArray(step.children)) {
|
|
804
|
+
step.children.forEach(child => {
|
|
805
|
+
if (child) {
|
|
806
|
+
this._hideStep(child);
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Properly shows a step and all its child elements
|
|
814
|
+
* @private
|
|
815
|
+
*/
|
|
816
|
+
_showStep(step) {
|
|
817
|
+
step.visible = true;
|
|
818
|
+
if (step.svgObject) {
|
|
819
|
+
step.svgObject.style.display = '';
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Also show operation display nodes if they exist
|
|
823
|
+
if (step.operationDisplayNode) {
|
|
824
|
+
step.operationDisplayNode.visible = true;
|
|
825
|
+
if (step.operationDisplayNode.svgObject) {
|
|
826
|
+
step.operationDisplayNode.svgObject.style.display = '';
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Show any child nodes recursively
|
|
831
|
+
if (step.children && Array.isArray(step.children)) {
|
|
832
|
+
step.children.forEach(child => {
|
|
833
|
+
if (child) {
|
|
834
|
+
this._showStep(child);
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Removes lines that connect to hidden dots
|
|
842
|
+
* @private
|
|
843
|
+
*/
|
|
844
|
+
_removeLinesToHiddenDots() {
|
|
845
|
+
const sv = this.stepVisualizer;
|
|
846
|
+
console.debug('Checking lines for hidden dot connections...');
|
|
847
|
+
|
|
848
|
+
// Get lines that connect to hidden dots
|
|
849
|
+
const linesToRemove = [];
|
|
850
|
+
sv.stepLines.forEach((line, lineIndex) => {
|
|
851
|
+
const fromDot = sv.stepDots[line.fromDotIndex];
|
|
852
|
+
const toDot = sv.stepDots[line.toDotIndex];
|
|
853
|
+
|
|
854
|
+
if ((fromDot && !fromDot.visible) || (toDot && !toDot.visible)) {
|
|
855
|
+
console.debug(`Line ${lineIndex} connects to hidden dot(s): from visible=${fromDot?.visible}, to visible=${toDot?.visible}`);
|
|
856
|
+
linesToRemove.push(line);
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
// Remove the problematic lines
|
|
861
|
+
console.debug(`Removing ${linesToRemove.length} lines connected to hidden dots`);
|
|
862
|
+
linesToRemove.forEach(line => {
|
|
863
|
+
if (line.parent === sv.visualContainer) {
|
|
864
|
+
sv.visualContainer.removeChild(line);
|
|
865
|
+
console.debug('Removed line from visual container');
|
|
866
|
+
}
|
|
867
|
+
const lineIndex = sv.stepLines.indexOf(line);
|
|
868
|
+
if (lineIndex >= 0) {
|
|
869
|
+
sv.stepLines.splice(lineIndex, 1);
|
|
870
|
+
console.debug(`Removed line from stepLines array at index ${lineIndex}`);
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
console.debug('Line cleanup complete');
|
|
875
|
+
}
|
|
245
876
|
}
|