@teachinglab/omd 0.6.0 → 0.6.2

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.
Files changed (198) hide show
  1. package/README.md +257 -251
  2. package/README.old.md +137 -137
  3. package/canvas/core/canvasConfig.js +202 -202
  4. package/canvas/drawing/segment.js +167 -167
  5. package/canvas/drawing/stroke.js +385 -385
  6. package/canvas/events/eventManager.js +444 -444
  7. package/canvas/events/pointerEventHandler.js +262 -262
  8. package/canvas/index.js +48 -48
  9. package/canvas/tools/PointerTool.js +71 -71
  10. package/canvas/tools/tool.js +222 -222
  11. package/canvas/utils/boundingBox.js +377 -377
  12. package/canvas/utils/mathUtils.js +258 -258
  13. package/docs/api/configuration-options.md +198 -198
  14. package/docs/api/eventManager.md +82 -82
  15. package/docs/api/focusFrameManager.md +144 -144
  16. package/docs/api/index.md +105 -105
  17. package/docs/api/main.md +62 -62
  18. package/docs/api/omdBinaryExpressionNode.md +86 -86
  19. package/docs/api/omdCanvas.md +83 -83
  20. package/docs/api/omdConfigManager.md +112 -112
  21. package/docs/api/omdConstantNode.md +52 -52
  22. package/docs/api/omdDisplay.md +87 -87
  23. package/docs/api/omdEquationNode.md +174 -174
  24. package/docs/api/omdEquationSequenceNode.md +258 -258
  25. package/docs/api/omdEquationStack.md +192 -192
  26. package/docs/api/omdFunctionNode.md +82 -82
  27. package/docs/api/omdGroupNode.md +78 -78
  28. package/docs/api/omdHelpers.md +87 -87
  29. package/docs/api/omdLeafNode.md +85 -85
  30. package/docs/api/omdNode.md +201 -201
  31. package/docs/api/omdOperationDisplayNode.md +117 -117
  32. package/docs/api/omdOperatorNode.md +91 -91
  33. package/docs/api/omdParenthesisNode.md +133 -133
  34. package/docs/api/omdPopup.md +191 -191
  35. package/docs/api/omdPowerNode.md +131 -131
  36. package/docs/api/omdRationalNode.md +144 -144
  37. package/docs/api/omdSequenceNode.md +128 -128
  38. package/docs/api/omdSimplification.md +78 -78
  39. package/docs/api/omdSqrtNode.md +144 -144
  40. package/docs/api/omdStepVisualizer.md +146 -146
  41. package/docs/api/omdStepVisualizerHighlighting.md +65 -65
  42. package/docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
  43. package/docs/api/omdStepVisualizerLayout.md +70 -70
  44. package/docs/api/omdStepVisualizerNodeUtils.md +140 -140
  45. package/docs/api/omdStepVisualizerTextBoxes.md +76 -76
  46. package/docs/api/omdToolbar.md +130 -130
  47. package/docs/api/omdTranscriptionService.md +95 -95
  48. package/docs/api/omdTreeDiff.md +169 -169
  49. package/docs/api/omdUnaryExpressionNode.md +137 -137
  50. package/docs/api/omdUtilities.md +82 -82
  51. package/docs/api/omdVariableNode.md +123 -123
  52. package/docs/api/selectTool.md +74 -74
  53. package/docs/api/simplificationEngine.md +97 -97
  54. package/docs/api/simplificationRules.md +76 -76
  55. package/docs/api/simplificationUtils.md +64 -64
  56. package/docs/api/transcribe.md +43 -43
  57. package/docs/api-reference.md +85 -85
  58. package/docs/index.html +453 -453
  59. package/docs/index.md +38 -38
  60. package/docs/omd-objects.md +258 -258
  61. package/index.js +79 -79
  62. package/jsvg/index.js +3 -0
  63. package/jsvg/jsvg.js +898 -898
  64. package/jsvg/jsvgComponents.js +357 -358
  65. package/npm-docs/DOCUMENTATION_SUMMARY.md +220 -220
  66. package/npm-docs/README.md +251 -251
  67. package/npm-docs/api/api-reference.md +85 -85
  68. package/npm-docs/api/configuration-options.md +198 -198
  69. package/npm-docs/api/eventManager.md +82 -82
  70. package/npm-docs/api/expression-nodes.md +561 -561
  71. package/npm-docs/api/focusFrameManager.md +144 -144
  72. package/npm-docs/api/index.md +105 -105
  73. package/npm-docs/api/main.md +62 -62
  74. package/npm-docs/api/omdBinaryExpressionNode.md +86 -86
  75. package/npm-docs/api/omdCanvas.md +83 -83
  76. package/npm-docs/api/omdConfigManager.md +112 -112
  77. package/npm-docs/api/omdConstantNode.md +52 -52
  78. package/npm-docs/api/omdDisplay.md +87 -87
  79. package/npm-docs/api/omdEquationNode.md +174 -174
  80. package/npm-docs/api/omdEquationSequenceNode.md +258 -258
  81. package/npm-docs/api/omdEquationStack.md +192 -192
  82. package/npm-docs/api/omdFunctionNode.md +82 -82
  83. package/npm-docs/api/omdGroupNode.md +78 -78
  84. package/npm-docs/api/omdHelpers.md +87 -87
  85. package/npm-docs/api/omdLeafNode.md +85 -85
  86. package/npm-docs/api/omdNode.md +201 -201
  87. package/npm-docs/api/omdOperationDisplayNode.md +117 -117
  88. package/npm-docs/api/omdOperatorNode.md +91 -91
  89. package/npm-docs/api/omdParenthesisNode.md +133 -133
  90. package/npm-docs/api/omdPopup.md +191 -191
  91. package/npm-docs/api/omdPowerNode.md +131 -131
  92. package/npm-docs/api/omdRationalNode.md +144 -144
  93. package/npm-docs/api/omdSequenceNode.md +128 -128
  94. package/npm-docs/api/omdSimplification.md +78 -78
  95. package/npm-docs/api/omdSqrtNode.md +144 -144
  96. package/npm-docs/api/omdStepVisualizer.md +146 -146
  97. package/npm-docs/api/omdStepVisualizerHighlighting.md +65 -65
  98. package/npm-docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
  99. package/npm-docs/api/omdStepVisualizerLayout.md +70 -70
  100. package/npm-docs/api/omdStepVisualizerNodeUtils.md +140 -140
  101. package/npm-docs/api/omdStepVisualizerTextBoxes.md +76 -76
  102. package/npm-docs/api/omdToolbar.md +130 -130
  103. package/npm-docs/api/omdTranscriptionService.md +95 -95
  104. package/npm-docs/api/omdTreeDiff.md +169 -169
  105. package/npm-docs/api/omdUnaryExpressionNode.md +137 -137
  106. package/npm-docs/api/omdUtilities.md +82 -82
  107. package/npm-docs/api/omdVariableNode.md +123 -123
  108. package/npm-docs/api/selectTool.md +74 -74
  109. package/npm-docs/api/simplificationEngine.md +97 -97
  110. package/npm-docs/api/simplificationRules.md +76 -76
  111. package/npm-docs/api/simplificationUtils.md +64 -64
  112. package/npm-docs/api/transcribe.md +43 -43
  113. package/npm-docs/guides/equations.md +854 -854
  114. package/npm-docs/guides/factory-functions.md +354 -354
  115. package/npm-docs/guides/getting-started.md +318 -318
  116. package/npm-docs/guides/quick-examples.md +525 -525
  117. package/npm-docs/guides/visualizations.md +682 -682
  118. package/npm-docs/index.html +12 -0
  119. package/npm-docs/json-schemas.md +826 -826
  120. package/omd/config/omdConfigManager.js +279 -267
  121. package/omd/core/index.js +158 -158
  122. package/omd/core/omdEquationStack.js +546 -546
  123. package/omd/core/omdUtilities.js +113 -113
  124. package/omd/display/omdDisplay.js +969 -962
  125. package/omd/display/omdToolbar.js +501 -501
  126. package/omd/nodes/omdBinaryExpressionNode.js +459 -459
  127. package/omd/nodes/omdConstantNode.js +141 -141
  128. package/omd/nodes/omdEquationNode.js +1327 -1327
  129. package/omd/nodes/omdFunctionNode.js +351 -351
  130. package/omd/nodes/omdGroupNode.js +67 -67
  131. package/omd/nodes/omdLeafNode.js +76 -76
  132. package/omd/nodes/omdNode.js +556 -556
  133. package/omd/nodes/omdOperationDisplayNode.js +321 -321
  134. package/omd/nodes/omdOperatorNode.js +108 -108
  135. package/omd/nodes/omdParenthesisNode.js +292 -292
  136. package/omd/nodes/omdPowerNode.js +235 -235
  137. package/omd/nodes/omdRationalNode.js +295 -295
  138. package/omd/nodes/omdSqrtNode.js +307 -307
  139. package/omd/nodes/omdUnaryExpressionNode.js +227 -227
  140. package/omd/nodes/omdVariableNode.js +122 -122
  141. package/omd/simplification/omdSimplification.js +140 -140
  142. package/omd/simplification/omdSimplificationEngine.js +887 -887
  143. package/omd/simplification/package.json +5 -5
  144. package/omd/simplification/rules/binaryRules.js +1037 -1037
  145. package/omd/simplification/rules/functionRules.js +111 -111
  146. package/omd/simplification/rules/index.js +48 -48
  147. package/omd/simplification/rules/parenthesisRules.js +19 -19
  148. package/omd/simplification/rules/powerRules.js +143 -143
  149. package/omd/simplification/rules/rationalRules.js +725 -725
  150. package/omd/simplification/rules/sqrtRules.js +48 -48
  151. package/omd/simplification/rules/unaryRules.js +37 -37
  152. package/omd/simplification/simplificationRules.js +31 -31
  153. package/omd/simplification/simplificationUtils.js +1055 -1055
  154. package/omd/step-visualizer/omdStepVisualizer.js +947 -947
  155. package/omd/step-visualizer/omdStepVisualizerHighlighting.js +246 -246
  156. package/omd/step-visualizer/omdStepVisualizerLayout.js +892 -892
  157. package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +200 -200
  158. package/omd/utils/aiNextEquationStep.js +106 -106
  159. package/omd/utils/omdNodeOverlay.js +638 -638
  160. package/omd/utils/omdPopup.js +1203 -1203
  161. package/omd/utils/omdStepVisualizerInteractiveSteps.js +684 -684
  162. package/omd/utils/omdStepVisualizerNodeUtils.js +267 -267
  163. package/omd/utils/omdTranscriptionService.js +123 -123
  164. package/omd/utils/omdTreeDiff.js +733 -733
  165. package/package.json +59 -56
  166. package/readme.html +184 -120
  167. package/src/index.js +74 -74
  168. package/src/json-schemas.md +576 -576
  169. package/src/omd-json-samples.js +147 -147
  170. package/src/omdApp.js +391 -391
  171. package/src/omdAppCanvas.js +335 -335
  172. package/src/omdBalanceHanger.js +199 -199
  173. package/src/omdColor.js +13 -13
  174. package/src/omdCoordinatePlane.js +541 -541
  175. package/src/omdExpression.js +115 -115
  176. package/src/omdFactory.js +150 -150
  177. package/src/omdFunction.js +114 -114
  178. package/src/omdMetaExpression.js +290 -290
  179. package/src/omdNaturalExpression.js +563 -563
  180. package/src/omdNode.js +383 -383
  181. package/src/omdNumber.js +52 -52
  182. package/src/omdNumberLine.js +114 -112
  183. package/src/omdNumberTile.js +118 -118
  184. package/src/omdOperator.js +72 -72
  185. package/src/omdPowerExpression.js +91 -91
  186. package/src/omdProblem.js +259 -259
  187. package/src/omdRatioChart.js +251 -251
  188. package/src/omdRationalExpression.js +114 -114
  189. package/src/omdSampleData.js +215 -215
  190. package/src/omdShapes.js +512 -512
  191. package/src/omdSpinner.js +151 -151
  192. package/src/omdString.js +49 -49
  193. package/src/omdTable.js +498 -498
  194. package/src/omdTapeDiagram.js +244 -244
  195. package/src/omdTerm.js +91 -91
  196. package/src/omdTileEquation.js +349 -349
  197. package/src/omdUtils.js +84 -84
  198. package/src/omdVariable.js +51 -51
@@ -1,685 +1,685 @@
1
- import { omdColor } from '../../src/omdColor.js';
2
- import { jsvgLayoutGroup, jsvgTextBox, jsvgRect } from '@teachinglab/jsvg';
3
- /**
4
- * Creates interactive step elements using jsvgLayoutGroup for multiple simplification steps
5
- * Each step is a separate jsvgTextBox that can have hover interactions with the omdSequence
6
- */
7
- export class omdStepVisualizerInteractiveSteps {
8
- constructor(stepVisualizer, simplificationData, stylingOptions = {}) {
9
- this.stepVisualizer = stepVisualizer;
10
- this.simplificationData = simplificationData || {};
11
- this.stylingOptions = stylingOptions || {};
12
- this.messages = this._extractMessages(simplificationData);
13
- this.ruleNames = this._extractRuleNames(simplificationData);
14
- this.stepElements = [];
15
- this.layoutGroup = new jsvgLayoutGroup();
16
- this.layoutGroup.setSpacer(4); // Minimal spacing for tight layout
17
-
18
- // Styling configuration with defaults that can be overridden
19
- this.stepWidth = this.stylingOptions.maxWidth || 300; // Use maxWidth from styling options
20
- this.baseStepHeight = 30; // Minimal height for tight fit
21
- this.headerHeight = 28; // Minimal header height
22
- this.fontSize = this.stylingOptions.fontSize || 14;
23
- this.smallFontSize = 12;
24
-
25
- this.setupLayoutGroup();
26
- this.createStepElements();
27
- }
28
-
29
- /**
30
- * Extracts messages from simplification data
31
- * @param {Object} data - Simplification data
32
- * @returns {Array} Array of clean messages
33
- * @private
34
- */
35
- _extractMessages(data) {
36
- if (!data) return [];
37
-
38
- let messages = [];
39
- if (data.rawMessages && Array.isArray(data.rawMessages)) {
40
- messages = data.rawMessages;
41
- } else if (data.message) {
42
- messages = [data.message];
43
- }
44
-
45
- // Clean up messages - remove HTML tags and bullet points
46
- return messages.map(msg => {
47
- let clean = msg.replace(/<[^>]*>/g, ''); // Strip HTML tags
48
- clean = clean.replace(/^[•·◦▪▫‣⁃]\s*/, ''); // Strip bullet points
49
- return clean.trim();
50
- });
51
- }
52
-
53
- /**
54
- * Extracts rule names from simplification data
55
- * @param {Object} data - Simplification data
56
- * @returns {Array} Array of rule names
57
- * @private
58
- */
59
- _extractRuleNames(data) {
60
- if (!data) return ['Operation'];
61
-
62
- if (data.ruleNames && Array.isArray(data.ruleNames)) {
63
- return data.ruleNames;
64
- }
65
-
66
- // Default based on data type
67
- if (data.multipleSimplifications) {
68
- return ['Multiple Rules'];
69
- } else {
70
- return ['Operation'];
71
- }
72
- }
73
-
74
- /**
75
- * Sets up the main layout group properties
76
- * @private
77
- */
78
- setupLayoutGroup() {
79
- // Add background using styling options for the entire step group
80
- this.backgroundRect = new jsvgRect();
81
- this.backgroundRect.setWidthAndHeight(this.stepWidth + 16, 60); // Minimal padding and height for tight fit
82
-
83
- // Apply styling options to the background container
84
- const backgroundColor = this.stylingOptions.backgroundColor || omdColor.lightGray;
85
- const borderColor = this.stylingOptions.borderColor || '#e0e0e0';
86
- const borderWidth = this.stylingOptions.borderWidth || 1;
87
- const borderRadius = this.stylingOptions.borderRadius || 6;
88
-
89
- this.backgroundRect.setFillColor(backgroundColor);
90
- this.backgroundRect.setStrokeColor(borderColor);
91
- this.backgroundRect.setStrokeWidth(borderWidth);
92
- this.backgroundRect.setCornerRadius(borderRadius);
93
- this.backgroundRect.setPosition(0, 0); // Start at origin, not negative offset
94
-
95
- // Apply drop shadow to the SVG element if requested
96
- if (this.stylingOptions.dropShadow && this.backgroundRect.svgObject) {
97
- this.backgroundRect.svgObject.style.filter = 'drop-shadow(0 2px 8px rgba(0,0,0,0.15))';
98
- }
99
-
100
- this.layoutGroup.addChild(this.backgroundRect);
101
- }
102
-
103
- /**
104
- * Creates individual step elements from the messages array
105
- * @private
106
- */
107
- createStepElements() {
108
- if (!this.messages || this.messages.length === 0) return;
109
-
110
- // Create content container to separate from background
111
- this.contentGroup = new jsvgLayoutGroup();
112
- this.contentGroup.setSpacer(2); // Minimal spacing between elements
113
- this.contentGroup.setPosition(8, 8); // Minimal offset for tight fit
114
-
115
- if (this.messages.length === 1) {
116
-
117
- this.createSingleStepElement(this.messages[0], 0);
118
- } else {
119
-
120
- this.createMultipleStepElements();
121
- }
122
-
123
- this.contentGroup.doVerticalLayout();
124
- this.layoutGroup.addChild(this.contentGroup);
125
- this.updateBackgroundSize();
126
-
127
- // Apply drop shadow after SVG element is created
128
- setTimeout(() => {
129
- this.applyDropShadowIfNeeded();
130
- }, 10);
131
-
132
- // Debug logging
133
-
134
- }
135
-
136
- /**
137
- * Creates a single step element with header
138
- * @param {string} message - The step message
139
- * @param {number} index - Step index
140
- * @private
141
- */
142
- createSingleStepElement(message, index) {
143
- // Create header for single step using rule name
144
- const ruleName = this.ruleNames[0] || 'Operation';
145
- const headerBox = this.createHeaderBox(ruleName + ':');
146
- this.contentGroup.addChild(headerBox);
147
-
148
- // Create the step box
149
- const stepBox = this.createStepTextBox(message, index, false);
150
- this.stepElements.push(stepBox);
151
- this.contentGroup.addChild(stepBox);
152
- }
153
-
154
- /**
155
- * Creates multiple step elements with header
156
- * @private
157
- */
158
- createMultipleStepElements() {
159
- // Only create header for truly multiple steps (more than 1)
160
- if (this.messages.length > 1) {
161
- // Create header showing rule names
162
- let headerText;
163
- if (this.ruleNames.length === 1) {
164
- headerText = this.ruleNames[0] + ':';
165
- } else if (this.ruleNames.length <= 3) {
166
- headerText = this.ruleNames.join(' + ') + ':';
167
- } else {
168
- headerText = `${this.ruleNames.length} Rules Applied:`;
169
- }
170
-
171
- const headerBox = this.createHeaderBox(headerText);
172
- this.contentGroup.addChild(headerBox);
173
- }
174
-
175
- // Create individual step elements
176
- this.messages.forEach((message, index) => {
177
- const stepBox = this.createStepTextBox(message, index, this.messages.length > 1);
178
- this.stepElements.push(stepBox);
179
- this.contentGroup.addChild(stepBox);
180
- });
181
- }
182
-
183
- /**
184
- * Creates a header box with custom text
185
- * @param {string} headerText - Text to display in header
186
- * @returns {jsvgTextBox} Header text box
187
- * @private
188
- */
189
- createHeaderBox(headerText = 'Operation:') {
190
- const headerBox = new jsvgTextBox();
191
- const headerHeight = Math.max(this.headerHeight, 50); // Increased minimum height
192
-
193
-
194
- headerBox.setWidthAndHeight(this.stepWidth, headerHeight);
195
- headerBox.setText(headerText);
196
- headerBox.setFontSize(this.fontSize);
197
- headerBox.setFontWeight('600');
198
- headerBox.setFontColor('#2c3e50');
199
-
200
- // Style the header with border and minimal spacing
201
- if (headerBox.div) {
202
- Object.assign(headerBox.div.style, {
203
- borderBottom: '1px solid #e0e0e0',
204
- padding: '6px 8px 4px 8px', // Minimal padding for tight fit
205
- margin: '0',
206
- boxSizing: 'border-box',
207
- minHeight: `${headerHeight}px`,
208
- overflow: 'visible',
209
- whiteSpace: 'normal',
210
- wordWrap: 'break-word',
211
- overflowWrap: 'break-word',
212
- width: '100%',
213
- lineHeight: '1.2', // Tight line height
214
- fontFamily: 'Albert Sans, Arial, sans-serif',
215
- display: 'flex',
216
- alignItems: 'center', // Center text vertically
217
- justifyContent: 'flex-start'
218
- });
219
- }
220
-
221
-
222
- return headerBox;
223
- }
224
-
225
- /**
226
- * Creates an individual step text box
227
- * @param {string} message - Step message
228
- * @param {number} index - Step index
229
- * @param {boolean} isMultiple - Whether this is part of multiple steps
230
- * @returns {jsvgTextBox} Step text box
231
- * @private
232
- */
233
- createStepTextBox(message, index, isMultiple) {
234
- const stepBox = new jsvgTextBox();
235
- // Calculate actual height needed for content with minimal padding
236
- const contentHeight = this.calculateContentHeight(message, index, isMultiple);
237
- const height = Math.max(contentHeight, this.baseStepHeight); // Use calculated height
238
-
239
-
240
-
241
- stepBox.setWidthAndHeight(this.stepWidth, height);
242
- stepBox.setFontSize(this.fontSize);
243
- stepBox.setFontColor('#2c3e50');
244
-
245
- // Store step data for interactions
246
- stepBox.stepIndex = index;
247
- stepBox.stepMessage = message;
248
- stepBox.isMultiple = isMultiple;
249
-
250
- // Format the step content
251
- const formattedContent = this.formatStepContent(message, index, isMultiple);
252
-
253
- // Apply styling and content
254
- if (stepBox.div) {
255
- this.applyStepStyling(stepBox, formattedContent, isMultiple, height);
256
- this.setupStepInteractions(stepBox);
257
-
258
- // Force the jsvgTextBox to respect our sizing
259
- stepBox.div.style.height = `${height}px`;
260
- stepBox.div.style.minHeight = `${height}px`;
261
- stepBox.div.style.display = 'block';
262
-
263
- // Add a more aggressive override after a delay to ensure it sticks
264
- if (stepBox.div) {
265
- const actualPadding = this.stylingOptions.padding || 6; // Get padding from styling options
266
- stepBox.div.style.cssText += `
267
- height: ${height}px !important;
268
- min-height: ${height}px !important;
269
- max-height: ${height}px !important;
270
- padding: ${actualPadding}px ${actualPadding + 2}px !important;
271
- line-height: 1.3 !important;
272
- font-size: ${this.fontSize}px !important;
273
- font-family: Albert Sans, Arial, sans-serif !important;
274
- box-sizing: border-box !important;
275
- display: flex !important;
276
- flex-direction: column !important;
277
- justify-content: center !important;
278
- align-items: flex-start !important;
279
- word-spacing: normal !important;
280
- letter-spacing: normal !important;
281
- transition: none !important;
282
- transform: none !important;
283
- animation: none !important;
284
- `;
285
- }
286
-
287
- }
288
-
289
- return stepBox;
290
- }
291
-
292
- /**
293
- * Formats the content for a step
294
- * @param {string} message - Raw message
295
- * @param {number} index - Step index
296
- * @param {boolean} isMultiple - Whether part of multiple steps
297
- * @returns {string} Formatted content
298
- * @private
299
- */
300
- formatStepContent(message, index, isMultiple) {
301
- const cleanMessage = message.trim();
302
- let content = '';
303
-
304
- // Only show step numbers for multiple steps
305
- if (isMultiple && this.messages.length > 1) {
306
- content += `<div class="step-number" style="color: #666; font-size: ${this.smallFontSize}px; margin: 0 0 2px 0; font-weight: 500; line-height: 1.2; font-family: Albert Sans, Arial, sans-serif;">Step ${index + 1}</div>`; // Minimal margin
307
- }
308
-
309
- content += '<div class="step-content" style="display: flex; align-items: center; gap: 6px; margin: 0; width: 100%; line-height: 1.3; font-family: Albert Sans, Arial, sans-serif;">'; // Center align and minimal spacing
310
- content += '<span class="bullet" style="color: #666; font-weight: bold; flex-shrink: 0; font-size: 10px;">•</span>'; // Smaller bullet
311
- content += '<div class="step-text" style="margin: 0; flex: 1; min-width: 0; word-wrap: break-word; overflow-wrap: break-word; line-height: 1.3; padding: 0;">';
312
-
313
- // Parse operation details
314
- if (this.isOperationMessage(cleanMessage)) {
315
- const action = this.extractOperationAction(cleanMessage);
316
- const value = this.extractOperationValue(cleanMessage);
317
- const valueNode = this.extractOperationValueNode(cleanMessage);
318
-
319
- if (action && (value || valueNode)) {
320
- content += `<span style="font-weight: 600; color: #2c3e50; margin-right: 4px;">${action}</span> `;
321
- const displayValue = valueNode ? valueNode.toString() : value;
322
- content += `<span style="background: #f5f5f5; padding: 3px 8px; border-radius: 4px; font-family: 'Courier New', monospace; color: #d63384; margin: 0 3px;">${displayValue}</span>`;
323
- content += `<span style="color: #666; font-size: ${this.smallFontSize}px; margin-left: 4px;"> to both sides</span>`;
324
- } else {
325
- content += `<span style="padding: 2px 0;">${cleanMessage}</span>`;
326
- }
327
- } else {
328
- content += `<span style="font-weight: 500; padding: 2px 0;">${cleanMessage}</span>`;
329
- }
330
-
331
- content += '</div></div>';
332
- return content;
333
- }
334
-
335
- /**
336
- * Applies styling to a step text box
337
- * @param {jsvgTextBox} stepBox - The step box
338
- * @param {string} content - Formatted content
339
- * @param {boolean} isMultiple - Whether part of multiple steps
340
- * @param {number} height - The calculated height for the step box
341
- * @private
342
- */
343
- applyStepStyling(stepBox, content, isMultiple, height) {
344
- const backgroundColor = this.stylingOptions.backgroundColor || omdColor.white;
345
- const borderColor = this.stylingOptions.borderColor || omdColor.lightGray;
346
- const borderWidth = this.stylingOptions.borderWidth || 1;
347
- const borderRadius = this.stylingOptions.borderRadius || 4;
348
- const padding = this.stylingOptions.padding || 6; // Minimal padding for tight fit
349
-
350
- const baseStyles = {
351
- padding: `${padding}px ${padding + 2}px !important`, // Minimal padding for tight fit
352
- borderRadius: `${borderRadius}px`,
353
- border: `${borderWidth}px solid ${borderColor}`,
354
- backgroundColor: backgroundColor,
355
- cursor: 'pointer',
356
- transition: 'none !important', // Explicitly disable all transitions
357
- transform: 'none !important', // Explicitly disable all transforms
358
- animation: 'none !important', // Explicitly disable all animations
359
- lineHeight: '1.3 !important', // Tight line height
360
- margin: '0',
361
- boxSizing: 'border-box',
362
- overflow: 'visible',
363
- minHeight: `${height}px !important`, // Use calculated height
364
- height: `${height}px !important`, // Fixed height to content
365
- width: '100%',
366
- whiteSpace: 'normal',
367
- wordWrap: 'break-word',
368
- overflowWrap: 'break-word',
369
- maxWidth: '100%',
370
- fontSize: `${this.fontSize}px !important`, // Force font size
371
- fontFamily: 'Albert Sans, Arial, sans-serif !important', // Albert Sans font
372
- display: 'flex !important', // Use flex for centering
373
- flexDirection: 'column !important',
374
- justifyContent: 'center !important', // Center content vertically
375
- alignItems: 'flex-start !important'
376
- };
377
-
378
- // Add drop shadow if requested - but NOT to individual step boxes
379
- // The drop shadow should only be on the outer background rectangle
380
- // Remove any previous drop shadow from individual steps
381
- if (stepBox.div) {
382
- stepBox.div.style.boxShadow = 'none';
383
- }
384
-
385
- // Set font family if specified
386
- if (this.stylingOptions.fontFamily) {
387
- baseStyles.fontFamily = this.stylingOptions.fontFamily;
388
- }
389
-
390
- Object.assign(stepBox.div.style, baseStyles);
391
- stepBox.div.innerHTML = content;
392
-
393
- // Additional CSS to force proper text spacing
394
- if (stepBox.div) {
395
- stepBox.div.style.cssText += `
396
- padding: ${padding + 6}px ${padding + 10}px !important;
397
- line-height: 1.7 !important;
398
- min-height: ${this.baseStepHeight + 20}px !important;
399
- font-size: ${this.fontSize}px !important;
400
- display: flex !important;
401
- flex-direction: column !important;
402
- `;
403
-
404
- // Apply proper layout to nested content - DO NOT use position absolute
405
- const contentElements = stepBox.div.querySelectorAll('.step-content, .step-text');
406
- contentElements.forEach(el => {
407
- el.style.lineHeight = '1.3 !important';
408
- el.style.margin = '0 !important';
409
- el.style.fontFamily = 'Albert Sans, Arial, sans-serif !important';
410
- // Remove any position absolute that might be inherited
411
- el.style.position = 'static !important';
412
- });
413
-
414
- // Ensure bullet points and text spans stay in normal flow
415
- const allSpans = stepBox.div.querySelectorAll('span');
416
- allSpans.forEach(span => {
417
- span.style.position = 'static !important';
418
- });
419
- }
420
-
421
- // Force a reflow to ensure proper sizing
422
- stepBox.div.offsetHeight;
423
- }
424
-
425
- /**
426
- * Sets up hover and click interactions for a step
427
- * @param {jsvgTextBox} stepBox - The step box
428
- * @private
429
- */
430
- setupStepInteractions(stepBox) {
431
- // Store the original background color to restore on mouseleave
432
- const originalBackgroundColor = stepBox.div.style.backgroundColor || '';
433
-
434
- // Hover effects
435
- stepBox.div.addEventListener('mouseenter', () => {
436
- stepBox.div.style.backgroundColor = omdColor.mediumGray; // Slightly darker version of explainColor
437
-
438
- // Call hover callback if provided
439
- this.onStepHover?.(stepBox.stepIndex, stepBox.stepMessage, true);
440
- });
441
-
442
- stepBox.div.addEventListener('mouseleave', () => {
443
- // Restore the original background color instead of setting to transparent
444
- stepBox.div.style.backgroundColor = originalBackgroundColor;
445
-
446
- // Call hover callback if provided
447
- this.onStepHover?.(stepBox.stepIndex, stepBox.stepMessage, false);
448
- });
449
-
450
- // Click interactions
451
- stepBox.div.addEventListener('click', () => {
452
- this.onStepClick?.(stepBox.stepIndex, stepBox.stepMessage);
453
- });
454
- }
455
-
456
- /**
457
- * Calculates the exact height needed for content with minimal padding
458
- * @param {string} message - Step message
459
- * @param {number} index - Step index
460
- * @param {boolean} isMultiple - Whether part of multiple steps
461
- * @returns {number} Tight-fitting height in pixels
462
- * @private
463
- */
464
- calculateContentHeight(message, index, isMultiple) {
465
- // Create a temporary element to measure actual text height
466
- const tempDiv = document.createElement('div');
467
- tempDiv.style.position = 'absolute';
468
- tempDiv.style.visibility = 'hidden';
469
- tempDiv.style.width = `${this.stepWidth - 16}px`; // Account for minimal padding
470
- tempDiv.style.fontSize = `${this.fontSize}px`;
471
- tempDiv.style.lineHeight = '1.3'; // Tight line height
472
- tempDiv.style.fontFamily = 'Albert Sans, Arial, sans-serif';
473
- tempDiv.style.whiteSpace = 'normal';
474
- tempDiv.style.wordWrap = 'break-word';
475
- tempDiv.style.overflowWrap = 'break-word';
476
- tempDiv.style.padding = '6px 8px'; // Match the minimal padding
477
- tempDiv.style.boxSizing = 'border-box';
478
- tempDiv.style.display = 'flex';
479
- tempDiv.style.flexDirection = 'column';
480
- tempDiv.style.justifyContent = 'center';
481
-
482
- // Use actual formatted content for measurement
483
- const formattedContent = this.formatStepContent(message, index, isMultiple);
484
- tempDiv.innerHTML = formattedContent;
485
-
486
- // Append to document to measure
487
- document.body.appendChild(tempDiv);
488
- const measuredHeight = tempDiv.offsetHeight;
489
- document.body.removeChild(tempDiv);
490
-
491
- // Return exact measured height with minimal buffer
492
- return Math.max(this.baseStepHeight, measuredHeight + 2); // Just 2px buffer
493
- }
494
-
495
- /**
496
- * Calculates the height needed for a step box (legacy method, kept for compatibility)
497
- * @param {string} message - Step message
498
- * @returns {number} Height in pixels
499
- * @private
500
- */
501
- calculateStepHeight(message) {
502
- // Use the new tight-fitting calculation
503
- return this.calculateContentHeight(message, 0, false);
504
- }
505
-
506
- /**
507
- * Updates the background rectangle size after layout
508
- * @private
509
- */
510
- updateBackgroundSize() {
511
- if (this.backgroundRect && this.contentGroup) {
512
- const totalHeight = this.contentGroup.height + 16; // Minimal padding for tight fit
513
- const totalWidth = this.stepWidth + 16; // Minimal padding for tight fit
514
- this.backgroundRect.setWidthAndHeight(totalWidth, totalHeight);
515
- }
516
- }
517
-
518
- /**
519
- * Applies drop shadow to the background container if SVG element exists
520
- * @private
521
- */
522
- applyDropShadowIfNeeded() {
523
- if (this.stylingOptions.dropShadow && this.backgroundRect && this.backgroundRect.svgObject) {
524
- this.backgroundRect.svgObject.style.filter = 'drop-shadow(0 2px 8px rgba(0,0,0,0.15))';
525
- }
526
- }
527
-
528
- /**
529
- * Sets callback for step hover events
530
- * @param {Function} callback - Function called with (stepIndex, message, isEntering)
531
- */
532
- setOnStepHover(callback) {
533
- this.onStepHover = callback;
534
- }
535
-
536
- /**
537
- * Sets callback for step click events
538
- * @param {Function} callback - Function called with (stepIndex, message)
539
- */
540
- setOnStepClick(callback) {
541
- this.onStepClick = callback;
542
- }
543
-
544
- /**
545
- * Gets the main layout group for adding to parent containers
546
- * @returns {jsvgLayoutGroup} The layout group
547
- */
548
- getLayoutGroup() {
549
- return this.layoutGroup;
550
- }
551
-
552
- /**
553
- * Sets the position of the entire step group
554
- * @param {number} x - X position
555
- * @param {number} y - Y position
556
- */
557
- setPosition(x, y) {
558
- this.layoutGroup.setPosition(x, y);
559
- }
560
-
561
- /**
562
- * Gets the dimensions of the step group
563
- * @returns {Object} Width and height
564
- */
565
- getDimensions() {
566
- return {
567
- width: this.backgroundRect ? this.backgroundRect.width : this.stepWidth + 16,
568
- height: this.backgroundRect ? this.backgroundRect.height : 100
569
- };
570
- }
571
-
572
- /**
573
- * Updates the styling options and re-applies them to existing elements
574
- * @param {Object} newStylingOptions - New styling options
575
- */
576
- updateStyling(newStylingOptions = {}) {
577
- this.stylingOptions = { ...this.stylingOptions, ...newStylingOptions };
578
-
579
- // Update width if maxWidth changed
580
- if (newStylingOptions.maxWidth) {
581
- this.stepWidth = newStylingOptions.maxWidth;
582
- }
583
-
584
- // Update font size if changed
585
- if (newStylingOptions.fontSize) {
586
- this.fontSize = newStylingOptions.fontSize;
587
- }
588
-
589
- // Update background container styling
590
- if (this.backgroundRect) {
591
- const backgroundColor = this.stylingOptions.backgroundColor || omdColor.lightGray;
592
- const borderColor = this.stylingOptions.borderColor || '#e0e0e0';
593
- const borderWidth = this.stylingOptions.borderWidth || 1;
594
- const borderRadius = this.stylingOptions.borderRadius || 6;
595
-
596
- this.backgroundRect.setFillColor(backgroundColor);
597
- this.backgroundRect.setStrokeColor(borderColor);
598
- this.backgroundRect.setStrokeWidth(borderWidth);
599
- this.backgroundRect.setCornerRadius(borderRadius);
600
-
601
- // Apply or remove drop shadow
602
- if (this.backgroundRect.svgObject) {
603
- if (this.stylingOptions.dropShadow) {
604
- this.backgroundRect.svgObject.style.filter = 'drop-shadow(0 2px 8px rgba(0,0,0,0.15))';
605
- } else {
606
- this.backgroundRect.svgObject.style.filter = '';
607
- }
608
- }
609
- }
610
-
611
- // Re-apply styling to all existing step elements
612
- this.stepElements.forEach((stepBox, index) => {
613
- if (stepBox.div) {
614
- const content = stepBox.div.innerHTML;
615
- // Calculate new height for the step
616
- const height = this.calculateContentHeight(stepBox.stepMessage, index, stepBox.isMultiple);
617
- this.applyStepStyling(stepBox, content, stepBox.isMultiple, height);
618
-
619
- // Update font size
620
- stepBox.setFontSize(this.fontSize);
621
-
622
- // Update dimensions if needed
623
- stepBox.setWidthAndHeight(this.stepWidth, height);
624
- }
625
- });
626
-
627
- // Update background size
628
- this.updateBackgroundSize();
629
- }
630
-
631
- /**
632
- * Gets the current styling options
633
- * @returns {Object} Current styling options
634
- */
635
- getStyling() {
636
- return { ...this.stylingOptions };
637
- }
638
-
639
- // Helper methods for message parsing (same as in formatter)
640
-
641
- isOperationMessage(message) {
642
- const operationKeywords = ['Applied', 'added', 'subtracted', 'multiplied', 'divided', 'both sides'];
643
- return operationKeywords.some(keyword =>
644
- message.toLowerCase().includes(keyword.toLowerCase())
645
- );
646
- }
647
-
648
- extractOperationAction(message) {
649
- const match = message.match(/^(Added|Subtracted|Multiplied|Divided)/i);
650
- return match ? match[0] : null;
651
- }
652
-
653
- extractOperationValue(message) {
654
- // Updated regex to handle simple values and expressions
655
- const match = message.match(/(?:Added|Subtracted|Multiplied|Divided)\s(.*?)\s(?:to|by)/i);
656
- if (match && match[1]) {
657
- // Avoid returning "[object Object]"
658
- if (match[1].includes('[object Object]')) {
659
- return null;
660
- }
661
- return match[1];
662
- }
663
- return null;
664
- }
665
-
666
- extractOperationValueNode(message) {
667
- if (this.simplificationData && this.simplificationData.operationValueNode) {
668
- return this.simplificationData.operationValueNode;
669
- }
670
- return null;
671
- }
672
-
673
- /**
674
- * Destroys the step group and cleans up resources
675
- */
676
- destroy() {
677
- this.stepElements = [];
678
- if (this.contentGroup) {
679
- this.contentGroup.removeAllChildren();
680
- }
681
- this.layoutGroup.removeAllChildren();
682
- this.onStepHover = null;
683
- this.onStepClick = null;
684
- }
1
+ import { omdColor } from '../../src/omdColor.js';
2
+ import { jsvgLayoutGroup, jsvgTextBox, jsvgRect } from '@teachinglab/jsvg';
3
+ /**
4
+ * Creates interactive step elements using jsvgLayoutGroup for multiple simplification steps
5
+ * Each step is a separate jsvgTextBox that can have hover interactions with the omdSequence
6
+ */
7
+ export class omdStepVisualizerInteractiveSteps {
8
+ constructor(stepVisualizer, simplificationData, stylingOptions = {}) {
9
+ this.stepVisualizer = stepVisualizer;
10
+ this.simplificationData = simplificationData || {};
11
+ this.stylingOptions = stylingOptions || {};
12
+ this.messages = this._extractMessages(simplificationData);
13
+ this.ruleNames = this._extractRuleNames(simplificationData);
14
+ this.stepElements = [];
15
+ this.layoutGroup = new jsvgLayoutGroup();
16
+ this.layoutGroup.setSpacer(4); // Minimal spacing for tight layout
17
+
18
+ // Styling configuration with defaults that can be overridden
19
+ this.stepWidth = this.stylingOptions.maxWidth || 300; // Use maxWidth from styling options
20
+ this.baseStepHeight = 30; // Minimal height for tight fit
21
+ this.headerHeight = 28; // Minimal header height
22
+ this.fontSize = this.stylingOptions.fontSize || 14;
23
+ this.smallFontSize = 12;
24
+
25
+ this.setupLayoutGroup();
26
+ this.createStepElements();
27
+ }
28
+
29
+ /**
30
+ * Extracts messages from simplification data
31
+ * @param {Object} data - Simplification data
32
+ * @returns {Array} Array of clean messages
33
+ * @private
34
+ */
35
+ _extractMessages(data) {
36
+ if (!data) return [];
37
+
38
+ let messages = [];
39
+ if (data.rawMessages && Array.isArray(data.rawMessages)) {
40
+ messages = data.rawMessages;
41
+ } else if (data.message) {
42
+ messages = [data.message];
43
+ }
44
+
45
+ // Clean up messages - remove HTML tags and bullet points
46
+ return messages.map(msg => {
47
+ let clean = msg.replace(/<[^>]*>/g, ''); // Strip HTML tags
48
+ clean = clean.replace(/^[•·◦▪▫‣⁃]\s*/, ''); // Strip bullet points
49
+ return clean.trim();
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Extracts rule names from simplification data
55
+ * @param {Object} data - Simplification data
56
+ * @returns {Array} Array of rule names
57
+ * @private
58
+ */
59
+ _extractRuleNames(data) {
60
+ if (!data) return ['Operation'];
61
+
62
+ if (data.ruleNames && Array.isArray(data.ruleNames)) {
63
+ return data.ruleNames;
64
+ }
65
+
66
+ // Default based on data type
67
+ if (data.multipleSimplifications) {
68
+ return ['Multiple Rules'];
69
+ } else {
70
+ return ['Operation'];
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Sets up the main layout group properties
76
+ * @private
77
+ */
78
+ setupLayoutGroup() {
79
+ // Add background using styling options for the entire step group
80
+ this.backgroundRect = new jsvgRect();
81
+ this.backgroundRect.setWidthAndHeight(this.stepWidth + 16, 60); // Minimal padding and height for tight fit
82
+
83
+ // Apply styling options to the background container
84
+ const backgroundColor = this.stylingOptions.backgroundColor || omdColor.lightGray;
85
+ const borderColor = this.stylingOptions.borderColor || '#e0e0e0';
86
+ const borderWidth = this.stylingOptions.borderWidth || 1;
87
+ const borderRadius = this.stylingOptions.borderRadius || 6;
88
+
89
+ this.backgroundRect.setFillColor(backgroundColor);
90
+ this.backgroundRect.setStrokeColor(borderColor);
91
+ this.backgroundRect.setStrokeWidth(borderWidth);
92
+ this.backgroundRect.setCornerRadius(borderRadius);
93
+ this.backgroundRect.setPosition(0, 0); // Start at origin, not negative offset
94
+
95
+ // Apply drop shadow to the SVG element if requested
96
+ if (this.stylingOptions.dropShadow && this.backgroundRect.svgObject) {
97
+ this.backgroundRect.svgObject.style.filter = 'drop-shadow(0 2px 8px rgba(0,0,0,0.15))';
98
+ }
99
+
100
+ this.layoutGroup.addChild(this.backgroundRect);
101
+ }
102
+
103
+ /**
104
+ * Creates individual step elements from the messages array
105
+ * @private
106
+ */
107
+ createStepElements() {
108
+ if (!this.messages || this.messages.length === 0) return;
109
+
110
+ // Create content container to separate from background
111
+ this.contentGroup = new jsvgLayoutGroup();
112
+ this.contentGroup.setSpacer(2); // Minimal spacing between elements
113
+ this.contentGroup.setPosition(8, 8); // Minimal offset for tight fit
114
+
115
+ if (this.messages.length === 1) {
116
+
117
+ this.createSingleStepElement(this.messages[0], 0);
118
+ } else {
119
+
120
+ this.createMultipleStepElements();
121
+ }
122
+
123
+ this.contentGroup.doVerticalLayout();
124
+ this.layoutGroup.addChild(this.contentGroup);
125
+ this.updateBackgroundSize();
126
+
127
+ // Apply drop shadow after SVG element is created
128
+ setTimeout(() => {
129
+ this.applyDropShadowIfNeeded();
130
+ }, 10);
131
+
132
+ // Debug logging
133
+
134
+ }
135
+
136
+ /**
137
+ * Creates a single step element with header
138
+ * @param {string} message - The step message
139
+ * @param {number} index - Step index
140
+ * @private
141
+ */
142
+ createSingleStepElement(message, index) {
143
+ // Create header for single step using rule name
144
+ const ruleName = this.ruleNames[0] || 'Operation';
145
+ const headerBox = this.createHeaderBox(ruleName + ':');
146
+ this.contentGroup.addChild(headerBox);
147
+
148
+ // Create the step box
149
+ const stepBox = this.createStepTextBox(message, index, false);
150
+ this.stepElements.push(stepBox);
151
+ this.contentGroup.addChild(stepBox);
152
+ }
153
+
154
+ /**
155
+ * Creates multiple step elements with header
156
+ * @private
157
+ */
158
+ createMultipleStepElements() {
159
+ // Only create header for truly multiple steps (more than 1)
160
+ if (this.messages.length > 1) {
161
+ // Create header showing rule names
162
+ let headerText;
163
+ if (this.ruleNames.length === 1) {
164
+ headerText = this.ruleNames[0] + ':';
165
+ } else if (this.ruleNames.length <= 3) {
166
+ headerText = this.ruleNames.join(' + ') + ':';
167
+ } else {
168
+ headerText = `${this.ruleNames.length} Rules Applied:`;
169
+ }
170
+
171
+ const headerBox = this.createHeaderBox(headerText);
172
+ this.contentGroup.addChild(headerBox);
173
+ }
174
+
175
+ // Create individual step elements
176
+ this.messages.forEach((message, index) => {
177
+ const stepBox = this.createStepTextBox(message, index, this.messages.length > 1);
178
+ this.stepElements.push(stepBox);
179
+ this.contentGroup.addChild(stepBox);
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Creates a header box with custom text
185
+ * @param {string} headerText - Text to display in header
186
+ * @returns {jsvgTextBox} Header text box
187
+ * @private
188
+ */
189
+ createHeaderBox(headerText = 'Operation:') {
190
+ const headerBox = new jsvgTextBox();
191
+ const headerHeight = Math.max(this.headerHeight, 50); // Increased minimum height
192
+
193
+
194
+ headerBox.setWidthAndHeight(this.stepWidth, headerHeight);
195
+ headerBox.setText(headerText);
196
+ headerBox.setFontSize(this.fontSize);
197
+ headerBox.setFontWeight('600');
198
+ headerBox.setFontColor('#2c3e50');
199
+
200
+ // Style the header with border and minimal spacing
201
+ if (headerBox.div) {
202
+ Object.assign(headerBox.div.style, {
203
+ borderBottom: '1px solid #e0e0e0',
204
+ padding: '6px 8px 4px 8px', // Minimal padding for tight fit
205
+ margin: '0',
206
+ boxSizing: 'border-box',
207
+ minHeight: `${headerHeight}px`,
208
+ overflow: 'visible',
209
+ whiteSpace: 'normal',
210
+ wordWrap: 'break-word',
211
+ overflowWrap: 'break-word',
212
+ width: '100%',
213
+ lineHeight: '1.2', // Tight line height
214
+ fontFamily: 'Albert Sans, Arial, sans-serif',
215
+ display: 'flex',
216
+ alignItems: 'center', // Center text vertically
217
+ justifyContent: 'flex-start'
218
+ });
219
+ }
220
+
221
+
222
+ return headerBox;
223
+ }
224
+
225
+ /**
226
+ * Creates an individual step text box
227
+ * @param {string} message - Step message
228
+ * @param {number} index - Step index
229
+ * @param {boolean} isMultiple - Whether this is part of multiple steps
230
+ * @returns {jsvgTextBox} Step text box
231
+ * @private
232
+ */
233
+ createStepTextBox(message, index, isMultiple) {
234
+ const stepBox = new jsvgTextBox();
235
+ // Calculate actual height needed for content with minimal padding
236
+ const contentHeight = this.calculateContentHeight(message, index, isMultiple);
237
+ const height = Math.max(contentHeight, this.baseStepHeight); // Use calculated height
238
+
239
+
240
+
241
+ stepBox.setWidthAndHeight(this.stepWidth, height);
242
+ stepBox.setFontSize(this.fontSize);
243
+ stepBox.setFontColor('#2c3e50');
244
+
245
+ // Store step data for interactions
246
+ stepBox.stepIndex = index;
247
+ stepBox.stepMessage = message;
248
+ stepBox.isMultiple = isMultiple;
249
+
250
+ // Format the step content
251
+ const formattedContent = this.formatStepContent(message, index, isMultiple);
252
+
253
+ // Apply styling and content
254
+ if (stepBox.div) {
255
+ this.applyStepStyling(stepBox, formattedContent, isMultiple, height);
256
+ this.setupStepInteractions(stepBox);
257
+
258
+ // Force the jsvgTextBox to respect our sizing
259
+ stepBox.div.style.height = `${height}px`;
260
+ stepBox.div.style.minHeight = `${height}px`;
261
+ stepBox.div.style.display = 'block';
262
+
263
+ // Add a more aggressive override after a delay to ensure it sticks
264
+ if (stepBox.div) {
265
+ const actualPadding = this.stylingOptions.padding || 6; // Get padding from styling options
266
+ stepBox.div.style.cssText += `
267
+ height: ${height}px !important;
268
+ min-height: ${height}px !important;
269
+ max-height: ${height}px !important;
270
+ padding: ${actualPadding}px ${actualPadding + 2}px !important;
271
+ line-height: 1.3 !important;
272
+ font-size: ${this.fontSize}px !important;
273
+ font-family: Albert Sans, Arial, sans-serif !important;
274
+ box-sizing: border-box !important;
275
+ display: flex !important;
276
+ flex-direction: column !important;
277
+ justify-content: center !important;
278
+ align-items: flex-start !important;
279
+ word-spacing: normal !important;
280
+ letter-spacing: normal !important;
281
+ transition: none !important;
282
+ transform: none !important;
283
+ animation: none !important;
284
+ `;
285
+ }
286
+
287
+ }
288
+
289
+ return stepBox;
290
+ }
291
+
292
+ /**
293
+ * Formats the content for a step
294
+ * @param {string} message - Raw message
295
+ * @param {number} index - Step index
296
+ * @param {boolean} isMultiple - Whether part of multiple steps
297
+ * @returns {string} Formatted content
298
+ * @private
299
+ */
300
+ formatStepContent(message, index, isMultiple) {
301
+ const cleanMessage = message.trim();
302
+ let content = '';
303
+
304
+ // Only show step numbers for multiple steps
305
+ if (isMultiple && this.messages.length > 1) {
306
+ content += `<div class="step-number" style="color: #666; font-size: ${this.smallFontSize}px; margin: 0 0 2px 0; font-weight: 500; line-height: 1.2; font-family: Albert Sans, Arial, sans-serif;">Step ${index + 1}</div>`; // Minimal margin
307
+ }
308
+
309
+ content += '<div class="step-content" style="display: flex; align-items: center; gap: 6px; margin: 0; width: 100%; line-height: 1.3; font-family: Albert Sans, Arial, sans-serif;">'; // Center align and minimal spacing
310
+ content += '<span class="bullet" style="color: #666; font-weight: bold; flex-shrink: 0; font-size: 10px;">•</span>'; // Smaller bullet
311
+ content += '<div class="step-text" style="margin: 0; flex: 1; min-width: 0; word-wrap: break-word; overflow-wrap: break-word; line-height: 1.3; padding: 0;">';
312
+
313
+ // Parse operation details
314
+ if (this.isOperationMessage(cleanMessage)) {
315
+ const action = this.extractOperationAction(cleanMessage);
316
+ const value = this.extractOperationValue(cleanMessage);
317
+ const valueNode = this.extractOperationValueNode(cleanMessage);
318
+
319
+ if (action && (value || valueNode)) {
320
+ content += `<span style="font-weight: 600; color: #2c3e50; margin-right: 4px;">${action}</span> `;
321
+ const displayValue = valueNode ? valueNode.toString() : value;
322
+ content += `<span style="background: #f5f5f5; padding: 3px 8px; border-radius: 4px; font-family: 'Courier New', monospace; color: #d63384; margin: 0 3px;">${displayValue}</span>`;
323
+ content += `<span style="color: #666; font-size: ${this.smallFontSize}px; margin-left: 4px;"> to both sides</span>`;
324
+ } else {
325
+ content += `<span style="padding: 2px 0;">${cleanMessage}</span>`;
326
+ }
327
+ } else {
328
+ content += `<span style="font-weight: 500; padding: 2px 0;">${cleanMessage}</span>`;
329
+ }
330
+
331
+ content += '</div></div>';
332
+ return content;
333
+ }
334
+
335
+ /**
336
+ * Applies styling to a step text box
337
+ * @param {jsvgTextBox} stepBox - The step box
338
+ * @param {string} content - Formatted content
339
+ * @param {boolean} isMultiple - Whether part of multiple steps
340
+ * @param {number} height - The calculated height for the step box
341
+ * @private
342
+ */
343
+ applyStepStyling(stepBox, content, isMultiple, height) {
344
+ const backgroundColor = this.stylingOptions.backgroundColor || omdColor.white;
345
+ const borderColor = this.stylingOptions.borderColor || omdColor.lightGray;
346
+ const borderWidth = this.stylingOptions.borderWidth || 1;
347
+ const borderRadius = this.stylingOptions.borderRadius || 4;
348
+ const padding = this.stylingOptions.padding || 6; // Minimal padding for tight fit
349
+
350
+ const baseStyles = {
351
+ padding: `${padding}px ${padding + 2}px !important`, // Minimal padding for tight fit
352
+ borderRadius: `${borderRadius}px`,
353
+ border: `${borderWidth}px solid ${borderColor}`,
354
+ backgroundColor: backgroundColor,
355
+ cursor: 'pointer',
356
+ transition: 'none !important', // Explicitly disable all transitions
357
+ transform: 'none !important', // Explicitly disable all transforms
358
+ animation: 'none !important', // Explicitly disable all animations
359
+ lineHeight: '1.3 !important', // Tight line height
360
+ margin: '0',
361
+ boxSizing: 'border-box',
362
+ overflow: 'visible',
363
+ minHeight: `${height}px !important`, // Use calculated height
364
+ height: `${height}px !important`, // Fixed height to content
365
+ width: '100%',
366
+ whiteSpace: 'normal',
367
+ wordWrap: 'break-word',
368
+ overflowWrap: 'break-word',
369
+ maxWidth: '100%',
370
+ fontSize: `${this.fontSize}px !important`, // Force font size
371
+ fontFamily: 'Albert Sans, Arial, sans-serif !important', // Albert Sans font
372
+ display: 'flex !important', // Use flex for centering
373
+ flexDirection: 'column !important',
374
+ justifyContent: 'center !important', // Center content vertically
375
+ alignItems: 'flex-start !important'
376
+ };
377
+
378
+ // Add drop shadow if requested - but NOT to individual step boxes
379
+ // The drop shadow should only be on the outer background rectangle
380
+ // Remove any previous drop shadow from individual steps
381
+ if (stepBox.div) {
382
+ stepBox.div.style.boxShadow = 'none';
383
+ }
384
+
385
+ // Set font family if specified
386
+ if (this.stylingOptions.fontFamily) {
387
+ baseStyles.fontFamily = this.stylingOptions.fontFamily;
388
+ }
389
+
390
+ Object.assign(stepBox.div.style, baseStyles);
391
+ stepBox.div.innerHTML = content;
392
+
393
+ // Additional CSS to force proper text spacing
394
+ if (stepBox.div) {
395
+ stepBox.div.style.cssText += `
396
+ padding: ${padding + 6}px ${padding + 10}px !important;
397
+ line-height: 1.7 !important;
398
+ min-height: ${this.baseStepHeight + 20}px !important;
399
+ font-size: ${this.fontSize}px !important;
400
+ display: flex !important;
401
+ flex-direction: column !important;
402
+ `;
403
+
404
+ // Apply proper layout to nested content - DO NOT use position absolute
405
+ const contentElements = stepBox.div.querySelectorAll('.step-content, .step-text');
406
+ contentElements.forEach(el => {
407
+ el.style.lineHeight = '1.3 !important';
408
+ el.style.margin = '0 !important';
409
+ el.style.fontFamily = 'Albert Sans, Arial, sans-serif !important';
410
+ // Remove any position absolute that might be inherited
411
+ el.style.position = 'static !important';
412
+ });
413
+
414
+ // Ensure bullet points and text spans stay in normal flow
415
+ const allSpans = stepBox.div.querySelectorAll('span');
416
+ allSpans.forEach(span => {
417
+ span.style.position = 'static !important';
418
+ });
419
+ }
420
+
421
+ // Force a reflow to ensure proper sizing
422
+ stepBox.div.offsetHeight;
423
+ }
424
+
425
+ /**
426
+ * Sets up hover and click interactions for a step
427
+ * @param {jsvgTextBox} stepBox - The step box
428
+ * @private
429
+ */
430
+ setupStepInteractions(stepBox) {
431
+ // Store the original background color to restore on mouseleave
432
+ const originalBackgroundColor = stepBox.div.style.backgroundColor || '';
433
+
434
+ // Hover effects
435
+ stepBox.div.addEventListener('mouseenter', () => {
436
+ stepBox.div.style.backgroundColor = omdColor.mediumGray; // Slightly darker version of explainColor
437
+
438
+ // Call hover callback if provided
439
+ this.onStepHover?.(stepBox.stepIndex, stepBox.stepMessage, true);
440
+ });
441
+
442
+ stepBox.div.addEventListener('mouseleave', () => {
443
+ // Restore the original background color instead of setting to transparent
444
+ stepBox.div.style.backgroundColor = originalBackgroundColor;
445
+
446
+ // Call hover callback if provided
447
+ this.onStepHover?.(stepBox.stepIndex, stepBox.stepMessage, false);
448
+ });
449
+
450
+ // Click interactions
451
+ stepBox.div.addEventListener('click', () => {
452
+ this.onStepClick?.(stepBox.stepIndex, stepBox.stepMessage);
453
+ });
454
+ }
455
+
456
+ /**
457
+ * Calculates the exact height needed for content with minimal padding
458
+ * @param {string} message - Step message
459
+ * @param {number} index - Step index
460
+ * @param {boolean} isMultiple - Whether part of multiple steps
461
+ * @returns {number} Tight-fitting height in pixels
462
+ * @private
463
+ */
464
+ calculateContentHeight(message, index, isMultiple) {
465
+ // Create a temporary element to measure actual text height
466
+ const tempDiv = document.createElement('div');
467
+ tempDiv.style.position = 'absolute';
468
+ tempDiv.style.visibility = 'hidden';
469
+ tempDiv.style.width = `${this.stepWidth - 16}px`; // Account for minimal padding
470
+ tempDiv.style.fontSize = `${this.fontSize}px`;
471
+ tempDiv.style.lineHeight = '1.3'; // Tight line height
472
+ tempDiv.style.fontFamily = 'Albert Sans, Arial, sans-serif';
473
+ tempDiv.style.whiteSpace = 'normal';
474
+ tempDiv.style.wordWrap = 'break-word';
475
+ tempDiv.style.overflowWrap = 'break-word';
476
+ tempDiv.style.padding = '6px 8px'; // Match the minimal padding
477
+ tempDiv.style.boxSizing = 'border-box';
478
+ tempDiv.style.display = 'flex';
479
+ tempDiv.style.flexDirection = 'column';
480
+ tempDiv.style.justifyContent = 'center';
481
+
482
+ // Use actual formatted content for measurement
483
+ const formattedContent = this.formatStepContent(message, index, isMultiple);
484
+ tempDiv.innerHTML = formattedContent;
485
+
486
+ // Append to document to measure
487
+ document.body.appendChild(tempDiv);
488
+ const measuredHeight = tempDiv.offsetHeight;
489
+ document.body.removeChild(tempDiv);
490
+
491
+ // Return exact measured height with minimal buffer
492
+ return Math.max(this.baseStepHeight, measuredHeight + 2); // Just 2px buffer
493
+ }
494
+
495
+ /**
496
+ * Calculates the height needed for a step box (legacy method, kept for compatibility)
497
+ * @param {string} message - Step message
498
+ * @returns {number} Height in pixels
499
+ * @private
500
+ */
501
+ calculateStepHeight(message) {
502
+ // Use the new tight-fitting calculation
503
+ return this.calculateContentHeight(message, 0, false);
504
+ }
505
+
506
+ /**
507
+ * Updates the background rectangle size after layout
508
+ * @private
509
+ */
510
+ updateBackgroundSize() {
511
+ if (this.backgroundRect && this.contentGroup) {
512
+ const totalHeight = this.contentGroup.height + 16; // Minimal padding for tight fit
513
+ const totalWidth = this.stepWidth + 16; // Minimal padding for tight fit
514
+ this.backgroundRect.setWidthAndHeight(totalWidth, totalHeight);
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Applies drop shadow to the background container if SVG element exists
520
+ * @private
521
+ */
522
+ applyDropShadowIfNeeded() {
523
+ if (this.stylingOptions.dropShadow && this.backgroundRect && this.backgroundRect.svgObject) {
524
+ this.backgroundRect.svgObject.style.filter = 'drop-shadow(0 2px 8px rgba(0,0,0,0.15))';
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Sets callback for step hover events
530
+ * @param {Function} callback - Function called with (stepIndex, message, isEntering)
531
+ */
532
+ setOnStepHover(callback) {
533
+ this.onStepHover = callback;
534
+ }
535
+
536
+ /**
537
+ * Sets callback for step click events
538
+ * @param {Function} callback - Function called with (stepIndex, message)
539
+ */
540
+ setOnStepClick(callback) {
541
+ this.onStepClick = callback;
542
+ }
543
+
544
+ /**
545
+ * Gets the main layout group for adding to parent containers
546
+ * @returns {jsvgLayoutGroup} The layout group
547
+ */
548
+ getLayoutGroup() {
549
+ return this.layoutGroup;
550
+ }
551
+
552
+ /**
553
+ * Sets the position of the entire step group
554
+ * @param {number} x - X position
555
+ * @param {number} y - Y position
556
+ */
557
+ setPosition(x, y) {
558
+ this.layoutGroup.setPosition(x, y);
559
+ }
560
+
561
+ /**
562
+ * Gets the dimensions of the step group
563
+ * @returns {Object} Width and height
564
+ */
565
+ getDimensions() {
566
+ return {
567
+ width: this.backgroundRect ? this.backgroundRect.width : this.stepWidth + 16,
568
+ height: this.backgroundRect ? this.backgroundRect.height : 100
569
+ };
570
+ }
571
+
572
+ /**
573
+ * Updates the styling options and re-applies them to existing elements
574
+ * @param {Object} newStylingOptions - New styling options
575
+ */
576
+ updateStyling(newStylingOptions = {}) {
577
+ this.stylingOptions = { ...this.stylingOptions, ...newStylingOptions };
578
+
579
+ // Update width if maxWidth changed
580
+ if (newStylingOptions.maxWidth) {
581
+ this.stepWidth = newStylingOptions.maxWidth;
582
+ }
583
+
584
+ // Update font size if changed
585
+ if (newStylingOptions.fontSize) {
586
+ this.fontSize = newStylingOptions.fontSize;
587
+ }
588
+
589
+ // Update background container styling
590
+ if (this.backgroundRect) {
591
+ const backgroundColor = this.stylingOptions.backgroundColor || omdColor.lightGray;
592
+ const borderColor = this.stylingOptions.borderColor || '#e0e0e0';
593
+ const borderWidth = this.stylingOptions.borderWidth || 1;
594
+ const borderRadius = this.stylingOptions.borderRadius || 6;
595
+
596
+ this.backgroundRect.setFillColor(backgroundColor);
597
+ this.backgroundRect.setStrokeColor(borderColor);
598
+ this.backgroundRect.setStrokeWidth(borderWidth);
599
+ this.backgroundRect.setCornerRadius(borderRadius);
600
+
601
+ // Apply or remove drop shadow
602
+ if (this.backgroundRect.svgObject) {
603
+ if (this.stylingOptions.dropShadow) {
604
+ this.backgroundRect.svgObject.style.filter = 'drop-shadow(0 2px 8px rgba(0,0,0,0.15))';
605
+ } else {
606
+ this.backgroundRect.svgObject.style.filter = '';
607
+ }
608
+ }
609
+ }
610
+
611
+ // Re-apply styling to all existing step elements
612
+ this.stepElements.forEach((stepBox, index) => {
613
+ if (stepBox.div) {
614
+ const content = stepBox.div.innerHTML;
615
+ // Calculate new height for the step
616
+ const height = this.calculateContentHeight(stepBox.stepMessage, index, stepBox.isMultiple);
617
+ this.applyStepStyling(stepBox, content, stepBox.isMultiple, height);
618
+
619
+ // Update font size
620
+ stepBox.setFontSize(this.fontSize);
621
+
622
+ // Update dimensions if needed
623
+ stepBox.setWidthAndHeight(this.stepWidth, height);
624
+ }
625
+ });
626
+
627
+ // Update background size
628
+ this.updateBackgroundSize();
629
+ }
630
+
631
+ /**
632
+ * Gets the current styling options
633
+ * @returns {Object} Current styling options
634
+ */
635
+ getStyling() {
636
+ return { ...this.stylingOptions };
637
+ }
638
+
639
+ // Helper methods for message parsing (same as in formatter)
640
+
641
+ isOperationMessage(message) {
642
+ const operationKeywords = ['Applied', 'added', 'subtracted', 'multiplied', 'divided', 'both sides'];
643
+ return operationKeywords.some(keyword =>
644
+ message.toLowerCase().includes(keyword.toLowerCase())
645
+ );
646
+ }
647
+
648
+ extractOperationAction(message) {
649
+ const match = message.match(/^(Added|Subtracted|Multiplied|Divided)/i);
650
+ return match ? match[0] : null;
651
+ }
652
+
653
+ extractOperationValue(message) {
654
+ // Updated regex to handle simple values and expressions
655
+ const match = message.match(/(?:Added|Subtracted|Multiplied|Divided)\s(.*?)\s(?:to|by)/i);
656
+ if (match && match[1]) {
657
+ // Avoid returning "[object Object]"
658
+ if (match[1].includes('[object Object]')) {
659
+ return null;
660
+ }
661
+ return match[1];
662
+ }
663
+ return null;
664
+ }
665
+
666
+ extractOperationValueNode(message) {
667
+ if (this.simplificationData && this.simplificationData.operationValueNode) {
668
+ return this.simplificationData.operationValueNode;
669
+ }
670
+ return null;
671
+ }
672
+
673
+ /**
674
+ * Destroys the step group and cleans up resources
675
+ */
676
+ destroy() {
677
+ this.stepElements = [];
678
+ if (this.contentGroup) {
679
+ this.contentGroup.removeAllChildren();
680
+ }
681
+ this.layoutGroup.removeAllChildren();
682
+ this.onStepHover = null;
683
+ this.onStepClick = null;
684
+ }
685
685
  }