@teachinglab/omd 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/api/configuration-options.md +198 -198
- package/docs/api/eventManager.md +82 -82
- package/docs/api/focusFrameManager.md +144 -144
- package/docs/api/index.md +105 -105
- package/docs/api/main.md +62 -62
- package/docs/api/omdBinaryExpressionNode.md +86 -86
- package/docs/api/omdCanvas.md +83 -83
- package/docs/api/omdConfigManager.md +112 -112
- package/docs/api/omdConstantNode.md +52 -52
- package/docs/api/omdDisplay.md +87 -87
- package/docs/api/omdEquationNode.md +174 -174
- package/docs/api/omdEquationSequenceNode.md +258 -258
- package/docs/api/omdEquationStack.md +156 -156
- package/docs/api/omdFunctionNode.md +82 -82
- package/docs/api/omdGroupNode.md +78 -78
- package/docs/api/omdHelpers.md +87 -87
- package/docs/api/omdLeafNode.md +85 -85
- package/docs/api/omdNode.md +201 -201
- package/docs/api/omdOperationDisplayNode.md +117 -117
- package/docs/api/omdOperatorNode.md +91 -91
- package/docs/api/omdParenthesisNode.md +133 -133
- package/docs/api/omdPopup.md +191 -191
- package/docs/api/omdPowerNode.md +131 -131
- package/docs/api/omdRationalNode.md +144 -144
- package/docs/api/omdSimplification.md +78 -78
- package/docs/api/omdSqrtNode.md +144 -144
- package/docs/api/omdStepVisualizer.md +146 -146
- package/docs/api/omdStepVisualizerHighlighting.md +65 -65
- package/docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
- package/docs/api/omdStepVisualizerLayout.md +70 -70
- package/docs/api/omdStepVisualizerTextBoxes.md +76 -76
- package/docs/api/omdTranscriptionService.md +95 -95
- package/docs/api/omdTreeDiff.md +169 -169
- package/docs/api/omdUnaryExpressionNode.md +137 -137
- package/docs/api/omdUtilities.md +82 -82
- package/docs/api/omdVariableNode.md +123 -123
- package/omd/nodes/omdConstantNode.js +141 -141
- package/omd/nodes/omdGroupNode.js +67 -67
- package/omd/nodes/omdLeafNode.js +76 -76
- package/omd/nodes/omdOperatorNode.js +108 -108
- package/omd/nodes/omdParenthesisNode.js +292 -292
- package/omd/nodes/omdPowerNode.js +235 -235
- package/omd/nodes/omdRationalNode.js +295 -295
- package/omd/nodes/omdVariableNode.js +122 -122
- package/omd/simplification/omdSimplification.js +140 -140
- package/omd/step-visualizer/omdStepVisualizer.js +947 -947
- package/omd/step-visualizer/omdStepVisualizerLayout.js +892 -892
- package/package.json +1 -1
- package/src/index.js +11 -0
- package/src/omdBalanceHanger.js +2 -1
- package/src/omdEquation.js +1 -1
- package/src/omdNumber.js +1 -1
- package/src/omdNumberLine.js +13 -7
- package/src/omdRatioChart.js +11 -0
- package/src/omdShapes.js +1 -1
- package/src/omdTapeDiagram.js +1 -1
- package/src/omdTerm.js +1 -1
|
@@ -1,948 +1,948 @@
|
|
|
1
|
-
import { omdEquationSequenceNode } from "../nodes/omdEquationSequenceNode.js";
|
|
2
|
-
import { omdEquationNode } from "../nodes/omdEquationNode.js";
|
|
3
|
-
import { omdOperationDisplayNode } from "../nodes/omdOperationDisplayNode.js";
|
|
4
|
-
import { omdColor } from "../../src/omdColor.js";
|
|
5
|
-
import { omdStepVisualizerHighlighting } from "./omdStepVisualizerHighlighting.js";
|
|
6
|
-
import { omdStepVisualizerTextBoxes } from "./omdStepVisualizerTextBoxes.js";
|
|
7
|
-
import { omdStepVisualizerLayout } from "./omdStepVisualizerLayout.js";
|
|
8
|
-
import { getDotRadius } from "../config/omdConfigManager.js";
|
|
9
|
-
import { jsvgLayoutGroup, jsvgEllipse, jsvgLine } from '@teachinglab/jsvg';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* A visual step tracker that extends omdEquationSequenceNode to show step progression
|
|
14
|
-
* with dots and connecting lines to the right of the sequence.
|
|
15
|
-
* @extends omdEquationSequenceNode
|
|
16
|
-
*/
|
|
17
|
-
export class omdStepVisualizer extends omdEquationSequenceNode {
|
|
18
|
-
constructor(steps, styling = {}) {
|
|
19
|
-
super(steps);
|
|
20
|
-
|
|
21
|
-
// Store styling options with defaults
|
|
22
|
-
this.styling = this._mergeWithDefaults(styling || {});
|
|
23
|
-
|
|
24
|
-
// Visual elements for step tracking
|
|
25
|
-
this.stepDots = [];
|
|
26
|
-
this.stepLines = [];
|
|
27
|
-
this.visualContainer = new jsvgLayoutGroup();
|
|
28
|
-
|
|
29
|
-
// Use styling values for these properties
|
|
30
|
-
this.dotRadius = this.styling.dotRadius;
|
|
31
|
-
this.lineWidth = this.styling.lineWidth;
|
|
32
|
-
this.visualSpacing = this.styling.visualSpacing;
|
|
33
|
-
|
|
34
|
-
this.activeDotIndex = -1;
|
|
35
|
-
this.dotsClickable = true;
|
|
36
|
-
this.nodeToStepMap = new Map();
|
|
37
|
-
|
|
38
|
-
// Highlighting system
|
|
39
|
-
this.stepVisualizerHighlights = new Set();
|
|
40
|
-
this.highlighting = new omdStepVisualizerHighlighting(this);
|
|
41
|
-
|
|
42
|
-
// Pass textbox options through styling parameter
|
|
43
|
-
const textBoxOptions = this.styling.textBoxOptions || {};
|
|
44
|
-
this.textBoxManager = new omdStepVisualizerTextBoxes(this, this.highlighting, textBoxOptions);
|
|
45
|
-
this.layoutManager = new omdStepVisualizerLayout(this);
|
|
46
|
-
|
|
47
|
-
this.addChild(this.visualContainer);
|
|
48
|
-
this._initializeVisualElements();
|
|
49
|
-
|
|
50
|
-
// Set default filter level to show only major steps (stepMark = 0)
|
|
51
|
-
// This ensures intermediate steps are hidden and expansion dots can be created
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (this.setFilterLevel && typeof this.setFilterLevel === 'function') {
|
|
56
|
-
this.setFilterLevel(0);
|
|
57
|
-
|
|
58
|
-
} else {
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
this.computeDimensions();
|
|
63
|
-
this.updateLayout();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Public: programmatically toggle a dot (simulate user click behavior)
|
|
68
|
-
* @param {number} dotIndex
|
|
69
|
-
*/
|
|
70
|
-
toggleDot(dotIndex) {
|
|
71
|
-
if (typeof dotIndex !== 'number') return;
|
|
72
|
-
if (dotIndex < 0 || dotIndex >= this.stepDots.length) return;
|
|
73
|
-
const dot = this.stepDots[dotIndex];
|
|
74
|
-
this._handleDotClick(dot, dotIndex);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Public: close currently active dot textbox if any
|
|
79
|
-
*/
|
|
80
|
-
closeActiveDot() {
|
|
81
|
-
// Always clear all boxes to be safe (even if activeDotIndex already reset)
|
|
82
|
-
try {
|
|
83
|
-
const before = this.textBoxManager?.stepTextBoxes?.length || 0;
|
|
84
|
-
this._clearActiveDot();
|
|
85
|
-
if (this.textBoxManager && typeof this.textBoxManager.clearAllTextBoxes === 'function') {
|
|
86
|
-
this.textBoxManager.clearAllTextBoxes();
|
|
87
|
-
}
|
|
88
|
-
const after = this.textBoxManager?.stepTextBoxes?.length || 0;
|
|
89
|
-
} catch (e) { /* no-op */ }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Public: close all text boxes (future-proof; currently only one can be active)
|
|
94
|
-
*/
|
|
95
|
-
closeAllTextBoxes() {
|
|
96
|
-
this.closeActiveDot();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Public: force close EVERYTHING related to active explanation UI
|
|
101
|
-
*/
|
|
102
|
-
forceCloseAll() {
|
|
103
|
-
this.closeActiveDot();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Merges user styling with default styling options
|
|
108
|
-
* @param {Object} userStyling - User-provided styling options
|
|
109
|
-
* @returns {Object} Merged styling object with defaults
|
|
110
|
-
* @private
|
|
111
|
-
*/
|
|
112
|
-
_mergeWithDefaults(userStyling) {
|
|
113
|
-
const defaults = {
|
|
114
|
-
// Dot styling
|
|
115
|
-
dotColor: omdColor.stepColor,
|
|
116
|
-
dotRadius: getDotRadius(0),
|
|
117
|
-
dotStrokeWidth: 2,
|
|
118
|
-
activeDotColor: omdColor.explainColor,
|
|
119
|
-
expansionDotScale: 0.4,
|
|
120
|
-
|
|
121
|
-
// Line styling
|
|
122
|
-
lineColor: omdColor.stepColor,
|
|
123
|
-
lineWidth: 2,
|
|
124
|
-
activeLineColor: omdColor.explainColor,
|
|
125
|
-
|
|
126
|
-
// Colors
|
|
127
|
-
explainColor: omdColor.explainColor,
|
|
128
|
-
highlightColor: omdColor.hiliteColor,
|
|
129
|
-
|
|
130
|
-
// Layout
|
|
131
|
-
visualSpacing: 30,
|
|
132
|
-
fixedVisualizerPosition: 250,
|
|
133
|
-
dotVerticalOffset: 15,
|
|
134
|
-
|
|
135
|
-
// Text boxes
|
|
136
|
-
textBoxOptions: {
|
|
137
|
-
backgroundColor: omdColor.white,
|
|
138
|
-
borderColor: 'none',
|
|
139
|
-
borderWidth: 1,
|
|
140
|
-
borderRadius: 5,
|
|
141
|
-
padding: 8, // Minimal padding for tight fit
|
|
142
|
-
fontSize: 14,
|
|
143
|
-
fontFamily: 'Albert Sans, Arial, sans-serif',
|
|
144
|
-
maxWidth: 300, // More reasonable width for compact layout
|
|
145
|
-
dropShadow: true
|
|
146
|
-
// Removed zIndex and position from defaults - these should only apply to container
|
|
147
|
-
},
|
|
148
|
-
|
|
149
|
-
// Visual effects
|
|
150
|
-
enableAnimations: true,
|
|
151
|
-
hoverEffects: true,
|
|
152
|
-
|
|
153
|
-
// Background styling (inherited from equation styling)
|
|
154
|
-
backgroundColor: null,
|
|
155
|
-
cornerRadius: null,
|
|
156
|
-
pill: null
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
return this._deepMerge(defaults, userStyling);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Deep merge two objects
|
|
164
|
-
* @param {Object} target - Target object
|
|
165
|
-
* @param {Object} source - Source object
|
|
166
|
-
* @returns {Object} Merged object
|
|
167
|
-
* @private
|
|
168
|
-
*/
|
|
169
|
-
_deepMerge(target, source) {
|
|
170
|
-
const result = { ...target };
|
|
171
|
-
|
|
172
|
-
for (const key in source) {
|
|
173
|
-
if (source.hasOwnProperty(key)) {
|
|
174
|
-
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
|
|
175
|
-
result[key] = this._deepMerge(result[key] || {}, source[key]);
|
|
176
|
-
} else {
|
|
177
|
-
result[key] = source[key];
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return result;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Updates the styling options and applies them to existing visual elements
|
|
187
|
-
* @param {Object} newStyling - New styling options to apply
|
|
188
|
-
*/
|
|
189
|
-
setStyling(newStyling) {
|
|
190
|
-
this.styling = this._mergeWithDefaults({ ...this.styling, ...newStyling });
|
|
191
|
-
|
|
192
|
-
// Update instance properties that are used elsewhere
|
|
193
|
-
this.dotRadius = this.styling.dotRadius;
|
|
194
|
-
this.lineWidth = this.styling.lineWidth;
|
|
195
|
-
this.visualSpacing = this.styling.visualSpacing;
|
|
196
|
-
|
|
197
|
-
this._applyStylingToExistingElements();
|
|
198
|
-
|
|
199
|
-
// Update layout spacing if changed
|
|
200
|
-
if (newStyling.visualSpacing !== undefined) {
|
|
201
|
-
this.visualSpacing = this.styling.visualSpacing;
|
|
202
|
-
}
|
|
203
|
-
if (newStyling.fixedVisualizerPosition !== undefined && this.layoutManager) {
|
|
204
|
-
this.layoutManager.setFixedVisualizerPosition(this.styling.fixedVisualizerPosition);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Refresh layout and visual elements
|
|
208
|
-
this.updateLayout();
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Gets the current styling options
|
|
213
|
-
* @returns {Object} Current styling configuration
|
|
214
|
-
*/
|
|
215
|
-
getStyling() {
|
|
216
|
-
return { ...this.styling };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Sets a specific styling property
|
|
221
|
-
* @param {string} property - The property to set (supports dot notation like 'textBoxOptions.backgroundColor')
|
|
222
|
-
* @param {any} value - The value to set
|
|
223
|
-
*/
|
|
224
|
-
setStyleProperty(property, value) {
|
|
225
|
-
const keys = property.split('.');
|
|
226
|
-
const lastKey = keys.pop();
|
|
227
|
-
const target = keys.reduce((obj, key) => {
|
|
228
|
-
if (!obj[key]) obj[key] = {};
|
|
229
|
-
return obj[key];
|
|
230
|
-
}, this.styling);
|
|
231
|
-
|
|
232
|
-
target[lastKey] = value;
|
|
233
|
-
|
|
234
|
-
// Update instance properties if they were changed
|
|
235
|
-
if (property === 'dotRadius') this.dotRadius = value;
|
|
236
|
-
if (property === 'lineWidth') this.lineWidth = value;
|
|
237
|
-
if (property === 'visualSpacing') this.visualSpacing = value;
|
|
238
|
-
if (property === 'fixedVisualizerPosition' && this.layoutManager) {
|
|
239
|
-
this.layoutManager.setFixedVisualizerPosition(value);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
this._applyStylingToExistingElements();
|
|
243
|
-
this.updateLayout();
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Gets a specific styling property
|
|
248
|
-
* @param {string} property - The property to get (supports dot notation)
|
|
249
|
-
* @returns {any} The property value
|
|
250
|
-
*/
|
|
251
|
-
getStyleProperty(property) {
|
|
252
|
-
return property.split('.').reduce((obj, key) => obj?.[key], this.styling);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Applies current styling to all existing visual elements
|
|
257
|
-
* @private
|
|
258
|
-
*/
|
|
259
|
-
_applyStylingToExistingElements() {
|
|
260
|
-
// Update dots
|
|
261
|
-
this.stepDots.forEach((dot, index) => {
|
|
262
|
-
if (dot && dot.equationRef) {
|
|
263
|
-
const isActive = this.activeDotIndex === index;
|
|
264
|
-
const color = isActive ? this.styling.activeDotColor : this.styling.dotColor;
|
|
265
|
-
dot.setFillColor(color);
|
|
266
|
-
dot.setStrokeColor(color);
|
|
267
|
-
dot.setStrokeWidth(this.styling.dotStrokeWidth);
|
|
268
|
-
|
|
269
|
-
// Update radius based on step mark
|
|
270
|
-
const stepMark = dot.equationRef.stepMark ?? 0;
|
|
271
|
-
const radius = this.styling.dotRadius || getDotRadius(stepMark);
|
|
272
|
-
dot.setWidthAndHeight(radius * 2, radius * 2);
|
|
273
|
-
dot.radius = radius;
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// Update lines
|
|
278
|
-
this.stepLines.forEach((line, index) => {
|
|
279
|
-
if (line) {
|
|
280
|
-
const isActive = this.activeDotIndex >= 0 &&
|
|
281
|
-
(line.toDotIndex === this.activeDotIndex || line.fromDotIndex === this.activeDotIndex);
|
|
282
|
-
const color = isActive ? this.styling.activeLineColor : this.styling.lineColor;
|
|
283
|
-
line.setStrokeColor(color);
|
|
284
|
-
line.setStrokeWidth(this.styling.lineWidth);
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// Update expansion dots
|
|
289
|
-
if (this.layoutManager && this.layoutManager.expansionDots) {
|
|
290
|
-
this.layoutManager.expansionDots.forEach(expansionDot => {
|
|
291
|
-
if (expansionDot) {
|
|
292
|
-
const baseRadius = this.styling.dotRadius || getDotRadius(0);
|
|
293
|
-
const radius = Math.max(3, baseRadius * this.styling.expansionDotScale);
|
|
294
|
-
expansionDot.setWidthAndHeight(radius * 2, radius * 2);
|
|
295
|
-
expansionDot.radius = radius;
|
|
296
|
-
expansionDot.setFillColor(this.styling.dotColor);
|
|
297
|
-
expansionDot.setStrokeColor(this.styling.dotColor);
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Update text box styling if manager exists
|
|
303
|
-
if (this.textBoxManager && typeof this.textBoxManager.updateStyling === 'function') {
|
|
304
|
-
this.textBoxManager.updateStyling(this.styling.textBoxOptions);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Sets the visual background style (inherits from equation styling)
|
|
310
|
-
* @param {Object} style - Background style options
|
|
311
|
-
*/
|
|
312
|
-
setBackgroundStyle(style = {}) {
|
|
313
|
-
this.styling.backgroundColor = style.backgroundColor || this.styling.backgroundColor;
|
|
314
|
-
this.styling.cornerRadius = style.cornerRadius || this.styling.cornerRadius;
|
|
315
|
-
this.styling.pill = style.pill !== undefined ? style.pill : this.styling.pill;
|
|
316
|
-
|
|
317
|
-
// Apply to equation background if this step visualizer has equation styling
|
|
318
|
-
if (typeof this.setDefaultEquationBackground === 'function') {
|
|
319
|
-
this.setDefaultEquationBackground(style);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Gets the current background style
|
|
325
|
-
* @returns {Object} Current background style
|
|
326
|
-
*/
|
|
327
|
-
getBackgroundStyle() {
|
|
328
|
-
return {
|
|
329
|
-
backgroundColor: this.styling.backgroundColor,
|
|
330
|
-
cornerRadius: this.styling.cornerRadius,
|
|
331
|
-
pill: this.styling.pill
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Sets the fixed position for the step visualizer
|
|
337
|
-
* @param {number} position - The x position from the left edge where the visualizer should be positioned
|
|
338
|
-
*/
|
|
339
|
-
setFixedVisualizerPosition(position) {
|
|
340
|
-
if (this.layoutManager) {
|
|
341
|
-
this.layoutManager.setFixedVisualizerPosition(position);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Force rebuild visual container (dots/lines) from scratch
|
|
347
|
-
*/
|
|
348
|
-
rebuildVisualizer() {
|
|
349
|
-
// Clear all step visualizer highlights before rebuilding
|
|
350
|
-
if (this.highlighting && typeof this.highlighting.clearAllExplainHighlights === 'function') {
|
|
351
|
-
this.highlighting.clearAllExplainHighlights();
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (this.visualContainer) {
|
|
355
|
-
this.removeChild(this.visualContainer);
|
|
356
|
-
}
|
|
357
|
-
this.visualContainer = new jsvgLayoutGroup();
|
|
358
|
-
this.addChild(this.visualContainer);
|
|
359
|
-
this._initializeVisualElements();
|
|
360
|
-
this.computeDimensions();
|
|
361
|
-
this.updateLayout();
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Initializes visual elements (dots and lines) for all existing steps
|
|
366
|
-
* @private
|
|
367
|
-
*/
|
|
368
|
-
_initializeVisualElements() {
|
|
369
|
-
this._clearVisualElements();
|
|
370
|
-
this.nodeToStepMap.clear();
|
|
371
|
-
|
|
372
|
-
const equations = this.steps.filter(step => step instanceof omdEquationNode);
|
|
373
|
-
|
|
374
|
-
equations.forEach((equation, index) => {
|
|
375
|
-
this._createStepDot(equation, index);
|
|
376
|
-
equation.findAllNodes().forEach(node => {
|
|
377
|
-
this.nodeToStepMap.set(node.id, index);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
if (index > 0) {
|
|
381
|
-
this._createStepLine(index - 1, index);
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
this.layoutManager.updateVisualZOrder();
|
|
386
|
-
this.layoutManager.updateVisualLayout(true);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Creates a visual dot for a step
|
|
391
|
-
* @private
|
|
392
|
-
*/
|
|
393
|
-
_createStepDot(equation, index) {
|
|
394
|
-
const stepMark = equation.stepMark ?? 0;
|
|
395
|
-
const radius = this.styling.dotRadius || getDotRadius(stepMark);
|
|
396
|
-
const dot = new jsvgEllipse();
|
|
397
|
-
dot.setWidthAndHeight(radius * 2, radius * 2);
|
|
398
|
-
const dotColor = this.styling.dotColor;
|
|
399
|
-
dot.setFillColor(dotColor);
|
|
400
|
-
dot.setStrokeColor(dotColor);
|
|
401
|
-
dot.setStrokeWidth(this.styling.dotStrokeWidth);
|
|
402
|
-
dot.radius = radius;
|
|
403
|
-
|
|
404
|
-
dot.equationRef = equation;
|
|
405
|
-
dot.stepIndex = index;
|
|
406
|
-
|
|
407
|
-
if (equation.visible === false) {
|
|
408
|
-
dot.hide();
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
this.layoutManager.updateDotClickability(dot);
|
|
412
|
-
|
|
413
|
-
this.stepDots.push(dot);
|
|
414
|
-
this.visualContainer.addChild(dot);
|
|
415
|
-
|
|
416
|
-
return dot;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Creates a connecting line between two step dots
|
|
421
|
-
* @private
|
|
422
|
-
*/
|
|
423
|
-
_createStepLine(fromIndex, toIndex) {
|
|
424
|
-
const line = new jsvgLine();
|
|
425
|
-
const lineColor = this.styling.lineColor;
|
|
426
|
-
line.setStrokeColor(lineColor);
|
|
427
|
-
line.setStrokeWidth(this.styling.lineWidth);
|
|
428
|
-
|
|
429
|
-
line.fromDotIndex = fromIndex;
|
|
430
|
-
line.toDotIndex = toIndex;
|
|
431
|
-
|
|
432
|
-
const fromEquation = this.stepDots[fromIndex]?.equationRef;
|
|
433
|
-
const toEquation = this.stepDots[toIndex]?.equationRef;
|
|
434
|
-
|
|
435
|
-
if (fromEquation?.visible === false || toEquation?.visible === false) {
|
|
436
|
-
line.hide();
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
this.stepLines.push(line);
|
|
440
|
-
this.visualContainer.addChild(line);
|
|
441
|
-
|
|
442
|
-
return line;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Clears all visual elements
|
|
447
|
-
* @private
|
|
448
|
-
*/
|
|
449
|
-
_clearVisualElements() {
|
|
450
|
-
this.stepDots.forEach(dot => this.visualContainer.removeChild(dot));
|
|
451
|
-
this.stepLines.forEach(line => this.visualContainer.removeChild(line));
|
|
452
|
-
this.textBoxManager.clearAllTextBoxes();
|
|
453
|
-
|
|
454
|
-
this.stepDots = [];
|
|
455
|
-
this.stepLines = [];
|
|
456
|
-
this.activeDotIndex = -1;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Override addStep to update visual elements when new steps are added
|
|
461
|
-
*/
|
|
462
|
-
addStep(step, options = {}) {
|
|
463
|
-
// Clear all step visualizer highlights when adding new steps (stack expansion)
|
|
464
|
-
if (this.highlighting && typeof this.highlighting.clearAllExplainHighlights === 'function') {
|
|
465
|
-
this.highlighting.clearAllExplainHighlights();
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Call parent first to add the step properly
|
|
469
|
-
super.addStep(step, options);
|
|
470
|
-
|
|
471
|
-
// Now create visual elements for equation nodes only
|
|
472
|
-
if (step instanceof omdEquationNode) {
|
|
473
|
-
// Find the actual index of this equation in the steps array
|
|
474
|
-
const equationIndex = this.steps.filter(s => s instanceof omdEquationNode).indexOf(step);
|
|
475
|
-
|
|
476
|
-
if (equationIndex >= 0) {
|
|
477
|
-
const createdDot = this._createStepDot(step, equationIndex);
|
|
478
|
-
|
|
479
|
-
// Update the node to step mapping
|
|
480
|
-
step.findAllNodes().forEach(node => {
|
|
481
|
-
this.nodeToStepMap.set(node.id, equationIndex);
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
// Create connecting line if this isn't the first equation
|
|
485
|
-
if (equationIndex > 0) {
|
|
486
|
-
this._createStepLine(equationIndex - 1, equationIndex);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// After stepMark is set, adjust dot radius
|
|
490
|
-
if (createdDot) {
|
|
491
|
-
const radius = getDotRadius(step.stepMark ?? 0);
|
|
492
|
-
createdDot.setWidthAndHeight(radius * 2, radius * 2);
|
|
493
|
-
createdDot.radius = radius;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Update layout after adding the step
|
|
499
|
-
this.computeDimensions();
|
|
500
|
-
this.updateLayout();
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Gets the step number for a given node ID.
|
|
505
|
-
*/
|
|
506
|
-
getNodeStepNumber(nodeId) {
|
|
507
|
-
return this.nodeToStepMap.get(nodeId);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Override computeDimensions to account for visual elements
|
|
512
|
-
*/
|
|
513
|
-
computeDimensions() {
|
|
514
|
-
super.computeDimensions();
|
|
515
|
-
// Store original dimensions before visualizer expansion
|
|
516
|
-
this.sequenceWidth = this.width;
|
|
517
|
-
this.sequenceHeight = this.height;
|
|
518
|
-
|
|
519
|
-
// Set width to include the fixed visualizer position plus visualizer width
|
|
520
|
-
if (this.stepDots && this.stepDots.length > 0 && this.layoutManager) {
|
|
521
|
-
const containerWidth = this.dotRadius * 3;
|
|
522
|
-
const fixedVisualizerPosition = this.layoutManager.fixedVisualizerPosition || 250;
|
|
523
|
-
const totalWidth = fixedVisualizerPosition + this.visualSpacing + containerWidth;
|
|
524
|
-
this.setWidthAndHeight(totalWidth, this.height);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* Override updateLayout to update visual elements as well
|
|
530
|
-
*/
|
|
531
|
-
updateLayout() {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
super.updateLayout();
|
|
535
|
-
|
|
536
|
-
// Only update visual layout if layoutManager is initialized
|
|
537
|
-
if (this.layoutManager) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
this.layoutManager.updateVisualLayout(true); // Allow repositioning for main layout updates
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
this.layoutManager.updateVisualVisibility();
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
this.layoutManager.updateAllLinePositions();
|
|
547
|
-
} else {
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/**
|
|
555
|
-
* Removes the most recent operation and refreshes visual dots/lines accordingly.
|
|
556
|
-
* @returns {boolean} Whether an operation was undone
|
|
557
|
-
*/
|
|
558
|
-
undoLastOperation() {
|
|
559
|
-
// Clear all step visualizer highlights before undoing
|
|
560
|
-
if (this.highlighting && typeof this.highlighting.clearAllExplainHighlights === 'function') {
|
|
561
|
-
this.highlighting.clearAllExplainHighlights();
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Remove bottom-most equation and its preceding operation display
|
|
565
|
-
const beforeCount = this.steps.length;
|
|
566
|
-
const removed = super.undoLastOperation ? super.undoLastOperation() : false;
|
|
567
|
-
if (removed || this.steps.length < beforeCount) {
|
|
568
|
-
// Hard rebuild the visual container to avoid stale dots/lines
|
|
569
|
-
this.rebuildVisualizer();
|
|
570
|
-
return true;
|
|
571
|
-
}
|
|
572
|
-
return false;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Sets the color of a specific dot and its associated lines
|
|
577
|
-
*/
|
|
578
|
-
setDotColor(dotIndex, color) {
|
|
579
|
-
if (this.stepDots && dotIndex >= 0 && dotIndex < this.stepDots.length) {
|
|
580
|
-
const dot = this.stepDots[dotIndex];
|
|
581
|
-
dot.setFillColor(color);
|
|
582
|
-
dot.setStrokeColor(color);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Sets the color of the line above a specific dot
|
|
588
|
-
*/
|
|
589
|
-
setLineAboveColor(dotIndex, color) {
|
|
590
|
-
let targetLine = this.stepLines.find(line =>
|
|
591
|
-
line.toDotIndex === dotIndex && line.isTemporary && line.svgObject && line.svgObject.style.display !== 'none'
|
|
592
|
-
);
|
|
593
|
-
|
|
594
|
-
if (!targetLine) {
|
|
595
|
-
targetLine = this.stepLines.find(line =>
|
|
596
|
-
line.toDotIndex === dotIndex && !line.isTemporary && line.svgObject && line.svgObject.style.display !== 'none'
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
if (targetLine) {
|
|
601
|
-
targetLine.setStrokeColor(color);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
/**
|
|
606
|
-
* Enables or disables dot clicking functionality
|
|
607
|
-
*/
|
|
608
|
-
setDotsClickable(enabled) {
|
|
609
|
-
this.dotsClickable = enabled;
|
|
610
|
-
|
|
611
|
-
// If disabling, clear any active highlights and dots
|
|
612
|
-
if (!enabled) {
|
|
613
|
-
this._clearActiveDot();
|
|
614
|
-
// Use the more thorough clearing to ensure no stale highlights remain
|
|
615
|
-
this.highlighting.clearAllExplainHighlights();
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
this.stepDots.forEach(dot => {
|
|
619
|
-
this.layoutManager.updateDotClickability(dot);
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Handles clicking on a step dot
|
|
625
|
-
* @private
|
|
626
|
-
*/
|
|
627
|
-
_handleDotClick(dot, dotIndex) {
|
|
628
|
-
if (!this.dotsClickable) return;
|
|
629
|
-
// Guard against stale dot references
|
|
630
|
-
if (dotIndex < 0 || dotIndex >= this.stepDots.length) return;
|
|
631
|
-
if (this.stepDots[dotIndex] !== dot) {
|
|
632
|
-
// try to resolve current index
|
|
633
|
-
const idx = this.stepDots.indexOf(dot);
|
|
634
|
-
if (idx === -1) return;
|
|
635
|
-
dotIndex = idx;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
try {
|
|
639
|
-
if (this.activeDotIndex === dotIndex) {
|
|
640
|
-
this._clearActiveDot();
|
|
641
|
-
} else {
|
|
642
|
-
if (this.activeDotIndex !== -1) {
|
|
643
|
-
this._clearActiveDot();
|
|
644
|
-
}
|
|
645
|
-
this.setActiveDot(dotIndex);
|
|
646
|
-
const equation = this.stepDots[dotIndex].equationRef;
|
|
647
|
-
const equationIndex = this.steps.indexOf(equation);
|
|
648
|
-
const isOperation = this._checkForOperationStep(equationIndex);
|
|
649
|
-
this.highlighting.highlightAffectedNodes(dotIndex, isOperation);
|
|
650
|
-
}
|
|
651
|
-
} catch (error) {
|
|
652
|
-
console.error('Error handling dot click:', error);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* Sets a dot to be the visually active one.
|
|
658
|
-
* @private
|
|
659
|
-
*/
|
|
660
|
-
setActiveDot(dotIndex) {
|
|
661
|
-
if (!this.stepDots || dotIndex < 0 || dotIndex >= this.stepDots.length) return;
|
|
662
|
-
|
|
663
|
-
this.activeDotIndex = dotIndex;
|
|
664
|
-
this.activeDot = this.stepDots[dotIndex];
|
|
665
|
-
|
|
666
|
-
const dot = this.stepDots[dotIndex];
|
|
667
|
-
const explainColor = this.styling.activeDotColor;
|
|
668
|
-
dot.setFillColor(explainColor);
|
|
669
|
-
dot.setStrokeColor(explainColor);
|
|
670
|
-
|
|
671
|
-
this.setLineAboveColor(dotIndex, this.styling.activeLineColor);
|
|
672
|
-
this.textBoxManager.createTextBoxForDot(dotIndex);
|
|
673
|
-
|
|
674
|
-
// Temporarily disable equation repositioning for simple dot state changes
|
|
675
|
-
const originalRepositioning = this.layoutManager.allowEquationRepositioning;
|
|
676
|
-
this.layoutManager.allowEquationRepositioning = false;
|
|
677
|
-
this.layoutManager.updateVisualZOrder();
|
|
678
|
-
this.layoutManager.allowEquationRepositioning = originalRepositioning;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
/**
|
|
682
|
-
* Clears the currently active dot
|
|
683
|
-
* @private
|
|
684
|
-
*/
|
|
685
|
-
/**
|
|
686
|
-
* Clears the currently active dot
|
|
687
|
-
* @private
|
|
688
|
-
*/
|
|
689
|
-
_clearActiveDot() {
|
|
690
|
-
try {
|
|
691
|
-
if (this.activeDotIndex !== -1) {
|
|
692
|
-
const dot = this.stepDots[this.activeDotIndex];
|
|
693
|
-
const dotColor = this.styling.dotColor;
|
|
694
|
-
dot.setFillColor(dotColor);
|
|
695
|
-
dot.setStrokeColor(dotColor);
|
|
696
|
-
|
|
697
|
-
this.setLineAboveColor(this.activeDotIndex, this.styling.lineColor);
|
|
698
|
-
this.textBoxManager.removeTextBoxForDot(this.activeDotIndex);
|
|
699
|
-
|
|
700
|
-
// Use thorough clearing to ensure no stale highlights remain
|
|
701
|
-
this.highlighting.clearAllExplainHighlights();
|
|
702
|
-
|
|
703
|
-
// Temporarily disable equation repositioning for simple dot state changes
|
|
704
|
-
const originalRepositioning = this.layoutManager.allowEquationRepositioning;
|
|
705
|
-
this.layoutManager.allowEquationRepositioning = false;
|
|
706
|
-
this.layoutManager.updateVisualZOrder();
|
|
707
|
-
this.layoutManager.allowEquationRepositioning = originalRepositioning;
|
|
708
|
-
|
|
709
|
-
this.activeDot = null;
|
|
710
|
-
this.activeDotIndex = -1;
|
|
711
|
-
}
|
|
712
|
-
} catch (error) {
|
|
713
|
-
console.error('Error clearing active dot:', error);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* Gets simplification data for a specific dot
|
|
719
|
-
* @private
|
|
720
|
-
*/
|
|
721
|
-
_getSimplificationDataForDot(dotIndex) {
|
|
722
|
-
try {
|
|
723
|
-
const dot = this.stepDots[dotIndex];
|
|
724
|
-
if (!dot || !dot.equationRef) {
|
|
725
|
-
return this._createDefaultSimplificationData("No equation found for this step");
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const equationIndex = this.steps.indexOf(dot.equationRef);
|
|
729
|
-
if (equationIndex === -1) {
|
|
730
|
-
return this._createDefaultSimplificationData("Step not found in sequence");
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// Find the previous visible equation
|
|
734
|
-
const previousVisibleIndex = this._findPreviousVisibleEquationIndex(equationIndex);
|
|
735
|
-
|
|
736
|
-
// Get all steps between previous visible equation and current
|
|
737
|
-
const allSteps = [];
|
|
738
|
-
|
|
739
|
-
// Get simplifications
|
|
740
|
-
const simplificationHistory = this.getSimplificationHistory();
|
|
741
|
-
const relevantSimplifications = this._getRelevantSimplifications(
|
|
742
|
-
simplificationHistory,
|
|
743
|
-
previousVisibleIndex,
|
|
744
|
-
equationIndex
|
|
745
|
-
);
|
|
746
|
-
allSteps.push(...relevantSimplifications);
|
|
747
|
-
|
|
748
|
-
// Get operations
|
|
749
|
-
for (let i = previousVisibleIndex + 1; i <= equationIndex; i++) {
|
|
750
|
-
const operationData = this._checkForOperationStep(i);
|
|
751
|
-
if (operationData) {
|
|
752
|
-
allSteps.push({
|
|
753
|
-
message: operationData.message,
|
|
754
|
-
affectedNodes: operationData.affectedNodes,
|
|
755
|
-
stepNumber: i - 1
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// Sort steps by step number
|
|
761
|
-
allSteps.sort((a, b) => a.stepNumber - b.stepNumber);
|
|
762
|
-
|
|
763
|
-
if (allSteps.length > 0) {
|
|
764
|
-
return this._createMultipleSimplificationsData(allSteps);
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// Check for single simplification
|
|
768
|
-
const singleSimplificationData = this._checkForSingleSimplification(
|
|
769
|
-
simplificationHistory,
|
|
770
|
-
equationIndex
|
|
771
|
-
);
|
|
772
|
-
if (singleSimplificationData) {
|
|
773
|
-
return singleSimplificationData;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// Fallback cases
|
|
777
|
-
return this._getFallbackSimplificationData(equationIndex);
|
|
778
|
-
|
|
779
|
-
} catch (error) {
|
|
780
|
-
console.error('Error getting simplification data for dot:', error);
|
|
781
|
-
return this._createDefaultSimplificationData("Error retrieving step data");
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
/**
|
|
786
|
-
* Gets the step text boxes
|
|
787
|
-
*/
|
|
788
|
-
getStepTextBoxes() {
|
|
789
|
-
return this.textBoxManager.getStepTextBoxes();
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// ===== SIMPLIFICATION DATA METHODS =====
|
|
793
|
-
|
|
794
|
-
/**
|
|
795
|
-
* Creates default simplification data
|
|
796
|
-
* @param {string} message - The message to display
|
|
797
|
-
* @returns {Object} Default data object
|
|
798
|
-
* @private
|
|
799
|
-
*/
|
|
800
|
-
_createDefaultSimplificationData(message) {
|
|
801
|
-
return {
|
|
802
|
-
message: message,
|
|
803
|
-
rawMessages: [message],
|
|
804
|
-
ruleNames: ['Step Description'],
|
|
805
|
-
affectedNodes: [],
|
|
806
|
-
resultNodeIds: [],
|
|
807
|
-
resultProvSources: [],
|
|
808
|
-
multipleSimplifications: false
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
/**
|
|
813
|
-
* Finds the index of the previous visible equation
|
|
814
|
-
* @param {number} currentIndex - Current equation index
|
|
815
|
-
* @returns {number} Index of previous visible equation, or -1 if none found
|
|
816
|
-
* @private
|
|
817
|
-
*/
|
|
818
|
-
_findPreviousVisibleEquationIndex(currentIndex) {
|
|
819
|
-
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
820
|
-
if (this.steps[i] instanceof omdEquationNode && this.steps[i].visible !== false) {
|
|
821
|
-
return i;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
return -1;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
/**
|
|
828
|
-
* Gets relevant simplifications between two step indices
|
|
829
|
-
* @param {Array} simplificationHistory - Full simplification history
|
|
830
|
-
* @param {number} startIndex - Starting step index
|
|
831
|
-
* @param {number} endIndex - Ending step index
|
|
832
|
-
* @returns {Array} Array of relevant simplification entries
|
|
833
|
-
* @private
|
|
834
|
-
*/
|
|
835
|
-
_getRelevantSimplifications(simplificationHistory, startIndex, endIndex) {
|
|
836
|
-
const relevantSimplifications = [];
|
|
837
|
-
|
|
838
|
-
for (let stepNum = startIndex; stepNum < endIndex; stepNum++) {
|
|
839
|
-
const entries = simplificationHistory.filter(entry => entry.stepNumber === stepNum);
|
|
840
|
-
if (entries.length > 0) {
|
|
841
|
-
relevantSimplifications.push(...entries);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
return relevantSimplifications;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
/**
|
|
849
|
-
* Creates data object for multiple simplifications
|
|
850
|
-
* @param {Array} simplifications - Array of simplification entries
|
|
851
|
-
* @returns {Object} Data object for multiple simplifications
|
|
852
|
-
* @private
|
|
853
|
-
*/
|
|
854
|
-
_createMultipleSimplificationsData(simplifications) {
|
|
855
|
-
const messages = simplifications.map(s => s.message);
|
|
856
|
-
const ruleNames = simplifications.map(s => s.name || 'Operation').filter(Boolean);
|
|
857
|
-
|
|
858
|
-
const allAffectedNodes = [];
|
|
859
|
-
const allResultNodeIds = [];
|
|
860
|
-
const allResultProvSources = [];
|
|
861
|
-
|
|
862
|
-
simplifications.forEach(entry => {
|
|
863
|
-
if (entry.affectedNodes) {
|
|
864
|
-
allAffectedNodes.push(...entry.affectedNodes);
|
|
865
|
-
}
|
|
866
|
-
if (entry.resultNodeId) {
|
|
867
|
-
allResultNodeIds.push(entry.resultNodeId);
|
|
868
|
-
}
|
|
869
|
-
if (entry.resultProvSources) {
|
|
870
|
-
allResultProvSources.push(...entry.resultProvSources);
|
|
871
|
-
}
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
return {
|
|
875
|
-
message: messages.join('. '),
|
|
876
|
-
rawMessages: messages,
|
|
877
|
-
ruleNames: ruleNames,
|
|
878
|
-
affectedNodes: allAffectedNodes,
|
|
879
|
-
resultNodeIds: allResultNodeIds,
|
|
880
|
-
resultProvSources: allResultProvSources,
|
|
881
|
-
multipleSimplifications: true
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
/**
|
|
886
|
-
* Checks for operation step data
|
|
887
|
-
* @param {number} equationIndex - Current equation index
|
|
888
|
-
* @returns {Object|null} Operation data object or null
|
|
889
|
-
* @private
|
|
890
|
-
*/
|
|
891
|
-
_checkForOperationStep(equationIndex) {
|
|
892
|
-
if (equationIndex > 0) {
|
|
893
|
-
const step = this.steps[equationIndex - 1];
|
|
894
|
-
if (step instanceof omdOperationDisplayNode) {
|
|
895
|
-
return {
|
|
896
|
-
message: `Applied ${step.operation} ${step.value} to both sides`,
|
|
897
|
-
affectedNodes: [step.operatorLeft, step.valueLeft, step.operatorRight, step.valueRight]
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
return null;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
/**
|
|
905
|
-
* Checks for single simplification data
|
|
906
|
-
* @param {Array} simplificationHistory - Full simplification history
|
|
907
|
-
* @param {number} equationIndex - Current equation index
|
|
908
|
-
* @returns {Object|null} Single simplification data or null
|
|
909
|
-
* @private
|
|
910
|
-
*/
|
|
911
|
-
_checkForSingleSimplification(simplificationHistory, equationIndex) {
|
|
912
|
-
const relevantSimplification = simplificationHistory.find(entry =>
|
|
913
|
-
entry.stepNumber === equationIndex - 1
|
|
914
|
-
);
|
|
915
|
-
|
|
916
|
-
if (relevantSimplification) {
|
|
917
|
-
return {
|
|
918
|
-
message: relevantSimplification.message,
|
|
919
|
-
rawMessages: [relevantSimplification.message],
|
|
920
|
-
ruleNames: [relevantSimplification.name || 'Operation'],
|
|
921
|
-
affectedNodes: relevantSimplification.affectedNodes || [],
|
|
922
|
-
resultNodeIds: relevantSimplification.resultNodeId ? [relevantSimplification.resultNodeId] : [],
|
|
923
|
-
resultProvSources: relevantSimplification.resultProvSources || [],
|
|
924
|
-
multipleSimplifications: false
|
|
925
|
-
};
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
return null;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
/**
|
|
932
|
-
* Gets fallback data for special cases
|
|
933
|
-
* @param {number} equationIndex - Current equation index
|
|
934
|
-
* @returns {Object} Fallback data object
|
|
935
|
-
* @private
|
|
936
|
-
*/
|
|
937
|
-
_getFallbackSimplificationData(equationIndex) {
|
|
938
|
-
const currentStep = this.steps[equationIndex];
|
|
939
|
-
if (equationIndex === 0 && currentStep.stepMark === 0) {
|
|
940
|
-
const equationStr = currentStep.toString();
|
|
941
|
-
return this._createDefaultSimplificationData(`Starting with equation: ${equationStr}`);
|
|
942
|
-
} else if (currentStep && currentStep.description) {
|
|
943
|
-
return this._createDefaultSimplificationData(currentStep.description);
|
|
944
|
-
} else {
|
|
945
|
-
return this._createDefaultSimplificationData("Step explanation not available");
|
|
946
|
-
}
|
|
947
|
-
}
|
|
1
|
+
import { omdEquationSequenceNode } from "../nodes/omdEquationSequenceNode.js";
|
|
2
|
+
import { omdEquationNode } from "../nodes/omdEquationNode.js";
|
|
3
|
+
import { omdOperationDisplayNode } from "../nodes/omdOperationDisplayNode.js";
|
|
4
|
+
import { omdColor } from "../../src/omdColor.js";
|
|
5
|
+
import { omdStepVisualizerHighlighting } from "./omdStepVisualizerHighlighting.js";
|
|
6
|
+
import { omdStepVisualizerTextBoxes } from "./omdStepVisualizerTextBoxes.js";
|
|
7
|
+
import { omdStepVisualizerLayout } from "./omdStepVisualizerLayout.js";
|
|
8
|
+
import { getDotRadius } from "../config/omdConfigManager.js";
|
|
9
|
+
import { jsvgLayoutGroup, jsvgEllipse, jsvgLine } from '@teachinglab/jsvg';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A visual step tracker that extends omdEquationSequenceNode to show step progression
|
|
14
|
+
* with dots and connecting lines to the right of the sequence.
|
|
15
|
+
* @extends omdEquationSequenceNode
|
|
16
|
+
*/
|
|
17
|
+
export class omdStepVisualizer extends omdEquationSequenceNode {
|
|
18
|
+
constructor(steps, styling = {}) {
|
|
19
|
+
super(steps);
|
|
20
|
+
|
|
21
|
+
// Store styling options with defaults
|
|
22
|
+
this.styling = this._mergeWithDefaults(styling || {});
|
|
23
|
+
|
|
24
|
+
// Visual elements for step tracking
|
|
25
|
+
this.stepDots = [];
|
|
26
|
+
this.stepLines = [];
|
|
27
|
+
this.visualContainer = new jsvgLayoutGroup();
|
|
28
|
+
|
|
29
|
+
// Use styling values for these properties
|
|
30
|
+
this.dotRadius = this.styling.dotRadius;
|
|
31
|
+
this.lineWidth = this.styling.lineWidth;
|
|
32
|
+
this.visualSpacing = this.styling.visualSpacing;
|
|
33
|
+
|
|
34
|
+
this.activeDotIndex = -1;
|
|
35
|
+
this.dotsClickable = true;
|
|
36
|
+
this.nodeToStepMap = new Map();
|
|
37
|
+
|
|
38
|
+
// Highlighting system
|
|
39
|
+
this.stepVisualizerHighlights = new Set();
|
|
40
|
+
this.highlighting = new omdStepVisualizerHighlighting(this);
|
|
41
|
+
|
|
42
|
+
// Pass textbox options through styling parameter
|
|
43
|
+
const textBoxOptions = this.styling.textBoxOptions || {};
|
|
44
|
+
this.textBoxManager = new omdStepVisualizerTextBoxes(this, this.highlighting, textBoxOptions);
|
|
45
|
+
this.layoutManager = new omdStepVisualizerLayout(this);
|
|
46
|
+
|
|
47
|
+
this.addChild(this.visualContainer);
|
|
48
|
+
this._initializeVisualElements();
|
|
49
|
+
|
|
50
|
+
// Set default filter level to show only major steps (stepMark = 0)
|
|
51
|
+
// This ensures intermediate steps are hidden and expansion dots can be created
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if (this.setFilterLevel && typeof this.setFilterLevel === 'function') {
|
|
56
|
+
this.setFilterLevel(0);
|
|
57
|
+
|
|
58
|
+
} else {
|
|
59
|
+
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.computeDimensions();
|
|
63
|
+
this.updateLayout();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Public: programmatically toggle a dot (simulate user click behavior)
|
|
68
|
+
* @param {number} dotIndex
|
|
69
|
+
*/
|
|
70
|
+
toggleDot(dotIndex) {
|
|
71
|
+
if (typeof dotIndex !== 'number') return;
|
|
72
|
+
if (dotIndex < 0 || dotIndex >= this.stepDots.length) return;
|
|
73
|
+
const dot = this.stepDots[dotIndex];
|
|
74
|
+
this._handleDotClick(dot, dotIndex);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Public: close currently active dot textbox if any
|
|
79
|
+
*/
|
|
80
|
+
closeActiveDot() {
|
|
81
|
+
// Always clear all boxes to be safe (even if activeDotIndex already reset)
|
|
82
|
+
try {
|
|
83
|
+
const before = this.textBoxManager?.stepTextBoxes?.length || 0;
|
|
84
|
+
this._clearActiveDot();
|
|
85
|
+
if (this.textBoxManager && typeof this.textBoxManager.clearAllTextBoxes === 'function') {
|
|
86
|
+
this.textBoxManager.clearAllTextBoxes();
|
|
87
|
+
}
|
|
88
|
+
const after = this.textBoxManager?.stepTextBoxes?.length || 0;
|
|
89
|
+
} catch (e) { /* no-op */ }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Public: close all text boxes (future-proof; currently only one can be active)
|
|
94
|
+
*/
|
|
95
|
+
closeAllTextBoxes() {
|
|
96
|
+
this.closeActiveDot();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Public: force close EVERYTHING related to active explanation UI
|
|
101
|
+
*/
|
|
102
|
+
forceCloseAll() {
|
|
103
|
+
this.closeActiveDot();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Merges user styling with default styling options
|
|
108
|
+
* @param {Object} userStyling - User-provided styling options
|
|
109
|
+
* @returns {Object} Merged styling object with defaults
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
112
|
+
_mergeWithDefaults(userStyling) {
|
|
113
|
+
const defaults = {
|
|
114
|
+
// Dot styling
|
|
115
|
+
dotColor: omdColor.stepColor,
|
|
116
|
+
dotRadius: getDotRadius(0),
|
|
117
|
+
dotStrokeWidth: 2,
|
|
118
|
+
activeDotColor: omdColor.explainColor,
|
|
119
|
+
expansionDotScale: 0.4,
|
|
120
|
+
|
|
121
|
+
// Line styling
|
|
122
|
+
lineColor: omdColor.stepColor,
|
|
123
|
+
lineWidth: 2,
|
|
124
|
+
activeLineColor: omdColor.explainColor,
|
|
125
|
+
|
|
126
|
+
// Colors
|
|
127
|
+
explainColor: omdColor.explainColor,
|
|
128
|
+
highlightColor: omdColor.hiliteColor,
|
|
129
|
+
|
|
130
|
+
// Layout
|
|
131
|
+
visualSpacing: 30,
|
|
132
|
+
fixedVisualizerPosition: 250,
|
|
133
|
+
dotVerticalOffset: 15,
|
|
134
|
+
|
|
135
|
+
// Text boxes
|
|
136
|
+
textBoxOptions: {
|
|
137
|
+
backgroundColor: omdColor.white,
|
|
138
|
+
borderColor: 'none',
|
|
139
|
+
borderWidth: 1,
|
|
140
|
+
borderRadius: 5,
|
|
141
|
+
padding: 8, // Minimal padding for tight fit
|
|
142
|
+
fontSize: 14,
|
|
143
|
+
fontFamily: 'Albert Sans, Arial, sans-serif',
|
|
144
|
+
maxWidth: 300, // More reasonable width for compact layout
|
|
145
|
+
dropShadow: true
|
|
146
|
+
// Removed zIndex and position from defaults - these should only apply to container
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// Visual effects
|
|
150
|
+
enableAnimations: true,
|
|
151
|
+
hoverEffects: true,
|
|
152
|
+
|
|
153
|
+
// Background styling (inherited from equation styling)
|
|
154
|
+
backgroundColor: null,
|
|
155
|
+
cornerRadius: null,
|
|
156
|
+
pill: null
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return this._deepMerge(defaults, userStyling);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Deep merge two objects
|
|
164
|
+
* @param {Object} target - Target object
|
|
165
|
+
* @param {Object} source - Source object
|
|
166
|
+
* @returns {Object} Merged object
|
|
167
|
+
* @private
|
|
168
|
+
*/
|
|
169
|
+
_deepMerge(target, source) {
|
|
170
|
+
const result = { ...target };
|
|
171
|
+
|
|
172
|
+
for (const key in source) {
|
|
173
|
+
if (source.hasOwnProperty(key)) {
|
|
174
|
+
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
|
|
175
|
+
result[key] = this._deepMerge(result[key] || {}, source[key]);
|
|
176
|
+
} else {
|
|
177
|
+
result[key] = source[key];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Updates the styling options and applies them to existing visual elements
|
|
187
|
+
* @param {Object} newStyling - New styling options to apply
|
|
188
|
+
*/
|
|
189
|
+
setStyling(newStyling) {
|
|
190
|
+
this.styling = this._mergeWithDefaults({ ...this.styling, ...newStyling });
|
|
191
|
+
|
|
192
|
+
// Update instance properties that are used elsewhere
|
|
193
|
+
this.dotRadius = this.styling.dotRadius;
|
|
194
|
+
this.lineWidth = this.styling.lineWidth;
|
|
195
|
+
this.visualSpacing = this.styling.visualSpacing;
|
|
196
|
+
|
|
197
|
+
this._applyStylingToExistingElements();
|
|
198
|
+
|
|
199
|
+
// Update layout spacing if changed
|
|
200
|
+
if (newStyling.visualSpacing !== undefined) {
|
|
201
|
+
this.visualSpacing = this.styling.visualSpacing;
|
|
202
|
+
}
|
|
203
|
+
if (newStyling.fixedVisualizerPosition !== undefined && this.layoutManager) {
|
|
204
|
+
this.layoutManager.setFixedVisualizerPosition(this.styling.fixedVisualizerPosition);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Refresh layout and visual elements
|
|
208
|
+
this.updateLayout();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Gets the current styling options
|
|
213
|
+
* @returns {Object} Current styling configuration
|
|
214
|
+
*/
|
|
215
|
+
getStyling() {
|
|
216
|
+
return { ...this.styling };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Sets a specific styling property
|
|
221
|
+
* @param {string} property - The property to set (supports dot notation like 'textBoxOptions.backgroundColor')
|
|
222
|
+
* @param {any} value - The value to set
|
|
223
|
+
*/
|
|
224
|
+
setStyleProperty(property, value) {
|
|
225
|
+
const keys = property.split('.');
|
|
226
|
+
const lastKey = keys.pop();
|
|
227
|
+
const target = keys.reduce((obj, key) => {
|
|
228
|
+
if (!obj[key]) obj[key] = {};
|
|
229
|
+
return obj[key];
|
|
230
|
+
}, this.styling);
|
|
231
|
+
|
|
232
|
+
target[lastKey] = value;
|
|
233
|
+
|
|
234
|
+
// Update instance properties if they were changed
|
|
235
|
+
if (property === 'dotRadius') this.dotRadius = value;
|
|
236
|
+
if (property === 'lineWidth') this.lineWidth = value;
|
|
237
|
+
if (property === 'visualSpacing') this.visualSpacing = value;
|
|
238
|
+
if (property === 'fixedVisualizerPosition' && this.layoutManager) {
|
|
239
|
+
this.layoutManager.setFixedVisualizerPosition(value);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this._applyStylingToExistingElements();
|
|
243
|
+
this.updateLayout();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Gets a specific styling property
|
|
248
|
+
* @param {string} property - The property to get (supports dot notation)
|
|
249
|
+
* @returns {any} The property value
|
|
250
|
+
*/
|
|
251
|
+
getStyleProperty(property) {
|
|
252
|
+
return property.split('.').reduce((obj, key) => obj?.[key], this.styling);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Applies current styling to all existing visual elements
|
|
257
|
+
* @private
|
|
258
|
+
*/
|
|
259
|
+
_applyStylingToExistingElements() {
|
|
260
|
+
// Update dots
|
|
261
|
+
this.stepDots.forEach((dot, index) => {
|
|
262
|
+
if (dot && dot.equationRef) {
|
|
263
|
+
const isActive = this.activeDotIndex === index;
|
|
264
|
+
const color = isActive ? this.styling.activeDotColor : this.styling.dotColor;
|
|
265
|
+
dot.setFillColor(color);
|
|
266
|
+
dot.setStrokeColor(color);
|
|
267
|
+
dot.setStrokeWidth(this.styling.dotStrokeWidth);
|
|
268
|
+
|
|
269
|
+
// Update radius based on step mark
|
|
270
|
+
const stepMark = dot.equationRef.stepMark ?? 0;
|
|
271
|
+
const radius = this.styling.dotRadius || getDotRadius(stepMark);
|
|
272
|
+
dot.setWidthAndHeight(radius * 2, radius * 2);
|
|
273
|
+
dot.radius = radius;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Update lines
|
|
278
|
+
this.stepLines.forEach((line, index) => {
|
|
279
|
+
if (line) {
|
|
280
|
+
const isActive = this.activeDotIndex >= 0 &&
|
|
281
|
+
(line.toDotIndex === this.activeDotIndex || line.fromDotIndex === this.activeDotIndex);
|
|
282
|
+
const color = isActive ? this.styling.activeLineColor : this.styling.lineColor;
|
|
283
|
+
line.setStrokeColor(color);
|
|
284
|
+
line.setStrokeWidth(this.styling.lineWidth);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Update expansion dots
|
|
289
|
+
if (this.layoutManager && this.layoutManager.expansionDots) {
|
|
290
|
+
this.layoutManager.expansionDots.forEach(expansionDot => {
|
|
291
|
+
if (expansionDot) {
|
|
292
|
+
const baseRadius = this.styling.dotRadius || getDotRadius(0);
|
|
293
|
+
const radius = Math.max(3, baseRadius * this.styling.expansionDotScale);
|
|
294
|
+
expansionDot.setWidthAndHeight(radius * 2, radius * 2);
|
|
295
|
+
expansionDot.radius = radius;
|
|
296
|
+
expansionDot.setFillColor(this.styling.dotColor);
|
|
297
|
+
expansionDot.setStrokeColor(this.styling.dotColor);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Update text box styling if manager exists
|
|
303
|
+
if (this.textBoxManager && typeof this.textBoxManager.updateStyling === 'function') {
|
|
304
|
+
this.textBoxManager.updateStyling(this.styling.textBoxOptions);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Sets the visual background style (inherits from equation styling)
|
|
310
|
+
* @param {Object} style - Background style options
|
|
311
|
+
*/
|
|
312
|
+
setBackgroundStyle(style = {}) {
|
|
313
|
+
this.styling.backgroundColor = style.backgroundColor || this.styling.backgroundColor;
|
|
314
|
+
this.styling.cornerRadius = style.cornerRadius || this.styling.cornerRadius;
|
|
315
|
+
this.styling.pill = style.pill !== undefined ? style.pill : this.styling.pill;
|
|
316
|
+
|
|
317
|
+
// Apply to equation background if this step visualizer has equation styling
|
|
318
|
+
if (typeof this.setDefaultEquationBackground === 'function') {
|
|
319
|
+
this.setDefaultEquationBackground(style);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Gets the current background style
|
|
325
|
+
* @returns {Object} Current background style
|
|
326
|
+
*/
|
|
327
|
+
getBackgroundStyle() {
|
|
328
|
+
return {
|
|
329
|
+
backgroundColor: this.styling.backgroundColor,
|
|
330
|
+
cornerRadius: this.styling.cornerRadius,
|
|
331
|
+
pill: this.styling.pill
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Sets the fixed position for the step visualizer
|
|
337
|
+
* @param {number} position - The x position from the left edge where the visualizer should be positioned
|
|
338
|
+
*/
|
|
339
|
+
setFixedVisualizerPosition(position) {
|
|
340
|
+
if (this.layoutManager) {
|
|
341
|
+
this.layoutManager.setFixedVisualizerPosition(position);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Force rebuild visual container (dots/lines) from scratch
|
|
347
|
+
*/
|
|
348
|
+
rebuildVisualizer() {
|
|
349
|
+
// Clear all step visualizer highlights before rebuilding
|
|
350
|
+
if (this.highlighting && typeof this.highlighting.clearAllExplainHighlights === 'function') {
|
|
351
|
+
this.highlighting.clearAllExplainHighlights();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (this.visualContainer) {
|
|
355
|
+
this.removeChild(this.visualContainer);
|
|
356
|
+
}
|
|
357
|
+
this.visualContainer = new jsvgLayoutGroup();
|
|
358
|
+
this.addChild(this.visualContainer);
|
|
359
|
+
this._initializeVisualElements();
|
|
360
|
+
this.computeDimensions();
|
|
361
|
+
this.updateLayout();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Initializes visual elements (dots and lines) for all existing steps
|
|
366
|
+
* @private
|
|
367
|
+
*/
|
|
368
|
+
_initializeVisualElements() {
|
|
369
|
+
this._clearVisualElements();
|
|
370
|
+
this.nodeToStepMap.clear();
|
|
371
|
+
|
|
372
|
+
const equations = this.steps.filter(step => step instanceof omdEquationNode);
|
|
373
|
+
|
|
374
|
+
equations.forEach((equation, index) => {
|
|
375
|
+
this._createStepDot(equation, index);
|
|
376
|
+
equation.findAllNodes().forEach(node => {
|
|
377
|
+
this.nodeToStepMap.set(node.id, index);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
if (index > 0) {
|
|
381
|
+
this._createStepLine(index - 1, index);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
this.layoutManager.updateVisualZOrder();
|
|
386
|
+
this.layoutManager.updateVisualLayout(true);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Creates a visual dot for a step
|
|
391
|
+
* @private
|
|
392
|
+
*/
|
|
393
|
+
_createStepDot(equation, index) {
|
|
394
|
+
const stepMark = equation.stepMark ?? 0;
|
|
395
|
+
const radius = this.styling.dotRadius || getDotRadius(stepMark);
|
|
396
|
+
const dot = new jsvgEllipse();
|
|
397
|
+
dot.setWidthAndHeight(radius * 2, radius * 2);
|
|
398
|
+
const dotColor = this.styling.dotColor;
|
|
399
|
+
dot.setFillColor(dotColor);
|
|
400
|
+
dot.setStrokeColor(dotColor);
|
|
401
|
+
dot.setStrokeWidth(this.styling.dotStrokeWidth);
|
|
402
|
+
dot.radius = radius;
|
|
403
|
+
|
|
404
|
+
dot.equationRef = equation;
|
|
405
|
+
dot.stepIndex = index;
|
|
406
|
+
|
|
407
|
+
if (equation.visible === false) {
|
|
408
|
+
dot.hide();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
this.layoutManager.updateDotClickability(dot);
|
|
412
|
+
|
|
413
|
+
this.stepDots.push(dot);
|
|
414
|
+
this.visualContainer.addChild(dot);
|
|
415
|
+
|
|
416
|
+
return dot;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Creates a connecting line between two step dots
|
|
421
|
+
* @private
|
|
422
|
+
*/
|
|
423
|
+
_createStepLine(fromIndex, toIndex) {
|
|
424
|
+
const line = new jsvgLine();
|
|
425
|
+
const lineColor = this.styling.lineColor;
|
|
426
|
+
line.setStrokeColor(lineColor);
|
|
427
|
+
line.setStrokeWidth(this.styling.lineWidth);
|
|
428
|
+
|
|
429
|
+
line.fromDotIndex = fromIndex;
|
|
430
|
+
line.toDotIndex = toIndex;
|
|
431
|
+
|
|
432
|
+
const fromEquation = this.stepDots[fromIndex]?.equationRef;
|
|
433
|
+
const toEquation = this.stepDots[toIndex]?.equationRef;
|
|
434
|
+
|
|
435
|
+
if (fromEquation?.visible === false || toEquation?.visible === false) {
|
|
436
|
+
line.hide();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
this.stepLines.push(line);
|
|
440
|
+
this.visualContainer.addChild(line);
|
|
441
|
+
|
|
442
|
+
return line;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Clears all visual elements
|
|
447
|
+
* @private
|
|
448
|
+
*/
|
|
449
|
+
_clearVisualElements() {
|
|
450
|
+
this.stepDots.forEach(dot => this.visualContainer.removeChild(dot));
|
|
451
|
+
this.stepLines.forEach(line => this.visualContainer.removeChild(line));
|
|
452
|
+
this.textBoxManager.clearAllTextBoxes();
|
|
453
|
+
|
|
454
|
+
this.stepDots = [];
|
|
455
|
+
this.stepLines = [];
|
|
456
|
+
this.activeDotIndex = -1;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Override addStep to update visual elements when new steps are added
|
|
461
|
+
*/
|
|
462
|
+
addStep(step, options = {}) {
|
|
463
|
+
// Clear all step visualizer highlights when adding new steps (stack expansion)
|
|
464
|
+
if (this.highlighting && typeof this.highlighting.clearAllExplainHighlights === 'function') {
|
|
465
|
+
this.highlighting.clearAllExplainHighlights();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Call parent first to add the step properly
|
|
469
|
+
super.addStep(step, options);
|
|
470
|
+
|
|
471
|
+
// Now create visual elements for equation nodes only
|
|
472
|
+
if (step instanceof omdEquationNode) {
|
|
473
|
+
// Find the actual index of this equation in the steps array
|
|
474
|
+
const equationIndex = this.steps.filter(s => s instanceof omdEquationNode).indexOf(step);
|
|
475
|
+
|
|
476
|
+
if (equationIndex >= 0) {
|
|
477
|
+
const createdDot = this._createStepDot(step, equationIndex);
|
|
478
|
+
|
|
479
|
+
// Update the node to step mapping
|
|
480
|
+
step.findAllNodes().forEach(node => {
|
|
481
|
+
this.nodeToStepMap.set(node.id, equationIndex);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Create connecting line if this isn't the first equation
|
|
485
|
+
if (equationIndex > 0) {
|
|
486
|
+
this._createStepLine(equationIndex - 1, equationIndex);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// After stepMark is set, adjust dot radius
|
|
490
|
+
if (createdDot) {
|
|
491
|
+
const radius = getDotRadius(step.stepMark ?? 0);
|
|
492
|
+
createdDot.setWidthAndHeight(radius * 2, radius * 2);
|
|
493
|
+
createdDot.radius = radius;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Update layout after adding the step
|
|
499
|
+
this.computeDimensions();
|
|
500
|
+
this.updateLayout();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Gets the step number for a given node ID.
|
|
505
|
+
*/
|
|
506
|
+
getNodeStepNumber(nodeId) {
|
|
507
|
+
return this.nodeToStepMap.get(nodeId);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Override computeDimensions to account for visual elements
|
|
512
|
+
*/
|
|
513
|
+
computeDimensions() {
|
|
514
|
+
super.computeDimensions();
|
|
515
|
+
// Store original dimensions before visualizer expansion
|
|
516
|
+
this.sequenceWidth = this.width;
|
|
517
|
+
this.sequenceHeight = this.height;
|
|
518
|
+
|
|
519
|
+
// Set width to include the fixed visualizer position plus visualizer width
|
|
520
|
+
if (this.stepDots && this.stepDots.length > 0 && this.layoutManager) {
|
|
521
|
+
const containerWidth = this.dotRadius * 3;
|
|
522
|
+
const fixedVisualizerPosition = this.layoutManager.fixedVisualizerPosition || 250;
|
|
523
|
+
const totalWidth = fixedVisualizerPosition + this.visualSpacing + containerWidth;
|
|
524
|
+
this.setWidthAndHeight(totalWidth, this.height);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Override updateLayout to update visual elements as well
|
|
530
|
+
*/
|
|
531
|
+
updateLayout() {
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
super.updateLayout();
|
|
535
|
+
|
|
536
|
+
// Only update visual layout if layoutManager is initialized
|
|
537
|
+
if (this.layoutManager) {
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
this.layoutManager.updateVisualLayout(true); // Allow repositioning for main layout updates
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
this.layoutManager.updateVisualVisibility();
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
this.layoutManager.updateAllLinePositions();
|
|
547
|
+
} else {
|
|
548
|
+
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Removes the most recent operation and refreshes visual dots/lines accordingly.
|
|
556
|
+
* @returns {boolean} Whether an operation was undone
|
|
557
|
+
*/
|
|
558
|
+
undoLastOperation() {
|
|
559
|
+
// Clear all step visualizer highlights before undoing
|
|
560
|
+
if (this.highlighting && typeof this.highlighting.clearAllExplainHighlights === 'function') {
|
|
561
|
+
this.highlighting.clearAllExplainHighlights();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Remove bottom-most equation and its preceding operation display
|
|
565
|
+
const beforeCount = this.steps.length;
|
|
566
|
+
const removed = super.undoLastOperation ? super.undoLastOperation() : false;
|
|
567
|
+
if (removed || this.steps.length < beforeCount) {
|
|
568
|
+
// Hard rebuild the visual container to avoid stale dots/lines
|
|
569
|
+
this.rebuildVisualizer();
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Sets the color of a specific dot and its associated lines
|
|
577
|
+
*/
|
|
578
|
+
setDotColor(dotIndex, color) {
|
|
579
|
+
if (this.stepDots && dotIndex >= 0 && dotIndex < this.stepDots.length) {
|
|
580
|
+
const dot = this.stepDots[dotIndex];
|
|
581
|
+
dot.setFillColor(color);
|
|
582
|
+
dot.setStrokeColor(color);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Sets the color of the line above a specific dot
|
|
588
|
+
*/
|
|
589
|
+
setLineAboveColor(dotIndex, color) {
|
|
590
|
+
let targetLine = this.stepLines.find(line =>
|
|
591
|
+
line.toDotIndex === dotIndex && line.isTemporary && line.svgObject && line.svgObject.style.display !== 'none'
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
if (!targetLine) {
|
|
595
|
+
targetLine = this.stepLines.find(line =>
|
|
596
|
+
line.toDotIndex === dotIndex && !line.isTemporary && line.svgObject && line.svgObject.style.display !== 'none'
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (targetLine) {
|
|
601
|
+
targetLine.setStrokeColor(color);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Enables or disables dot clicking functionality
|
|
607
|
+
*/
|
|
608
|
+
setDotsClickable(enabled) {
|
|
609
|
+
this.dotsClickable = enabled;
|
|
610
|
+
|
|
611
|
+
// If disabling, clear any active highlights and dots
|
|
612
|
+
if (!enabled) {
|
|
613
|
+
this._clearActiveDot();
|
|
614
|
+
// Use the more thorough clearing to ensure no stale highlights remain
|
|
615
|
+
this.highlighting.clearAllExplainHighlights();
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
this.stepDots.forEach(dot => {
|
|
619
|
+
this.layoutManager.updateDotClickability(dot);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Handles clicking on a step dot
|
|
625
|
+
* @private
|
|
626
|
+
*/
|
|
627
|
+
_handleDotClick(dot, dotIndex) {
|
|
628
|
+
if (!this.dotsClickable) return;
|
|
629
|
+
// Guard against stale dot references
|
|
630
|
+
if (dotIndex < 0 || dotIndex >= this.stepDots.length) return;
|
|
631
|
+
if (this.stepDots[dotIndex] !== dot) {
|
|
632
|
+
// try to resolve current index
|
|
633
|
+
const idx = this.stepDots.indexOf(dot);
|
|
634
|
+
if (idx === -1) return;
|
|
635
|
+
dotIndex = idx;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
try {
|
|
639
|
+
if (this.activeDotIndex === dotIndex) {
|
|
640
|
+
this._clearActiveDot();
|
|
641
|
+
} else {
|
|
642
|
+
if (this.activeDotIndex !== -1) {
|
|
643
|
+
this._clearActiveDot();
|
|
644
|
+
}
|
|
645
|
+
this.setActiveDot(dotIndex);
|
|
646
|
+
const equation = this.stepDots[dotIndex].equationRef;
|
|
647
|
+
const equationIndex = this.steps.indexOf(equation);
|
|
648
|
+
const isOperation = this._checkForOperationStep(equationIndex);
|
|
649
|
+
this.highlighting.highlightAffectedNodes(dotIndex, isOperation);
|
|
650
|
+
}
|
|
651
|
+
} catch (error) {
|
|
652
|
+
console.error('Error handling dot click:', error);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Sets a dot to be the visually active one.
|
|
658
|
+
* @private
|
|
659
|
+
*/
|
|
660
|
+
setActiveDot(dotIndex) {
|
|
661
|
+
if (!this.stepDots || dotIndex < 0 || dotIndex >= this.stepDots.length) return;
|
|
662
|
+
|
|
663
|
+
this.activeDotIndex = dotIndex;
|
|
664
|
+
this.activeDot = this.stepDots[dotIndex];
|
|
665
|
+
|
|
666
|
+
const dot = this.stepDots[dotIndex];
|
|
667
|
+
const explainColor = this.styling.activeDotColor;
|
|
668
|
+
dot.setFillColor(explainColor);
|
|
669
|
+
dot.setStrokeColor(explainColor);
|
|
670
|
+
|
|
671
|
+
this.setLineAboveColor(dotIndex, this.styling.activeLineColor);
|
|
672
|
+
this.textBoxManager.createTextBoxForDot(dotIndex);
|
|
673
|
+
|
|
674
|
+
// Temporarily disable equation repositioning for simple dot state changes
|
|
675
|
+
const originalRepositioning = this.layoutManager.allowEquationRepositioning;
|
|
676
|
+
this.layoutManager.allowEquationRepositioning = false;
|
|
677
|
+
this.layoutManager.updateVisualZOrder();
|
|
678
|
+
this.layoutManager.allowEquationRepositioning = originalRepositioning;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Clears the currently active dot
|
|
683
|
+
* @private
|
|
684
|
+
*/
|
|
685
|
+
/**
|
|
686
|
+
* Clears the currently active dot
|
|
687
|
+
* @private
|
|
688
|
+
*/
|
|
689
|
+
_clearActiveDot() {
|
|
690
|
+
try {
|
|
691
|
+
if (this.activeDotIndex !== -1) {
|
|
692
|
+
const dot = this.stepDots[this.activeDotIndex];
|
|
693
|
+
const dotColor = this.styling.dotColor;
|
|
694
|
+
dot.setFillColor(dotColor);
|
|
695
|
+
dot.setStrokeColor(dotColor);
|
|
696
|
+
|
|
697
|
+
this.setLineAboveColor(this.activeDotIndex, this.styling.lineColor);
|
|
698
|
+
this.textBoxManager.removeTextBoxForDot(this.activeDotIndex);
|
|
699
|
+
|
|
700
|
+
// Use thorough clearing to ensure no stale highlights remain
|
|
701
|
+
this.highlighting.clearAllExplainHighlights();
|
|
702
|
+
|
|
703
|
+
// Temporarily disable equation repositioning for simple dot state changes
|
|
704
|
+
const originalRepositioning = this.layoutManager.allowEquationRepositioning;
|
|
705
|
+
this.layoutManager.allowEquationRepositioning = false;
|
|
706
|
+
this.layoutManager.updateVisualZOrder();
|
|
707
|
+
this.layoutManager.allowEquationRepositioning = originalRepositioning;
|
|
708
|
+
|
|
709
|
+
this.activeDot = null;
|
|
710
|
+
this.activeDotIndex = -1;
|
|
711
|
+
}
|
|
712
|
+
} catch (error) {
|
|
713
|
+
console.error('Error clearing active dot:', error);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Gets simplification data for a specific dot
|
|
719
|
+
* @private
|
|
720
|
+
*/
|
|
721
|
+
_getSimplificationDataForDot(dotIndex) {
|
|
722
|
+
try {
|
|
723
|
+
const dot = this.stepDots[dotIndex];
|
|
724
|
+
if (!dot || !dot.equationRef) {
|
|
725
|
+
return this._createDefaultSimplificationData("No equation found for this step");
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const equationIndex = this.steps.indexOf(dot.equationRef);
|
|
729
|
+
if (equationIndex === -1) {
|
|
730
|
+
return this._createDefaultSimplificationData("Step not found in sequence");
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Find the previous visible equation
|
|
734
|
+
const previousVisibleIndex = this._findPreviousVisibleEquationIndex(equationIndex);
|
|
735
|
+
|
|
736
|
+
// Get all steps between previous visible equation and current
|
|
737
|
+
const allSteps = [];
|
|
738
|
+
|
|
739
|
+
// Get simplifications
|
|
740
|
+
const simplificationHistory = this.getSimplificationHistory();
|
|
741
|
+
const relevantSimplifications = this._getRelevantSimplifications(
|
|
742
|
+
simplificationHistory,
|
|
743
|
+
previousVisibleIndex,
|
|
744
|
+
equationIndex
|
|
745
|
+
);
|
|
746
|
+
allSteps.push(...relevantSimplifications);
|
|
747
|
+
|
|
748
|
+
// Get operations
|
|
749
|
+
for (let i = previousVisibleIndex + 1; i <= equationIndex; i++) {
|
|
750
|
+
const operationData = this._checkForOperationStep(i);
|
|
751
|
+
if (operationData) {
|
|
752
|
+
allSteps.push({
|
|
753
|
+
message: operationData.message,
|
|
754
|
+
affectedNodes: operationData.affectedNodes,
|
|
755
|
+
stepNumber: i - 1
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Sort steps by step number
|
|
761
|
+
allSteps.sort((a, b) => a.stepNumber - b.stepNumber);
|
|
762
|
+
|
|
763
|
+
if (allSteps.length > 0) {
|
|
764
|
+
return this._createMultipleSimplificationsData(allSteps);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Check for single simplification
|
|
768
|
+
const singleSimplificationData = this._checkForSingleSimplification(
|
|
769
|
+
simplificationHistory,
|
|
770
|
+
equationIndex
|
|
771
|
+
);
|
|
772
|
+
if (singleSimplificationData) {
|
|
773
|
+
return singleSimplificationData;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Fallback cases
|
|
777
|
+
return this._getFallbackSimplificationData(equationIndex);
|
|
778
|
+
|
|
779
|
+
} catch (error) {
|
|
780
|
+
console.error('Error getting simplification data for dot:', error);
|
|
781
|
+
return this._createDefaultSimplificationData("Error retrieving step data");
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Gets the step text boxes
|
|
787
|
+
*/
|
|
788
|
+
getStepTextBoxes() {
|
|
789
|
+
return this.textBoxManager.getStepTextBoxes();
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// ===== SIMPLIFICATION DATA METHODS =====
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Creates default simplification data
|
|
796
|
+
* @param {string} message - The message to display
|
|
797
|
+
* @returns {Object} Default data object
|
|
798
|
+
* @private
|
|
799
|
+
*/
|
|
800
|
+
_createDefaultSimplificationData(message) {
|
|
801
|
+
return {
|
|
802
|
+
message: message,
|
|
803
|
+
rawMessages: [message],
|
|
804
|
+
ruleNames: ['Step Description'],
|
|
805
|
+
affectedNodes: [],
|
|
806
|
+
resultNodeIds: [],
|
|
807
|
+
resultProvSources: [],
|
|
808
|
+
multipleSimplifications: false
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Finds the index of the previous visible equation
|
|
814
|
+
* @param {number} currentIndex - Current equation index
|
|
815
|
+
* @returns {number} Index of previous visible equation, or -1 if none found
|
|
816
|
+
* @private
|
|
817
|
+
*/
|
|
818
|
+
_findPreviousVisibleEquationIndex(currentIndex) {
|
|
819
|
+
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
820
|
+
if (this.steps[i] instanceof omdEquationNode && this.steps[i].visible !== false) {
|
|
821
|
+
return i;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return -1;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Gets relevant simplifications between two step indices
|
|
829
|
+
* @param {Array} simplificationHistory - Full simplification history
|
|
830
|
+
* @param {number} startIndex - Starting step index
|
|
831
|
+
* @param {number} endIndex - Ending step index
|
|
832
|
+
* @returns {Array} Array of relevant simplification entries
|
|
833
|
+
* @private
|
|
834
|
+
*/
|
|
835
|
+
_getRelevantSimplifications(simplificationHistory, startIndex, endIndex) {
|
|
836
|
+
const relevantSimplifications = [];
|
|
837
|
+
|
|
838
|
+
for (let stepNum = startIndex; stepNum < endIndex; stepNum++) {
|
|
839
|
+
const entries = simplificationHistory.filter(entry => entry.stepNumber === stepNum);
|
|
840
|
+
if (entries.length > 0) {
|
|
841
|
+
relevantSimplifications.push(...entries);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return relevantSimplifications;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Creates data object for multiple simplifications
|
|
850
|
+
* @param {Array} simplifications - Array of simplification entries
|
|
851
|
+
* @returns {Object} Data object for multiple simplifications
|
|
852
|
+
* @private
|
|
853
|
+
*/
|
|
854
|
+
_createMultipleSimplificationsData(simplifications) {
|
|
855
|
+
const messages = simplifications.map(s => s.message);
|
|
856
|
+
const ruleNames = simplifications.map(s => s.name || 'Operation').filter(Boolean);
|
|
857
|
+
|
|
858
|
+
const allAffectedNodes = [];
|
|
859
|
+
const allResultNodeIds = [];
|
|
860
|
+
const allResultProvSources = [];
|
|
861
|
+
|
|
862
|
+
simplifications.forEach(entry => {
|
|
863
|
+
if (entry.affectedNodes) {
|
|
864
|
+
allAffectedNodes.push(...entry.affectedNodes);
|
|
865
|
+
}
|
|
866
|
+
if (entry.resultNodeId) {
|
|
867
|
+
allResultNodeIds.push(entry.resultNodeId);
|
|
868
|
+
}
|
|
869
|
+
if (entry.resultProvSources) {
|
|
870
|
+
allResultProvSources.push(...entry.resultProvSources);
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
return {
|
|
875
|
+
message: messages.join('. '),
|
|
876
|
+
rawMessages: messages,
|
|
877
|
+
ruleNames: ruleNames,
|
|
878
|
+
affectedNodes: allAffectedNodes,
|
|
879
|
+
resultNodeIds: allResultNodeIds,
|
|
880
|
+
resultProvSources: allResultProvSources,
|
|
881
|
+
multipleSimplifications: true
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Checks for operation step data
|
|
887
|
+
* @param {number} equationIndex - Current equation index
|
|
888
|
+
* @returns {Object|null} Operation data object or null
|
|
889
|
+
* @private
|
|
890
|
+
*/
|
|
891
|
+
_checkForOperationStep(equationIndex) {
|
|
892
|
+
if (equationIndex > 0) {
|
|
893
|
+
const step = this.steps[equationIndex - 1];
|
|
894
|
+
if (step instanceof omdOperationDisplayNode) {
|
|
895
|
+
return {
|
|
896
|
+
message: `Applied ${step.operation} ${step.value} to both sides`,
|
|
897
|
+
affectedNodes: [step.operatorLeft, step.valueLeft, step.operatorRight, step.valueRight]
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Checks for single simplification data
|
|
906
|
+
* @param {Array} simplificationHistory - Full simplification history
|
|
907
|
+
* @param {number} equationIndex - Current equation index
|
|
908
|
+
* @returns {Object|null} Single simplification data or null
|
|
909
|
+
* @private
|
|
910
|
+
*/
|
|
911
|
+
_checkForSingleSimplification(simplificationHistory, equationIndex) {
|
|
912
|
+
const relevantSimplification = simplificationHistory.find(entry =>
|
|
913
|
+
entry.stepNumber === equationIndex - 1
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
if (relevantSimplification) {
|
|
917
|
+
return {
|
|
918
|
+
message: relevantSimplification.message,
|
|
919
|
+
rawMessages: [relevantSimplification.message],
|
|
920
|
+
ruleNames: [relevantSimplification.name || 'Operation'],
|
|
921
|
+
affectedNodes: relevantSimplification.affectedNodes || [],
|
|
922
|
+
resultNodeIds: relevantSimplification.resultNodeId ? [relevantSimplification.resultNodeId] : [],
|
|
923
|
+
resultProvSources: relevantSimplification.resultProvSources || [],
|
|
924
|
+
multipleSimplifications: false
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Gets fallback data for special cases
|
|
933
|
+
* @param {number} equationIndex - Current equation index
|
|
934
|
+
* @returns {Object} Fallback data object
|
|
935
|
+
* @private
|
|
936
|
+
*/
|
|
937
|
+
_getFallbackSimplificationData(equationIndex) {
|
|
938
|
+
const currentStep = this.steps[equationIndex];
|
|
939
|
+
if (equationIndex === 0 && currentStep.stepMark === 0) {
|
|
940
|
+
const equationStr = currentStep.toString();
|
|
941
|
+
return this._createDefaultSimplificationData(`Starting with equation: ${equationStr}`);
|
|
942
|
+
} else if (currentStep && currentStep.description) {
|
|
943
|
+
return this._createDefaultSimplificationData(currentStep.description);
|
|
944
|
+
} else {
|
|
945
|
+
return this._createDefaultSimplificationData("Step explanation not available");
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
948
|
}
|