@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,893 +1,893 @@
|
|
|
1
|
-
import { omdEquationNode } from '../nodes/omdEquationNode.js';
|
|
2
|
-
import { omdColor } from '../../src/omdColor.js';
|
|
3
|
-
import { jsvgLine, jsvgEllipse } from '@teachinglab/jsvg';
|
|
4
|
-
import { getDotRadius } from '../config/omdConfigManager.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Handles visual layout, positioning, and visibility management for step visualizations
|
|
8
|
-
*/
|
|
9
|
-
export class omdStepVisualizerLayout {
|
|
10
|
-
constructor(stepVisualizer) {
|
|
11
|
-
this.stepVisualizer = stepVisualizer;
|
|
12
|
-
this.expansionDots = []; // Small dots that show/hide hidden steps
|
|
13
|
-
this.fixedVisualizerPosition = 250; // Fixed position for the step visualizer from left edge
|
|
14
|
-
this.allowEquationRepositioning = true; // Flag to control when equations can be repositioned
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Sets the fixed position for the step visualizer
|
|
19
|
-
* @param {number} position - The x position from the left edge where the visualizer should be positioned
|
|
20
|
-
*/
|
|
21
|
-
setFixedVisualizerPosition(position) {
|
|
22
|
-
// Only update if position actually changes
|
|
23
|
-
if (this.fixedVisualizerPosition !== position) {
|
|
24
|
-
this.fixedVisualizerPosition = position;
|
|
25
|
-
// Trigger a layout update if the visualizer is already initialized
|
|
26
|
-
if (this.stepVisualizer && this.stepVisualizer.stepDots.length > 0) {
|
|
27
|
-
this.updateVisualLayout(true); // Allow repositioning for position changes
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Updates the layout of visual elements relative to the sequence
|
|
34
|
-
* @param {boolean} allowRepositioning - Whether to allow equation repositioning (default: false)
|
|
35
|
-
*/
|
|
36
|
-
updateVisualLayout(allowRepositioning = false) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (this.stepVisualizer.stepDots.length === 0) return;
|
|
42
|
-
|
|
43
|
-
// Calculate the total width needed for equations (including any padding)
|
|
44
|
-
const baseEquationWidth = (this.stepVisualizer.sequenceWidth || this.stepVisualizer.width);
|
|
45
|
-
const extraPaddingX = this._getMaxEquationEffectivePaddingX();
|
|
46
|
-
const totalEquationWidth = baseEquationWidth + extraPaddingX;
|
|
47
|
-
|
|
48
|
-
// Position visual container at a fixed position
|
|
49
|
-
const visualX = this.fixedVisualizerPosition;
|
|
50
|
-
this.stepVisualizer.visualContainer.setPosition(visualX, 0);
|
|
51
|
-
|
|
52
|
-
// Only reposition equations if explicitly allowed (not during simple dot clicks)
|
|
53
|
-
if (this.allowEquationRepositioning && allowRepositioning) {
|
|
54
|
-
|
|
55
|
-
// Calculate how much space is available for equations before the visualizer
|
|
56
|
-
const availableEquationSpace = this.fixedVisualizerPosition - this.stepVisualizer.visualSpacing;
|
|
57
|
-
|
|
58
|
-
// If equations are too wide, shift them left to fit
|
|
59
|
-
let equationOffsetX = 0;
|
|
60
|
-
if (totalEquationWidth > availableEquationSpace) {
|
|
61
|
-
equationOffsetX = availableEquationSpace - totalEquationWidth;
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Apply the offset to equation positioning
|
|
66
|
-
this._adjustEquationPositions(equationOffsetX);
|
|
67
|
-
} else {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Position dots based on visible equations
|
|
73
|
-
const visibleSteps = this.stepVisualizer.steps.filter(s => s.visible !== false);
|
|
74
|
-
let currentY = 0;
|
|
75
|
-
const verticalPadding = 15 * this.stepVisualizer.getFontSize() / this.stepVisualizer.getRootFontSize();
|
|
76
|
-
|
|
77
|
-
visibleSteps.forEach((step, visIndex) => {
|
|
78
|
-
if (step instanceof omdEquationNode) {
|
|
79
|
-
const dotIndex = this.findDotIndexForEquation(step);
|
|
80
|
-
if (dotIndex >= 0 && dotIndex < this.stepVisualizer.stepDots.length) {
|
|
81
|
-
const dot = this.stepVisualizer.stepDots[dotIndex];
|
|
82
|
-
|
|
83
|
-
// Center dot vertically with the equation
|
|
84
|
-
let equationCenter;
|
|
85
|
-
if (step.equalsSign && step.equalsSign.ypos !== undefined) {
|
|
86
|
-
equationCenter = step.equalsSign.ypos + (step.equalsSign.height / 2);
|
|
87
|
-
} else {
|
|
88
|
-
equationCenter = step.getAlignmentBaseline ? step.getAlignmentBaseline() : step.height / 2;
|
|
89
|
-
}
|
|
90
|
-
const dotY = currentY + equationCenter;
|
|
91
|
-
const dotX = (this.stepVisualizer.dotRadius * 3) / 2;
|
|
92
|
-
|
|
93
|
-
dot.setPosition(dotX, dotY);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
currentY += step.height;
|
|
98
|
-
if (visIndex < visibleSteps.length - 1) {
|
|
99
|
-
currentY += verticalPadding;
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
this.updateAllLinePositions();
|
|
104
|
-
|
|
105
|
-
// Update container dimensions
|
|
106
|
-
let containerWidth = this.stepVisualizer.dotRadius * 3;
|
|
107
|
-
let containerHeight = this.stepVisualizer.height;
|
|
108
|
-
|
|
109
|
-
// Store the original height before expansion for autoscale calculations
|
|
110
|
-
if (!this.stepVisualizer.sequenceHeight) {
|
|
111
|
-
this.stepVisualizer.sequenceHeight = containerHeight;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const textBoxes = this.stepVisualizer.textBoxManager.getStepTextBoxes();
|
|
115
|
-
if (textBoxes.length > 0) {
|
|
116
|
-
const textBoxWidth = 280;
|
|
117
|
-
containerWidth = Math.max(containerWidth, textBoxWidth + this.stepVisualizer.dotRadius * 2 + 10 + 20);
|
|
118
|
-
|
|
119
|
-
// Calculate the maximum extent of any text box to prevent clipping
|
|
120
|
-
textBoxes.forEach(textBox => {
|
|
121
|
-
if (textBox.interactiveSteps) {
|
|
122
|
-
const dimensions = textBox.interactiveSteps.getDimensions();
|
|
123
|
-
const layoutGroup = textBox.interactiveSteps.getLayoutGroup();
|
|
124
|
-
|
|
125
|
-
// Calculate the bottom of this text box
|
|
126
|
-
const textBoxBottom = layoutGroup.ypos + dimensions.height;
|
|
127
|
-
containerHeight = Math.max(containerHeight, textBoxBottom + 20); // Add some buffer
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (this.stepVisualizer.stepDots.length > 0) {
|
|
133
|
-
const maxRadius = Math.max(...this.stepVisualizer.stepDots.map(d=>d.radius||this.stepVisualizer.dotRadius));
|
|
134
|
-
const containerWidth = maxRadius * 3;
|
|
135
|
-
const maxDotY = Math.max(...this.stepVisualizer.stepDots.map(dot => dot.ypos + this.stepVisualizer.dotRadius));
|
|
136
|
-
containerHeight = Math.max(containerHeight, maxDotY);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
this.stepVisualizer.visualContainer.setWidthAndHeight(containerWidth, containerHeight);
|
|
140
|
-
this.updateVisualZOrder();
|
|
141
|
-
|
|
142
|
-
// Position expansion dots after main dots are positioned
|
|
143
|
-
this._positionExpansionDots();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Adjusts the horizontal position of all equations by the specified offset
|
|
148
|
-
* @private
|
|
149
|
-
*/
|
|
150
|
-
_adjustEquationPositions(offsetX) {
|
|
151
|
-
if (offsetX === 0) return; // No adjustment needed
|
|
152
|
-
|
|
153
|
-
const sv = this.stepVisualizer;
|
|
154
|
-
|
|
155
|
-
// Adjust position of all steps (equations and operation display nodes)
|
|
156
|
-
sv.steps.forEach(step => {
|
|
157
|
-
if (step && step.setPosition) {
|
|
158
|
-
const currentX = step.xpos || 0;
|
|
159
|
-
const currentY = step.ypos || 0;
|
|
160
|
-
step.setPosition(currentX + offsetX, currentY);
|
|
161
|
-
|
|
162
|
-
// Also adjust operation display nodes if they exist
|
|
163
|
-
if (step.operationDisplayNode && step.operationDisplayNode.setPosition) {
|
|
164
|
-
const opCurrentX = step.operationDisplayNode.xpos || 0;
|
|
165
|
-
const opCurrentY = step.operationDisplayNode.ypos || 0;
|
|
166
|
-
step.operationDisplayNode.setPosition(opCurrentX + offsetX, opCurrentY);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Computes the maximum horizontal padding (x) among visible equations, if configured.
|
|
176
|
-
* This allows dots to shift further right when pill background padding is added.
|
|
177
|
-
* @returns {number}
|
|
178
|
-
* @private
|
|
179
|
-
*/
|
|
180
|
-
_getMaxEquationEffectivePaddingX() {
|
|
181
|
-
try {
|
|
182
|
-
const steps = this.stepVisualizer.steps || [];
|
|
183
|
-
let maxPadX = 0;
|
|
184
|
-
steps.forEach(step => {
|
|
185
|
-
if (step instanceof omdEquationNode && step.visible !== false) {
|
|
186
|
-
if (typeof step.getEffectiveBackgroundPaddingX === 'function') {
|
|
187
|
-
const px = Number(step.getEffectiveBackgroundPaddingX());
|
|
188
|
-
maxPadX = Math.max(maxPadX, isNaN(px) ? 0 : px);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
return maxPadX;
|
|
193
|
-
} catch (_) {
|
|
194
|
-
return 0;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Finds the dot index for a given equation
|
|
200
|
-
*/
|
|
201
|
-
findDotIndexForEquation(equation) {
|
|
202
|
-
return this.stepVisualizer.stepDots.findIndex(dot => dot.equationRef === equation);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Updates the z-order of visual elements
|
|
207
|
-
*/
|
|
208
|
-
updateVisualZOrder() {
|
|
209
|
-
if (!this.stepVisualizer.visualContainer) return;
|
|
210
|
-
|
|
211
|
-
// Lines behind (z-index 1)
|
|
212
|
-
this.stepVisualizer.stepLines.forEach(line => {
|
|
213
|
-
if (line && line.svgObject) {
|
|
214
|
-
line.svgObject.style.zIndex = '1';
|
|
215
|
-
if (line.parentNode !== this.stepVisualizer.visualContainer) {
|
|
216
|
-
this.stepVisualizer.visualContainer.addChild(line);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
// Dots in front (z-index 2)
|
|
222
|
-
this.stepVisualizer.stepDots.forEach(dot => {
|
|
223
|
-
if (dot && dot.svgObject) {
|
|
224
|
-
dot.svgObject.style.zIndex = '2';
|
|
225
|
-
if (dot.parentNode !== this.stepVisualizer.visualContainer) {
|
|
226
|
-
this.stepVisualizer.visualContainer.addChild(dot);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// Text boxes on top (z-index 3)
|
|
232
|
-
const textBoxes = this.stepVisualizer.textBoxManager.getStepTextBoxes();
|
|
233
|
-
textBoxes.forEach(textBox => {
|
|
234
|
-
if (textBox && textBox.svgObject) {
|
|
235
|
-
textBox.svgObject.style.zIndex = '3';
|
|
236
|
-
if (textBox.parentNode !== this.stepVisualizer.visualContainer) {
|
|
237
|
-
this.stepVisualizer.visualContainer.addChild(textBox);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// Expansion dots on top of regular dots (z-index 4)
|
|
243
|
-
this.expansionDots.forEach(dot => {
|
|
244
|
-
if (dot && dot.svgObject) {
|
|
245
|
-
dot.svgObject.style.zIndex = '4';
|
|
246
|
-
if (dot.parentNode !== this.stepVisualizer.visualContainer) {
|
|
247
|
-
this.stepVisualizer.visualContainer.addChild(dot);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Updates all line positions to connect dot centers
|
|
255
|
-
*/
|
|
256
|
-
updateAllLinePositions() {
|
|
257
|
-
this.stepVisualizer.stepLines.forEach(line => {
|
|
258
|
-
const fromDot = this.stepVisualizer.stepDots[line.fromDotIndex];
|
|
259
|
-
const toDot = this.stepVisualizer.stepDots[line.toDotIndex];
|
|
260
|
-
|
|
261
|
-
if (fromDot && toDot) {
|
|
262
|
-
line.setEndpoints(fromDot.xpos, fromDot.ypos, toDot.xpos, toDot.ypos);
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Updates visibility of visual elements based on equation visibility
|
|
269
|
-
*/
|
|
270
|
-
updateVisualVisibility() {
|
|
271
|
-
|
|
272
|
-
const sv = this.stepVisualizer;
|
|
273
|
-
|
|
274
|
-
// Update dot visibility and color first, which is the source of truth
|
|
275
|
-
const dotColor = sv.styling?.dotColor || omdColor.stepColor;
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
sv.stepDots.forEach((dot, index) => {
|
|
279
|
-
if (dot.equationRef && dot.equationRef.visible !== false) {
|
|
280
|
-
dot.setFillColor(dotColor);
|
|
281
|
-
dot.setStrokeColor(dotColor);
|
|
282
|
-
dot.show();
|
|
283
|
-
dot.visible = true; // Use the dot's own visibility property
|
|
284
|
-
|
|
285
|
-
} else {
|
|
286
|
-
dot.hide();
|
|
287
|
-
dot.visible = false;
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// Clear existing expansion dots
|
|
293
|
-
|
|
294
|
-
this._clearExpansionDots();
|
|
295
|
-
|
|
296
|
-
// Remove all old lines from the container and the array
|
|
297
|
-
|
|
298
|
-
sv.stepLines.forEach(line => {
|
|
299
|
-
// Remove the line if it is currently a child of the visualContainer
|
|
300
|
-
if (line.parent === sv.visualContainer) {
|
|
301
|
-
sv.visualContainer.removeChild(line);
|
|
302
|
-
}
|
|
303
|
-
});
|
|
304
|
-
sv.stepLines = [];
|
|
305
|
-
|
|
306
|
-
// Get the dots that are currently visible
|
|
307
|
-
const visibleDots = sv.stepDots.filter(dot => dot.visible);
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// Re-create connecting lines only between the visible dots
|
|
311
|
-
|
|
312
|
-
for (let i = 0; i < visibleDots.length - 1; i++) {
|
|
313
|
-
const fromDot = visibleDots[i];
|
|
314
|
-
const toDot = visibleDots[i + 1];
|
|
315
|
-
|
|
316
|
-
const line = new jsvgLine();
|
|
317
|
-
const lineColor = sv.styling?.lineColor || omdColor.stepColor;
|
|
318
|
-
line.setStrokeColor(lineColor);
|
|
319
|
-
line.setStrokeWidth(sv.styling?.lineWidth || sv.lineWidth);
|
|
320
|
-
line.fromDotIndex = sv.stepDots.indexOf(fromDot);
|
|
321
|
-
line.toDotIndex = sv.stepDots.indexOf(toDot);
|
|
322
|
-
|
|
323
|
-
sv.visualContainer.addChild(line);
|
|
324
|
-
sv.stepLines.push(line);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
// After creating the lines, update their positions
|
|
329
|
-
this.updateAllLinePositions();
|
|
330
|
-
|
|
331
|
-
// Create expansion dots for dots that have hidden steps before them
|
|
332
|
-
|
|
333
|
-
this._createExpansionDots();
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
this._positionExpansionDots();
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Updates the clickability of a dot
|
|
343
|
-
*/
|
|
344
|
-
updateDotClickability(dot) {
|
|
345
|
-
if (this.stepVisualizer.dotsClickable) {
|
|
346
|
-
dot.svgObject.style.cursor = "pointer";
|
|
347
|
-
dot.svgObject.onclick = (event) => {
|
|
348
|
-
try {
|
|
349
|
-
const idx = this.stepVisualizer.stepDots.indexOf(dot);
|
|
350
|
-
if (idx < 0) return; // orphan dot, ignore
|
|
351
|
-
this.stepVisualizer._handleDotClick(dot, idx);
|
|
352
|
-
event.stopPropagation();
|
|
353
|
-
} catch (error) {
|
|
354
|
-
console.error('Error in dot click handler:', error);
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
} else {
|
|
358
|
-
dot.svgObject.style.cursor = "default";
|
|
359
|
-
dot.svgObject.onclick = null;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Clears all expansion dots
|
|
365
|
-
* @private
|
|
366
|
-
*/
|
|
367
|
-
_clearExpansionDots() {
|
|
368
|
-
this.expansionDots.forEach(dot => {
|
|
369
|
-
if (dot.parentNode === this.stepVisualizer.visualContainer) {
|
|
370
|
-
this.stepVisualizer.visualContainer.removeChild(dot);
|
|
371
|
-
}
|
|
372
|
-
});
|
|
373
|
-
this.expansionDots = [];
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Creates expansion dots for visible dots that have hidden steps before them
|
|
378
|
-
* @private
|
|
379
|
-
*/
|
|
380
|
-
_createExpansionDots() {
|
|
381
|
-
|
|
382
|
-
const sv = this.stepVisualizer;
|
|
383
|
-
const allDots = sv.stepDots;
|
|
384
|
-
const visibleDots = sv.stepDots.filter(dot => dot.visible);
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
// Debug all steps and their properties
|
|
389
|
-
|
|
390
|
-
sv.steps.forEach((step, i) => {
|
|
391
|
-
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
392
|
-
|
|
393
|
-
} else {
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
// Debug all dots and their properties
|
|
399
|
-
|
|
400
|
-
allDots.forEach((dot, i) => {
|
|
401
|
-
if (dot && dot.equationRef) {
|
|
402
|
-
|
|
403
|
-
} else {
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
// Check for hidden intermediate steps between consecutive visible major steps (stepMark = 0)
|
|
411
|
-
const visibleMajorSteps = [];
|
|
412
|
-
sv.steps.forEach((step, stepIndex) => {
|
|
413
|
-
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
414
|
-
if (step.stepMark === 0 && step.visible === true) {
|
|
415
|
-
visibleMajorSteps.push(stepIndex);
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
// Check between consecutive visible major steps for hidden intermediate steps
|
|
424
|
-
for (let i = 1; i < visibleMajorSteps.length; i++) {
|
|
425
|
-
const previousMajorStepIndex = visibleMajorSteps[i - 1];
|
|
426
|
-
const currentMajorStepIndex = visibleMajorSteps[i];
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
// Count hidden intermediate steps between these major steps
|
|
431
|
-
let hiddenIntermediateCount = 0;
|
|
432
|
-
for (let j = previousMajorStepIndex + 1; j < currentMajorStepIndex; j++) {
|
|
433
|
-
const step = sv.steps[j];
|
|
434
|
-
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
435
|
-
if (step.stepMark > 0 && step.visible === false) {
|
|
436
|
-
hiddenIntermediateCount++;
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (hiddenIntermediateCount > 0) {
|
|
445
|
-
|
|
446
|
-
// Find the dot for the current major step to position the expansion dot above it
|
|
447
|
-
const currentMajorStep = sv.steps[currentMajorStepIndex];
|
|
448
|
-
const currentDotIndex = sv.stepDots.findIndex(dot => dot.equationRef === currentMajorStep);
|
|
449
|
-
|
|
450
|
-
if (currentDotIndex >= 0) {
|
|
451
|
-
// Find the position in the visible dots array
|
|
452
|
-
const visibleDotIndex = i; // i is the position in visibleMajorSteps array
|
|
453
|
-
|
|
454
|
-
const expansionDot = this._createSingleExpansionDot(visibleDotIndex, previousMajorStepIndex, hiddenIntermediateCount);
|
|
455
|
-
expansionDot.majorStepIndex = currentMajorStepIndex; // Store for reference
|
|
456
|
-
this.expansionDots.push(expansionDot);
|
|
457
|
-
sv.visualContainer.addChild(expansionDot);
|
|
458
|
-
|
|
459
|
-
} else {
|
|
460
|
-
|
|
461
|
-
}
|
|
462
|
-
} else {
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
// Also create collapse dots for expanded sequences
|
|
469
|
-
this._createCollapseDots();
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Counts intermediate steps (stepMark > 0) between two visible dots
|
|
477
|
-
* @private
|
|
478
|
-
*/
|
|
479
|
-
_countIntermediateStepsBetween(fromDotIndex, toDotIndex) {
|
|
480
|
-
const sv = this.stepVisualizer;
|
|
481
|
-
let count = 0;
|
|
482
|
-
|
|
483
|
-
// Get the equation references for the from and to dots
|
|
484
|
-
const fromEquation = sv.stepDots[fromDotIndex]?.equationRef;
|
|
485
|
-
const toEquation = sv.stepDots[toDotIndex]?.equationRef;
|
|
486
|
-
|
|
487
|
-
if (!fromEquation || !toEquation) {
|
|
488
|
-
|
|
489
|
-
return 0;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Find the step indices in the main steps array
|
|
493
|
-
const fromStepIndex = sv.steps.indexOf(fromEquation);
|
|
494
|
-
const toStepIndex = sv.steps.indexOf(toEquation);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
// Count intermediate steps between these two major steps
|
|
499
|
-
for (let i = fromStepIndex + 1; i < toStepIndex; i++) {
|
|
500
|
-
const step = sv.steps[i];
|
|
501
|
-
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
502
|
-
// Count intermediate steps (stepMark > 0) that are currently hidden
|
|
503
|
-
if (step.stepMark !== undefined && step.stepMark > 0 && step.visible === false) {
|
|
504
|
-
|
|
505
|
-
count++;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
return count;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Counts hidden steps between two step indices (legacy method for backward compatibility)
|
|
516
|
-
* @private
|
|
517
|
-
*/
|
|
518
|
-
_countHiddenStepsBetween(fromIndex, toIndex) {
|
|
519
|
-
return this._countIntermediateStepsBetween(fromIndex, toIndex);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Creates a single expansion dot
|
|
524
|
-
* @private
|
|
525
|
-
*/
|
|
526
|
-
_createSingleExpansionDot(currentStepIndex, previousStepIndex, hiddenCount) {
|
|
527
|
-
|
|
528
|
-
const sv = this.stepVisualizer;
|
|
529
|
-
const baseRadius = sv.styling?.dotRadius || getDotRadius(0);
|
|
530
|
-
const expansionRadius = Math.max(3, baseRadius * (sv.styling?.expansionDotScale || 0.4));
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const expansionDot = new jsvgEllipse();
|
|
535
|
-
expansionDot.setWidthAndHeight(expansionRadius * 2, expansionRadius * 2);
|
|
536
|
-
|
|
537
|
-
// Use same color as regular dots from styling
|
|
538
|
-
const dotColor = sv.styling?.dotColor || omdColor.stepColor;
|
|
539
|
-
|
|
540
|
-
expansionDot.setFillColor(dotColor);
|
|
541
|
-
expansionDot.setStrokeColor(dotColor);
|
|
542
|
-
expansionDot.setStrokeWidth(sv.styling?.dotStrokeWidth || 1);
|
|
543
|
-
|
|
544
|
-
// Store metadata
|
|
545
|
-
expansionDot.isExpansionDot = true;
|
|
546
|
-
expansionDot.currentStepIndex = currentStepIndex;
|
|
547
|
-
expansionDot.previousStepIndex = previousStepIndex;
|
|
548
|
-
expansionDot.hiddenCount = hiddenCount;
|
|
549
|
-
expansionDot.radius = expansionRadius;
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
// Make it clickable
|
|
553
|
-
expansionDot.svgObject.style.cursor = "pointer";
|
|
554
|
-
expansionDot.svgObject.onclick = (event) => {
|
|
555
|
-
try {
|
|
556
|
-
|
|
557
|
-
this._handleExpansionDotClick(expansionDot);
|
|
558
|
-
event.stopPropagation();
|
|
559
|
-
} catch (error) {
|
|
560
|
-
console.error('Error in expansion dot click handler:', error);
|
|
561
|
-
}
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
return expansionDot;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* Positions expansion dots above their corresponding main dots
|
|
572
|
-
* @private
|
|
573
|
-
*/
|
|
574
|
-
_positionExpansionDots() {
|
|
575
|
-
|
|
576
|
-
const sv = this.stepVisualizer;
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
this.expansionDots.forEach((expansionDot, index) => {
|
|
581
|
-
|
|
582
|
-
let targetDot;
|
|
583
|
-
|
|
584
|
-
if (expansionDot.isCollapseDot) {
|
|
585
|
-
|
|
586
|
-
// For collapse dots, use the currentStepIndex which points to the dot index
|
|
587
|
-
const dotIndex = expansionDot.currentStepIndex;
|
|
588
|
-
targetDot = sv.stepDots[dotIndex];
|
|
589
|
-
|
|
590
|
-
} else {
|
|
591
|
-
|
|
592
|
-
// For expansion dots, we need to find the actual visible dot that corresponds to the major step
|
|
593
|
-
const majorStepIndex = expansionDot.majorStepIndex;
|
|
594
|
-
const majorStep = sv.steps[majorStepIndex];
|
|
595
|
-
|
|
596
|
-
if (majorStep) {
|
|
597
|
-
// Find the dot that corresponds to this major step
|
|
598
|
-
const dotIndex = sv.stepDots.findIndex(dot => dot.equationRef === majorStep);
|
|
599
|
-
targetDot = sv.stepDots[dotIndex];
|
|
600
|
-
|
|
601
|
-
} else {
|
|
602
|
-
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if (targetDot && targetDot.visible) {
|
|
607
|
-
const offsetY = -(expansionDot.radius * 2 + 8); // Position above main dot
|
|
608
|
-
const newX = targetDot.xpos;
|
|
609
|
-
const newY = targetDot.ypos + offsetY;
|
|
610
|
-
|
|
611
|
-
expansionDot.setPosition(newX, newY);
|
|
612
|
-
|
|
613
|
-
} else {
|
|
614
|
-
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
/**
|
|
622
|
-
* Creates collapse dots for expanded sequences
|
|
623
|
-
* @private
|
|
624
|
-
*/
|
|
625
|
-
_createCollapseDots() {
|
|
626
|
-
|
|
627
|
-
const sv = this.stepVisualizer;
|
|
628
|
-
const allDots = sv.stepDots;
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
// Group visible intermediate steps by their consecutive sequences
|
|
633
|
-
const intermediateGroups = [];
|
|
634
|
-
let currentGroup = [];
|
|
635
|
-
|
|
636
|
-
allDots.forEach((dot, index) => {
|
|
637
|
-
if (dot && dot.visible && dot.equationRef) {
|
|
638
|
-
const stepMark = dot.equationRef.stepMark;
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if (stepMark !== undefined && stepMark > 0) {
|
|
642
|
-
currentGroup.push(index);
|
|
643
|
-
|
|
644
|
-
} else if (currentGroup.length > 0) {
|
|
645
|
-
// We hit a major step, so end the current group
|
|
646
|
-
intermediateGroups.push([...currentGroup]);
|
|
647
|
-
|
|
648
|
-
currentGroup = [];
|
|
649
|
-
}
|
|
650
|
-
} else if (currentGroup.length > 0) {
|
|
651
|
-
// We hit a non-visible dot, so end the current group
|
|
652
|
-
intermediateGroups.push([...currentGroup]);
|
|
653
|
-
|
|
654
|
-
currentGroup = [];
|
|
655
|
-
}
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
// Don't forget the last group if it exists
|
|
659
|
-
if (currentGroup.length > 0) {
|
|
660
|
-
intermediateGroups.push([...currentGroup]);
|
|
661
|
-
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
// Create a collapse dot for each group
|
|
667
|
-
intermediateGroups.forEach((group, groupIndex) => {
|
|
668
|
-
if (group.length > 0) {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
// Find the major step that comes after the last intermediate step in this group
|
|
672
|
-
const lastIntermediateIndex = group[group.length - 1];
|
|
673
|
-
const lastIntermediateDot = sv.stepDots[lastIntermediateIndex];
|
|
674
|
-
const lastIntermediateStep = lastIntermediateDot.equationRef;
|
|
675
|
-
const lastIntermediateStepIndex = sv.steps.indexOf(lastIntermediateStep);
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
// Find the next major step (stepMark = 0) after the intermediate steps
|
|
680
|
-
let majorStepAfterIndex = -1;
|
|
681
|
-
for (let i = lastIntermediateStepIndex + 1; i < sv.steps.length; i++) {
|
|
682
|
-
const step = sv.steps[i];
|
|
683
|
-
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
684
|
-
if (step.stepMark === 0 && step.visible === true) {
|
|
685
|
-
majorStepAfterIndex = i;
|
|
686
|
-
|
|
687
|
-
break;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
if (majorStepAfterIndex >= 0) {
|
|
693
|
-
// Find the dot index for this major step
|
|
694
|
-
const majorStepAfter = sv.steps[majorStepAfterIndex];
|
|
695
|
-
const majorDotIndex = sv.stepDots.findIndex(dot => dot.equationRef === majorStepAfter);
|
|
696
|
-
|
|
697
|
-
if (majorDotIndex >= 0) {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const collapseDot = this._createSingleExpansionDot(majorDotIndex, -1, group.length);
|
|
701
|
-
collapseDot.isCollapseDot = true;
|
|
702
|
-
collapseDot.intermediateSteps = group;
|
|
703
|
-
collapseDot.groupIndex = groupIndex; // Store group reference
|
|
704
|
-
this.expansionDots.push(collapseDot);
|
|
705
|
-
sv.visualContainer.addChild(collapseDot);
|
|
706
|
-
|
|
707
|
-
} else {
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
} else {
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* Handles clicking on an expansion dot to toggle hidden steps
|
|
721
|
-
* @private
|
|
722
|
-
*/
|
|
723
|
-
_handleExpansionDotClick(expansionDot) {
|
|
724
|
-
const sv = this.stepVisualizer;
|
|
725
|
-
|
|
726
|
-
// Clear all step visualizer highlights when expanding/contracting
|
|
727
|
-
if (sv.highlighting && typeof sv.highlighting.clearAllExplainHighlights === 'function') {
|
|
728
|
-
sv.highlighting.clearAllExplainHighlights();
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
if (expansionDot.isCollapseDot) {
|
|
732
|
-
// Handle collapse dot click - hide only the specific group of intermediate steps
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
// Hide only the intermediate steps in this specific group
|
|
736
|
-
const intermediateSteps = expansionDot.intermediateSteps || [];
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
intermediateSteps.forEach(dotIndex => {
|
|
740
|
-
const dot = sv.stepDots[dotIndex];
|
|
741
|
-
if (dot && dot.equationRef) {
|
|
742
|
-
|
|
743
|
-
this._hideStep(dot.equationRef);
|
|
744
|
-
|
|
745
|
-
// Also hide the corresponding dot
|
|
746
|
-
dot.hide();
|
|
747
|
-
dot.visible = false;
|
|
748
|
-
|
|
749
|
-
}
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
// Remove any lines that connect to the hidden dots
|
|
753
|
-
|
|
754
|
-
this._removeLinesToHiddenDots();
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
} else {
|
|
758
|
-
// Handle expansion dot click - show steps between the major steps
|
|
759
|
-
const { majorStepIndex, previousStepIndex } = expansionDot;
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
// Remove this expansion dot immediately since we're expanding
|
|
764
|
-
|
|
765
|
-
if (expansionDot.parentNode === sv.visualContainer) {
|
|
766
|
-
sv.visualContainer.removeChild(expansionDot);
|
|
767
|
-
}
|
|
768
|
-
const dotIndex = this.expansionDots.indexOf(expansionDot);
|
|
769
|
-
if (dotIndex >= 0) {
|
|
770
|
-
this.expansionDots.splice(dotIndex, 1);
|
|
771
|
-
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// Show all intermediate steps between the previous and current major steps
|
|
775
|
-
for (let i = previousStepIndex + 1; i < majorStepIndex; i++) {
|
|
776
|
-
const step = sv.steps[i];
|
|
777
|
-
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
778
|
-
if (step.stepMark > 0) {
|
|
779
|
-
|
|
780
|
-
this._showStep(step);
|
|
781
|
-
|
|
782
|
-
// Also show the corresponding dot
|
|
783
|
-
const stepDotIndex = sv.stepDots.findIndex(dot => dot.equationRef === step);
|
|
784
|
-
if (stepDotIndex >= 0) {
|
|
785
|
-
const stepDot = sv.stepDots[stepDotIndex];
|
|
786
|
-
stepDot.show();
|
|
787
|
-
stepDot.visible = true;
|
|
788
|
-
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// Force a complete refresh of the visualizer to clean up artifacts and rebuild lines
|
|
798
|
-
sv.rebuildVisualizer();
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
/**
|
|
802
|
-
* Properly hides a step and all its child elements
|
|
803
|
-
* @private
|
|
804
|
-
*/
|
|
805
|
-
_hideStep(step) {
|
|
806
|
-
step.visible = false;
|
|
807
|
-
if (step.svgObject) {
|
|
808
|
-
step.svgObject.style.display = 'none';
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Also hide operation display nodes if they exist
|
|
812
|
-
if (step.operationDisplayNode) {
|
|
813
|
-
step.operationDisplayNode.visible = false;
|
|
814
|
-
if (step.operationDisplayNode.svgObject) {
|
|
815
|
-
step.operationDisplayNode.svgObject.style.display = 'none';
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
// Hide any child nodes recursively
|
|
820
|
-
if (step.children && Array.isArray(step.children)) {
|
|
821
|
-
step.children.forEach(child => {
|
|
822
|
-
if (child) {
|
|
823
|
-
this._hideStep(child);
|
|
824
|
-
}
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
/**
|
|
830
|
-
* Properly shows a step and all its child elements
|
|
831
|
-
* @private
|
|
832
|
-
*/
|
|
833
|
-
_showStep(step) {
|
|
834
|
-
step.visible = true;
|
|
835
|
-
if (step.svgObject) {
|
|
836
|
-
step.svgObject.style.display = '';
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// Also show operation display nodes if they exist
|
|
840
|
-
if (step.operationDisplayNode) {
|
|
841
|
-
step.operationDisplayNode.visible = true;
|
|
842
|
-
if (step.operationDisplayNode.svgObject) {
|
|
843
|
-
step.operationDisplayNode.svgObject.style.display = '';
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// Show any child nodes recursively
|
|
848
|
-
if (step.children && Array.isArray(step.children)) {
|
|
849
|
-
step.children.forEach(child => {
|
|
850
|
-
if (child) {
|
|
851
|
-
this._showStep(child);
|
|
852
|
-
}
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
/**
|
|
858
|
-
* Removes lines that connect to hidden dots
|
|
859
|
-
* @private
|
|
860
|
-
*/
|
|
861
|
-
_removeLinesToHiddenDots() {
|
|
862
|
-
const sv = this.stepVisualizer;
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
// Get lines that connect to hidden dots
|
|
866
|
-
const linesToRemove = [];
|
|
867
|
-
sv.stepLines.forEach((line, lineIndex) => {
|
|
868
|
-
const fromDot = sv.stepDots[line.fromDotIndex];
|
|
869
|
-
const toDot = sv.stepDots[line.toDotIndex];
|
|
870
|
-
|
|
871
|
-
if ((fromDot && !fromDot.visible) || (toDot && !toDot.visible)) {
|
|
872
|
-
|
|
873
|
-
linesToRemove.push(line);
|
|
874
|
-
}
|
|
875
|
-
});
|
|
876
|
-
|
|
877
|
-
// Remove the problematic lines
|
|
878
|
-
|
|
879
|
-
linesToRemove.forEach(line => {
|
|
880
|
-
if (line.parent === sv.visualContainer) {
|
|
881
|
-
sv.visualContainer.removeChild(line);
|
|
882
|
-
|
|
883
|
-
}
|
|
884
|
-
const lineIndex = sv.stepLines.indexOf(line);
|
|
885
|
-
if (lineIndex >= 0) {
|
|
886
|
-
sv.stepLines.splice(lineIndex, 1);
|
|
887
|
-
|
|
888
|
-
}
|
|
889
|
-
});
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
}
|
|
1
|
+
import { omdEquationNode } from '../nodes/omdEquationNode.js';
|
|
2
|
+
import { omdColor } from '../../src/omdColor.js';
|
|
3
|
+
import { jsvgLine, jsvgEllipse } from '@teachinglab/jsvg';
|
|
4
|
+
import { getDotRadius } from '../config/omdConfigManager.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handles visual layout, positioning, and visibility management for step visualizations
|
|
8
|
+
*/
|
|
9
|
+
export class omdStepVisualizerLayout {
|
|
10
|
+
constructor(stepVisualizer) {
|
|
11
|
+
this.stepVisualizer = stepVisualizer;
|
|
12
|
+
this.expansionDots = []; // Small dots that show/hide hidden steps
|
|
13
|
+
this.fixedVisualizerPosition = 250; // Fixed position for the step visualizer from left edge
|
|
14
|
+
this.allowEquationRepositioning = true; // Flag to control when equations can be repositioned
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sets the fixed position for the step visualizer
|
|
19
|
+
* @param {number} position - The x position from the left edge where the visualizer should be positioned
|
|
20
|
+
*/
|
|
21
|
+
setFixedVisualizerPosition(position) {
|
|
22
|
+
// Only update if position actually changes
|
|
23
|
+
if (this.fixedVisualizerPosition !== position) {
|
|
24
|
+
this.fixedVisualizerPosition = position;
|
|
25
|
+
// Trigger a layout update if the visualizer is already initialized
|
|
26
|
+
if (this.stepVisualizer && this.stepVisualizer.stepDots.length > 0) {
|
|
27
|
+
this.updateVisualLayout(true); // Allow repositioning for position changes
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Updates the layout of visual elements relative to the sequence
|
|
34
|
+
* @param {boolean} allowRepositioning - Whether to allow equation repositioning (default: false)
|
|
35
|
+
*/
|
|
36
|
+
updateVisualLayout(allowRepositioning = false) {
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if (this.stepVisualizer.stepDots.length === 0) return;
|
|
42
|
+
|
|
43
|
+
// Calculate the total width needed for equations (including any padding)
|
|
44
|
+
const baseEquationWidth = (this.stepVisualizer.sequenceWidth || this.stepVisualizer.width);
|
|
45
|
+
const extraPaddingX = this._getMaxEquationEffectivePaddingX();
|
|
46
|
+
const totalEquationWidth = baseEquationWidth + extraPaddingX;
|
|
47
|
+
|
|
48
|
+
// Position visual container at a fixed position
|
|
49
|
+
const visualX = this.fixedVisualizerPosition;
|
|
50
|
+
this.stepVisualizer.visualContainer.setPosition(visualX, 0);
|
|
51
|
+
|
|
52
|
+
// Only reposition equations if explicitly allowed (not during simple dot clicks)
|
|
53
|
+
if (this.allowEquationRepositioning && allowRepositioning) {
|
|
54
|
+
|
|
55
|
+
// Calculate how much space is available for equations before the visualizer
|
|
56
|
+
const availableEquationSpace = this.fixedVisualizerPosition - this.stepVisualizer.visualSpacing;
|
|
57
|
+
|
|
58
|
+
// If equations are too wide, shift them left to fit
|
|
59
|
+
let equationOffsetX = 0;
|
|
60
|
+
if (totalEquationWidth > availableEquationSpace) {
|
|
61
|
+
equationOffsetX = availableEquationSpace - totalEquationWidth;
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Apply the offset to equation positioning
|
|
66
|
+
this._adjustEquationPositions(equationOffsetX);
|
|
67
|
+
} else {
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Position dots based on visible equations
|
|
73
|
+
const visibleSteps = this.stepVisualizer.steps.filter(s => s.visible !== false);
|
|
74
|
+
let currentY = 0;
|
|
75
|
+
const verticalPadding = 15 * this.stepVisualizer.getFontSize() / this.stepVisualizer.getRootFontSize();
|
|
76
|
+
|
|
77
|
+
visibleSteps.forEach((step, visIndex) => {
|
|
78
|
+
if (step instanceof omdEquationNode) {
|
|
79
|
+
const dotIndex = this.findDotIndexForEquation(step);
|
|
80
|
+
if (dotIndex >= 0 && dotIndex < this.stepVisualizer.stepDots.length) {
|
|
81
|
+
const dot = this.stepVisualizer.stepDots[dotIndex];
|
|
82
|
+
|
|
83
|
+
// Center dot vertically with the equation
|
|
84
|
+
let equationCenter;
|
|
85
|
+
if (step.equalsSign && step.equalsSign.ypos !== undefined) {
|
|
86
|
+
equationCenter = step.equalsSign.ypos + (step.equalsSign.height / 2);
|
|
87
|
+
} else {
|
|
88
|
+
equationCenter = step.getAlignmentBaseline ? step.getAlignmentBaseline() : step.height / 2;
|
|
89
|
+
}
|
|
90
|
+
const dotY = currentY + equationCenter;
|
|
91
|
+
const dotX = (this.stepVisualizer.dotRadius * 3) / 2;
|
|
92
|
+
|
|
93
|
+
dot.setPosition(dotX, dotY);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
currentY += step.height;
|
|
98
|
+
if (visIndex < visibleSteps.length - 1) {
|
|
99
|
+
currentY += verticalPadding;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
this.updateAllLinePositions();
|
|
104
|
+
|
|
105
|
+
// Update container dimensions
|
|
106
|
+
let containerWidth = this.stepVisualizer.dotRadius * 3;
|
|
107
|
+
let containerHeight = this.stepVisualizer.height;
|
|
108
|
+
|
|
109
|
+
// Store the original height before expansion for autoscale calculations
|
|
110
|
+
if (!this.stepVisualizer.sequenceHeight) {
|
|
111
|
+
this.stepVisualizer.sequenceHeight = containerHeight;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const textBoxes = this.stepVisualizer.textBoxManager.getStepTextBoxes();
|
|
115
|
+
if (textBoxes.length > 0) {
|
|
116
|
+
const textBoxWidth = 280;
|
|
117
|
+
containerWidth = Math.max(containerWidth, textBoxWidth + this.stepVisualizer.dotRadius * 2 + 10 + 20);
|
|
118
|
+
|
|
119
|
+
// Calculate the maximum extent of any text box to prevent clipping
|
|
120
|
+
textBoxes.forEach(textBox => {
|
|
121
|
+
if (textBox.interactiveSteps) {
|
|
122
|
+
const dimensions = textBox.interactiveSteps.getDimensions();
|
|
123
|
+
const layoutGroup = textBox.interactiveSteps.getLayoutGroup();
|
|
124
|
+
|
|
125
|
+
// Calculate the bottom of this text box
|
|
126
|
+
const textBoxBottom = layoutGroup.ypos + dimensions.height;
|
|
127
|
+
containerHeight = Math.max(containerHeight, textBoxBottom + 20); // Add some buffer
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (this.stepVisualizer.stepDots.length > 0) {
|
|
133
|
+
const maxRadius = Math.max(...this.stepVisualizer.stepDots.map(d=>d.radius||this.stepVisualizer.dotRadius));
|
|
134
|
+
const containerWidth = maxRadius * 3;
|
|
135
|
+
const maxDotY = Math.max(...this.stepVisualizer.stepDots.map(dot => dot.ypos + this.stepVisualizer.dotRadius));
|
|
136
|
+
containerHeight = Math.max(containerHeight, maxDotY);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.stepVisualizer.visualContainer.setWidthAndHeight(containerWidth, containerHeight);
|
|
140
|
+
this.updateVisualZOrder();
|
|
141
|
+
|
|
142
|
+
// Position expansion dots after main dots are positioned
|
|
143
|
+
this._positionExpansionDots();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Adjusts the horizontal position of all equations by the specified offset
|
|
148
|
+
* @private
|
|
149
|
+
*/
|
|
150
|
+
_adjustEquationPositions(offsetX) {
|
|
151
|
+
if (offsetX === 0) return; // No adjustment needed
|
|
152
|
+
|
|
153
|
+
const sv = this.stepVisualizer;
|
|
154
|
+
|
|
155
|
+
// Adjust position of all steps (equations and operation display nodes)
|
|
156
|
+
sv.steps.forEach(step => {
|
|
157
|
+
if (step && step.setPosition) {
|
|
158
|
+
const currentX = step.xpos || 0;
|
|
159
|
+
const currentY = step.ypos || 0;
|
|
160
|
+
step.setPosition(currentX + offsetX, currentY);
|
|
161
|
+
|
|
162
|
+
// Also adjust operation display nodes if they exist
|
|
163
|
+
if (step.operationDisplayNode && step.operationDisplayNode.setPosition) {
|
|
164
|
+
const opCurrentX = step.operationDisplayNode.xpos || 0;
|
|
165
|
+
const opCurrentY = step.operationDisplayNode.ypos || 0;
|
|
166
|
+
step.operationDisplayNode.setPosition(opCurrentX + offsetX, opCurrentY);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Computes the maximum horizontal padding (x) among visible equations, if configured.
|
|
176
|
+
* This allows dots to shift further right when pill background padding is added.
|
|
177
|
+
* @returns {number}
|
|
178
|
+
* @private
|
|
179
|
+
*/
|
|
180
|
+
_getMaxEquationEffectivePaddingX() {
|
|
181
|
+
try {
|
|
182
|
+
const steps = this.stepVisualizer.steps || [];
|
|
183
|
+
let maxPadX = 0;
|
|
184
|
+
steps.forEach(step => {
|
|
185
|
+
if (step instanceof omdEquationNode && step.visible !== false) {
|
|
186
|
+
if (typeof step.getEffectiveBackgroundPaddingX === 'function') {
|
|
187
|
+
const px = Number(step.getEffectiveBackgroundPaddingX());
|
|
188
|
+
maxPadX = Math.max(maxPadX, isNaN(px) ? 0 : px);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
return maxPadX;
|
|
193
|
+
} catch (_) {
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Finds the dot index for a given equation
|
|
200
|
+
*/
|
|
201
|
+
findDotIndexForEquation(equation) {
|
|
202
|
+
return this.stepVisualizer.stepDots.findIndex(dot => dot.equationRef === equation);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Updates the z-order of visual elements
|
|
207
|
+
*/
|
|
208
|
+
updateVisualZOrder() {
|
|
209
|
+
if (!this.stepVisualizer.visualContainer) return;
|
|
210
|
+
|
|
211
|
+
// Lines behind (z-index 1)
|
|
212
|
+
this.stepVisualizer.stepLines.forEach(line => {
|
|
213
|
+
if (line && line.svgObject) {
|
|
214
|
+
line.svgObject.style.zIndex = '1';
|
|
215
|
+
if (line.parentNode !== this.stepVisualizer.visualContainer) {
|
|
216
|
+
this.stepVisualizer.visualContainer.addChild(line);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Dots in front (z-index 2)
|
|
222
|
+
this.stepVisualizer.stepDots.forEach(dot => {
|
|
223
|
+
if (dot && dot.svgObject) {
|
|
224
|
+
dot.svgObject.style.zIndex = '2';
|
|
225
|
+
if (dot.parentNode !== this.stepVisualizer.visualContainer) {
|
|
226
|
+
this.stepVisualizer.visualContainer.addChild(dot);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Text boxes on top (z-index 3)
|
|
232
|
+
const textBoxes = this.stepVisualizer.textBoxManager.getStepTextBoxes();
|
|
233
|
+
textBoxes.forEach(textBox => {
|
|
234
|
+
if (textBox && textBox.svgObject) {
|
|
235
|
+
textBox.svgObject.style.zIndex = '3';
|
|
236
|
+
if (textBox.parentNode !== this.stepVisualizer.visualContainer) {
|
|
237
|
+
this.stepVisualizer.visualContainer.addChild(textBox);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Expansion dots on top of regular dots (z-index 4)
|
|
243
|
+
this.expansionDots.forEach(dot => {
|
|
244
|
+
if (dot && dot.svgObject) {
|
|
245
|
+
dot.svgObject.style.zIndex = '4';
|
|
246
|
+
if (dot.parentNode !== this.stepVisualizer.visualContainer) {
|
|
247
|
+
this.stepVisualizer.visualContainer.addChild(dot);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Updates all line positions to connect dot centers
|
|
255
|
+
*/
|
|
256
|
+
updateAllLinePositions() {
|
|
257
|
+
this.stepVisualizer.stepLines.forEach(line => {
|
|
258
|
+
const fromDot = this.stepVisualizer.stepDots[line.fromDotIndex];
|
|
259
|
+
const toDot = this.stepVisualizer.stepDots[line.toDotIndex];
|
|
260
|
+
|
|
261
|
+
if (fromDot && toDot) {
|
|
262
|
+
line.setEndpoints(fromDot.xpos, fromDot.ypos, toDot.xpos, toDot.ypos);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Updates visibility of visual elements based on equation visibility
|
|
269
|
+
*/
|
|
270
|
+
updateVisualVisibility() {
|
|
271
|
+
|
|
272
|
+
const sv = this.stepVisualizer;
|
|
273
|
+
|
|
274
|
+
// Update dot visibility and color first, which is the source of truth
|
|
275
|
+
const dotColor = sv.styling?.dotColor || omdColor.stepColor;
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
sv.stepDots.forEach((dot, index) => {
|
|
279
|
+
if (dot.equationRef && dot.equationRef.visible !== false) {
|
|
280
|
+
dot.setFillColor(dotColor);
|
|
281
|
+
dot.setStrokeColor(dotColor);
|
|
282
|
+
dot.show();
|
|
283
|
+
dot.visible = true; // Use the dot's own visibility property
|
|
284
|
+
|
|
285
|
+
} else {
|
|
286
|
+
dot.hide();
|
|
287
|
+
dot.visible = false;
|
|
288
|
+
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Clear existing expansion dots
|
|
293
|
+
|
|
294
|
+
this._clearExpansionDots();
|
|
295
|
+
|
|
296
|
+
// Remove all old lines from the container and the array
|
|
297
|
+
|
|
298
|
+
sv.stepLines.forEach(line => {
|
|
299
|
+
// Remove the line if it is currently a child of the visualContainer
|
|
300
|
+
if (line.parent === sv.visualContainer) {
|
|
301
|
+
sv.visualContainer.removeChild(line);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
sv.stepLines = [];
|
|
305
|
+
|
|
306
|
+
// Get the dots that are currently visible
|
|
307
|
+
const visibleDots = sv.stepDots.filter(dot => dot.visible);
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
// Re-create connecting lines only between the visible dots
|
|
311
|
+
|
|
312
|
+
for (let i = 0; i < visibleDots.length - 1; i++) {
|
|
313
|
+
const fromDot = visibleDots[i];
|
|
314
|
+
const toDot = visibleDots[i + 1];
|
|
315
|
+
|
|
316
|
+
const line = new jsvgLine();
|
|
317
|
+
const lineColor = sv.styling?.lineColor || omdColor.stepColor;
|
|
318
|
+
line.setStrokeColor(lineColor);
|
|
319
|
+
line.setStrokeWidth(sv.styling?.lineWidth || sv.lineWidth);
|
|
320
|
+
line.fromDotIndex = sv.stepDots.indexOf(fromDot);
|
|
321
|
+
line.toDotIndex = sv.stepDots.indexOf(toDot);
|
|
322
|
+
|
|
323
|
+
sv.visualContainer.addChild(line);
|
|
324
|
+
sv.stepLines.push(line);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
// After creating the lines, update their positions
|
|
329
|
+
this.updateAllLinePositions();
|
|
330
|
+
|
|
331
|
+
// Create expansion dots for dots that have hidden steps before them
|
|
332
|
+
|
|
333
|
+
this._createExpansionDots();
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
this._positionExpansionDots();
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Updates the clickability of a dot
|
|
343
|
+
*/
|
|
344
|
+
updateDotClickability(dot) {
|
|
345
|
+
if (this.stepVisualizer.dotsClickable) {
|
|
346
|
+
dot.svgObject.style.cursor = "pointer";
|
|
347
|
+
dot.svgObject.onclick = (event) => {
|
|
348
|
+
try {
|
|
349
|
+
const idx = this.stepVisualizer.stepDots.indexOf(dot);
|
|
350
|
+
if (idx < 0) return; // orphan dot, ignore
|
|
351
|
+
this.stepVisualizer._handleDotClick(dot, idx);
|
|
352
|
+
event.stopPropagation();
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error('Error in dot click handler:', error);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
} else {
|
|
358
|
+
dot.svgObject.style.cursor = "default";
|
|
359
|
+
dot.svgObject.onclick = null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Clears all expansion dots
|
|
365
|
+
* @private
|
|
366
|
+
*/
|
|
367
|
+
_clearExpansionDots() {
|
|
368
|
+
this.expansionDots.forEach(dot => {
|
|
369
|
+
if (dot.parentNode === this.stepVisualizer.visualContainer) {
|
|
370
|
+
this.stepVisualizer.visualContainer.removeChild(dot);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
this.expansionDots = [];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Creates expansion dots for visible dots that have hidden steps before them
|
|
378
|
+
* @private
|
|
379
|
+
*/
|
|
380
|
+
_createExpansionDots() {
|
|
381
|
+
|
|
382
|
+
const sv = this.stepVisualizer;
|
|
383
|
+
const allDots = sv.stepDots;
|
|
384
|
+
const visibleDots = sv.stepDots.filter(dot => dot.visible);
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
// Debug all steps and their properties
|
|
389
|
+
|
|
390
|
+
sv.steps.forEach((step, i) => {
|
|
391
|
+
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
392
|
+
|
|
393
|
+
} else {
|
|
394
|
+
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Debug all dots and their properties
|
|
399
|
+
|
|
400
|
+
allDots.forEach((dot, i) => {
|
|
401
|
+
if (dot && dot.equationRef) {
|
|
402
|
+
|
|
403
|
+
} else {
|
|
404
|
+
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
// Check for hidden intermediate steps between consecutive visible major steps (stepMark = 0)
|
|
411
|
+
const visibleMajorSteps = [];
|
|
412
|
+
sv.steps.forEach((step, stepIndex) => {
|
|
413
|
+
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
414
|
+
if (step.stepMark === 0 && step.visible === true) {
|
|
415
|
+
visibleMajorSteps.push(stepIndex);
|
|
416
|
+
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
// Check between consecutive visible major steps for hidden intermediate steps
|
|
424
|
+
for (let i = 1; i < visibleMajorSteps.length; i++) {
|
|
425
|
+
const previousMajorStepIndex = visibleMajorSteps[i - 1];
|
|
426
|
+
const currentMajorStepIndex = visibleMajorSteps[i];
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
// Count hidden intermediate steps between these major steps
|
|
431
|
+
let hiddenIntermediateCount = 0;
|
|
432
|
+
for (let j = previousMajorStepIndex + 1; j < currentMajorStepIndex; j++) {
|
|
433
|
+
const step = sv.steps[j];
|
|
434
|
+
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
435
|
+
if (step.stepMark > 0 && step.visible === false) {
|
|
436
|
+
hiddenIntermediateCount++;
|
|
437
|
+
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
if (hiddenIntermediateCount > 0) {
|
|
445
|
+
|
|
446
|
+
// Find the dot for the current major step to position the expansion dot above it
|
|
447
|
+
const currentMajorStep = sv.steps[currentMajorStepIndex];
|
|
448
|
+
const currentDotIndex = sv.stepDots.findIndex(dot => dot.equationRef === currentMajorStep);
|
|
449
|
+
|
|
450
|
+
if (currentDotIndex >= 0) {
|
|
451
|
+
// Find the position in the visible dots array
|
|
452
|
+
const visibleDotIndex = i; // i is the position in visibleMajorSteps array
|
|
453
|
+
|
|
454
|
+
const expansionDot = this._createSingleExpansionDot(visibleDotIndex, previousMajorStepIndex, hiddenIntermediateCount);
|
|
455
|
+
expansionDot.majorStepIndex = currentMajorStepIndex; // Store for reference
|
|
456
|
+
this.expansionDots.push(expansionDot);
|
|
457
|
+
sv.visualContainer.addChild(expansionDot);
|
|
458
|
+
|
|
459
|
+
} else {
|
|
460
|
+
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
// Also create collapse dots for expanded sequences
|
|
469
|
+
this._createCollapseDots();
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Counts intermediate steps (stepMark > 0) between two visible dots
|
|
477
|
+
* @private
|
|
478
|
+
*/
|
|
479
|
+
_countIntermediateStepsBetween(fromDotIndex, toDotIndex) {
|
|
480
|
+
const sv = this.stepVisualizer;
|
|
481
|
+
let count = 0;
|
|
482
|
+
|
|
483
|
+
// Get the equation references for the from and to dots
|
|
484
|
+
const fromEquation = sv.stepDots[fromDotIndex]?.equationRef;
|
|
485
|
+
const toEquation = sv.stepDots[toDotIndex]?.equationRef;
|
|
486
|
+
|
|
487
|
+
if (!fromEquation || !toEquation) {
|
|
488
|
+
|
|
489
|
+
return 0;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Find the step indices in the main steps array
|
|
493
|
+
const fromStepIndex = sv.steps.indexOf(fromEquation);
|
|
494
|
+
const toStepIndex = sv.steps.indexOf(toEquation);
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
// Count intermediate steps between these two major steps
|
|
499
|
+
for (let i = fromStepIndex + 1; i < toStepIndex; i++) {
|
|
500
|
+
const step = sv.steps[i];
|
|
501
|
+
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
502
|
+
// Count intermediate steps (stepMark > 0) that are currently hidden
|
|
503
|
+
if (step.stepMark !== undefined && step.stepMark > 0 && step.visible === false) {
|
|
504
|
+
|
|
505
|
+
count++;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
return count;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Counts hidden steps between two step indices (legacy method for backward compatibility)
|
|
516
|
+
* @private
|
|
517
|
+
*/
|
|
518
|
+
_countHiddenStepsBetween(fromIndex, toIndex) {
|
|
519
|
+
return this._countIntermediateStepsBetween(fromIndex, toIndex);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Creates a single expansion dot
|
|
524
|
+
* @private
|
|
525
|
+
*/
|
|
526
|
+
_createSingleExpansionDot(currentStepIndex, previousStepIndex, hiddenCount) {
|
|
527
|
+
|
|
528
|
+
const sv = this.stepVisualizer;
|
|
529
|
+
const baseRadius = sv.styling?.dotRadius || getDotRadius(0);
|
|
530
|
+
const expansionRadius = Math.max(3, baseRadius * (sv.styling?.expansionDotScale || 0.4));
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
const expansionDot = new jsvgEllipse();
|
|
535
|
+
expansionDot.setWidthAndHeight(expansionRadius * 2, expansionRadius * 2);
|
|
536
|
+
|
|
537
|
+
// Use same color as regular dots from styling
|
|
538
|
+
const dotColor = sv.styling?.dotColor || omdColor.stepColor;
|
|
539
|
+
|
|
540
|
+
expansionDot.setFillColor(dotColor);
|
|
541
|
+
expansionDot.setStrokeColor(dotColor);
|
|
542
|
+
expansionDot.setStrokeWidth(sv.styling?.dotStrokeWidth || 1);
|
|
543
|
+
|
|
544
|
+
// Store metadata
|
|
545
|
+
expansionDot.isExpansionDot = true;
|
|
546
|
+
expansionDot.currentStepIndex = currentStepIndex;
|
|
547
|
+
expansionDot.previousStepIndex = previousStepIndex;
|
|
548
|
+
expansionDot.hiddenCount = hiddenCount;
|
|
549
|
+
expansionDot.radius = expansionRadius;
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
// Make it clickable
|
|
553
|
+
expansionDot.svgObject.style.cursor = "pointer";
|
|
554
|
+
expansionDot.svgObject.onclick = (event) => {
|
|
555
|
+
try {
|
|
556
|
+
|
|
557
|
+
this._handleExpansionDotClick(expansionDot);
|
|
558
|
+
event.stopPropagation();
|
|
559
|
+
} catch (error) {
|
|
560
|
+
console.error('Error in expansion dot click handler:', error);
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
return expansionDot;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Positions expansion dots above their corresponding main dots
|
|
572
|
+
* @private
|
|
573
|
+
*/
|
|
574
|
+
_positionExpansionDots() {
|
|
575
|
+
|
|
576
|
+
const sv = this.stepVisualizer;
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
this.expansionDots.forEach((expansionDot, index) => {
|
|
581
|
+
|
|
582
|
+
let targetDot;
|
|
583
|
+
|
|
584
|
+
if (expansionDot.isCollapseDot) {
|
|
585
|
+
|
|
586
|
+
// For collapse dots, use the currentStepIndex which points to the dot index
|
|
587
|
+
const dotIndex = expansionDot.currentStepIndex;
|
|
588
|
+
targetDot = sv.stepDots[dotIndex];
|
|
589
|
+
|
|
590
|
+
} else {
|
|
591
|
+
|
|
592
|
+
// For expansion dots, we need to find the actual visible dot that corresponds to the major step
|
|
593
|
+
const majorStepIndex = expansionDot.majorStepIndex;
|
|
594
|
+
const majorStep = sv.steps[majorStepIndex];
|
|
595
|
+
|
|
596
|
+
if (majorStep) {
|
|
597
|
+
// Find the dot that corresponds to this major step
|
|
598
|
+
const dotIndex = sv.stepDots.findIndex(dot => dot.equationRef === majorStep);
|
|
599
|
+
targetDot = sv.stepDots[dotIndex];
|
|
600
|
+
|
|
601
|
+
} else {
|
|
602
|
+
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (targetDot && targetDot.visible) {
|
|
607
|
+
const offsetY = -(expansionDot.radius * 2 + 8); // Position above main dot
|
|
608
|
+
const newX = targetDot.xpos;
|
|
609
|
+
const newY = targetDot.ypos + offsetY;
|
|
610
|
+
|
|
611
|
+
expansionDot.setPosition(newX, newY);
|
|
612
|
+
|
|
613
|
+
} else {
|
|
614
|
+
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Creates collapse dots for expanded sequences
|
|
623
|
+
* @private
|
|
624
|
+
*/
|
|
625
|
+
_createCollapseDots() {
|
|
626
|
+
|
|
627
|
+
const sv = this.stepVisualizer;
|
|
628
|
+
const allDots = sv.stepDots;
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
// Group visible intermediate steps by their consecutive sequences
|
|
633
|
+
const intermediateGroups = [];
|
|
634
|
+
let currentGroup = [];
|
|
635
|
+
|
|
636
|
+
allDots.forEach((dot, index) => {
|
|
637
|
+
if (dot && dot.visible && dot.equationRef) {
|
|
638
|
+
const stepMark = dot.equationRef.stepMark;
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
if (stepMark !== undefined && stepMark > 0) {
|
|
642
|
+
currentGroup.push(index);
|
|
643
|
+
|
|
644
|
+
} else if (currentGroup.length > 0) {
|
|
645
|
+
// We hit a major step, so end the current group
|
|
646
|
+
intermediateGroups.push([...currentGroup]);
|
|
647
|
+
|
|
648
|
+
currentGroup = [];
|
|
649
|
+
}
|
|
650
|
+
} else if (currentGroup.length > 0) {
|
|
651
|
+
// We hit a non-visible dot, so end the current group
|
|
652
|
+
intermediateGroups.push([...currentGroup]);
|
|
653
|
+
|
|
654
|
+
currentGroup = [];
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// Don't forget the last group if it exists
|
|
659
|
+
if (currentGroup.length > 0) {
|
|
660
|
+
intermediateGroups.push([...currentGroup]);
|
|
661
|
+
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
// Create a collapse dot for each group
|
|
667
|
+
intermediateGroups.forEach((group, groupIndex) => {
|
|
668
|
+
if (group.length > 0) {
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
// Find the major step that comes after the last intermediate step in this group
|
|
672
|
+
const lastIntermediateIndex = group[group.length - 1];
|
|
673
|
+
const lastIntermediateDot = sv.stepDots[lastIntermediateIndex];
|
|
674
|
+
const lastIntermediateStep = lastIntermediateDot.equationRef;
|
|
675
|
+
const lastIntermediateStepIndex = sv.steps.indexOf(lastIntermediateStep);
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
// Find the next major step (stepMark = 0) after the intermediate steps
|
|
680
|
+
let majorStepAfterIndex = -1;
|
|
681
|
+
for (let i = lastIntermediateStepIndex + 1; i < sv.steps.length; i++) {
|
|
682
|
+
const step = sv.steps[i];
|
|
683
|
+
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
684
|
+
if (step.stepMark === 0 && step.visible === true) {
|
|
685
|
+
majorStepAfterIndex = i;
|
|
686
|
+
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (majorStepAfterIndex >= 0) {
|
|
693
|
+
// Find the dot index for this major step
|
|
694
|
+
const majorStepAfter = sv.steps[majorStepAfterIndex];
|
|
695
|
+
const majorDotIndex = sv.stepDots.findIndex(dot => dot.equationRef === majorStepAfter);
|
|
696
|
+
|
|
697
|
+
if (majorDotIndex >= 0) {
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
const collapseDot = this._createSingleExpansionDot(majorDotIndex, -1, group.length);
|
|
701
|
+
collapseDot.isCollapseDot = true;
|
|
702
|
+
collapseDot.intermediateSteps = group;
|
|
703
|
+
collapseDot.groupIndex = groupIndex; // Store group reference
|
|
704
|
+
this.expansionDots.push(collapseDot);
|
|
705
|
+
sv.visualContainer.addChild(collapseDot);
|
|
706
|
+
|
|
707
|
+
} else {
|
|
708
|
+
|
|
709
|
+
}
|
|
710
|
+
} else {
|
|
711
|
+
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Handles clicking on an expansion dot to toggle hidden steps
|
|
721
|
+
* @private
|
|
722
|
+
*/
|
|
723
|
+
_handleExpansionDotClick(expansionDot) {
|
|
724
|
+
const sv = this.stepVisualizer;
|
|
725
|
+
|
|
726
|
+
// Clear all step visualizer highlights when expanding/contracting
|
|
727
|
+
if (sv.highlighting && typeof sv.highlighting.clearAllExplainHighlights === 'function') {
|
|
728
|
+
sv.highlighting.clearAllExplainHighlights();
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (expansionDot.isCollapseDot) {
|
|
732
|
+
// Handle collapse dot click - hide only the specific group of intermediate steps
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
// Hide only the intermediate steps in this specific group
|
|
736
|
+
const intermediateSteps = expansionDot.intermediateSteps || [];
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
intermediateSteps.forEach(dotIndex => {
|
|
740
|
+
const dot = sv.stepDots[dotIndex];
|
|
741
|
+
if (dot && dot.equationRef) {
|
|
742
|
+
|
|
743
|
+
this._hideStep(dot.equationRef);
|
|
744
|
+
|
|
745
|
+
// Also hide the corresponding dot
|
|
746
|
+
dot.hide();
|
|
747
|
+
dot.visible = false;
|
|
748
|
+
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
// Remove any lines that connect to the hidden dots
|
|
753
|
+
|
|
754
|
+
this._removeLinesToHiddenDots();
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
} else {
|
|
758
|
+
// Handle expansion dot click - show steps between the major steps
|
|
759
|
+
const { majorStepIndex, previousStepIndex } = expansionDot;
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
// Remove this expansion dot immediately since we're expanding
|
|
764
|
+
|
|
765
|
+
if (expansionDot.parentNode === sv.visualContainer) {
|
|
766
|
+
sv.visualContainer.removeChild(expansionDot);
|
|
767
|
+
}
|
|
768
|
+
const dotIndex = this.expansionDots.indexOf(expansionDot);
|
|
769
|
+
if (dotIndex >= 0) {
|
|
770
|
+
this.expansionDots.splice(dotIndex, 1);
|
|
771
|
+
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Show all intermediate steps between the previous and current major steps
|
|
775
|
+
for (let i = previousStepIndex + 1; i < majorStepIndex; i++) {
|
|
776
|
+
const step = sv.steps[i];
|
|
777
|
+
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
778
|
+
if (step.stepMark > 0) {
|
|
779
|
+
|
|
780
|
+
this._showStep(step);
|
|
781
|
+
|
|
782
|
+
// Also show the corresponding dot
|
|
783
|
+
const stepDotIndex = sv.stepDots.findIndex(dot => dot.equationRef === step);
|
|
784
|
+
if (stepDotIndex >= 0) {
|
|
785
|
+
const stepDot = sv.stepDots[stepDotIndex];
|
|
786
|
+
stepDot.show();
|
|
787
|
+
stepDot.visible = true;
|
|
788
|
+
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Force a complete refresh of the visualizer to clean up artifacts and rebuild lines
|
|
798
|
+
sv.rebuildVisualizer();
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Properly hides a step and all its child elements
|
|
803
|
+
* @private
|
|
804
|
+
*/
|
|
805
|
+
_hideStep(step) {
|
|
806
|
+
step.visible = false;
|
|
807
|
+
if (step.svgObject) {
|
|
808
|
+
step.svgObject.style.display = 'none';
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Also hide operation display nodes if they exist
|
|
812
|
+
if (step.operationDisplayNode) {
|
|
813
|
+
step.operationDisplayNode.visible = false;
|
|
814
|
+
if (step.operationDisplayNode.svgObject) {
|
|
815
|
+
step.operationDisplayNode.svgObject.style.display = 'none';
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Hide any child nodes recursively
|
|
820
|
+
if (step.children && Array.isArray(step.children)) {
|
|
821
|
+
step.children.forEach(child => {
|
|
822
|
+
if (child) {
|
|
823
|
+
this._hideStep(child);
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Properly shows a step and all its child elements
|
|
831
|
+
* @private
|
|
832
|
+
*/
|
|
833
|
+
_showStep(step) {
|
|
834
|
+
step.visible = true;
|
|
835
|
+
if (step.svgObject) {
|
|
836
|
+
step.svgObject.style.display = '';
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Also show operation display nodes if they exist
|
|
840
|
+
if (step.operationDisplayNode) {
|
|
841
|
+
step.operationDisplayNode.visible = true;
|
|
842
|
+
if (step.operationDisplayNode.svgObject) {
|
|
843
|
+
step.operationDisplayNode.svgObject.style.display = '';
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Show any child nodes recursively
|
|
848
|
+
if (step.children && Array.isArray(step.children)) {
|
|
849
|
+
step.children.forEach(child => {
|
|
850
|
+
if (child) {
|
|
851
|
+
this._showStep(child);
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Removes lines that connect to hidden dots
|
|
859
|
+
* @private
|
|
860
|
+
*/
|
|
861
|
+
_removeLinesToHiddenDots() {
|
|
862
|
+
const sv = this.stepVisualizer;
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
// Get lines that connect to hidden dots
|
|
866
|
+
const linesToRemove = [];
|
|
867
|
+
sv.stepLines.forEach((line, lineIndex) => {
|
|
868
|
+
const fromDot = sv.stepDots[line.fromDotIndex];
|
|
869
|
+
const toDot = sv.stepDots[line.toDotIndex];
|
|
870
|
+
|
|
871
|
+
if ((fromDot && !fromDot.visible) || (toDot && !toDot.visible)) {
|
|
872
|
+
|
|
873
|
+
linesToRemove.push(line);
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
// Remove the problematic lines
|
|
878
|
+
|
|
879
|
+
linesToRemove.forEach(line => {
|
|
880
|
+
if (line.parent === sv.visualContainer) {
|
|
881
|
+
sv.visualContainer.removeChild(line);
|
|
882
|
+
|
|
883
|
+
}
|
|
884
|
+
const lineIndex = sv.stepLines.indexOf(line);
|
|
885
|
+
if (lineIndex >= 0) {
|
|
886
|
+
sv.stepLines.splice(lineIndex, 1);
|
|
887
|
+
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
}
|
|
893
893
|
}
|