@teachinglab/omd 0.6.1 → 0.6.3

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 (198) hide show
  1. package/README.md +257 -251
  2. package/README.old.md +137 -137
  3. package/canvas/core/canvasConfig.js +202 -202
  4. package/canvas/drawing/segment.js +167 -167
  5. package/canvas/drawing/stroke.js +385 -385
  6. package/canvas/events/eventManager.js +444 -444
  7. package/canvas/events/pointerEventHandler.js +262 -262
  8. package/canvas/index.js +48 -48
  9. package/canvas/tools/PointerTool.js +71 -71
  10. package/canvas/tools/tool.js +222 -222
  11. package/canvas/utils/boundingBox.js +377 -377
  12. package/canvas/utils/mathUtils.js +258 -258
  13. package/docs/api/configuration-options.md +198 -198
  14. package/docs/api/eventManager.md +82 -82
  15. package/docs/api/focusFrameManager.md +144 -144
  16. package/docs/api/index.md +105 -105
  17. package/docs/api/main.md +62 -62
  18. package/docs/api/omdBinaryExpressionNode.md +86 -86
  19. package/docs/api/omdCanvas.md +83 -83
  20. package/docs/api/omdConfigManager.md +112 -112
  21. package/docs/api/omdConstantNode.md +52 -52
  22. package/docs/api/omdDisplay.md +87 -87
  23. package/docs/api/omdEquationNode.md +174 -174
  24. package/docs/api/omdEquationSequenceNode.md +258 -258
  25. package/docs/api/omdEquationStack.md +192 -192
  26. package/docs/api/omdFunctionNode.md +82 -82
  27. package/docs/api/omdGroupNode.md +78 -78
  28. package/docs/api/omdHelpers.md +87 -87
  29. package/docs/api/omdLeafNode.md +85 -85
  30. package/docs/api/omdNode.md +201 -201
  31. package/docs/api/omdOperationDisplayNode.md +117 -117
  32. package/docs/api/omdOperatorNode.md +91 -91
  33. package/docs/api/omdParenthesisNode.md +133 -133
  34. package/docs/api/omdPopup.md +191 -191
  35. package/docs/api/omdPowerNode.md +131 -131
  36. package/docs/api/omdRationalNode.md +144 -144
  37. package/docs/api/omdSequenceNode.md +128 -128
  38. package/docs/api/omdSimplification.md +78 -78
  39. package/docs/api/omdSqrtNode.md +144 -144
  40. package/docs/api/omdStepVisualizer.md +146 -146
  41. package/docs/api/omdStepVisualizerHighlighting.md +65 -65
  42. package/docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
  43. package/docs/api/omdStepVisualizerLayout.md +70 -70
  44. package/docs/api/omdStepVisualizerNodeUtils.md +140 -140
  45. package/docs/api/omdStepVisualizerTextBoxes.md +76 -76
  46. package/docs/api/omdToolbar.md +130 -130
  47. package/docs/api/omdTranscriptionService.md +95 -95
  48. package/docs/api/omdTreeDiff.md +169 -169
  49. package/docs/api/omdUnaryExpressionNode.md +137 -137
  50. package/docs/api/omdUtilities.md +82 -82
  51. package/docs/api/omdVariableNode.md +123 -123
  52. package/docs/api/selectTool.md +74 -74
  53. package/docs/api/simplificationEngine.md +97 -97
  54. package/docs/api/simplificationRules.md +76 -76
  55. package/docs/api/simplificationUtils.md +64 -64
  56. package/docs/api/transcribe.md +43 -43
  57. package/docs/api-reference.md +85 -85
  58. package/docs/index.html +453 -453
  59. package/docs/index.md +38 -38
  60. package/docs/omd-objects.md +258 -258
  61. package/index.js +79 -79
  62. package/jsvg/index.js +3 -0
  63. package/jsvg/jsvg.js +898 -898
  64. package/jsvg/jsvgComponents.js +357 -358
  65. package/npm-docs/DOCUMENTATION_SUMMARY.md +220 -220
  66. package/npm-docs/README.md +251 -251
  67. package/npm-docs/api/api-reference.md +85 -85
  68. package/npm-docs/api/configuration-options.md +198 -198
  69. package/npm-docs/api/eventManager.md +82 -82
  70. package/npm-docs/api/expression-nodes.md +561 -561
  71. package/npm-docs/api/focusFrameManager.md +144 -144
  72. package/npm-docs/api/index.md +105 -105
  73. package/npm-docs/api/main.md +62 -62
  74. package/npm-docs/api/omdBinaryExpressionNode.md +86 -86
  75. package/npm-docs/api/omdCanvas.md +83 -83
  76. package/npm-docs/api/omdConfigManager.md +112 -112
  77. package/npm-docs/api/omdConstantNode.md +52 -52
  78. package/npm-docs/api/omdDisplay.md +87 -87
  79. package/npm-docs/api/omdEquationNode.md +174 -174
  80. package/npm-docs/api/omdEquationSequenceNode.md +258 -258
  81. package/npm-docs/api/omdEquationStack.md +192 -192
  82. package/npm-docs/api/omdFunctionNode.md +82 -82
  83. package/npm-docs/api/omdGroupNode.md +78 -78
  84. package/npm-docs/api/omdHelpers.md +87 -87
  85. package/npm-docs/api/omdLeafNode.md +85 -85
  86. package/npm-docs/api/omdNode.md +201 -201
  87. package/npm-docs/api/omdOperationDisplayNode.md +117 -117
  88. package/npm-docs/api/omdOperatorNode.md +91 -91
  89. package/npm-docs/api/omdParenthesisNode.md +133 -133
  90. package/npm-docs/api/omdPopup.md +191 -191
  91. package/npm-docs/api/omdPowerNode.md +131 -131
  92. package/npm-docs/api/omdRationalNode.md +144 -144
  93. package/npm-docs/api/omdSequenceNode.md +128 -128
  94. package/npm-docs/api/omdSimplification.md +78 -78
  95. package/npm-docs/api/omdSqrtNode.md +144 -144
  96. package/npm-docs/api/omdStepVisualizer.md +146 -146
  97. package/npm-docs/api/omdStepVisualizerHighlighting.md +65 -65
  98. package/npm-docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
  99. package/npm-docs/api/omdStepVisualizerLayout.md +70 -70
  100. package/npm-docs/api/omdStepVisualizerNodeUtils.md +140 -140
  101. package/npm-docs/api/omdStepVisualizerTextBoxes.md +76 -76
  102. package/npm-docs/api/omdToolbar.md +130 -130
  103. package/npm-docs/api/omdTranscriptionService.md +95 -95
  104. package/npm-docs/api/omdTreeDiff.md +169 -169
  105. package/npm-docs/api/omdUnaryExpressionNode.md +137 -137
  106. package/npm-docs/api/omdUtilities.md +82 -82
  107. package/npm-docs/api/omdVariableNode.md +123 -123
  108. package/npm-docs/api/selectTool.md +74 -74
  109. package/npm-docs/api/simplificationEngine.md +97 -97
  110. package/npm-docs/api/simplificationRules.md +76 -76
  111. package/npm-docs/api/simplificationUtils.md +64 -64
  112. package/npm-docs/api/transcribe.md +43 -43
  113. package/npm-docs/guides/equations.md +854 -854
  114. package/npm-docs/guides/factory-functions.md +354 -354
  115. package/npm-docs/guides/getting-started.md +318 -318
  116. package/npm-docs/guides/quick-examples.md +525 -525
  117. package/npm-docs/guides/visualizations.md +682 -682
  118. package/npm-docs/index.html +12 -0
  119. package/npm-docs/json-schemas.md +826 -826
  120. package/omd/config/omdConfigManager.js +279 -267
  121. package/omd/core/index.js +158 -158
  122. package/omd/core/omdEquationStack.js +606 -547
  123. package/omd/core/omdUtilities.js +113 -113
  124. package/omd/display/omdDisplay.js +1045 -963
  125. package/omd/display/omdToolbar.js +501 -501
  126. package/omd/nodes/omdBinaryExpressionNode.js +459 -459
  127. package/omd/nodes/omdConstantNode.js +141 -141
  128. package/omd/nodes/omdEquationNode.js +1327 -1327
  129. package/omd/nodes/omdFunctionNode.js +351 -351
  130. package/omd/nodes/omdGroupNode.js +67 -67
  131. package/omd/nodes/omdLeafNode.js +76 -76
  132. package/omd/nodes/omdNode.js +556 -556
  133. package/omd/nodes/omdOperationDisplayNode.js +321 -321
  134. package/omd/nodes/omdOperatorNode.js +108 -108
  135. package/omd/nodes/omdParenthesisNode.js +292 -292
  136. package/omd/nodes/omdPowerNode.js +235 -235
  137. package/omd/nodes/omdRationalNode.js +295 -295
  138. package/omd/nodes/omdSqrtNode.js +307 -307
  139. package/omd/nodes/omdUnaryExpressionNode.js +227 -227
  140. package/omd/nodes/omdVariableNode.js +122 -122
  141. package/omd/simplification/omdSimplification.js +140 -140
  142. package/omd/simplification/omdSimplificationEngine.js +887 -887
  143. package/omd/simplification/package.json +5 -5
  144. package/omd/simplification/rules/binaryRules.js +1037 -1037
  145. package/omd/simplification/rules/functionRules.js +111 -111
  146. package/omd/simplification/rules/index.js +48 -48
  147. package/omd/simplification/rules/parenthesisRules.js +19 -19
  148. package/omd/simplification/rules/powerRules.js +143 -143
  149. package/omd/simplification/rules/rationalRules.js +725 -725
  150. package/omd/simplification/rules/sqrtRules.js +48 -48
  151. package/omd/simplification/rules/unaryRules.js +37 -37
  152. package/omd/simplification/simplificationRules.js +31 -31
  153. package/omd/simplification/simplificationUtils.js +1055 -1055
  154. package/omd/step-visualizer/omdStepVisualizer.js +947 -947
  155. package/omd/step-visualizer/omdStepVisualizerHighlighting.js +246 -246
  156. package/omd/step-visualizer/omdStepVisualizerLayout.js +892 -892
  157. package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +200 -200
  158. package/omd/utils/aiNextEquationStep.js +106 -106
  159. package/omd/utils/omdNodeOverlay.js +638 -638
  160. package/omd/utils/omdPopup.js +1203 -1203
  161. package/omd/utils/omdStepVisualizerInteractiveSteps.js +684 -684
  162. package/omd/utils/omdStepVisualizerNodeUtils.js +267 -267
  163. package/omd/utils/omdTranscriptionService.js +123 -123
  164. package/omd/utils/omdTreeDiff.js +733 -733
  165. package/package.json +59 -57
  166. package/readme.html +184 -120
  167. package/src/index.js +74 -74
  168. package/src/json-schemas.md +576 -576
  169. package/src/omd-json-samples.js +147 -147
  170. package/src/omdApp.js +391 -391
  171. package/src/omdAppCanvas.js +335 -335
  172. package/src/omdBalanceHanger.js +199 -199
  173. package/src/omdColor.js +13 -13
  174. package/src/omdCoordinatePlane.js +541 -541
  175. package/src/omdExpression.js +115 -115
  176. package/src/omdFactory.js +150 -150
  177. package/src/omdFunction.js +114 -114
  178. package/src/omdMetaExpression.js +290 -290
  179. package/src/omdNaturalExpression.js +563 -563
  180. package/src/omdNode.js +383 -383
  181. package/src/omdNumber.js +52 -52
  182. package/src/omdNumberLine.js +114 -112
  183. package/src/omdNumberTile.js +118 -118
  184. package/src/omdOperator.js +72 -72
  185. package/src/omdPowerExpression.js +91 -91
  186. package/src/omdProblem.js +259 -259
  187. package/src/omdRatioChart.js +251 -251
  188. package/src/omdRationalExpression.js +114 -114
  189. package/src/omdSampleData.js +215 -215
  190. package/src/omdShapes.js +512 -512
  191. package/src/omdSpinner.js +151 -151
  192. package/src/omdString.js +49 -49
  193. package/src/omdTable.js +498 -498
  194. package/src/omdTapeDiagram.js +244 -244
  195. package/src/omdTerm.js +91 -91
  196. package/src/omdTileEquation.js +349 -349
  197. package/src/omdUtils.js +84 -84
  198. package/src/omdVariable.js +51 -51
@@ -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
  }