@teachinglab/omd 0.2.7 → 0.2.9

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.
@@ -18,16 +18,19 @@ export class omdStepVisualizer extends omdEquationSequenceNode {
18
18
  constructor(steps, styling = {}) {
19
19
  super(steps);
20
20
 
21
- // Store styling options
22
- this.styling = styling || {};
21
+ // Store styling options with defaults
22
+ this.styling = this._mergeWithDefaults(styling || {});
23
23
 
24
24
  // Visual elements for step tracking
25
25
  this.stepDots = [];
26
26
  this.stepLines = [];
27
27
  this.visualContainer = new jsvgLayoutGroup();
28
- this.dotRadius = getDotRadius(0);
29
- this.lineWidth = 2;
30
- this.visualSpacing = 30;
28
+
29
+ // Use styling values for these properties
30
+ this.dotRadius = this.styling.dotRadius;
31
+ this.lineWidth = this.styling.lineWidth;
32
+ this.visualSpacing = this.styling.visualSpacing;
33
+
31
34
  this.activeDotIndex = -1;
32
35
  this.dotsClickable = true;
33
36
  this.nodeToStepMap = new Map();
@@ -35,7 +38,10 @@ export class omdStepVisualizer extends omdEquationSequenceNode {
35
38
  // Highlighting system
36
39
  this.stepVisualizerHighlights = new Set();
37
40
  this.highlighting = new omdStepVisualizerHighlighting(this);
38
- this.textBoxManager = new omdStepVisualizerTextBoxes(this, this.highlighting);
41
+
42
+ // Pass textbox options through styling parameter
43
+ const textBoxOptions = this.styling.textBoxOptions || {};
44
+ this.textBoxManager = new omdStepVisualizerTextBoxes(this, this.highlighting, textBoxOptions);
39
45
  this.layoutManager = new omdStepVisualizerLayout(this);
40
46
 
41
47
  this.addChild(this.visualContainer);
@@ -43,19 +49,288 @@ export class omdStepVisualizer extends omdEquationSequenceNode {
43
49
 
44
50
  // Set default filter level to show only major steps (stepMark = 0)
45
51
  // This ensures intermediate steps are hidden and expansion dots can be created
46
- console.debug('Step visualizer: Setting default filter level to 0');
47
- console.debug('Steps before filter:', this.steps.map(s => ({type: s.constructor.name, stepMark: s.stepMark, visible: s.visible})));
52
+
53
+
48
54
 
49
55
  if (this.setFilterLevel && typeof this.setFilterLevel === 'function') {
50
56
  this.setFilterLevel(0);
51
- console.debug('Steps after filter:', this.steps.map(s => ({type: s.constructor.name, stepMark: s.stepMark, visible: s.visible})));
57
+
52
58
  } else {
53
- console.debug('setFilterLevel method not available');
59
+
54
60
  }
55
61
 
56
62
  this.computeDimensions();
57
63
  this.updateLayout();
58
64
  }
65
+
66
+ /**
67
+ * Public: programmatically toggle a dot (simulate user click behavior)
68
+ * @param {number} dotIndex
69
+ */
70
+ toggleDot(dotIndex) {
71
+ if (typeof dotIndex !== 'number') return;
72
+ if (dotIndex < 0 || dotIndex >= this.stepDots.length) return;
73
+ const dot = this.stepDots[dotIndex];
74
+ this._handleDotClick(dot, dotIndex);
75
+ }
76
+
77
+ /**
78
+ * Public: close currently active dot textbox if any
79
+ */
80
+ closeActiveDot() {
81
+ // Always clear all boxes to be safe (even if activeDotIndex already reset)
82
+ try {
83
+ const before = this.textBoxManager?.stepTextBoxes?.length || 0;
84
+ this._clearActiveDot();
85
+ if (this.textBoxManager && typeof this.textBoxManager.clearAllTextBoxes === 'function') {
86
+ this.textBoxManager.clearAllTextBoxes();
87
+ }
88
+ const after = this.textBoxManager?.stepTextBoxes?.length || 0;
89
+ } catch (e) { /* no-op */ }
90
+ }
91
+
92
+ /**
93
+ * Public: close all text boxes (future-proof; currently only one can be active)
94
+ */
95
+ closeAllTextBoxes() {
96
+ this.closeActiveDot();
97
+ }
98
+
99
+ /**
100
+ * Public: force close EVERYTHING related to active explanation UI
101
+ */
102
+ forceCloseAll() {
103
+ this.closeActiveDot();
104
+ }
105
+
106
+ /**
107
+ * Merges user styling with default styling options
108
+ * @param {Object} userStyling - User-provided styling options
109
+ * @returns {Object} Merged styling object with defaults
110
+ * @private
111
+ */
112
+ _mergeWithDefaults(userStyling) {
113
+ const defaults = {
114
+ // Dot styling
115
+ dotColor: omdColor.stepColor,
116
+ dotRadius: getDotRadius(0),
117
+ dotStrokeWidth: 2,
118
+ activeDotColor: omdColor.explainColor,
119
+ expansionDotScale: 0.4,
120
+
121
+ // Line styling
122
+ lineColor: omdColor.stepColor,
123
+ lineWidth: 2,
124
+ activeLineColor: omdColor.explainColor,
125
+
126
+ // Colors
127
+ explainColor: omdColor.explainColor,
128
+ highlightColor: omdColor.hiliteColor,
129
+
130
+ // Layout
131
+ visualSpacing: 30,
132
+ fixedVisualizerPosition: 250,
133
+ dotVerticalOffset: 15,
134
+
135
+ // Text boxes
136
+ textBoxOptions: {
137
+ backgroundColor: omdColor.white,
138
+ borderColor: 'none',
139
+ borderWidth: 1,
140
+ borderRadius: 5,
141
+ padding: 8, // Minimal padding for tight fit
142
+ fontSize: 14,
143
+ fontFamily: 'Albert Sans, Arial, sans-serif',
144
+ maxWidth: 300, // More reasonable width for compact layout
145
+ dropShadow: true
146
+ // Removed zIndex and position from defaults - these should only apply to container
147
+ },
148
+
149
+ // Visual effects
150
+ enableAnimations: true,
151
+ hoverEffects: true,
152
+
153
+ // Background styling (inherited from equation styling)
154
+ backgroundColor: null,
155
+ cornerRadius: null,
156
+ pill: null
157
+ };
158
+
159
+ return this._deepMerge(defaults, userStyling);
160
+ }
161
+
162
+ /**
163
+ * Deep merge two objects
164
+ * @param {Object} target - Target object
165
+ * @param {Object} source - Source object
166
+ * @returns {Object} Merged object
167
+ * @private
168
+ */
169
+ _deepMerge(target, source) {
170
+ const result = { ...target };
171
+
172
+ for (const key in source) {
173
+ if (source.hasOwnProperty(key)) {
174
+ if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
175
+ result[key] = this._deepMerge(result[key] || {}, source[key]);
176
+ } else {
177
+ result[key] = source[key];
178
+ }
179
+ }
180
+ }
181
+
182
+ return result;
183
+ }
184
+
185
+ /**
186
+ * Updates the styling options and applies them to existing visual elements
187
+ * @param {Object} newStyling - New styling options to apply
188
+ */
189
+ setStyling(newStyling) {
190
+ this.styling = this._mergeWithDefaults({ ...this.styling, ...newStyling });
191
+
192
+ // Update instance properties that are used elsewhere
193
+ this.dotRadius = this.styling.dotRadius;
194
+ this.lineWidth = this.styling.lineWidth;
195
+ this.visualSpacing = this.styling.visualSpacing;
196
+
197
+ this._applyStylingToExistingElements();
198
+
199
+ // Update layout spacing if changed
200
+ if (newStyling.visualSpacing !== undefined) {
201
+ this.visualSpacing = this.styling.visualSpacing;
202
+ }
203
+ if (newStyling.fixedVisualizerPosition !== undefined && this.layoutManager) {
204
+ this.layoutManager.setFixedVisualizerPosition(this.styling.fixedVisualizerPosition);
205
+ }
206
+
207
+ // Refresh layout and visual elements
208
+ this.updateLayout();
209
+ }
210
+
211
+ /**
212
+ * Gets the current styling options
213
+ * @returns {Object} Current styling configuration
214
+ */
215
+ getStyling() {
216
+ return { ...this.styling };
217
+ }
218
+
219
+ /**
220
+ * Sets a specific styling property
221
+ * @param {string} property - The property to set (supports dot notation like 'textBoxOptions.backgroundColor')
222
+ * @param {any} value - The value to set
223
+ */
224
+ setStyleProperty(property, value) {
225
+ const keys = property.split('.');
226
+ const lastKey = keys.pop();
227
+ const target = keys.reduce((obj, key) => {
228
+ if (!obj[key]) obj[key] = {};
229
+ return obj[key];
230
+ }, this.styling);
231
+
232
+ target[lastKey] = value;
233
+
234
+ // Update instance properties if they were changed
235
+ if (property === 'dotRadius') this.dotRadius = value;
236
+ if (property === 'lineWidth') this.lineWidth = value;
237
+ if (property === 'visualSpacing') this.visualSpacing = value;
238
+ if (property === 'fixedVisualizerPosition' && this.layoutManager) {
239
+ this.layoutManager.setFixedVisualizerPosition(value);
240
+ }
241
+
242
+ this._applyStylingToExistingElements();
243
+ this.updateLayout();
244
+ }
245
+
246
+ /**
247
+ * Gets a specific styling property
248
+ * @param {string} property - The property to get (supports dot notation)
249
+ * @returns {any} The property value
250
+ */
251
+ getStyleProperty(property) {
252
+ return property.split('.').reduce((obj, key) => obj?.[key], this.styling);
253
+ }
254
+
255
+ /**
256
+ * Applies current styling to all existing visual elements
257
+ * @private
258
+ */
259
+ _applyStylingToExistingElements() {
260
+ // Update dots
261
+ this.stepDots.forEach((dot, index) => {
262
+ if (dot && dot.equationRef) {
263
+ const isActive = this.activeDotIndex === index;
264
+ const color = isActive ? this.styling.activeDotColor : this.styling.dotColor;
265
+ dot.setFillColor(color);
266
+ dot.setStrokeColor(color);
267
+ dot.setStrokeWidth(this.styling.dotStrokeWidth);
268
+
269
+ // Update radius based on step mark
270
+ const stepMark = dot.equationRef.stepMark ?? 0;
271
+ const radius = this.styling.dotRadius || getDotRadius(stepMark);
272
+ dot.setWidthAndHeight(radius * 2, radius * 2);
273
+ dot.radius = radius;
274
+ }
275
+ });
276
+
277
+ // Update lines
278
+ this.stepLines.forEach((line, index) => {
279
+ if (line) {
280
+ const isActive = this.activeDotIndex >= 0 &&
281
+ (line.toDotIndex === this.activeDotIndex || line.fromDotIndex === this.activeDotIndex);
282
+ const color = isActive ? this.styling.activeLineColor : this.styling.lineColor;
283
+ line.setStrokeColor(color);
284
+ line.setStrokeWidth(this.styling.lineWidth);
285
+ }
286
+ });
287
+
288
+ // Update expansion dots
289
+ if (this.layoutManager && this.layoutManager.expansionDots) {
290
+ this.layoutManager.expansionDots.forEach(expansionDot => {
291
+ if (expansionDot) {
292
+ const baseRadius = this.styling.dotRadius || getDotRadius(0);
293
+ const radius = Math.max(3, baseRadius * this.styling.expansionDotScale);
294
+ expansionDot.setWidthAndHeight(radius * 2, radius * 2);
295
+ expansionDot.radius = radius;
296
+ expansionDot.setFillColor(this.styling.dotColor);
297
+ expansionDot.setStrokeColor(this.styling.dotColor);
298
+ }
299
+ });
300
+ }
301
+
302
+ // Update text box styling if manager exists
303
+ if (this.textBoxManager && typeof this.textBoxManager.updateStyling === 'function') {
304
+ this.textBoxManager.updateStyling(this.styling.textBoxOptions);
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Sets the visual background style (inherits from equation styling)
310
+ * @param {Object} style - Background style options
311
+ */
312
+ setBackgroundStyle(style = {}) {
313
+ this.styling.backgroundColor = style.backgroundColor || this.styling.backgroundColor;
314
+ this.styling.cornerRadius = style.cornerRadius || this.styling.cornerRadius;
315
+ this.styling.pill = style.pill !== undefined ? style.pill : this.styling.pill;
316
+
317
+ // Apply to equation background if this step visualizer has equation styling
318
+ if (typeof this.setDefaultEquationBackground === 'function') {
319
+ this.setDefaultEquationBackground(style);
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Gets the current background style
325
+ * @returns {Object} Current background style
326
+ */
327
+ getBackgroundStyle() {
328
+ return {
329
+ backgroundColor: this.styling.backgroundColor,
330
+ cornerRadius: this.styling.cornerRadius,
331
+ pill: this.styling.pill
332
+ };
333
+ }
59
334
 
60
335
  /**
61
336
  * Sets the fixed position for the step visualizer
@@ -103,7 +378,7 @@ export class omdStepVisualizer extends omdEquationSequenceNode {
103
378
  });
104
379
 
105
380
  this.layoutManager.updateVisualZOrder();
106
- this.layoutManager.updateVisualLayout();
381
+ this.layoutManager.updateVisualLayout(true);
107
382
  }
108
383
 
109
384
  /**
@@ -111,13 +386,14 @@ export class omdStepVisualizer extends omdEquationSequenceNode {
111
386
  * @private
112
387
  */
113
388
  _createStepDot(equation, index) {
114
- const radius = getDotRadius(equation.stepMark ?? 0);
389
+ const stepMark = equation.stepMark ?? 0;
390
+ const radius = this.styling.dotRadius || getDotRadius(stepMark);
115
391
  const dot = new jsvgEllipse();
116
392
  dot.setWidthAndHeight(radius * 2, radius * 2);
117
- const dotColor = this.styling.dotColor || omdColor.stepColor;
393
+ const dotColor = this.styling.dotColor;
118
394
  dot.setFillColor(dotColor);
119
395
  dot.setStrokeColor(dotColor);
120
- dot.setStrokeWidth(this.lineWidth);
396
+ dot.setStrokeWidth(this.styling.dotStrokeWidth);
121
397
  dot.radius = radius;
122
398
 
123
399
  dot.equationRef = equation;
@@ -141,9 +417,9 @@ export class omdStepVisualizer extends omdEquationSequenceNode {
141
417
  */
142
418
  _createStepLine(fromIndex, toIndex) {
143
419
  const line = new jsvgLine();
144
- const lineColor = this.styling.lineColor || omdColor.stepColor;
420
+ const lineColor = this.styling.lineColor;
145
421
  line.setStrokeColor(lineColor);
146
- line.setStrokeWidth(this.lineWidth);
422
+ line.setStrokeWidth(this.styling.lineWidth);
147
423
 
148
424
  line.fromDotIndex = fromIndex;
149
425
  line.toDotIndex = toIndex;
@@ -226,7 +502,9 @@ export class omdStepVisualizer extends omdEquationSequenceNode {
226
502
  */
227
503
  computeDimensions() {
228
504
  super.computeDimensions();
505
+ // Store original dimensions before visualizer expansion
229
506
  this.sequenceWidth = this.width;
507
+ this.sequenceHeight = this.height;
230
508
 
231
509
  // Set width to include the fixed visualizer position plus visualizer width
232
510
  if (this.stepDots && this.stepDots.length > 0 && this.layoutManager) {
@@ -241,26 +519,26 @@ export class omdStepVisualizer extends omdEquationSequenceNode {
241
519
  * Override updateLayout to update visual elements as well
242
520
  */
243
521
  updateLayout() {
244
- console.debug('\n=== omdStepVisualizer.updateLayout START ===');
245
- console.debug('Calling super.updateLayout()...');
522
+
523
+
246
524
  super.updateLayout();
247
525
 
248
526
  // Only update visual layout if layoutManager is initialized
249
527
  if (this.layoutManager) {
250
- console.debug('LayoutManager found, updating visual elements...');
251
- console.debug('Calling layoutManager.updateVisualLayout()...');
252
- this.layoutManager.updateVisualLayout();
253
528
 
254
- console.debug('Calling layoutManager.updateVisualVisibility()...');
529
+
530
+ this.layoutManager.updateVisualLayout(true); // Allow repositioning for main layout updates
531
+
532
+
255
533
  this.layoutManager.updateVisualVisibility();
256
534
 
257
- console.debug('Calling layoutManager.updateAllLinePositions()...');
535
+
258
536
  this.layoutManager.updateAllLinePositions();
259
537
  } else {
260
- console.debug('WARNING: LayoutManager not initialized!');
538
+
261
539
  }
262
540
 
263
- console.debug('=== omdStepVisualizer.updateLayout END ===\n');
541
+
264
542
  }
265
543
 
266
544
  /**
@@ -363,11 +641,11 @@ export class omdStepVisualizer extends omdEquationSequenceNode {
363
641
  this.activeDot = this.stepDots[dotIndex];
364
642
 
365
643
  const dot = this.stepDots[dotIndex];
366
- const explainColor = this.styling.explainColor || omdColor.explainColor;
644
+ const explainColor = this.styling.activeDotColor;
367
645
  dot.setFillColor(explainColor);
368
646
  dot.setStrokeColor(explainColor);
369
647
 
370
- this.setLineAboveColor(dotIndex, this.styling.explainColor || omdColor.explainColor);
648
+ this.setLineAboveColor(dotIndex, this.styling.activeLineColor);
371
649
  this.textBoxManager.createTextBoxForDot(dotIndex);
372
650
 
373
651
  // Temporarily disable equation repositioning for simple dot state changes
@@ -380,16 +658,20 @@ export class omdStepVisualizer extends omdEquationSequenceNode {
380
658
  /**
381
659
  * Clears the currently active dot
382
660
  * @private
661
+ */
662
+ /**
663
+ * Clears the currently active dot
664
+ * @private
383
665
  */
384
666
  _clearActiveDot() {
385
667
  try {
386
668
  if (this.activeDotIndex !== -1) {
387
669
  const dot = this.stepDots[this.activeDotIndex];
388
- const dotColor = this.styling.dotColor || omdColor.stepColor;
670
+ const dotColor = this.styling.dotColor;
389
671
  dot.setFillColor(dotColor);
390
672
  dot.setStrokeColor(dotColor);
391
673
 
392
- this.setLineAboveColor(this.activeDotIndex, this.styling.lineColor || omdColor.stepColor);
674
+ this.setLineAboveColor(this.activeDotIndex, this.styling.lineColor);
393
675
  this.textBoxManager.removeTextBoxForDot(this.activeDotIndex);
394
676
 
395
677
  this.highlighting.clearHighlights();