@teachinglab/omd 0.2.6 → 0.2.8

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