@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
- // Position visual container to the right of the sequence
20
- // Add extra offset based on equation background padding (if any)
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 visualX = baseRight + this.stepVisualizer.visualSpacing + extraPaddingX;
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.stepDots.forEach(dot => {
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
- line.setStrokeColor(omdColor.stepColor);
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
  }