@teachinglab/omd 0.2.6 → 0.2.8
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/canvas/ui/toolbar.js +0 -15
- package/omd/config/omdConfigManager.js +2 -2
- package/omd/core/omdEquationStack.js +11 -2
- package/omd/display/omdDisplay.js +494 -80
- package/omd/nodes/omdEquationNode.js +46 -6
- package/omd/step-visualizer/omdStepVisualizer.js +387 -42
- package/omd/step-visualizer/omdStepVisualizerLayout.js +654 -11
- package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +46 -8
- package/omd/utils/omdStepVisualizerInteractiveSteps.js +318 -121
- package/package.json +1 -1
- package/src/omdBalanceHanger.js +31 -1
- package/src/omdColor.js +1 -0
- package/src/omdCoordinatePlane.js +53 -3
- package/src/omdMetaExpression.js +8 -4
- package/src/omdTable.js +182 -52
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { omdEquationNode } from '../nodes/omdEquationNode.js';
|
|
2
2
|
import { omdColor } from '../../src/omdColor.js';
|
|
3
|
-
import { jsvgLine } from '@teachinglab/jsvg';
|
|
3
|
+
import { jsvgLine, jsvgEllipse } from '@teachinglab/jsvg';
|
|
4
|
+
import { getDotRadius } from '../config/omdConfigManager.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Handles visual layout, positioning, and visibility management for step visualizations
|
|
@@ -8,22 +9,66 @@ import { jsvgLine } from '@teachinglab/jsvg';
|
|
|
8
9
|
export class omdStepVisualizerLayout {
|
|
9
10
|
constructor(stepVisualizer) {
|
|
10
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
|
+
}
|
|
11
30
|
}
|
|
12
31
|
|
|
13
32
|
/**
|
|
14
33
|
* Updates the layout of visual elements relative to the sequence
|
|
34
|
+
* @param {boolean} allowRepositioning - Whether to allow equation repositioning (default: false)
|
|
15
35
|
*/
|
|
16
|
-
updateVisualLayout() {
|
|
36
|
+
updateVisualLayout(allowRepositioning = false) {
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
17
41
|
if (this.stepVisualizer.stepDots.length === 0) return;
|
|
18
42
|
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
const baseRight = (this.stepVisualizer.sequenceWidth || this.stepVisualizer.width);
|
|
22
|
-
// Use EFFECTIVE padding (after pill clamping) to avoid overlap when pills are wider
|
|
43
|
+
// Calculate the total width needed for equations (including any padding)
|
|
44
|
+
const baseEquationWidth = (this.stepVisualizer.sequenceWidth || this.stepVisualizer.width);
|
|
23
45
|
const extraPaddingX = this._getMaxEquationEffectivePaddingX();
|
|
24
|
-
const
|
|
46
|
+
const totalEquationWidth = baseEquationWidth + extraPaddingX;
|
|
47
|
+
|
|
48
|
+
// Position visual container at a fixed position
|
|
49
|
+
const visualX = this.fixedVisualizerPosition;
|
|
25
50
|
this.stepVisualizer.visualContainer.setPosition(visualX, 0);
|
|
26
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
|
+
|
|
27
72
|
// Position dots based on visible equations
|
|
28
73
|
const visibleSteps = this.stepVisualizer.steps.filter(s => s.visible !== false);
|
|
29
74
|
let currentY = 0;
|
|
@@ -61,6 +106,11 @@ export class omdStepVisualizerLayout {
|
|
|
61
106
|
let containerWidth = this.stepVisualizer.dotRadius * 3;
|
|
62
107
|
let containerHeight = this.stepVisualizer.height;
|
|
63
108
|
|
|
109
|
+
// Store the original height before expansion for autoscale calculations
|
|
110
|
+
if (!this.stepVisualizer.sequenceHeight) {
|
|
111
|
+
this.stepVisualizer.sequenceHeight = containerHeight;
|
|
112
|
+
}
|
|
113
|
+
|
|
64
114
|
const textBoxes = this.stepVisualizer.textBoxManager.getStepTextBoxes();
|
|
65
115
|
if (textBoxes.length > 0) {
|
|
66
116
|
const textBoxWidth = 280;
|
|
@@ -88,6 +138,37 @@ export class omdStepVisualizerLayout {
|
|
|
88
138
|
|
|
89
139
|
this.stepVisualizer.visualContainer.setWidthAndHeight(containerWidth, containerHeight);
|
|
90
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
|
+
|
|
91
172
|
}
|
|
92
173
|
|
|
93
174
|
/**
|
|
@@ -157,6 +238,16 @@ export class omdStepVisualizerLayout {
|
|
|
157
238
|
}
|
|
158
239
|
}
|
|
159
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
|
+
});
|
|
160
251
|
}
|
|
161
252
|
|
|
162
253
|
/**
|
|
@@ -177,20 +268,33 @@ export class omdStepVisualizerLayout {
|
|
|
177
268
|
* Updates visibility of visual elements based on equation visibility
|
|
178
269
|
*/
|
|
179
270
|
updateVisualVisibility() {
|
|
271
|
+
|
|
180
272
|
const sv = this.stepVisualizer;
|
|
181
273
|
|
|
182
|
-
// Update dot visibility first, which is the source of truth
|
|
183
|
-
sv.
|
|
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) => {
|
|
184
279
|
if (dot.equationRef && dot.equationRef.visible !== false) {
|
|
280
|
+
dot.setFillColor(dotColor);
|
|
281
|
+
dot.setStrokeColor(dotColor);
|
|
185
282
|
dot.show();
|
|
186
283
|
dot.visible = true; // Use the dot's own visibility property
|
|
284
|
+
|
|
187
285
|
} else {
|
|
188
286
|
dot.hide();
|
|
189
287
|
dot.visible = false;
|
|
288
|
+
|
|
190
289
|
}
|
|
191
290
|
});
|
|
192
291
|
|
|
292
|
+
// Clear existing expansion dots
|
|
293
|
+
|
|
294
|
+
this._clearExpansionDots();
|
|
295
|
+
|
|
193
296
|
// Remove all old lines from the container and the array
|
|
297
|
+
|
|
194
298
|
sv.stepLines.forEach(line => {
|
|
195
299
|
// Remove the line if it is currently a child of the visualContainer
|
|
196
300
|
if (line.parent === sv.visualContainer) {
|
|
@@ -201,24 +305,37 @@ export class omdStepVisualizerLayout {
|
|
|
201
305
|
|
|
202
306
|
// Get the dots that are currently visible
|
|
203
307
|
const visibleDots = sv.stepDots.filter(dot => dot.visible);
|
|
308
|
+
|
|
204
309
|
|
|
205
310
|
// Re-create connecting lines only between the visible dots
|
|
311
|
+
|
|
206
312
|
for (let i = 0; i < visibleDots.length - 1; i++) {
|
|
207
313
|
const fromDot = visibleDots[i];
|
|
208
314
|
const toDot = visibleDots[i + 1];
|
|
209
315
|
|
|
210
316
|
const line = new jsvgLine();
|
|
211
|
-
|
|
212
|
-
line.
|
|
317
|
+
const lineColor = sv.styling?.lineColor || omdColor.stepColor;
|
|
318
|
+
line.setStrokeColor(lineColor);
|
|
319
|
+
line.setStrokeWidth(sv.styling?.lineWidth || sv.lineWidth);
|
|
213
320
|
line.fromDotIndex = sv.stepDots.indexOf(fromDot);
|
|
214
321
|
line.toDotIndex = sv.stepDots.indexOf(toDot);
|
|
215
322
|
|
|
216
323
|
sv.visualContainer.addChild(line);
|
|
217
324
|
sv.stepLines.push(line);
|
|
218
325
|
}
|
|
326
|
+
|
|
219
327
|
|
|
220
328
|
// After creating the lines, update their positions
|
|
221
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
|
+
|
|
222
339
|
}
|
|
223
340
|
|
|
224
341
|
/**
|
|
@@ -242,4 +359,530 @@ export class omdStepVisualizerLayout {
|
|
|
242
359
|
dot.svgObject.onclick = null;
|
|
243
360
|
}
|
|
244
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
|
+
if (expansionDot.isCollapseDot) {
|
|
727
|
+
// Handle collapse dot click - hide only the specific group of intermediate steps
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
// Hide only the intermediate steps in this specific group
|
|
731
|
+
const intermediateSteps = expansionDot.intermediateSteps || [];
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
intermediateSteps.forEach(dotIndex => {
|
|
735
|
+
const dot = sv.stepDots[dotIndex];
|
|
736
|
+
if (dot && dot.equationRef) {
|
|
737
|
+
|
|
738
|
+
this._hideStep(dot.equationRef);
|
|
739
|
+
|
|
740
|
+
// Also hide the corresponding dot
|
|
741
|
+
dot.hide();
|
|
742
|
+
dot.visible = false;
|
|
743
|
+
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
// Remove any lines that connect to the hidden dots
|
|
748
|
+
|
|
749
|
+
this._removeLinesToHiddenDots();
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
} else {
|
|
753
|
+
// Handle expansion dot click - show steps between the major steps
|
|
754
|
+
const { majorStepIndex, previousStepIndex } = expansionDot;
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
// Remove this expansion dot immediately since we're expanding
|
|
759
|
+
|
|
760
|
+
if (expansionDot.parentNode === sv.visualContainer) {
|
|
761
|
+
sv.visualContainer.removeChild(expansionDot);
|
|
762
|
+
}
|
|
763
|
+
const dotIndex = this.expansionDots.indexOf(expansionDot);
|
|
764
|
+
if (dotIndex >= 0) {
|
|
765
|
+
this.expansionDots.splice(dotIndex, 1);
|
|
766
|
+
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Show all intermediate steps between the previous and current major steps
|
|
770
|
+
for (let i = previousStepIndex + 1; i < majorStepIndex; i++) {
|
|
771
|
+
const step = sv.steps[i];
|
|
772
|
+
if (step && (step instanceof omdEquationNode || step.constructor.name === 'omdEquationNode')) {
|
|
773
|
+
if (step.stepMark > 0) {
|
|
774
|
+
|
|
775
|
+
this._showStep(step);
|
|
776
|
+
|
|
777
|
+
// Also show the corresponding dot
|
|
778
|
+
const stepDotIndex = sv.stepDots.findIndex(dot => dot.equationRef === step);
|
|
779
|
+
if (stepDotIndex >= 0) {
|
|
780
|
+
const stepDot = sv.stepDots[stepDotIndex];
|
|
781
|
+
stepDot.show();
|
|
782
|
+
stepDot.visible = true;
|
|
783
|
+
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Force a complete refresh of the visualizer to clean up artifacts and rebuild lines
|
|
793
|
+
sv.rebuildVisualizer();
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Properly hides a step and all its child elements
|
|
798
|
+
* @private
|
|
799
|
+
*/
|
|
800
|
+
_hideStep(step) {
|
|
801
|
+
step.visible = false;
|
|
802
|
+
if (step.svgObject) {
|
|
803
|
+
step.svgObject.style.display = 'none';
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Also hide operation display nodes if they exist
|
|
807
|
+
if (step.operationDisplayNode) {
|
|
808
|
+
step.operationDisplayNode.visible = false;
|
|
809
|
+
if (step.operationDisplayNode.svgObject) {
|
|
810
|
+
step.operationDisplayNode.svgObject.style.display = 'none';
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Hide any child nodes recursively
|
|
815
|
+
if (step.children && Array.isArray(step.children)) {
|
|
816
|
+
step.children.forEach(child => {
|
|
817
|
+
if (child) {
|
|
818
|
+
this._hideStep(child);
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Properly shows a step and all its child elements
|
|
826
|
+
* @private
|
|
827
|
+
*/
|
|
828
|
+
_showStep(step) {
|
|
829
|
+
step.visible = true;
|
|
830
|
+
if (step.svgObject) {
|
|
831
|
+
step.svgObject.style.display = '';
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Also show operation display nodes if they exist
|
|
835
|
+
if (step.operationDisplayNode) {
|
|
836
|
+
step.operationDisplayNode.visible = true;
|
|
837
|
+
if (step.operationDisplayNode.svgObject) {
|
|
838
|
+
step.operationDisplayNode.svgObject.style.display = '';
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Show any child nodes recursively
|
|
843
|
+
if (step.children && Array.isArray(step.children)) {
|
|
844
|
+
step.children.forEach(child => {
|
|
845
|
+
if (child) {
|
|
846
|
+
this._showStep(child);
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Removes lines that connect to hidden dots
|
|
854
|
+
* @private
|
|
855
|
+
*/
|
|
856
|
+
_removeLinesToHiddenDots() {
|
|
857
|
+
const sv = this.stepVisualizer;
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
// Get lines that connect to hidden dots
|
|
861
|
+
const linesToRemove = [];
|
|
862
|
+
sv.stepLines.forEach((line, lineIndex) => {
|
|
863
|
+
const fromDot = sv.stepDots[line.fromDotIndex];
|
|
864
|
+
const toDot = sv.stepDots[line.toDotIndex];
|
|
865
|
+
|
|
866
|
+
if ((fromDot && !fromDot.visible) || (toDot && !toDot.visible)) {
|
|
867
|
+
|
|
868
|
+
linesToRemove.push(line);
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
// Remove the problematic lines
|
|
873
|
+
|
|
874
|
+
linesToRemove.forEach(line => {
|
|
875
|
+
if (line.parent === sv.visualContainer) {
|
|
876
|
+
sv.visualContainer.removeChild(line);
|
|
877
|
+
|
|
878
|
+
}
|
|
879
|
+
const lineIndex = sv.stepLines.indexOf(line);
|
|
880
|
+
if (lineIndex >= 0) {
|
|
881
|
+
sv.stepLines.splice(lineIndex, 1);
|
|
882
|
+
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
}
|
|
245
888
|
}
|