@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.
- package/README.md +138 -0
- package/canvas/core/canvasConfig.js +203 -0
- package/canvas/core/omdCanvas.js +475 -0
- package/canvas/drawing/segment.js +168 -0
- package/canvas/drawing/stroke.js +386 -0
- package/canvas/events/eventManager.js +435 -0
- package/canvas/events/pointerEventHandler.js +263 -0
- package/canvas/features/focusFrameManager.js +287 -0
- package/canvas/index.js +49 -0
- package/canvas/tools/eraserTool.js +322 -0
- package/canvas/tools/pencilTool.js +319 -0
- package/canvas/tools/selectTool.js +457 -0
- package/canvas/tools/tool.js +223 -0
- package/canvas/tools/toolManager.js +394 -0
- package/canvas/ui/cursor.js +438 -0
- package/canvas/ui/toolbar.js +304 -0
- package/canvas/utils/boundingBox.js +378 -0
- package/canvas/utils/mathUtils.js +259 -0
- package/docs/api/configuration-options.md +104 -0
- package/docs/api/eventManager.md +68 -0
- package/docs/api/focusFrameManager.md +150 -0
- package/docs/api/index.md +91 -0
- package/docs/api/main.md +58 -0
- package/docs/api/omdBinaryExpressionNode.md +227 -0
- package/docs/api/omdCanvas.md +142 -0
- package/docs/api/omdConfigManager.md +192 -0
- package/docs/api/omdConstantNode.md +117 -0
- package/docs/api/omdDisplay.md +121 -0
- package/docs/api/omdEquationNode.md +161 -0
- package/docs/api/omdEquationSequenceNode.md +301 -0
- package/docs/api/omdEquationStack.md +139 -0
- package/docs/api/omdFunctionNode.md +141 -0
- package/docs/api/omdGroupNode.md +182 -0
- package/docs/api/omdHelpers.md +96 -0
- package/docs/api/omdLeafNode.md +163 -0
- package/docs/api/omdNode.md +101 -0
- package/docs/api/omdOperationDisplayNode.md +139 -0
- package/docs/api/omdOperatorNode.md +127 -0
- package/docs/api/omdParenthesisNode.md +122 -0
- package/docs/api/omdPopup.md +117 -0
- package/docs/api/omdPowerNode.md +127 -0
- package/docs/api/omdRationalNode.md +128 -0
- package/docs/api/omdSequenceNode.md +128 -0
- package/docs/api/omdSimplification.md +110 -0
- package/docs/api/omdSqrtNode.md +79 -0
- package/docs/api/omdStepVisualizer.md +115 -0
- package/docs/api/omdStepVisualizerHighlighting.md +61 -0
- package/docs/api/omdStepVisualizerInteractiveSteps.md +129 -0
- package/docs/api/omdStepVisualizerLayout.md +60 -0
- package/docs/api/omdStepVisualizerNodeUtils.md +140 -0
- package/docs/api/omdStepVisualizerTextBoxes.md +68 -0
- package/docs/api/omdToolbar.md +102 -0
- package/docs/api/omdTranscriptionService.md +76 -0
- package/docs/api/omdTreeDiff.md +134 -0
- package/docs/api/omdUnaryExpressionNode.md +174 -0
- package/docs/api/omdUtilities.md +70 -0
- package/docs/api/omdVariableNode.md +148 -0
- package/docs/api/selectTool.md +74 -0
- package/docs/api/simplificationEngine.md +98 -0
- package/docs/api/simplificationRules.md +77 -0
- package/docs/api/simplificationUtils.md +64 -0
- package/docs/api/transcribe.md +43 -0
- package/docs/api-reference.md +85 -0
- package/docs/index.html +454 -0
- package/docs/user-guide.md +9 -0
- package/index.js +67 -0
- package/omd/config/omdConfigManager.js +267 -0
- package/omd/core/index.js +150 -0
- package/omd/core/omdEquationStack.js +347 -0
- package/omd/core/omdUtilities.js +115 -0
- package/omd/display/omdDisplay.js +443 -0
- package/omd/display/omdToolbar.js +502 -0
- package/omd/nodes/omdBinaryExpressionNode.js +460 -0
- package/omd/nodes/omdConstantNode.js +142 -0
- package/omd/nodes/omdEquationNode.js +1223 -0
- package/omd/nodes/omdEquationSequenceNode.js +1273 -0
- package/omd/nodes/omdFunctionNode.js +352 -0
- package/omd/nodes/omdGroupNode.js +68 -0
- package/omd/nodes/omdLeafNode.js +77 -0
- package/omd/nodes/omdNode.js +557 -0
- package/omd/nodes/omdOperationDisplayNode.js +322 -0
- package/omd/nodes/omdOperatorNode.js +109 -0
- package/omd/nodes/omdParenthesisNode.js +293 -0
- package/omd/nodes/omdPowerNode.js +236 -0
- package/omd/nodes/omdRationalNode.js +295 -0
- package/omd/nodes/omdSqrtNode.js +308 -0
- package/omd/nodes/omdUnaryExpressionNode.js +178 -0
- package/omd/nodes/omdVariableNode.js +123 -0
- package/omd/simplification/omdSimplification.js +171 -0
- package/omd/simplification/omdSimplificationEngine.js +886 -0
- package/omd/simplification/package.json +6 -0
- package/omd/simplification/rules/binaryRules.js +1037 -0
- package/omd/simplification/rules/functionRules.js +111 -0
- package/omd/simplification/rules/index.js +48 -0
- package/omd/simplification/rules/parenthesisRules.js +19 -0
- package/omd/simplification/rules/powerRules.js +143 -0
- package/omd/simplification/rules/rationalRules.js +475 -0
- package/omd/simplification/rules/sqrtRules.js +48 -0
- package/omd/simplification/rules/unaryRules.js +37 -0
- package/omd/simplification/simplificationRules.js +32 -0
- package/omd/simplification/simplificationUtils.js +1056 -0
- package/omd/step-visualizer/omdStepVisualizer.js +597 -0
- package/omd/step-visualizer/omdStepVisualizerHighlighting.js +206 -0
- package/omd/step-visualizer/omdStepVisualizerLayout.js +245 -0
- package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +163 -0
- package/omd/utils/omdNodeOverlay.js +638 -0
- package/omd/utils/omdPopup.js +1084 -0
- package/omd/utils/omdStepVisualizerInteractiveSteps.js +491 -0
- package/omd/utils/omdStepVisualizerNodeUtils.js +268 -0
- package/omd/utils/omdTranscriptionService.js +125 -0
- package/omd/utils/omdTreeDiff.js +734 -0
- package/package.json +46 -0
- package/src/index.js +62 -0
- package/src/json-schemas.md +109 -0
- package/src/omd-json-samples.js +115 -0
- package/src/omd.js +109 -0
- package/src/omdApp.js +391 -0
- package/src/omdAppCanvas.js +336 -0
- package/src/omdBalanceHanger.js +172 -0
- package/src/omdColor.js +13 -0
- package/src/omdCoordinatePlane.js +467 -0
- package/src/omdEquation.js +125 -0
- package/src/omdExpression.js +104 -0
- package/src/omdFunction.js +113 -0
- package/src/omdMetaExpression.js +287 -0
- package/src/omdNaturalExpression.js +564 -0
- package/src/omdNode.js +384 -0
- package/src/omdNumber.js +53 -0
- package/src/omdNumberLine.js +107 -0
- package/src/omdNumberTile.js +119 -0
- package/src/omdOperator.js +73 -0
- package/src/omdPowerExpression.js +92 -0
- package/src/omdProblem.js +55 -0
- package/src/omdRatioChart.js +232 -0
- package/src/omdRationalExpression.js +115 -0
- package/src/omdSampleData.js +215 -0
- package/src/omdShapes.js +476 -0
- package/src/omdSpinner.js +148 -0
- package/src/omdString.js +39 -0
- package/src/omdTable.js +369 -0
- package/src/omdTapeDiagram.js +245 -0
- package/src/omdTerm.js +92 -0
- package/src/omdTileEquation.js +349 -0
- 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
|
+
}
|