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