@teachinglab/omd 0.1.0

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 (144) hide show
  1. package/README.md +138 -0
  2. package/canvas/core/canvasConfig.js +203 -0
  3. package/canvas/core/omdCanvas.js +475 -0
  4. package/canvas/drawing/segment.js +168 -0
  5. package/canvas/drawing/stroke.js +386 -0
  6. package/canvas/events/eventManager.js +435 -0
  7. package/canvas/events/pointerEventHandler.js +263 -0
  8. package/canvas/features/focusFrameManager.js +287 -0
  9. package/canvas/index.js +49 -0
  10. package/canvas/tools/eraserTool.js +322 -0
  11. package/canvas/tools/pencilTool.js +319 -0
  12. package/canvas/tools/selectTool.js +457 -0
  13. package/canvas/tools/tool.js +223 -0
  14. package/canvas/tools/toolManager.js +394 -0
  15. package/canvas/ui/cursor.js +438 -0
  16. package/canvas/ui/toolbar.js +304 -0
  17. package/canvas/utils/boundingBox.js +378 -0
  18. package/canvas/utils/mathUtils.js +259 -0
  19. package/docs/api/configuration-options.md +104 -0
  20. package/docs/api/eventManager.md +68 -0
  21. package/docs/api/focusFrameManager.md +150 -0
  22. package/docs/api/index.md +91 -0
  23. package/docs/api/main.md +58 -0
  24. package/docs/api/omdBinaryExpressionNode.md +227 -0
  25. package/docs/api/omdCanvas.md +142 -0
  26. package/docs/api/omdConfigManager.md +192 -0
  27. package/docs/api/omdConstantNode.md +117 -0
  28. package/docs/api/omdDisplay.md +121 -0
  29. package/docs/api/omdEquationNode.md +161 -0
  30. package/docs/api/omdEquationSequenceNode.md +301 -0
  31. package/docs/api/omdEquationStack.md +139 -0
  32. package/docs/api/omdFunctionNode.md +141 -0
  33. package/docs/api/omdGroupNode.md +182 -0
  34. package/docs/api/omdHelpers.md +96 -0
  35. package/docs/api/omdLeafNode.md +163 -0
  36. package/docs/api/omdNode.md +101 -0
  37. package/docs/api/omdOperationDisplayNode.md +139 -0
  38. package/docs/api/omdOperatorNode.md +127 -0
  39. package/docs/api/omdParenthesisNode.md +122 -0
  40. package/docs/api/omdPopup.md +117 -0
  41. package/docs/api/omdPowerNode.md +127 -0
  42. package/docs/api/omdRationalNode.md +128 -0
  43. package/docs/api/omdSequenceNode.md +128 -0
  44. package/docs/api/omdSimplification.md +110 -0
  45. package/docs/api/omdSqrtNode.md +79 -0
  46. package/docs/api/omdStepVisualizer.md +115 -0
  47. package/docs/api/omdStepVisualizerHighlighting.md +61 -0
  48. package/docs/api/omdStepVisualizerInteractiveSteps.md +129 -0
  49. package/docs/api/omdStepVisualizerLayout.md +60 -0
  50. package/docs/api/omdStepVisualizerNodeUtils.md +140 -0
  51. package/docs/api/omdStepVisualizerTextBoxes.md +68 -0
  52. package/docs/api/omdToolbar.md +102 -0
  53. package/docs/api/omdTranscriptionService.md +76 -0
  54. package/docs/api/omdTreeDiff.md +134 -0
  55. package/docs/api/omdUnaryExpressionNode.md +174 -0
  56. package/docs/api/omdUtilities.md +70 -0
  57. package/docs/api/omdVariableNode.md +148 -0
  58. package/docs/api/selectTool.md +74 -0
  59. package/docs/api/simplificationEngine.md +98 -0
  60. package/docs/api/simplificationRules.md +77 -0
  61. package/docs/api/simplificationUtils.md +64 -0
  62. package/docs/api/transcribe.md +43 -0
  63. package/docs/api-reference.md +85 -0
  64. package/docs/index.html +454 -0
  65. package/docs/user-guide.md +9 -0
  66. package/index.js +67 -0
  67. package/omd/config/omdConfigManager.js +267 -0
  68. package/omd/core/index.js +150 -0
  69. package/omd/core/omdEquationStack.js +347 -0
  70. package/omd/core/omdUtilities.js +115 -0
  71. package/omd/display/omdDisplay.js +443 -0
  72. package/omd/display/omdToolbar.js +502 -0
  73. package/omd/nodes/omdBinaryExpressionNode.js +460 -0
  74. package/omd/nodes/omdConstantNode.js +142 -0
  75. package/omd/nodes/omdEquationNode.js +1223 -0
  76. package/omd/nodes/omdEquationSequenceNode.js +1273 -0
  77. package/omd/nodes/omdFunctionNode.js +352 -0
  78. package/omd/nodes/omdGroupNode.js +68 -0
  79. package/omd/nodes/omdLeafNode.js +77 -0
  80. package/omd/nodes/omdNode.js +557 -0
  81. package/omd/nodes/omdOperationDisplayNode.js +322 -0
  82. package/omd/nodes/omdOperatorNode.js +109 -0
  83. package/omd/nodes/omdParenthesisNode.js +293 -0
  84. package/omd/nodes/omdPowerNode.js +236 -0
  85. package/omd/nodes/omdRationalNode.js +295 -0
  86. package/omd/nodes/omdSqrtNode.js +308 -0
  87. package/omd/nodes/omdUnaryExpressionNode.js +178 -0
  88. package/omd/nodes/omdVariableNode.js +123 -0
  89. package/omd/simplification/omdSimplification.js +171 -0
  90. package/omd/simplification/omdSimplificationEngine.js +886 -0
  91. package/omd/simplification/package.json +6 -0
  92. package/omd/simplification/rules/binaryRules.js +1037 -0
  93. package/omd/simplification/rules/functionRules.js +111 -0
  94. package/omd/simplification/rules/index.js +48 -0
  95. package/omd/simplification/rules/parenthesisRules.js +19 -0
  96. package/omd/simplification/rules/powerRules.js +143 -0
  97. package/omd/simplification/rules/rationalRules.js +475 -0
  98. package/omd/simplification/rules/sqrtRules.js +48 -0
  99. package/omd/simplification/rules/unaryRules.js +37 -0
  100. package/omd/simplification/simplificationRules.js +32 -0
  101. package/omd/simplification/simplificationUtils.js +1056 -0
  102. package/omd/step-visualizer/omdStepVisualizer.js +597 -0
  103. package/omd/step-visualizer/omdStepVisualizerHighlighting.js +206 -0
  104. package/omd/step-visualizer/omdStepVisualizerLayout.js +245 -0
  105. package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +163 -0
  106. package/omd/utils/omdNodeOverlay.js +638 -0
  107. package/omd/utils/omdPopup.js +1084 -0
  108. package/omd/utils/omdStepVisualizerInteractiveSteps.js +491 -0
  109. package/omd/utils/omdStepVisualizerNodeUtils.js +268 -0
  110. package/omd/utils/omdTranscriptionService.js +125 -0
  111. package/omd/utils/omdTreeDiff.js +734 -0
  112. package/package.json +46 -0
  113. package/src/index.js +62 -0
  114. package/src/json-schemas.md +109 -0
  115. package/src/omd-json-samples.js +115 -0
  116. package/src/omd.js +109 -0
  117. package/src/omdApp.js +391 -0
  118. package/src/omdAppCanvas.js +336 -0
  119. package/src/omdBalanceHanger.js +172 -0
  120. package/src/omdColor.js +13 -0
  121. package/src/omdCoordinatePlane.js +467 -0
  122. package/src/omdEquation.js +125 -0
  123. package/src/omdExpression.js +104 -0
  124. package/src/omdFunction.js +113 -0
  125. package/src/omdMetaExpression.js +287 -0
  126. package/src/omdNaturalExpression.js +564 -0
  127. package/src/omdNode.js +384 -0
  128. package/src/omdNumber.js +53 -0
  129. package/src/omdNumberLine.js +107 -0
  130. package/src/omdNumberTile.js +119 -0
  131. package/src/omdOperator.js +73 -0
  132. package/src/omdPowerExpression.js +92 -0
  133. package/src/omdProblem.js +55 -0
  134. package/src/omdRatioChart.js +232 -0
  135. package/src/omdRationalExpression.js +115 -0
  136. package/src/omdSampleData.js +215 -0
  137. package/src/omdShapes.js +476 -0
  138. package/src/omdSpinner.js +148 -0
  139. package/src/omdString.js +39 -0
  140. package/src/omdTable.js +369 -0
  141. package/src/omdTapeDiagram.js +245 -0
  142. package/src/omdTerm.js +92 -0
  143. package/src/omdTileEquation.js +349 -0
  144. package/src/omdVariable.js +51 -0
@@ -0,0 +1,206 @@
1
+ import { omdColor } from '../../src/omdColor.js';
2
+ import { omdEquationNode } from '../nodes/omdEquationNode.js';
3
+ import { omdStepVisualizerNodeUtils } from '../utils/omdStepVisualizerNodeUtils.js';
4
+ import { omdTreeDiff } from '../utils/omdTreeDiff.js';
5
+
6
+ /**
7
+ * Step visualizer highlighting functionality using robust tree diff algorithm.
8
+ * This class implements optimal substructure matching to identify truly changed nodes
9
+ * between mathematical equation steps, eliminating the need for special cases.
10
+ */
11
+ export class omdStepVisualizerHighlighting {
12
+ constructor(stepVisualizer) {
13
+ this.stepVisualizer = stepVisualizer;
14
+ this.highlightedNodes = new Set();
15
+ this.educationalMode = true; // Enable highlighting of pedagogical simplifications
16
+ }
17
+
18
+ /**
19
+ * Main entry point for highlighting nodes based on robust tree diff.
20
+ * @param {number} dotIndex - Index of the dot/step.
21
+ */
22
+ highlightAffectedNodes(dotIndex, isOperation = false) {
23
+
24
+ this.clearHighlights();
25
+
26
+ const dot = this.stepVisualizer.stepDots[dotIndex];
27
+ if (!dot || !dot.equationRef) {
28
+ console.error("Highlighting failed: No equation reference for dot", dotIndex);
29
+ return;
30
+ }
31
+
32
+ const currentEquation = dot.equationRef;
33
+ const equationIndex = this.stepVisualizer.steps.indexOf(currentEquation);
34
+
35
+
36
+ const previousEquation = this._findNearestVisibleEquationAbove(equationIndex);
37
+
38
+ if (!previousEquation) {
39
+ const leafNodes = omdStepVisualizerNodeUtils.findLeafNodes(currentEquation);
40
+ leafNodes.forEach(node => this._highlightNode(node));
41
+ return;
42
+ }
43
+
44
+ const previousIndex = this.stepVisualizer.steps.indexOf(previousEquation);
45
+
46
+ // Use robust tree diff algorithm to find changed nodes
47
+ const changedNodes = omdTreeDiff.findChangedNodes(previousEquation, currentEquation, {
48
+ educationalMode: this.educationalMode
49
+ });
50
+
51
+
52
+ // Apply highlighting to changed nodes
53
+ changedNodes.forEach(node => this._highlightNode(node));
54
+
55
+ // Use provenance to highlight related nodes in the previous equation
56
+ if (!isOperation) {
57
+ this._highlightProvenanceNodes(changedNodes, previousEquation);
58
+ }
59
+
60
+ }
61
+
62
+ /**
63
+ * Highlights a single node with the standard explanation color.
64
+ * @param {omdNode} node - The node to highlight.
65
+ * @private
66
+ */
67
+ _highlightNode(node) {
68
+ if (node && typeof node.setExplainHighlight === 'function') {
69
+ node.setExplainHighlight(true);
70
+ this.highlightedNodes.add(node);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Finds the nearest visible equation above the given index.
76
+ * @param {number} currentIndex - Index of current equation.
77
+ * @returns {omdEquationNode|null} The nearest visible equation above, or null.
78
+ * @private
79
+ */
80
+ _findNearestVisibleEquationAbove(currentIndex) {
81
+ for (let i = currentIndex - 1; i >= 0; i--) {
82
+ const step = this.stepVisualizer.steps[i];
83
+ if (step instanceof omdEquationNode && step.visible !== false) {
84
+ return step;
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * Clears all highlights managed by this class.
92
+ */
93
+ clearHighlights() {
94
+ this.highlightedNodes.forEach(node => {
95
+ if (node && typeof node.setExplainHighlight === 'function') {
96
+ node.setExplainHighlight(false);
97
+ }
98
+ });
99
+
100
+ this.highlightedNodes.clear();
101
+ this.stepVisualizer.stepVisualizerHighlights.clear();
102
+ }
103
+
104
+ /**
105
+ * Highlights nodes in the previous equation based on provenance from changed nodes.
106
+ * This creates a visual connection between the current changes and their origins.
107
+ * @param {Array} changedNodes - Array of changed nodes in the current equation
108
+ * @param {omdEquationNode} previousEquation - The previous equation to highlight nodes in
109
+ * @private
110
+ */
111
+ _highlightProvenanceNodes(changedNodes, previousEquation) {
112
+
113
+ const rootNode = previousEquation.getRootNode();
114
+ if (!rootNode || !rootNode.nodeMap) {
115
+ return;
116
+ }
117
+
118
+ const currentIndex = this.stepVisualizer.steps.indexOf(previousEquation) + 1;
119
+ let targetEquation = null;
120
+
121
+ // Search backwards for the first visible equation
122
+ for (let i = currentIndex - 1; i >= 0; i--) {
123
+ const step = this.stepVisualizer.steps[i];
124
+ if (step instanceof omdEquationNode && step.visible !== false) {
125
+ targetEquation = step;
126
+ break;
127
+ }
128
+ }
129
+
130
+ if (!targetEquation) {
131
+ return;
132
+ }
133
+
134
+ const visited = new Set();
135
+ const nodesToProcess = [];
136
+
137
+ // Start with the changed nodes' provenance
138
+ changedNodes.forEach(node => {
139
+ if (node.provenance && Array.isArray(node.provenance)) {
140
+ node.provenance.forEach(id => {
141
+ if (!visited.has(id)) {
142
+ visited.add(id);
143
+ nodesToProcess.push(id);
144
+ }
145
+ });
146
+ }
147
+ });
148
+
149
+ // Process provenance IDs
150
+ while (nodesToProcess.length > 0) {
151
+ const id = nodesToProcess.shift();
152
+ const provenanceNode = rootNode.nodeMap.get(id);
153
+
154
+ if (provenanceNode) {
155
+ if (this._belongsToEquation(provenanceNode, targetEquation)) {
156
+ this._highlightProvenanceNode(provenanceNode);
157
+ }
158
+ // Add this node's provenance to the processing queue
159
+ if (provenanceNode.provenance && Array.isArray(provenanceNode.provenance)) {
160
+ provenanceNode.provenance.forEach(subId => {
161
+ if (!visited.has(subId)) {
162
+ visited.add(subId);
163
+ nodesToProcess.push(subId);
164
+
165
+ }
166
+ });
167
+ }
168
+ } else {
169
+
170
+ }
171
+ }
172
+
173
+
174
+ }
175
+
176
+ /**
177
+ * Checks if a node belongs to a specific equation by traversing up the tree
178
+ * @param {omdNode} node - The node to check
179
+ * @param {omdEquationNode} targetEquation - The equation to check against
180
+ * @returns {boolean} True if the node belongs to the equation
181
+ * @private
182
+ */
183
+ _belongsToEquation(node, targetEquation) {
184
+ let current = node;
185
+ while (current) {
186
+ if (current === targetEquation) {
187
+ return true;
188
+ }
189
+ current = current.parent;
190
+ }
191
+ return false;
192
+ }
193
+
194
+ /**
195
+ * Highlights a provenance node with secondary highlighting style
196
+ * @param {omdNode} node - The node to highlight with provenance style
197
+ * @private
198
+ */
199
+ _highlightProvenanceNode(node) {
200
+ if (node && typeof node.setExplainHighlight === 'function') {
201
+ // Use a slightly different color or style for provenance if desired
202
+ node.setExplainHighlight(true, omdColor.provenanceColor);
203
+ this.highlightedNodes.add(node);
204
+ }
205
+ }
206
+ }
@@ -0,0 +1,245 @@
1
+ import { omdEquationNode } from '../nodes/omdEquationNode.js';
2
+ import { omdColor } from '../../src/omdColor.js';
3
+ import { jsvgLine } from '@teachinglab/jsvg';
4
+
5
+ /**
6
+ * Handles visual layout, positioning, and visibility management for step visualizations
7
+ */
8
+ export class omdStepVisualizerLayout {
9
+ constructor(stepVisualizer) {
10
+ this.stepVisualizer = stepVisualizer;
11
+ }
12
+
13
+ /**
14
+ * Updates the layout of visual elements relative to the sequence
15
+ */
16
+ updateVisualLayout() {
17
+ if (this.stepVisualizer.stepDots.length === 0) return;
18
+
19
+ // Position visual container to the right of the sequence
20
+ // Add extra offset based on equation background padding (if any)
21
+ const baseRight = (this.stepVisualizer.sequenceWidth || this.stepVisualizer.width);
22
+ // Use EFFECTIVE padding (after pill clamping) to avoid overlap when pills are wider
23
+ const extraPaddingX = this._getMaxEquationEffectivePaddingX();
24
+ const visualX = baseRight + this.stepVisualizer.visualSpacing + extraPaddingX;
25
+ this.stepVisualizer.visualContainer.setPosition(visualX, 0);
26
+
27
+ // Position dots based on visible equations
28
+ const visibleSteps = this.stepVisualizer.steps.filter(s => s.visible !== false);
29
+ let currentY = 0;
30
+ const verticalPadding = 15 * this.stepVisualizer.getFontSize() / this.stepVisualizer.getRootFontSize();
31
+
32
+ visibleSteps.forEach((step, visIndex) => {
33
+ if (step instanceof omdEquationNode) {
34
+ const dotIndex = this.findDotIndexForEquation(step);
35
+ if (dotIndex >= 0 && dotIndex < this.stepVisualizer.stepDots.length) {
36
+ const dot = this.stepVisualizer.stepDots[dotIndex];
37
+
38
+ // Center dot vertically with the equation
39
+ let equationCenter;
40
+ if (step.equalsSign && step.equalsSign.ypos !== undefined) {
41
+ equationCenter = step.equalsSign.ypos + (step.equalsSign.height / 2);
42
+ } else {
43
+ equationCenter = step.getAlignmentBaseline ? step.getAlignmentBaseline() : step.height / 2;
44
+ }
45
+ const dotY = currentY + equationCenter;
46
+ const dotX = (this.stepVisualizer.dotRadius * 3) / 2;
47
+
48
+ dot.setPosition(dotX, dotY);
49
+ }
50
+ }
51
+
52
+ currentY += step.height;
53
+ if (visIndex < visibleSteps.length - 1) {
54
+ currentY += verticalPadding;
55
+ }
56
+ });
57
+
58
+ this.updateAllLinePositions();
59
+
60
+ // Update container dimensions
61
+ let containerWidth = this.stepVisualizer.dotRadius * 3;
62
+ let containerHeight = this.stepVisualizer.height;
63
+
64
+ const textBoxes = this.stepVisualizer.textBoxManager.getStepTextBoxes();
65
+ if (textBoxes.length > 0) {
66
+ const textBoxWidth = 280;
67
+ containerWidth = Math.max(containerWidth, textBoxWidth + this.stepVisualizer.dotRadius * 2 + 10 + 20);
68
+
69
+ // Calculate the maximum extent of any text box to prevent clipping
70
+ textBoxes.forEach(textBox => {
71
+ if (textBox.interactiveSteps) {
72
+ const dimensions = textBox.interactiveSteps.getDimensions();
73
+ const layoutGroup = textBox.interactiveSteps.getLayoutGroup();
74
+
75
+ // Calculate the bottom of this text box
76
+ const textBoxBottom = layoutGroup.ypos + dimensions.height;
77
+ containerHeight = Math.max(containerHeight, textBoxBottom + 20); // Add some buffer
78
+ }
79
+ });
80
+ }
81
+
82
+ if (this.stepVisualizer.stepDots.length > 0) {
83
+ const maxRadius = Math.max(...this.stepVisualizer.stepDots.map(d=>d.radius||this.stepVisualizer.dotRadius));
84
+ const containerWidth = maxRadius * 3;
85
+ const maxDotY = Math.max(...this.stepVisualizer.stepDots.map(dot => dot.ypos + this.stepVisualizer.dotRadius));
86
+ containerHeight = Math.max(containerHeight, maxDotY);
87
+ }
88
+
89
+ this.stepVisualizer.visualContainer.setWidthAndHeight(containerWidth, containerHeight);
90
+ this.updateVisualZOrder();
91
+ }
92
+
93
+ /**
94
+ * Computes the maximum horizontal padding (x) among visible equations, if configured.
95
+ * This allows dots to shift further right when pill background padding is added.
96
+ * @returns {number}
97
+ * @private
98
+ */
99
+ _getMaxEquationEffectivePaddingX() {
100
+ try {
101
+ const steps = this.stepVisualizer.steps || [];
102
+ let maxPadX = 0;
103
+ steps.forEach(step => {
104
+ if (step instanceof omdEquationNode && step.visible !== false) {
105
+ if (typeof step.getEffectiveBackgroundPaddingX === 'function') {
106
+ const px = Number(step.getEffectiveBackgroundPaddingX());
107
+ maxPadX = Math.max(maxPadX, isNaN(px) ? 0 : px);
108
+ }
109
+ }
110
+ });
111
+ return maxPadX;
112
+ } catch (_) {
113
+ return 0;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Finds the dot index for a given equation
119
+ */
120
+ findDotIndexForEquation(equation) {
121
+ return this.stepVisualizer.stepDots.findIndex(dot => dot.equationRef === equation);
122
+ }
123
+
124
+ /**
125
+ * Updates the z-order of visual elements
126
+ */
127
+ updateVisualZOrder() {
128
+ if (!this.stepVisualizer.visualContainer) return;
129
+
130
+ // Lines behind (z-index 1)
131
+ this.stepVisualizer.stepLines.forEach(line => {
132
+ if (line && line.svgObject) {
133
+ line.svgObject.style.zIndex = '1';
134
+ if (line.parentNode !== this.stepVisualizer.visualContainer) {
135
+ this.stepVisualizer.visualContainer.addChild(line);
136
+ }
137
+ }
138
+ });
139
+
140
+ // Dots in front (z-index 2)
141
+ this.stepVisualizer.stepDots.forEach(dot => {
142
+ if (dot && dot.svgObject) {
143
+ dot.svgObject.style.zIndex = '2';
144
+ if (dot.parentNode !== this.stepVisualizer.visualContainer) {
145
+ this.stepVisualizer.visualContainer.addChild(dot);
146
+ }
147
+ }
148
+ });
149
+
150
+ // Text boxes on top (z-index 3)
151
+ const textBoxes = this.stepVisualizer.textBoxManager.getStepTextBoxes();
152
+ textBoxes.forEach(textBox => {
153
+ if (textBox && textBox.svgObject) {
154
+ textBox.svgObject.style.zIndex = '3';
155
+ if (textBox.parentNode !== this.stepVisualizer.visualContainer) {
156
+ this.stepVisualizer.visualContainer.addChild(textBox);
157
+ }
158
+ }
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Updates all line positions to connect dot centers
164
+ */
165
+ updateAllLinePositions() {
166
+ this.stepVisualizer.stepLines.forEach(line => {
167
+ const fromDot = this.stepVisualizer.stepDots[line.fromDotIndex];
168
+ const toDot = this.stepVisualizer.stepDots[line.toDotIndex];
169
+
170
+ if (fromDot && toDot) {
171
+ line.setEndpoints(fromDot.xpos, fromDot.ypos, toDot.xpos, toDot.ypos);
172
+ }
173
+ });
174
+ }
175
+
176
+ /**
177
+ * Updates visibility of visual elements based on equation visibility
178
+ */
179
+ updateVisualVisibility() {
180
+ const sv = this.stepVisualizer;
181
+
182
+ // Update dot visibility first, which is the source of truth
183
+ sv.stepDots.forEach(dot => {
184
+ if (dot.equationRef && dot.equationRef.visible !== false) {
185
+ dot.show();
186
+ dot.visible = true; // Use the dot's own visibility property
187
+ } else {
188
+ dot.hide();
189
+ dot.visible = false;
190
+ }
191
+ });
192
+
193
+ // Remove all old lines from the container and the array
194
+ sv.stepLines.forEach(line => {
195
+ // Remove the line if it is currently a child of the visualContainer
196
+ if (line.parent === sv.visualContainer) {
197
+ sv.visualContainer.removeChild(line);
198
+ }
199
+ });
200
+ sv.stepLines = [];
201
+
202
+ // Get the dots that are currently visible
203
+ const visibleDots = sv.stepDots.filter(dot => dot.visible);
204
+
205
+ // Re-create connecting lines only between the visible dots
206
+ for (let i = 0; i < visibleDots.length - 1; i++) {
207
+ const fromDot = visibleDots[i];
208
+ const toDot = visibleDots[i + 1];
209
+
210
+ const line = new jsvgLine();
211
+ line.setStrokeColor(omdColor.stepColor);
212
+ line.setStrokeWidth(sv.lineWidth);
213
+ line.fromDotIndex = sv.stepDots.indexOf(fromDot);
214
+ line.toDotIndex = sv.stepDots.indexOf(toDot);
215
+
216
+ sv.visualContainer.addChild(line);
217
+ sv.stepLines.push(line);
218
+ }
219
+
220
+ // After creating the lines, update their positions
221
+ this.updateAllLinePositions();
222
+ }
223
+
224
+ /**
225
+ * Updates the clickability of a dot
226
+ */
227
+ updateDotClickability(dot) {
228
+ if (this.stepVisualizer.dotsClickable) {
229
+ dot.svgObject.style.cursor = "pointer";
230
+ dot.svgObject.onclick = (event) => {
231
+ try {
232
+ const idx = this.stepVisualizer.stepDots.indexOf(dot);
233
+ if (idx < 0) return; // orphan dot, ignore
234
+ this.stepVisualizer._handleDotClick(dot, idx);
235
+ event.stopPropagation();
236
+ } catch (error) {
237
+ console.error('Error in dot click handler:', error);
238
+ }
239
+ };
240
+ } else {
241
+ dot.svgObject.style.cursor = "default";
242
+ dot.svgObject.onclick = null;
243
+ }
244
+ }
245
+ }
@@ -0,0 +1,163 @@
1
+ import { omdEquationNode } from '../nodes/omdEquationNode.js';
2
+ import { omdStepVisualizerInteractiveSteps } from '../utils/omdStepVisualizerInteractiveSteps.js';
3
+
4
+ /**
5
+ * Manages interactive step text boxes that appear when dots are clicked
6
+ * Handles creation, positioning, and cleanup of explanation popups
7
+ */
8
+ export class omdStepVisualizerTextBoxes {
9
+ constructor(stepVisualizer, highlighting) {
10
+ this.stepVisualizer = stepVisualizer;
11
+ this.highlighting = highlighting;
12
+ this.stepTextBoxes = [];
13
+ }
14
+
15
+ /**
16
+ * Creates an interactive steps popup for a clicked dot
17
+ * @param {number} dotIndex - Index of the dot to create text box for
18
+ */
19
+ createTextBoxForDot(dotIndex) {
20
+ try {
21
+ this.removeTextBoxForDot(dotIndex);
22
+
23
+ const targetDot = this._findDotAboveForPositioning(dotIndex);
24
+ if (!targetDot) {
25
+ console.error('Target dot not found for positioning text box for dot index:', dotIndex);
26
+ return;
27
+ }
28
+
29
+ const simplificationData = this._getSimplificationDataForDot(dotIndex);
30
+ this._createInteractiveStepsForDot(dotIndex, targetDot, simplificationData);
31
+
32
+ } catch (error) {
33
+ console.error('Error creating text box for dot', dotIndex, ':', error);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Creates interactive steps for a dot with simplification data
39
+ * @param {number} dotIndex - Index of the dot
40
+ * @param {Object} targetDot - The dot to position relative to
41
+ * @param {Object} simplificationData - Full simplification data including rule names
42
+ * @private
43
+ */
44
+ _createInteractiveStepsForDot(dotIndex, targetDot, simplificationData) {
45
+ const interactiveSteps = new omdStepVisualizerInteractiveSteps(this.stepVisualizer, simplificationData);
46
+
47
+ // Position relative to the target dot
48
+ const x = targetDot.xpos + this.stepVisualizer.dotRadius * 2 + 10;
49
+ const y = targetDot.ypos - this.stepVisualizer.dotRadius;
50
+ interactiveSteps.setPosition(x, y);
51
+
52
+ // Set up hover interactions
53
+ interactiveSteps.setOnStepHover((stepIndex, message, isEntering) => {
54
+ });
55
+
56
+ // Set up click interactions
57
+ interactiveSteps.setOnStepClick((stepIndex, message) => {
58
+ });
59
+
60
+ // Add to visual container and track
61
+ const layoutGroup = interactiveSteps.getLayoutGroup();
62
+ layoutGroup.dotIndex = dotIndex;
63
+ this.stepVisualizer.visualContainer.addChild(layoutGroup);
64
+
65
+ this.stepTextBoxes.push({
66
+ dotIndex: dotIndex,
67
+ interactiveSteps: interactiveSteps,
68
+ layoutGroup: layoutGroup
69
+ });
70
+
71
+ // Update layout to prevent clipping after adding text box
72
+ this.stepVisualizer.layoutManager.updateVisualLayout();
73
+ }
74
+
75
+
76
+
77
+
78
+
79
+ /**
80
+ * Removes the text box for a specific dot
81
+ * @param {number} dotIndex - Index of the dot to remove text box for
82
+ */
83
+ removeTextBoxForDot(dotIndex) {
84
+ const textBoxIndex = this.stepTextBoxes.findIndex(tb => tb.dotIndex === dotIndex);
85
+ if (textBoxIndex >= 0) {
86
+ const item = this.stepTextBoxes[textBoxIndex];
87
+ this.stepVisualizer.visualContainer.removeChild(item.layoutGroup);
88
+ item.interactiveSteps.destroy();
89
+ this.stepTextBoxes.splice(textBoxIndex, 1);
90
+
91
+ // Update layout after removing text box to adjust container size
92
+ this.stepVisualizer.layoutManager.updateVisualLayout();
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Removes all text boxes
98
+ */
99
+ clearAllTextBoxes() {
100
+ this.stepTextBoxes.forEach(item => {
101
+ this.stepVisualizer.visualContainer.removeChild(item.layoutGroup);
102
+ item.interactiveSteps.destroy();
103
+ });
104
+ this.stepTextBoxes = [];
105
+
106
+ // Update layout after clearing all text boxes
107
+ this.stepVisualizer.layoutManager.updateVisualLayout();
108
+ }
109
+
110
+ /**
111
+ * Finds the appropriate dot above the clicked one for text box positioning
112
+ * @param {number} dotIndex - Index of the clicked dot
113
+ * @returns {Object|null} The dot to align with, or null if none found
114
+ * @private
115
+ */
116
+ _findDotAboveForPositioning(dotIndex) {
117
+ const currentDot = this.stepVisualizer.stepDots[dotIndex];
118
+ if (!currentDot || !currentDot.equationRef) {
119
+ return null;
120
+ }
121
+
122
+ const currentEquation = currentDot.equationRef;
123
+ const currentEquationIndex = this.stepVisualizer.steps.indexOf(currentEquation);
124
+
125
+ // Find the nearest visible equation above
126
+ for (let i = currentEquationIndex - 1; i >= 0; i--) {
127
+ const step = this.stepVisualizer.steps[i];
128
+ if (step instanceof omdEquationNode && step.visible !== false) {
129
+ // Find the corresponding dot
130
+ for (let dotIdx = dotIndex - 1; dotIdx >= 0; dotIdx--) {
131
+ const dot = this.stepVisualizer.stepDots[dotIdx];
132
+ if (dot && dot.equationRef === step) {
133
+ return dot;
134
+ }
135
+ }
136
+ break;
137
+ }
138
+ }
139
+
140
+ // If no visible equation above, use the clicked dot itself
141
+ return currentDot;
142
+ }
143
+
144
+ /**
145
+ * Gets the simplification data for a specific dot
146
+ * @param {number} dotIndex - Index of the dot
147
+ * @returns {Object} The simplification data for this step
148
+ * @private
149
+ */
150
+ _getSimplificationDataForDot(dotIndex) {
151
+ return this.stepVisualizer._getSimplificationDataForDot(dotIndex);
152
+ }
153
+
154
+
155
+
156
+ /**
157
+ * Gets all text boxes
158
+ * @returns {Array} Array of text box objects
159
+ */
160
+ getStepTextBoxes() {
161
+ return this.stepTextBoxes;
162
+ }
163
+ }