@teachinglab/omd 0.2.10 → 0.3.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.
Files changed (58) hide show
  1. package/docs/api/configuration-options.md +198 -198
  2. package/docs/api/eventManager.md +82 -82
  3. package/docs/api/focusFrameManager.md +144 -144
  4. package/docs/api/index.md +105 -105
  5. package/docs/api/main.md +62 -62
  6. package/docs/api/omdBinaryExpressionNode.md +86 -86
  7. package/docs/api/omdCanvas.md +83 -83
  8. package/docs/api/omdConfigManager.md +112 -112
  9. package/docs/api/omdConstantNode.md +52 -52
  10. package/docs/api/omdDisplay.md +87 -87
  11. package/docs/api/omdEquationNode.md +174 -174
  12. package/docs/api/omdEquationSequenceNode.md +258 -258
  13. package/docs/api/omdEquationStack.md +156 -156
  14. package/docs/api/omdFunctionNode.md +82 -82
  15. package/docs/api/omdGroupNode.md +78 -78
  16. package/docs/api/omdHelpers.md +87 -87
  17. package/docs/api/omdLeafNode.md +85 -85
  18. package/docs/api/omdNode.md +201 -201
  19. package/docs/api/omdOperationDisplayNode.md +117 -117
  20. package/docs/api/omdOperatorNode.md +91 -91
  21. package/docs/api/omdParenthesisNode.md +133 -133
  22. package/docs/api/omdPopup.md +191 -191
  23. package/docs/api/omdPowerNode.md +131 -131
  24. package/docs/api/omdRationalNode.md +144 -144
  25. package/docs/api/omdSimplification.md +78 -78
  26. package/docs/api/omdSqrtNode.md +144 -144
  27. package/docs/api/omdStepVisualizer.md +146 -146
  28. package/docs/api/omdStepVisualizerHighlighting.md +65 -65
  29. package/docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
  30. package/docs/api/omdStepVisualizerLayout.md +70 -70
  31. package/docs/api/omdStepVisualizerTextBoxes.md +76 -76
  32. package/docs/api/omdTranscriptionService.md +95 -95
  33. package/docs/api/omdTreeDiff.md +169 -169
  34. package/docs/api/omdUnaryExpressionNode.md +137 -137
  35. package/docs/api/omdUtilities.md +82 -82
  36. package/docs/api/omdVariableNode.md +123 -123
  37. package/omd/nodes/omdConstantNode.js +141 -141
  38. package/omd/nodes/omdGroupNode.js +67 -67
  39. package/omd/nodes/omdLeafNode.js +76 -76
  40. package/omd/nodes/omdOperatorNode.js +108 -108
  41. package/omd/nodes/omdParenthesisNode.js +292 -292
  42. package/omd/nodes/omdPowerNode.js +235 -235
  43. package/omd/nodes/omdRationalNode.js +295 -295
  44. package/omd/nodes/omdVariableNode.js +122 -122
  45. package/omd/simplification/omdSimplification.js +140 -140
  46. package/omd/step-visualizer/omdStepVisualizer.js +947 -947
  47. package/omd/step-visualizer/omdStepVisualizerLayout.js +892 -892
  48. package/omd/utils/omdStepVisualizerInteractiveSteps.js +5 -3
  49. package/package.json +1 -1
  50. package/src/index.js +11 -0
  51. package/src/omdBalanceHanger.js +2 -1
  52. package/src/omdEquation.js +1 -1
  53. package/src/omdNumber.js +1 -1
  54. package/src/omdNumberLine.js +13 -7
  55. package/src/omdRatioChart.js +11 -0
  56. package/src/omdShapes.js +1 -1
  57. package/src/omdTapeDiagram.js +1 -1
  58. package/src/omdTerm.js +1 -1
@@ -1,893 +1,893 @@
1
- import { omdEquationNode } from '../nodes/omdEquationNode.js';
2
- import { omdColor } from '../../src/omdColor.js';
3
- import { jsvgLine, jsvgEllipse } from '@teachinglab/jsvg';
4
- import { getDotRadius } from '../config/omdConfigManager.js';
5
-
6
- /**
7
- * Handles visual layout, positioning, and visibility management for step visualizations
8
- */
9
- export class omdStepVisualizerLayout {
10
- constructor(stepVisualizer) {
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
- }
30
- }
31
-
32
- /**
33
- * Updates the layout of visual elements relative to the sequence
34
- * @param {boolean} allowRepositioning - Whether to allow equation repositioning (default: false)
35
- */
36
- updateVisualLayout(allowRepositioning = false) {
37
-
38
-
39
-
40
-
41
- if (this.stepVisualizer.stepDots.length === 0) return;
42
-
43
- // Calculate the total width needed for equations (including any padding)
44
- const baseEquationWidth = (this.stepVisualizer.sequenceWidth || this.stepVisualizer.width);
45
- const extraPaddingX = this._getMaxEquationEffectivePaddingX();
46
- const totalEquationWidth = baseEquationWidth + extraPaddingX;
47
-
48
- // Position visual container at a fixed position
49
- const visualX = this.fixedVisualizerPosition;
50
- this.stepVisualizer.visualContainer.setPosition(visualX, 0);
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
-
72
- // Position dots based on visible equations
73
- const visibleSteps = this.stepVisualizer.steps.filter(s => s.visible !== false);
74
- let currentY = 0;
75
- const verticalPadding = 15 * this.stepVisualizer.getFontSize() / this.stepVisualizer.getRootFontSize();
76
-
77
- visibleSteps.forEach((step, visIndex) => {
78
- if (step instanceof omdEquationNode) {
79
- const dotIndex = this.findDotIndexForEquation(step);
80
- if (dotIndex >= 0 && dotIndex < this.stepVisualizer.stepDots.length) {
81
- const dot = this.stepVisualizer.stepDots[dotIndex];
82
-
83
- // Center dot vertically with the equation
84
- let equationCenter;
85
- if (step.equalsSign && step.equalsSign.ypos !== undefined) {
86
- equationCenter = step.equalsSign.ypos + (step.equalsSign.height / 2);
87
- } else {
88
- equationCenter = step.getAlignmentBaseline ? step.getAlignmentBaseline() : step.height / 2;
89
- }
90
- const dotY = currentY + equationCenter;
91
- const dotX = (this.stepVisualizer.dotRadius * 3) / 2;
92
-
93
- dot.setPosition(dotX, dotY);
94
- }
95
- }
96
-
97
- currentY += step.height;
98
- if (visIndex < visibleSteps.length - 1) {
99
- currentY += verticalPadding;
100
- }
101
- });
102
-
103
- this.updateAllLinePositions();
104
-
105
- // Update container dimensions
106
- let containerWidth = this.stepVisualizer.dotRadius * 3;
107
- let containerHeight = this.stepVisualizer.height;
108
-
109
- // Store the original height before expansion for autoscale calculations
110
- if (!this.stepVisualizer.sequenceHeight) {
111
- this.stepVisualizer.sequenceHeight = containerHeight;
112
- }
113
-
114
- const textBoxes = this.stepVisualizer.textBoxManager.getStepTextBoxes();
115
- if (textBoxes.length > 0) {
116
- const textBoxWidth = 280;
117
- containerWidth = Math.max(containerWidth, textBoxWidth + this.stepVisualizer.dotRadius * 2 + 10 + 20);
118
-
119
- // Calculate the maximum extent of any text box to prevent clipping
120
- textBoxes.forEach(textBox => {
121
- if (textBox.interactiveSteps) {
122
- const dimensions = textBox.interactiveSteps.getDimensions();
123
- const layoutGroup = textBox.interactiveSteps.getLayoutGroup();
124
-
125
- // Calculate the bottom of this text box
126
- const textBoxBottom = layoutGroup.ypos + dimensions.height;
127
- containerHeight = Math.max(containerHeight, textBoxBottom + 20); // Add some buffer
128
- }
129
- });
130
- }
131
-
132
- if (this.stepVisualizer.stepDots.length > 0) {
133
- const maxRadius = Math.max(...this.stepVisualizer.stepDots.map(d=>d.radius||this.stepVisualizer.dotRadius));
134
- const containerWidth = maxRadius * 3;
135
- const maxDotY = Math.max(...this.stepVisualizer.stepDots.map(dot => dot.ypos + this.stepVisualizer.dotRadius));
136
- containerHeight = Math.max(containerHeight, maxDotY);
137
- }
138
-
139
- this.stepVisualizer.visualContainer.setWidthAndHeight(containerWidth, containerHeight);
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
-
172
- }
173
-
174
- /**
175
- * Computes the maximum horizontal padding (x) among visible equations, if configured.
176
- * This allows dots to shift further right when pill background padding is added.
177
- * @returns {number}
178
- * @private
179
- */
180
- _getMaxEquationEffectivePaddingX() {
181
- try {
182
- const steps = this.stepVisualizer.steps || [];
183
- let maxPadX = 0;
184
- steps.forEach(step => {
185
- if (step instanceof omdEquationNode && step.visible !== false) {
186
- if (typeof step.getEffectiveBackgroundPaddingX === 'function') {
187
- const px = Number(step.getEffectiveBackgroundPaddingX());
188
- maxPadX = Math.max(maxPadX, isNaN(px) ? 0 : px);
189
- }
190
- }
191
- });
192
- return maxPadX;
193
- } catch (_) {
194
- return 0;
195
- }
196
- }
197
-
198
- /**
199
- * Finds the dot index for a given equation
200
- */
201
- findDotIndexForEquation(equation) {
202
- return this.stepVisualizer.stepDots.findIndex(dot => dot.equationRef === equation);
203
- }
204
-
205
- /**
206
- * Updates the z-order of visual elements
207
- */
208
- updateVisualZOrder() {
209
- if (!this.stepVisualizer.visualContainer) return;
210
-
211
- // Lines behind (z-index 1)
212
- this.stepVisualizer.stepLines.forEach(line => {
213
- if (line && line.svgObject) {
214
- line.svgObject.style.zIndex = '1';
215
- if (line.parentNode !== this.stepVisualizer.visualContainer) {
216
- this.stepVisualizer.visualContainer.addChild(line);
217
- }
218
- }
219
- });
220
-
221
- // Dots in front (z-index 2)
222
- this.stepVisualizer.stepDots.forEach(dot => {
223
- if (dot && dot.svgObject) {
224
- dot.svgObject.style.zIndex = '2';
225
- if (dot.parentNode !== this.stepVisualizer.visualContainer) {
226
- this.stepVisualizer.visualContainer.addChild(dot);
227
- }
228
- }
229
- });
230
-
231
- // Text boxes on top (z-index 3)
232
- const textBoxes = this.stepVisualizer.textBoxManager.getStepTextBoxes();
233
- textBoxes.forEach(textBox => {
234
- if (textBox && textBox.svgObject) {
235
- textBox.svgObject.style.zIndex = '3';
236
- if (textBox.parentNode !== this.stepVisualizer.visualContainer) {
237
- this.stepVisualizer.visualContainer.addChild(textBox);
238
- }
239
- }
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
- });
251
- }
252
-
253
- /**
254
- * Updates all line positions to connect dot centers
255
- */
256
- updateAllLinePositions() {
257
- this.stepVisualizer.stepLines.forEach(line => {
258
- const fromDot = this.stepVisualizer.stepDots[line.fromDotIndex];
259
- const toDot = this.stepVisualizer.stepDots[line.toDotIndex];
260
-
261
- if (fromDot && toDot) {
262
- line.setEndpoints(fromDot.xpos, fromDot.ypos, toDot.xpos, toDot.ypos);
263
- }
264
- });
265
- }
266
-
267
- /**
268
- * Updates visibility of visual elements based on equation visibility
269
- */
270
- updateVisualVisibility() {
271
-
272
- const sv = this.stepVisualizer;
273
-
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) => {
279
- if (dot.equationRef && dot.equationRef.visible !== false) {
280
- dot.setFillColor(dotColor);
281
- dot.setStrokeColor(dotColor);
282
- dot.show();
283
- dot.visible = true; // Use the dot's own visibility property
284
-
285
- } else {
286
- dot.hide();
287
- dot.visible = false;
288
-
289
- }
290
- });
291
-
292
- // Clear existing expansion dots
293
-
294
- this._clearExpansionDots();
295
-
296
- // Remove all old lines from the container and the array
297
-
298
- sv.stepLines.forEach(line => {
299
- // Remove the line if it is currently a child of the visualContainer
300
- if (line.parent === sv.visualContainer) {
301
- sv.visualContainer.removeChild(line);
302
- }
303
- });
304
- sv.stepLines = [];
305
-
306
- // Get the dots that are currently visible
307
- const visibleDots = sv.stepDots.filter(dot => dot.visible);
308
-
309
-
310
- // Re-create connecting lines only between the visible dots
311
-
312
- for (let i = 0; i < visibleDots.length - 1; i++) {
313
- const fromDot = visibleDots[i];
314
- const toDot = visibleDots[i + 1];
315
-
316
- const line = new jsvgLine();
317
- const lineColor = sv.styling?.lineColor || omdColor.stepColor;
318
- line.setStrokeColor(lineColor);
319
- line.setStrokeWidth(sv.styling?.lineWidth || sv.lineWidth);
320
- line.fromDotIndex = sv.stepDots.indexOf(fromDot);
321
- line.toDotIndex = sv.stepDots.indexOf(toDot);
322
-
323
- sv.visualContainer.addChild(line);
324
- sv.stepLines.push(line);
325
- }
326
-
327
-
328
- // After creating the lines, update their positions
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
-
339
- }
340
-
341
- /**
342
- * Updates the clickability of a dot
343
- */
344
- updateDotClickability(dot) {
345
- if (this.stepVisualizer.dotsClickable) {
346
- dot.svgObject.style.cursor = "pointer";
347
- dot.svgObject.onclick = (event) => {
348
- try {
349
- const idx = this.stepVisualizer.stepDots.indexOf(dot);
350
- if (idx < 0) return; // orphan dot, ignore
351
- this.stepVisualizer._handleDotClick(dot, idx);
352
- event.stopPropagation();
353
- } catch (error) {
354
- console.error('Error in dot click handler:', error);
355
- }
356
- };
357
- } else {
358
- dot.svgObject.style.cursor = "default";
359
- dot.svgObject.onclick = null;
360
- }
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
- // Clear all step visualizer highlights when expanding/contracting
727
- if (sv.highlighting && typeof sv.highlighting.clearAllExplainHighlights === 'function') {
728
- sv.highlighting.clearAllExplainHighlights();
729
- }
730
-
731
- if (expansionDot.isCollapseDot) {
732
- // Handle collapse dot click - hide only the specific group of intermediate steps
733
-
734
-
735
- // Hide only the intermediate steps in this specific group
736
- const intermediateSteps = expansionDot.intermediateSteps || [];
737
-
738
-
739
- intermediateSteps.forEach(dotIndex => {
740
- const dot = sv.stepDots[dotIndex];
741
- if (dot && dot.equationRef) {
742
-
743
- this._hideStep(dot.equationRef);
744
-
745
- // Also hide the corresponding dot
746
- dot.hide();
747
- dot.visible = false;
748
-
749
- }
750
- });
751
-
752
- // Remove any lines that connect to the hidden dots
753
-
754
- this._removeLinesToHiddenDots();
755
-
756
-
757
- } else {
758
- // Handle expansion dot click - show steps between the major steps
759
- const { majorStepIndex, previousStepIndex } = expansionDot;
760
-
761
-
762
-
763
- // Remove this expansion dot immediately since we're expanding
764
-
765
- if (expansionDot.parentNode === sv.visualContainer) {
766
- sv.visualContainer.removeChild(expansionDot);
767
- }
768
- const dotIndex = this.expansionDots.indexOf(expansionDot);
769
- if (dotIndex >= 0) {
770
- this.expansionDots.splice(dotIndex, 1);
771
-
772
- }
773
-
774
- // Show all intermediate steps between the previous and current major steps
775
- for (let i = previousStepIndex + 1; i < majorStepIndex; i++) {
776
- const step = sv.steps[i];
777
- if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
778
- if (step.stepMark > 0) {
779
-
780
- this._showStep(step);
781
-
782
- // Also show the corresponding dot
783
- const stepDotIndex = sv.stepDots.findIndex(dot => dot.equationRef === step);
784
- if (stepDotIndex >= 0) {
785
- const stepDot = sv.stepDots[stepDotIndex];
786
- stepDot.show();
787
- stepDot.visible = true;
788
-
789
- }
790
- }
791
- }
792
- }
793
-
794
-
795
- }
796
-
797
- // Force a complete refresh of the visualizer to clean up artifacts and rebuild lines
798
- sv.rebuildVisualizer();
799
- }
800
-
801
- /**
802
- * Properly hides a step and all its child elements
803
- * @private
804
- */
805
- _hideStep(step) {
806
- step.visible = false;
807
- if (step.svgObject) {
808
- step.svgObject.style.display = 'none';
809
- }
810
-
811
- // Also hide operation display nodes if they exist
812
- if (step.operationDisplayNode) {
813
- step.operationDisplayNode.visible = false;
814
- if (step.operationDisplayNode.svgObject) {
815
- step.operationDisplayNode.svgObject.style.display = 'none';
816
- }
817
- }
818
-
819
- // Hide any child nodes recursively
820
- if (step.children && Array.isArray(step.children)) {
821
- step.children.forEach(child => {
822
- if (child) {
823
- this._hideStep(child);
824
- }
825
- });
826
- }
827
- }
828
-
829
- /**
830
- * Properly shows a step and all its child elements
831
- * @private
832
- */
833
- _showStep(step) {
834
- step.visible = true;
835
- if (step.svgObject) {
836
- step.svgObject.style.display = '';
837
- }
838
-
839
- // Also show operation display nodes if they exist
840
- if (step.operationDisplayNode) {
841
- step.operationDisplayNode.visible = true;
842
- if (step.operationDisplayNode.svgObject) {
843
- step.operationDisplayNode.svgObject.style.display = '';
844
- }
845
- }
846
-
847
- // Show any child nodes recursively
848
- if (step.children && Array.isArray(step.children)) {
849
- step.children.forEach(child => {
850
- if (child) {
851
- this._showStep(child);
852
- }
853
- });
854
- }
855
- }
856
-
857
- /**
858
- * Removes lines that connect to hidden dots
859
- * @private
860
- */
861
- _removeLinesToHiddenDots() {
862
- const sv = this.stepVisualizer;
863
-
864
-
865
- // Get lines that connect to hidden dots
866
- const linesToRemove = [];
867
- sv.stepLines.forEach((line, lineIndex) => {
868
- const fromDot = sv.stepDots[line.fromDotIndex];
869
- const toDot = sv.stepDots[line.toDotIndex];
870
-
871
- if ((fromDot && !fromDot.visible) || (toDot && !toDot.visible)) {
872
-
873
- linesToRemove.push(line);
874
- }
875
- });
876
-
877
- // Remove the problematic lines
878
-
879
- linesToRemove.forEach(line => {
880
- if (line.parent === sv.visualContainer) {
881
- sv.visualContainer.removeChild(line);
882
-
883
- }
884
- const lineIndex = sv.stepLines.indexOf(line);
885
- if (lineIndex >= 0) {
886
- sv.stepLines.splice(lineIndex, 1);
887
-
888
- }
889
- });
890
-
891
-
892
- }
1
+ import { omdEquationNode } from '../nodes/omdEquationNode.js';
2
+ import { omdColor } from '../../src/omdColor.js';
3
+ import { jsvgLine, jsvgEllipse } from '@teachinglab/jsvg';
4
+ import { getDotRadius } from '../config/omdConfigManager.js';
5
+
6
+ /**
7
+ * Handles visual layout, positioning, and visibility management for step visualizations
8
+ */
9
+ export class omdStepVisualizerLayout {
10
+ constructor(stepVisualizer) {
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
+ }
30
+ }
31
+
32
+ /**
33
+ * Updates the layout of visual elements relative to the sequence
34
+ * @param {boolean} allowRepositioning - Whether to allow equation repositioning (default: false)
35
+ */
36
+ updateVisualLayout(allowRepositioning = false) {
37
+
38
+
39
+
40
+
41
+ if (this.stepVisualizer.stepDots.length === 0) return;
42
+
43
+ // Calculate the total width needed for equations (including any padding)
44
+ const baseEquationWidth = (this.stepVisualizer.sequenceWidth || this.stepVisualizer.width);
45
+ const extraPaddingX = this._getMaxEquationEffectivePaddingX();
46
+ const totalEquationWidth = baseEquationWidth + extraPaddingX;
47
+
48
+ // Position visual container at a fixed position
49
+ const visualX = this.fixedVisualizerPosition;
50
+ this.stepVisualizer.visualContainer.setPosition(visualX, 0);
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
+
72
+ // Position dots based on visible equations
73
+ const visibleSteps = this.stepVisualizer.steps.filter(s => s.visible !== false);
74
+ let currentY = 0;
75
+ const verticalPadding = 15 * this.stepVisualizer.getFontSize() / this.stepVisualizer.getRootFontSize();
76
+
77
+ visibleSteps.forEach((step, visIndex) => {
78
+ if (step instanceof omdEquationNode) {
79
+ const dotIndex = this.findDotIndexForEquation(step);
80
+ if (dotIndex >= 0 && dotIndex < this.stepVisualizer.stepDots.length) {
81
+ const dot = this.stepVisualizer.stepDots[dotIndex];
82
+
83
+ // Center dot vertically with the equation
84
+ let equationCenter;
85
+ if (step.equalsSign && step.equalsSign.ypos !== undefined) {
86
+ equationCenter = step.equalsSign.ypos + (step.equalsSign.height / 2);
87
+ } else {
88
+ equationCenter = step.getAlignmentBaseline ? step.getAlignmentBaseline() : step.height / 2;
89
+ }
90
+ const dotY = currentY + equationCenter;
91
+ const dotX = (this.stepVisualizer.dotRadius * 3) / 2;
92
+
93
+ dot.setPosition(dotX, dotY);
94
+ }
95
+ }
96
+
97
+ currentY += step.height;
98
+ if (visIndex < visibleSteps.length - 1) {
99
+ currentY += verticalPadding;
100
+ }
101
+ });
102
+
103
+ this.updateAllLinePositions();
104
+
105
+ // Update container dimensions
106
+ let containerWidth = this.stepVisualizer.dotRadius * 3;
107
+ let containerHeight = this.stepVisualizer.height;
108
+
109
+ // Store the original height before expansion for autoscale calculations
110
+ if (!this.stepVisualizer.sequenceHeight) {
111
+ this.stepVisualizer.sequenceHeight = containerHeight;
112
+ }
113
+
114
+ const textBoxes = this.stepVisualizer.textBoxManager.getStepTextBoxes();
115
+ if (textBoxes.length > 0) {
116
+ const textBoxWidth = 280;
117
+ containerWidth = Math.max(containerWidth, textBoxWidth + this.stepVisualizer.dotRadius * 2 + 10 + 20);
118
+
119
+ // Calculate the maximum extent of any text box to prevent clipping
120
+ textBoxes.forEach(textBox => {
121
+ if (textBox.interactiveSteps) {
122
+ const dimensions = textBox.interactiveSteps.getDimensions();
123
+ const layoutGroup = textBox.interactiveSteps.getLayoutGroup();
124
+
125
+ // Calculate the bottom of this text box
126
+ const textBoxBottom = layoutGroup.ypos + dimensions.height;
127
+ containerHeight = Math.max(containerHeight, textBoxBottom + 20); // Add some buffer
128
+ }
129
+ });
130
+ }
131
+
132
+ if (this.stepVisualizer.stepDots.length > 0) {
133
+ const maxRadius = Math.max(...this.stepVisualizer.stepDots.map(d=>d.radius||this.stepVisualizer.dotRadius));
134
+ const containerWidth = maxRadius * 3;
135
+ const maxDotY = Math.max(...this.stepVisualizer.stepDots.map(dot => dot.ypos + this.stepVisualizer.dotRadius));
136
+ containerHeight = Math.max(containerHeight, maxDotY);
137
+ }
138
+
139
+ this.stepVisualizer.visualContainer.setWidthAndHeight(containerWidth, containerHeight);
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
+
172
+ }
173
+
174
+ /**
175
+ * Computes the maximum horizontal padding (x) among visible equations, if configured.
176
+ * This allows dots to shift further right when pill background padding is added.
177
+ * @returns {number}
178
+ * @private
179
+ */
180
+ _getMaxEquationEffectivePaddingX() {
181
+ try {
182
+ const steps = this.stepVisualizer.steps || [];
183
+ let maxPadX = 0;
184
+ steps.forEach(step => {
185
+ if (step instanceof omdEquationNode && step.visible !== false) {
186
+ if (typeof step.getEffectiveBackgroundPaddingX === 'function') {
187
+ const px = Number(step.getEffectiveBackgroundPaddingX());
188
+ maxPadX = Math.max(maxPadX, isNaN(px) ? 0 : px);
189
+ }
190
+ }
191
+ });
192
+ return maxPadX;
193
+ } catch (_) {
194
+ return 0;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Finds the dot index for a given equation
200
+ */
201
+ findDotIndexForEquation(equation) {
202
+ return this.stepVisualizer.stepDots.findIndex(dot => dot.equationRef === equation);
203
+ }
204
+
205
+ /**
206
+ * Updates the z-order of visual elements
207
+ */
208
+ updateVisualZOrder() {
209
+ if (!this.stepVisualizer.visualContainer) return;
210
+
211
+ // Lines behind (z-index 1)
212
+ this.stepVisualizer.stepLines.forEach(line => {
213
+ if (line && line.svgObject) {
214
+ line.svgObject.style.zIndex = '1';
215
+ if (line.parentNode !== this.stepVisualizer.visualContainer) {
216
+ this.stepVisualizer.visualContainer.addChild(line);
217
+ }
218
+ }
219
+ });
220
+
221
+ // Dots in front (z-index 2)
222
+ this.stepVisualizer.stepDots.forEach(dot => {
223
+ if (dot && dot.svgObject) {
224
+ dot.svgObject.style.zIndex = '2';
225
+ if (dot.parentNode !== this.stepVisualizer.visualContainer) {
226
+ this.stepVisualizer.visualContainer.addChild(dot);
227
+ }
228
+ }
229
+ });
230
+
231
+ // Text boxes on top (z-index 3)
232
+ const textBoxes = this.stepVisualizer.textBoxManager.getStepTextBoxes();
233
+ textBoxes.forEach(textBox => {
234
+ if (textBox && textBox.svgObject) {
235
+ textBox.svgObject.style.zIndex = '3';
236
+ if (textBox.parentNode !== this.stepVisualizer.visualContainer) {
237
+ this.stepVisualizer.visualContainer.addChild(textBox);
238
+ }
239
+ }
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
+ });
251
+ }
252
+
253
+ /**
254
+ * Updates all line positions to connect dot centers
255
+ */
256
+ updateAllLinePositions() {
257
+ this.stepVisualizer.stepLines.forEach(line => {
258
+ const fromDot = this.stepVisualizer.stepDots[line.fromDotIndex];
259
+ const toDot = this.stepVisualizer.stepDots[line.toDotIndex];
260
+
261
+ if (fromDot && toDot) {
262
+ line.setEndpoints(fromDot.xpos, fromDot.ypos, toDot.xpos, toDot.ypos);
263
+ }
264
+ });
265
+ }
266
+
267
+ /**
268
+ * Updates visibility of visual elements based on equation visibility
269
+ */
270
+ updateVisualVisibility() {
271
+
272
+ const sv = this.stepVisualizer;
273
+
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) => {
279
+ if (dot.equationRef && dot.equationRef.visible !== false) {
280
+ dot.setFillColor(dotColor);
281
+ dot.setStrokeColor(dotColor);
282
+ dot.show();
283
+ dot.visible = true; // Use the dot's own visibility property
284
+
285
+ } else {
286
+ dot.hide();
287
+ dot.visible = false;
288
+
289
+ }
290
+ });
291
+
292
+ // Clear existing expansion dots
293
+
294
+ this._clearExpansionDots();
295
+
296
+ // Remove all old lines from the container and the array
297
+
298
+ sv.stepLines.forEach(line => {
299
+ // Remove the line if it is currently a child of the visualContainer
300
+ if (line.parent === sv.visualContainer) {
301
+ sv.visualContainer.removeChild(line);
302
+ }
303
+ });
304
+ sv.stepLines = [];
305
+
306
+ // Get the dots that are currently visible
307
+ const visibleDots = sv.stepDots.filter(dot => dot.visible);
308
+
309
+
310
+ // Re-create connecting lines only between the visible dots
311
+
312
+ for (let i = 0; i < visibleDots.length - 1; i++) {
313
+ const fromDot = visibleDots[i];
314
+ const toDot = visibleDots[i + 1];
315
+
316
+ const line = new jsvgLine();
317
+ const lineColor = sv.styling?.lineColor || omdColor.stepColor;
318
+ line.setStrokeColor(lineColor);
319
+ line.setStrokeWidth(sv.styling?.lineWidth || sv.lineWidth);
320
+ line.fromDotIndex = sv.stepDots.indexOf(fromDot);
321
+ line.toDotIndex = sv.stepDots.indexOf(toDot);
322
+
323
+ sv.visualContainer.addChild(line);
324
+ sv.stepLines.push(line);
325
+ }
326
+
327
+
328
+ // After creating the lines, update their positions
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
+
339
+ }
340
+
341
+ /**
342
+ * Updates the clickability of a dot
343
+ */
344
+ updateDotClickability(dot) {
345
+ if (this.stepVisualizer.dotsClickable) {
346
+ dot.svgObject.style.cursor = "pointer";
347
+ dot.svgObject.onclick = (event) => {
348
+ try {
349
+ const idx = this.stepVisualizer.stepDots.indexOf(dot);
350
+ if (idx < 0) return; // orphan dot, ignore
351
+ this.stepVisualizer._handleDotClick(dot, idx);
352
+ event.stopPropagation();
353
+ } catch (error) {
354
+ console.error('Error in dot click handler:', error);
355
+ }
356
+ };
357
+ } else {
358
+ dot.svgObject.style.cursor = "default";
359
+ dot.svgObject.onclick = null;
360
+ }
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
+ // Clear all step visualizer highlights when expanding/contracting
727
+ if (sv.highlighting && typeof sv.highlighting.clearAllExplainHighlights === 'function') {
728
+ sv.highlighting.clearAllExplainHighlights();
729
+ }
730
+
731
+ if (expansionDot.isCollapseDot) {
732
+ // Handle collapse dot click - hide only the specific group of intermediate steps
733
+
734
+
735
+ // Hide only the intermediate steps in this specific group
736
+ const intermediateSteps = expansionDot.intermediateSteps || [];
737
+
738
+
739
+ intermediateSteps.forEach(dotIndex => {
740
+ const dot = sv.stepDots[dotIndex];
741
+ if (dot && dot.equationRef) {
742
+
743
+ this._hideStep(dot.equationRef);
744
+
745
+ // Also hide the corresponding dot
746
+ dot.hide();
747
+ dot.visible = false;
748
+
749
+ }
750
+ });
751
+
752
+ // Remove any lines that connect to the hidden dots
753
+
754
+ this._removeLinesToHiddenDots();
755
+
756
+
757
+ } else {
758
+ // Handle expansion dot click - show steps between the major steps
759
+ const { majorStepIndex, previousStepIndex } = expansionDot;
760
+
761
+
762
+
763
+ // Remove this expansion dot immediately since we're expanding
764
+
765
+ if (expansionDot.parentNode === sv.visualContainer) {
766
+ sv.visualContainer.removeChild(expansionDot);
767
+ }
768
+ const dotIndex = this.expansionDots.indexOf(expansionDot);
769
+ if (dotIndex >= 0) {
770
+ this.expansionDots.splice(dotIndex, 1);
771
+
772
+ }
773
+
774
+ // Show all intermediate steps between the previous and current major steps
775
+ for (let i = previousStepIndex + 1; i < majorStepIndex; i++) {
776
+ const step = sv.steps[i];
777
+ if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
778
+ if (step.stepMark > 0) {
779
+
780
+ this._showStep(step);
781
+
782
+ // Also show the corresponding dot
783
+ const stepDotIndex = sv.stepDots.findIndex(dot => dot.equationRef === step);
784
+ if (stepDotIndex >= 0) {
785
+ const stepDot = sv.stepDots[stepDotIndex];
786
+ stepDot.show();
787
+ stepDot.visible = true;
788
+
789
+ }
790
+ }
791
+ }
792
+ }
793
+
794
+
795
+ }
796
+
797
+ // Force a complete refresh of the visualizer to clean up artifacts and rebuild lines
798
+ sv.rebuildVisualizer();
799
+ }
800
+
801
+ /**
802
+ * Properly hides a step and all its child elements
803
+ * @private
804
+ */
805
+ _hideStep(step) {
806
+ step.visible = false;
807
+ if (step.svgObject) {
808
+ step.svgObject.style.display = 'none';
809
+ }
810
+
811
+ // Also hide operation display nodes if they exist
812
+ if (step.operationDisplayNode) {
813
+ step.operationDisplayNode.visible = false;
814
+ if (step.operationDisplayNode.svgObject) {
815
+ step.operationDisplayNode.svgObject.style.display = 'none';
816
+ }
817
+ }
818
+
819
+ // Hide any child nodes recursively
820
+ if (step.children && Array.isArray(step.children)) {
821
+ step.children.forEach(child => {
822
+ if (child) {
823
+ this._hideStep(child);
824
+ }
825
+ });
826
+ }
827
+ }
828
+
829
+ /**
830
+ * Properly shows a step and all its child elements
831
+ * @private
832
+ */
833
+ _showStep(step) {
834
+ step.visible = true;
835
+ if (step.svgObject) {
836
+ step.svgObject.style.display = '';
837
+ }
838
+
839
+ // Also show operation display nodes if they exist
840
+ if (step.operationDisplayNode) {
841
+ step.operationDisplayNode.visible = true;
842
+ if (step.operationDisplayNode.svgObject) {
843
+ step.operationDisplayNode.svgObject.style.display = '';
844
+ }
845
+ }
846
+
847
+ // Show any child nodes recursively
848
+ if (step.children && Array.isArray(step.children)) {
849
+ step.children.forEach(child => {
850
+ if (child) {
851
+ this._showStep(child);
852
+ }
853
+ });
854
+ }
855
+ }
856
+
857
+ /**
858
+ * Removes lines that connect to hidden dots
859
+ * @private
860
+ */
861
+ _removeLinesToHiddenDots() {
862
+ const sv = this.stepVisualizer;
863
+
864
+
865
+ // Get lines that connect to hidden dots
866
+ const linesToRemove = [];
867
+ sv.stepLines.forEach((line, lineIndex) => {
868
+ const fromDot = sv.stepDots[line.fromDotIndex];
869
+ const toDot = sv.stepDots[line.toDotIndex];
870
+
871
+ if ((fromDot && !fromDot.visible) || (toDot && !toDot.visible)) {
872
+
873
+ linesToRemove.push(line);
874
+ }
875
+ });
876
+
877
+ // Remove the problematic lines
878
+
879
+ linesToRemove.forEach(line => {
880
+ if (line.parent === sv.visualContainer) {
881
+ sv.visualContainer.removeChild(line);
882
+
883
+ }
884
+ const lineIndex = sv.stepLines.indexOf(line);
885
+ if (lineIndex >= 0) {
886
+ sv.stepLines.splice(lineIndex, 1);
887
+
888
+ }
889
+ });
890
+
891
+
892
+ }
893
893
  }