@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,1204 +1,1204 @@
1
- import { jsvgGroup, jsvgRect, jsvgButton, jsvgLayoutGroup, jsvgTextInput } from '@teachinglab/jsvg';
2
- /**
3
- * omdPopup - Handles popup creation and management for node overlays
4
- */
5
- export class omdPopup {
6
- constructor(targetNode, parentElement, options = {}) {
7
- this.targetNode = targetNode;
8
- this.parentElement = parentElement;
9
- this.options = {
10
- editable: true,
11
- animationDuration: 200,
12
- ...options
13
- };
14
-
15
- // State
16
- this.popup = null;
17
- this.popupBackground = null;
18
- this.popupTextInput = null;
19
- this.penCanvas = null;
20
- this.penCanvasCleanup = null;
21
- this.currentMode = 'text'; // 'text' or 'pen'
22
- this.popupAnimationId = null;
23
-
24
- // Buttons
25
- this.penButton = null;
26
- this.textButton = null;
27
- this.clearButton = null;
28
- this.submitButton = null;
29
-
30
- // Callbacks
31
- this.onValidateCallback = null;
32
- this.onClearCallback = null;
33
-
34
- // Layout configuration - centralized variables
35
- this.popupWidth = 400;
36
- this.buttonSize = 18;
37
- this.margin = 10;
38
- this.buttonSpacing = 4;
39
- this.canvasMinWidth = 100;
40
- this.canvasMinHeight = 60;
41
- this.popupHeightMultiplier = 2;
42
- this.targetNodeDefaultHeight = 40;
43
- this.canvasTopOffset = this.margin;
44
- this.canvasLeftOffset = this.margin;
45
- }
46
-
47
- /**
48
- * Creates and shows the popup
49
- * @param {number} x - X position
50
- * @param {number} y - Y position
51
- * @returns {Promise} Promise that resolves when animation completes
52
- */
53
- show(x, y) {
54
- if (this.popup) {
55
- return this.hide().then(() => this.show(x, y));
56
- }
57
-
58
- this._createPopup();
59
- this._positionPopup(x, y);
60
- this.parentElement.addChild(this.popup);
61
-
62
- // Start invisible and animate in
63
- this.popup.setOpacity(0);
64
- return this._animateOpacity(0, 1, this.options.animationDuration);
65
- }
66
-
67
- /**
68
- * Hides the popup
69
- * @returns {Promise} Promise that resolves when animation completes
70
- */
71
- hide() {
72
- if (!this.popup) return Promise.resolve();
73
-
74
- // Store original opacities for animation
75
- const originalPopupOpacity = this.popup.opacity;
76
- const originalTextInputOpacity = this.popupTextInput?.div?.style.opacity || '1';
77
-
78
- // Ensure canvas and text input are visible for animation but preserve canvas drawing
79
- if (this.penCanvas && this.penCanvas.container) {
80
- this.penCanvas.container.style.display = 'block';
81
- // Don't fade canvas opacity - keep strokes visible during popup hide
82
- }
83
- if (this.popupTextInput && this.popupTextInput.div) {
84
- this.popupTextInput.div.style.display = 'flex';
85
- this.popupTextInput.div.style.opacity = originalTextInputOpacity;
86
- }
87
-
88
- // Animate popup and canvas together
89
- const duration = this.options.animationDuration || 300;
90
- const startTime = performance.now();
91
-
92
- const animate = (currentTime) => {
93
- const elapsed = currentTime - startTime;
94
- const progress = Math.min(elapsed / duration, 1);
95
-
96
- // Easing function for smooth animation
97
- const easeOut = 1 - Math.pow(1 - progress, 3);
98
- const currentOpacity = originalPopupOpacity * (1 - easeOut);
99
-
100
- // Animate popup
101
- if (this.popup) {
102
- this.popup.setOpacity(currentOpacity);
103
- }
104
-
105
- // Animate canvas container with same opacity curve
106
- if (this.penCanvas && this.penCanvas.container) {
107
- this.penCanvas.container.style.opacity = currentOpacity;
108
- }
109
-
110
- // Animate text input with proper opacity setting
111
- if (this.popupTextInput && this.popupTextInput.div) {
112
- this.popupTextInput.div.style.opacity = currentOpacity;
113
- }
114
-
115
- if (progress < 1) {
116
- this.popupAnimationId = requestAnimationFrame(animate);
117
- } else {
118
- // Animation complete - hide and cleanup both popup and canvas
119
- if (this.penCanvas && this.penCanvas.container) {
120
- this.penCanvas.container.style.display = 'none';
121
- this.penCanvas.container.style.opacity = '1'; // Reset for next show
122
- }
123
- if (this.popupTextInput && this.popupTextInput.div) {
124
- this.popupTextInput.div.style.display = 'none';
125
- }
126
- this._cleanup();
127
- }
128
- };
129
-
130
- this.popupAnimationId = requestAnimationFrame(animate);
131
-
132
- return new Promise((resolve) => {
133
- setTimeout(resolve, duration);
134
- });
135
- }
136
-
137
- /**
138
- * Toggles popup visibility
139
- * @param {number} x - X position for showing
140
- * @param {number} y - Y position for showing
141
- * @returns {Promise} Promise that resolves when animation completes
142
- */
143
- toggle(x, y) {
144
- if (this.popup && this.popup.visible) {
145
- return this.hide();
146
- } else {
147
- return this.show(x, y);
148
- }
149
- }
150
-
151
- /**
152
- * Sets the validation callback
153
- * @param {Function} callback - Function to call for validation
154
- */
155
- setValidationCallback(callback) {
156
- this.onValidateCallback = callback;
157
- }
158
-
159
- /**
160
- * Sets the clear callback
161
- * @param {Function} callback - Function to call when clearing
162
- */
163
- setClearCallback(callback) {
164
- this.onClearCallback = callback;
165
- }
166
-
167
- /**
168
- * Gets the current input value
169
- * @returns {string} Current input value
170
- */
171
- getValue() {
172
- // If we have transcribed text from pen mode, return it
173
- if (this.transcribedText) {
174
- const text = this.transcribedText;
175
- // Clear the transcribed text after returning it
176
- this.transcribedText = null;
177
- return text;
178
- }
179
-
180
- if (this.currentMode === 'text' && this.popupTextInput) {
181
- return this.popupTextInput.getText();
182
- }
183
- return '';
184
- }
185
-
186
- /**
187
- * Sets the input value
188
- * @param {string} value - Value to set
189
- */
190
- setValue(value) {
191
- if (this.currentMode === 'text' && this.popupTextInput) {
192
- this.popupTextInput.setText(value);
193
- }
194
- }
195
-
196
- /**
197
- * Switches between text and pen modes
198
- * @param {string} mode - 'text' or 'pen'
199
- */
200
- switchToMode(mode) {
201
- if (this.currentMode === mode || !this.popup) {
202
- return;
203
- }
204
- this.currentMode = mode;
205
- if (mode === 'pen') {
206
- this._showPenMode();
207
- } else {
208
- this._showTextMode();
209
- }
210
- // Update button states
211
- this._updateButtonStates();
212
- }
213
-
214
- /**
215
- * Creates the popup structure
216
- * @private
217
- */
218
- _createPopup() {
219
- const popupHeight = (this.targetNode?.height || this.targetNodeDefaultHeight) * this.popupHeightMultiplier;
220
-
221
- // Create popup container
222
- this.popup = new jsvgLayoutGroup();
223
-
224
- // Create popup background
225
- this.popupBackground = new jsvgRect();
226
- this.popupBackground.setWidthAndHeight(this.popupWidth, popupHeight);
227
- this.popupBackground.setFillColor('white');
228
- this.popupBackground.setStrokeColor('black');
229
- this.popupBackground.setStrokeWidth(2);
230
- this.popupBackground.setCornerRadius(8);
231
- this.popup.addChild(this.popupBackground);
232
-
233
- // Create buttons
234
- this._createButtons(this.popupWidth, popupHeight, this.buttonSize, this.margin, this.buttonSpacing);
235
-
236
- // Create text input (default mode)
237
- this._createTextInput(this.popupWidth, popupHeight, this.margin);
238
-
239
- // Set initial mode
240
- this.currentMode = 'text';
241
- this._updateButtonStates();
242
- }
243
-
244
- /**
245
- * Creates the popup buttons
246
- * @private
247
- */
248
- _createButtons(popupWidth, popupHeight, buttonSize, margin, buttonSpacing) {
249
- const buttonX = popupWidth - buttonSize - margin;
250
-
251
- // Clear button
252
- const clearButtonY = popupHeight - (buttonSize * 2) - margin - buttonSpacing;
253
- this.clearButton = new jsvgButton();
254
- this.clearButton.setText("C");
255
- this.clearButton.setWidthAndHeight(buttonSize, buttonSize);
256
- this.clearButton.setFillColor('#E65423');
257
- this.clearButton.setFontColor('white');
258
- this.clearButton.buttonText.setFontWeight('bold');
259
- this.clearButton.setPosition(buttonX, clearButtonY);
260
- this.clearButton.setClickCallback(() => {
261
- if (this.currentMode === 'pen' && this.penCanvas) {
262
- // Clear the canvas
263
- this.penCanvas.clear();
264
- } else if (this.currentMode === 'text' && this.popupTextInput) {
265
- // Clear the text input
266
- this.popupTextInput.setText('');
267
- }
268
- // Also call the external clear callback if provided
269
- if (this.onClearCallback) {
270
- this.onClearCallback();
271
- }
272
- });
273
- this.popup.addChild(this.clearButton);
274
-
275
- // Submit button
276
- const submitButtonY = popupHeight - buttonSize - margin;
277
- this.submitButton = new jsvgButton();
278
- this.submitButton.setText("✓");
279
- this.submitButton.setWidthAndHeight(buttonSize, buttonSize);
280
- this.submitButton.setFillColor('#2ECC71');
281
- this.submitButton.setFontColor('white');
282
- this.submitButton.buttonText.setFontWeight('bold');
283
- this.submitButton.setPosition(buttonX, submitButtonY);
284
- this.submitButton.setClickCallback(() => {
285
- if (this.currentMode === 'pen' && this.penCanvas) {
286
- // For pen mode, transcribe the canvas first
287
- this._setSubmitButtonLoading(true);
288
- this._downloadCanvasAsBitmap();
289
- } else if (this.onValidateCallback) {
290
- // For text mode, validate directly
291
- this.onValidateCallback();
292
- }
293
- });
294
- this.popup.addChild(this.submitButton);
295
-
296
- // Mode buttons (P and T) - positioned in bottom left, aligned with other buttons
297
- const leftMargin = margin;
298
-
299
- // Text button (above pen button, aligned with clear button)
300
- this.textButton = new jsvgButton();
301
- this.textButton.setText("T");
302
- this.textButton.setWidthAndHeight(buttonSize, buttonSize);
303
- this.textButton.setFillColor('#28a745');
304
- this.textButton.setFontColor('white');
305
- this.textButton.buttonText.setFontWeight('bold');
306
- this.textButton.setPosition(leftMargin, clearButtonY);
307
- this.textButton.setClickCallback(() => {
308
- this.switchToMode('text');
309
- });
310
- this.popup.addChild(this.textButton);
311
-
312
- // Pen button (bottom, aligned with submit button)
313
- this.penButton = new jsvgButton();
314
- this.penButton.setText("");
315
- this.penButton.setWidthAndHeight(buttonSize, buttonSize);
316
- this.penButton.setFillColor('#007bff');
317
- this.penButton.setPosition(leftMargin, submitButtonY);
318
- this.penButton.setClickCallback(() => this.switchToMode('pen'));
319
-
320
- // Add pencil icon (same as canvas toolbar)
321
- const pencilIconSvg = `<svg width="12" height="12" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
322
- <path d="M13.3658 4.68008C13.7041 4.34179 13.8943 3.88294 13.8943 3.40447C13.8944 2.926 13.7044 2.4671 13.3661 2.12872C13.0278 1.79035 12.5689 1.60022 12.0905 1.60016C11.612 1.6001 11.1531 1.79011 10.8147 2.1284L2.27329 10.6718C2.12469 10.8199 2.0148 11.0023 1.95329 11.203L1.10785 13.9882C1.09131 14.0436 1.09006 14.1024 1.10423 14.1584C1.11841 14.2144 1.14748 14.2655 1.18836 14.3063C1.22924 14.3471 1.28041 14.3761 1.33643 14.3902C1.39246 14.4043 1.45125 14.403 1.50657 14.3863L4.29249 13.5415C4.49292 13.4806 4.67532 13.3713 4.82369 13.2234L13.3658 4.68008Z" stroke="white" stroke-width="1.28" stroke-linecap="round" stroke-linejoin="round"/>
323
- <path d="M9.41443 3.52039L11.9744 6.08039" stroke="white" stroke-width="1.28" stroke-linecap="round" stroke-linejoin="round"/>
324
- </svg>`;
325
- const dataURI = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(pencilIconSvg);
326
- this.penButton.addImage(dataURI, 12, 12);
327
-
328
- this.popup.addChild(this.penButton);
329
- }
330
-
331
- /**
332
- * Creates the text input
333
- * @private
334
- */
335
- _createTextInput(popupWidth, popupHeight, margin) {
336
- const buttonAreaWidth = (this.buttonSize * 2) + (margin * 2) + this.margin;
337
-
338
- // Text input covers the middle area between the left and right button areas
339
- const inputWidth = popupWidth - buttonAreaWidth - (margin * 2);
340
- const inputHeight = popupHeight - (margin * 2);
341
- const inputX = this.buttonSize + (margin * 2);
342
- const inputY = margin;
343
-
344
- this.popupTextInput = new jsvgTextInput();
345
- this.popupTextInput.setWidthAndHeight(inputWidth, inputHeight);
346
- this.popupTextInput.setPosition(inputX, inputY);
347
-
348
- // Make background transparent/invisible
349
- this.popupTextInput.setFillColor('transparent');
350
- this.popupTextInput.setStrokeColor('transparent');
351
- this.popupTextInput.setStrokeWidth(0);
352
- this.popupTextInput.setPlaceholderText("");
353
-
354
- // Style the actual input for large centered text
355
- if (this.popupTextInput.div) {
356
- this.popupTextInput.div.style.border = 'none';
357
- this.popupTextInput.div.style.outline = 'none';
358
- this.popupTextInput.div.style.background = 'transparent';
359
- this.popupTextInput.div.style.textAlign = 'center';
360
- this.popupTextInput.div.style.fontSize = '48px';
361
- this.popupTextInput.div.style.fontFamily = 'Albert Sans, Arial, sans-serif';
362
- this.popupTextInput.div.style.resize = 'none';
363
- this.popupTextInput.div.style.width = '100%';
364
- this.popupTextInput.div.style.height = '100%';
365
- this.popupTextInput.div.style.display = 'flex';
366
- this.popupTextInput.div.style.alignItems = 'center';
367
- this.popupTextInput.div.style.justifyContent = 'center';
368
- this.popupTextInput.div.style.boxSizing = 'border-box';
369
- }
370
-
371
- // Add to popup in text mode
372
- this.popup.addChild(this.popupTextInput);
373
- }
374
-
375
- /**
376
- * Creates the pen canvas
377
- * @private
378
- */
379
- _createPenCanvas() {
380
- // Use class variables directly
381
- const popupWidth = this.popupWidth;
382
- const popupHeight = (this.targetNode?.height || this.targetNodeDefaultHeight) * this.popupHeightMultiplier;
383
-
384
- // Calculate canvas dimensions based on popup size
385
- const leftMargin = this.buttonSize;
386
- const rightMargin = 2 * this.buttonSize + this.buttonSpacing; // Space for buttons
387
-
388
- // Calculate canvas dimensions based on popup size
389
- const canvasWidth = Math.max(popupWidth - leftMargin - rightMargin, this.canvasMinWidth);
390
-
391
- // Calculate canvas height to fit in available space
392
- const buttonAreaHeight = (this.buttonSize * 2) + this.buttonSpacing + (this.margin * 2);
393
- const availableHeight = popupHeight - buttonAreaHeight;
394
- const canvasHeight = Math.max(availableHeight, this.canvasMinHeight);
395
- const canvasX = leftMargin;
396
-
397
- // Try to create a regular HTML container first (fallback approach)
398
- let canvasContainer;
399
- let foreignObject = null;
400
-
401
- // Check if we're in an SVG context
402
- if (this.parentElement && this.parentElement.element &&
403
- this.parentElement.element.namespaceURI === 'http://www.w3.org/2000/svg') {
404
-
405
- // Create an SVG foreignObject to embed HTML canvas
406
- foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
407
- foreignObject.setAttribute('width', canvasWidth);
408
- foreignObject.setAttribute('height', canvasHeight);
409
- foreignObject.setAttribute('x', canvasX);
410
- foreignObject.setAttribute('y', this.canvasTopOffset);
411
-
412
- // Create the HTML container inside foreignObject
413
- canvasContainer = document.createElement('div');
414
- canvasContainer.style.width = `${canvasWidth}px`;
415
- canvasContainer.style.height = `${canvasHeight}px`;
416
- canvasContainer.style.border = '1px solid #ddd';
417
- canvasContainer.style.borderRadius = '4px';
418
- canvasContainer.style.backgroundColor = '#f8f9fa';
419
- canvasContainer.style.margin = '0';
420
- canvasContainer.style.padding = '0';
421
-
422
- // Add the container to foreignObject
423
- foreignObject.appendChild(canvasContainer);
424
- } else {
425
- // Create a regular HTML container
426
- canvasContainer = document.createElement('div');
427
- canvasContainer.style.width = `${canvasWidth}px`;
428
- canvasContainer.style.height = `${canvasHeight}px`;
429
- canvasContainer.style.position = 'absolute';
430
- canvasContainer.style.left = `${canvasX}px`;
431
- canvasContainer.style.top = `${this.canvasTopOffset}px`;
432
- canvasContainer.style.border = 'none'; // No border
433
- canvasContainer.style.borderRadius = '0px';
434
- canvasContainer.style.backgroundColor = '#ffffff'; // White background
435
- canvasContainer.style.zIndex = '1000';
436
- canvasContainer.style.pointerEvents = 'auto';
437
- canvasContainer.style.cursor = 'crosshair';
438
- }
439
-
440
- // Import our new modular canvas system
441
- import('../../canvas/index.js').then(({ createCanvas }) => {
442
- // Ensure we have valid positive dimensions
443
- const finalWidth = canvasWidth;
444
- const finalHeight = canvasHeight;
445
-
446
- // Create the canvas using our new system
447
- this.penCanvas = createCanvas(canvasContainer, {
448
- width: finalWidth - this.buttonSize,
449
- height: finalHeight,
450
- showToolbar: false,
451
- showGrid: false,
452
- backgroundColor: '#ffffff', // White background
453
- strokeWidth: 3,
454
- strokeColor: '#000000'
455
- });
456
-
457
- // Store the container and foreignObject references for cleanup
458
- this.penCanvas.container = canvasContainer;
459
- this.penCanvas.foreignObject = foreignObject;
460
-
461
- // Set pencil as the default tool
462
- this.penCanvas.toolManager.setActiveTool('pencil');
463
-
464
- // Cursor will be shown/hidden by EventManager based on mouse enter/leave
465
-
466
- // Add event listeners for debugging
467
- this.penCanvas.on('strokeStarted', (event) => {});
468
- this.penCanvas.on('strokeCompleted', (event) => {});
469
-
470
- // Add pointer event debugging
471
- canvasContainer.addEventListener('pointerdown', (e) => {});
472
-
473
-
474
-
475
- // Add click debugging
476
- canvasContainer.addEventListener('click', (e) => {});
477
-
478
- // Store cleanup function
479
- this.penCanvasCleanup = () => {
480
- if (this.penCanvas) {
481
- this.penCanvas.destroy();
482
- }
483
- if (foreignObject && foreignObject.parentNode) {
484
- foreignObject.parentNode.removeChild(foreignObject);
485
- } else if (canvasContainer && canvasContainer.parentNode) {
486
- canvasContainer.parentNode.removeChild(canvasContainer);
487
- }
488
- // Remove step visualizer listeners
489
- this._removeStepVisualizerListeners();
490
- };
491
-
492
- // Set up step visualizer change detection
493
- this._setupStepVisualizerListeners(); // If we're currently in pen mode, show the canvas
494
- if (this.currentMode === 'pen' && this.popup) {
495
- this._addCanvasToParent(foreignObject || canvasContainer);
496
- }
497
-
498
- }).catch(console.error);
499
- }
500
-
501
- /**
502
- * Shows pen mode
503
- * @private
504
- */
505
- _showPenMode() {
506
- // Hide text input by making it invisible instead of removing it
507
- if (this.popupTextInput && this.popupTextInput.div) {
508
- this.popupTextInput.div.style.display = 'none';
509
- this.popupTextInput.div.style.visibility = 'hidden';
510
- }
511
-
512
- // Create pen canvas if it doesn't exist
513
- if (!this.penCanvas) {
514
- this._createPenCanvas();
515
- } else {
516
- // Show existing canvas and ensure it's fully opaque
517
- const element = this.penCanvas.foreignObject || this.penCanvas.container;
518
- if (element) {
519
- element.style.display = 'block';
520
- element.style.opacity = '1'; // Ensure full opacity for stroke visibility
521
- }
522
- this._addCanvasToParent();
523
- }
524
- }
525
-
526
- /**
527
- * Shows text mode
528
- * @private
529
- */
530
- _showTextMode() {
531
- // Hide pen canvas by removing its element
532
- if (this.penCanvas) {
533
- const element = this.penCanvas.foreignObject || this.penCanvas.container;
534
- if (element && element.parentNode) {
535
- element.parentNode.removeChild(element);
536
- }
537
- }
538
-
539
- // Show text input and ensure it's visible and focused
540
- if (this.popupTextInput && this.popup) {
541
- // Make sure text input is visible
542
- if (this.popupTextInput.div) {
543
- this.popupTextInput.div.style.display = 'flex';
544
- this.popupTextInput.div.style.visibility = 'visible';
545
- this.popupTextInput.div.style.opacity = '1';
546
- }
547
-
548
- // Add to popup (jsvgLayoutGroup handles duplicates automatically)
549
- this.popup.addChild(this.popupTextInput);
550
-
551
- // Focus the text input after a short delay to ensure it's rendered
552
- setTimeout(() => {
553
- if (this.popupTextInput.div) {
554
- this.popupTextInput.div.focus();
555
- }
556
- }, 100);
557
- }
558
- }
559
-
560
- /**
561
- * Adds canvas to parent element (same level as popup)
562
- * @private
563
- */
564
- _addCanvasToParent(element = null) {
565
- // If no element provided and canvas already exists, re-add it to parent
566
- if (!element && this.penCanvas && this.penCanvas.container) {
567
- // Re-add the canvas container to the parent
568
- if (this.penCanvas.container.parentNode) {
569
- this.penCanvas.container.parentNode.removeChild(this.penCanvas.container);
570
- }
571
- document.body.appendChild(this.penCanvas.container);
572
- this._updateCanvasPosition();
573
- return;
574
- }
575
-
576
- if (this.penCanvas && this.popup && this.parentElement) {
577
- const popupX = this.popup.xpos || 0;
578
- const popupY = this.popup.ypos || 0;
579
-
580
- // If we have an element to add
581
- if (element) {
582
- // Check if it's a foreignObject (SVG) or regular HTML element
583
- if (element.namespaceURI === 'http://www.w3.org/2000/svg') {
584
- // SVG foreignObject
585
- element.setAttribute('x', popupX + this.canvasLeftOffset);
586
- element.setAttribute('y', popupY + this.canvasTopOffset);
587
-
588
- // Add the foreignObject to the parent element
589
- if (this.parentElement.element) {
590
- this.parentElement.element.appendChild(element);
591
- } else if (this.parentElement.appendChild) {
592
- this.parentElement.appendChild(element);
593
- }
594
- } else {
595
- // Regular HTML element
596
- const popupRect = this.popup.svgObject ? this.popup.svgObject.getBoundingClientRect() : null;
597
-
598
- if (popupRect) {
599
- // Calculate position to match popup exactly
600
- const leftButtonArea = this.buttonSize + (this.margin * 2);
601
- const rightButtonArea = (this.buttonSize * 2) + (this.margin * 2) + this.margin;
602
-
603
- // Position canvas in the middle content area
604
- const absoluteX = popupRect.left + leftButtonArea;
605
- const absoluteY = popupRect.top + this.margin;
606
-
607
- element.style.left = `${absoluteX}px`;
608
- element.style.top = `${absoluteY}px`;
609
- element.style.position = 'fixed';
610
- element.style.zIndex = '9999';
611
- element.style.pointerEvents = 'auto';
612
-
613
- // Set canvas size to match popup content area
614
- const contentWidth = popupRect.width - leftButtonArea - rightButtonArea;
615
- element.style.width = `${contentWidth}px`;
616
-
617
- // Store popup reference for resize handling
618
- this.penCanvas.popupRect = popupRect;
619
-
620
- // Add resize observer to track popup changes
621
- this._setupResizeObserver();
622
-
623
- } else {
624
- // Fallback positioning
625
- const absoluteX = (window.innerWidth - 280) / 2;
626
- const absoluteY = (window.innerHeight - 60) / 2;
627
-
628
- element.style.left = `${absoluteX}px`;
629
- element.style.top = `${absoluteY}px`;
630
- element.style.position = 'fixed';
631
- element.style.zIndex = '9999';
632
- element.style.pointerEvents = 'auto';
633
- }
634
-
635
- // Add to document body for HTML approach
636
- document.body.appendChild(element);
637
- }
638
- }
639
- }
640
- }
641
-
642
- /**
643
- * Updates button visual states based on current mode
644
- * @private
645
- */
646
- _updateButtonStates() {
647
- if (!this.penButton || !this.textButton) return;
648
-
649
- if (this.currentMode === 'pen') {
650
- this.penButton.setFillColor('#2980B9'); // Darker blue for active
651
- this.textButton.setFillColor('#9B59B6'); // Normal purple
652
- } else {
653
- this.penButton.setFillColor('#3498DB'); // Normal blue
654
- this.textButton.setFillColor('#8E44AD'); // Darker purple for active
655
- }
656
- }
657
-
658
- /**
659
- * Positions the popup
660
- * @private
661
- */
662
- _positionPopup(x, y) {
663
- if (!this.popup) return;
664
- this.popup.setPosition(x, y);
665
- }
666
-
667
- /**
668
- * Animates popup opacity
669
- * @private
670
- */
671
- _animateOpacity(fromOpacity, toOpacity, duration) {
672
- return new Promise((resolve) => {
673
- if (!this.popup) {
674
- resolve();
675
- return;
676
- }
677
-
678
- if (this.popupAnimationId) {
679
- cancelAnimationFrame(this.popupAnimationId);
680
- }
681
-
682
- const startTime = performance.now();
683
- const deltaOpacity = toOpacity - fromOpacity;
684
-
685
- const animate = (currentTime) => {
686
- const elapsed = currentTime - startTime;
687
- const progress = Math.min(elapsed / duration, 1);
688
- const easedProgress = 1 - Math.pow(1 - progress, 3);
689
- const currentOpacity = fromOpacity + (deltaOpacity * easedProgress);
690
-
691
- // Animate popup
692
- this.popup.setOpacity(currentOpacity);
693
-
694
- // Animate canvas with same opacity if it exists
695
- if (this.penCanvas && this.penCanvas.container && this.currentMode === 'pen') {
696
- this.penCanvas.container.style.opacity = currentOpacity;
697
- }
698
-
699
- if (progress < 1) {
700
- this.popupAnimationId = requestAnimationFrame(animate);
701
- } else {
702
- this.popupAnimationId = null;
703
- // Ensure canvas is fully opaque when animation completes
704
- if (this.penCanvas && this.penCanvas.container && this.currentMode === 'pen') {
705
- this.penCanvas.container.style.opacity = '1';
706
- }
707
- resolve();
708
- }
709
- };
710
-
711
- this.popupAnimationId = requestAnimationFrame(animate);
712
- });
713
- }
714
-
715
- /**
716
- * Flashes the popup background to indicate validation result
717
- * @param {boolean} isValid - Whether validation was successful
718
- */
719
- flashValidation(isValid) {
720
- if (!this.popupBackground) return;
721
-
722
- const flashColor = isValid ? '#E8F5E8' : '#FFE6E6';
723
- this._flashAllElements(flashColor);
724
- }
725
-
726
- /**
727
- * Flash all elements with the same color and return to white
728
- * @private
729
- */
730
- _flashAllElements(flashColor) {
731
- // Flash popup background
732
- this.popupBackground.setFillColor(flashColor);
733
-
734
- // Flash canvas container
735
- if (this.penCanvas && this.penCanvas.container) {
736
- this.penCanvas.container.style.backgroundColor = flashColor;
737
- }
738
-
739
- // Flash canvas drawing area
740
- if (this.penCanvas && this.penCanvas.svg) {
741
- this.penCanvas.svg.style.backgroundColor = flashColor;
742
- }
743
-
744
- // Return all elements to white after 300ms
745
- setTimeout(() => {
746
- this.popupBackground.setFillColor('white');
747
-
748
- if (this.penCanvas && this.penCanvas.container) {
749
- this.penCanvas.container.style.backgroundColor = 'white';
750
- }
751
-
752
- if (this.penCanvas && this.penCanvas.svg) {
753
- this.penCanvas.svg.style.backgroundColor = 'white';
754
- }
755
- }, 300);
756
- }
757
-
758
- /**
759
- * Checks if two mathematical expressions are equivalent
760
- * @param {string} expr1 - First expression
761
- * @param {string} expr2 - Second expression
762
- * @returns {boolean} True if expressions are mathematically equivalent
763
- */
764
- areExpressionsEquivalent(expr1, expr2) {
765
- // Robust equivalence: compare evaluated results for random variable assignments
766
- if (!window.math || !window.math.simplify || !window.math.parse) {
767
- return false;
768
- }
769
- try {
770
- const expr1Trimmed = expr1.trim();
771
- const expr2Trimmed = expr2.trim();
772
- const node1 = window.math.simplify(expr1Trimmed);
773
- const node2 = window.math.simplify(expr2Trimmed);
774
-
775
- // If ASTs match, return true
776
- if (node1.equals(node2)) return true;
777
-
778
- // Otherwise, compare evaluated results for random variable assignments
779
- // Find all variable names
780
- const getVars = expr => {
781
- const node = window.math.parse(expr);
782
- const vars = new Set();
783
- node.traverse(n => {
784
- if (n.isSymbolNode) vars.add(n.name);
785
- });
786
- return Array.from(vars);
787
- };
788
- const vars = Array.from(new Set([...getVars(expr1Trimmed), ...getVars(expr2Trimmed)]));
789
- if (vars.length === 0) {
790
- // No variables, just compare evaluated results
791
- return node1.evaluate() === node2.evaluate();
792
- }
793
- // Try several random assignments
794
- for (let i = 0; i < 100; i++) {
795
- const scope = {};
796
- for (const v of vars) {
797
- scope[v] = Math.floor(Math.random() * 1000 + 1); // random int 1-10
798
- }
799
- const val1 = node1.evaluate(scope);
800
- const val2 = node2.evaluate(scope);
801
- if (Math.abs(val1 - val2) > 1e-9) return false;
802
- }
803
- return true;
804
- } catch (e) {
805
- return false;
806
- }
807
- }
808
-
809
- /**
810
- * Cleanup popup and all associated elements
811
- * @private
812
- */
813
- _cleanup() {
814
- if (this.popup && this.parentElement) {
815
- this.parentElement.removeChild(this.popup);
816
- }
817
-
818
- // Remove pen canvas from parent if it exists
819
- if (this.penCanvas) {
820
- // Remove both foreignObject and container if they exist
821
- if (this.penCanvas.foreignObject && this.penCanvas.foreignObject.parentNode) {
822
- this.penCanvas.foreignObject.parentNode.removeChild(this.penCanvas.foreignObject);
823
- }
824
-
825
- if (this.penCanvas.container && this.penCanvas.container.parentNode) {
826
- this.penCanvas.container.parentNode.removeChild(this.penCanvas.container);
827
- }
828
- }
829
-
830
- // Clean up pen canvas
831
- if (this.penCanvasCleanup) {
832
- this.penCanvasCleanup();
833
- this.penCanvasCleanup = null;
834
- }
835
-
836
- // Clean up resize observer
837
- if (this.resizeObserver) {
838
- this.resizeObserver.disconnect();
839
- this.resizeObserver = null;
840
- }
841
-
842
- // Clean up step visualizer listeners
843
- this._removeStepVisualizerListeners();
844
-
845
- // Clean up animation
846
- if (this.popupAnimationId) {
847
- cancelAnimationFrame(this.popupAnimationId);
848
- this.popupAnimationId = null;
849
- }
850
-
851
- // Reset all references
852
- this.popup = null;
853
- this.popupBackground = null;
854
- this.popupTextInput = null;
855
- this.penCanvas = null;
856
- this.penButton = null;
857
- this.textButton = null;
858
- this.clearButton = null;
859
- this.submitButton = null;
860
- }
861
-
862
- /**
863
- * Destroys the popup completely
864
- */
865
- destroy() {
866
- return this.hide();
867
- }
868
-
869
- /**
870
- * Debug function to test canvas positioning
871
- */
872
- debugCanvasPosition() {
873
- if (this.penCanvas && this.penCanvas.container) {
874
- const container = this.penCanvas.container;
875
- // Flash the border to make it visible
876
- container.style.border = '3px solid #00ff00';
877
- setTimeout(() => {
878
- container.style.border = '2px solid #ff0000';
879
- }, 1000);
880
- }
881
- }
882
-
883
- /**
884
- * Reposition canvas to center of screen
885
- */
886
- centerCanvas() {
887
- if (this.penCanvas && this.penCanvas.container) {
888
- const container = this.penCanvas.container;
889
- const centerX = (window.innerWidth - 280) / 2;
890
- const centerY = (window.innerHeight - 60) / 2;
891
- container.style.left = `${centerX}px`;
892
- container.style.top = `${centerY}px`;
893
- }
894
- }
895
-
896
- /**
897
- * Setup resize observer to track popup changes
898
- * @private
899
- */
900
- _setupResizeObserver() {
901
- if (!this.penCanvas || !this.popup || !this.popup.svgObject) return;
902
-
903
- // Create a resize observer to track popup changes
904
- this.resizeObserver = new ResizeObserver((entries) => {
905
- for (const entry of entries) {
906
- this._updateCanvasPosition();
907
- }
908
- });
909
-
910
- // Observe the popup element
911
- this.resizeObserver.observe(this.popup.svgObject);
912
-
913
- // Also observe the parent element for position changes
914
- if (this.parentElement && this.parentElement.svgObject) {
915
- this.resizeObserver.observe(this.parentElement.svgObject);
916
- }
917
-
918
- // Also observe the document body for zoom changes
919
- this.resizeObserver.observe(document.body);
920
- }
921
-
922
- /**
923
- * Setup step visualizer listeners to track expand/collapse
924
- * @private
925
- */
926
- _setupStepVisualizerListeners() {
927
- if (!this.penCanvas) return;
928
-
929
- // Listen for step visualizer events that might change layout
930
- this._stepVisualizerUpdateHandler = () => {
931
- console.log('[Step Visualizer Debug] Layout change detected, updating canvas position');
932
- // Small delay to allow layout to settle
933
- setTimeout(() => this._updateCanvasPosition(), 50);
934
- };
935
-
936
- // Listen for various events that might indicate step visualizer changes
937
- document.addEventListener('click', this._stepVisualizerUpdateHandler);
938
- window.addEventListener('resize', this._stepVisualizerUpdateHandler);
939
-
940
- // Listen for custom step visualizer events if they exist
941
- if (this.parentElement && this.parentElement.element) {
942
- this.parentElement.element.addEventListener('stepVisualizerChanged', this._stepVisualizerUpdateHandler);
943
- }
944
-
945
- // Set up mutation observer to detect DOM changes in step visualizer
946
- if (this.parentElement && this.parentElement.element) {
947
- this._mutationObserver = new MutationObserver((mutations) => {
948
- let shouldUpdate = false;
949
- for (const mutation of mutations) {
950
- // Check if any changes might affect layout
951
- if (mutation.type === 'attributes' &&
952
- (mutation.attributeName === 'style' ||
953
- mutation.attributeName === 'class' ||
954
- mutation.attributeName === 'transform')) {
955
- console.log('[Step Visualizer Debug] Mutation detected:', mutation.attributeName, mutation.target);
956
- shouldUpdate = true;
957
- break;
958
- }
959
- if (mutation.type === 'childList' &&
960
- (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) {
961
- console.log('[Step Visualizer Debug] Child list mutation detected');
962
- shouldUpdate = true;
963
- break;
964
- }
965
- }
966
- if (shouldUpdate) {
967
- setTimeout(() => this._updateCanvasPosition(), 10);
968
- }
969
- });
970
-
971
- // Observe the parent element and its children
972
- this._mutationObserver.observe(this.parentElement.element, {
973
- childList: true,
974
- attributes: true,
975
- subtree: true,
976
- attributeFilter: ['style', 'class', 'transform', 'viewBox']
977
- });
978
- }
979
- }
980
-
981
- /**
982
- * Remove step visualizer listeners
983
- * @private
984
- */
985
- _removeStepVisualizerListeners() {
986
- if (this._stepVisualizerUpdateHandler) {
987
- document.removeEventListener('click', this._stepVisualizerUpdateHandler);
988
- window.removeEventListener('resize', this._stepVisualizerUpdateHandler);
989
-
990
- if (this.parentElement && this.parentElement.element) {
991
- this.parentElement.element.removeEventListener('stepVisualizerChanged', this._stepVisualizerUpdateHandler);
992
- }
993
-
994
- this._stepVisualizerUpdateHandler = null;
995
- }
996
-
997
- if (this._mutationObserver) {
998
- this._mutationObserver.disconnect();
999
- this._mutationObserver = null;
1000
- }
1001
- }
1002
-
1003
- /**
1004
- * Update canvas position to match popup
1005
- * @private
1006
- */
1007
- _updateCanvasPosition() {
1008
- if (!this.penCanvas || !this.penCanvas.container || !this.popup) return;
1009
-
1010
- const container = this.penCanvas.container;
1011
- const popupRect = this.popup.svgObject ? this.popup.svgObject.getBoundingClientRect() : null;
1012
-
1013
- console.log('[Canvas Position Debug] Update triggered:', {
1014
- hasCanvas: !!this.penCanvas,
1015
- hasContainer: !!container,
1016
- strokeCount: this.penCanvas?.strokes?.size || 0,
1017
- popupRect: popupRect ? `${popupRect.width}x${popupRect.height}` : 'null'
1018
- });
1019
-
1020
- if (popupRect && popupRect.width > 0 && popupRect.height > 0) {
1021
- // Calculate button areas based on popup dimensions
1022
- const leftButtonArea = this.buttonSize + (this.margin * 2);
1023
- const rightButtonArea = (this.buttonSize * 2) + (this.margin * 2) + this.margin;
1024
-
1025
- // Calculate content area within popup
1026
- const contentWidth = Math.max(popupRect.width - leftButtonArea - rightButtonArea, this.canvasMinWidth);
1027
- const contentHeight = Math.max(popupRect.height - (this.margin * 2) - (this.buttonSize * 2) - this.buttonSpacing, this.canvasMinHeight);
1028
-
1029
- // Position canvas within popup bounds
1030
- const absoluteX = popupRect.left + leftButtonArea;
1031
- const absoluteY = popupRect.top + this.margin;
1032
-
1033
- // Ensure canvas doesn't exceed popup bounds
1034
- const maxWidth = popupRect.width - leftButtonArea - rightButtonArea;
1035
- const maxHeight = popupRect.height - (this.margin * 2) - (this.buttonSize * 2) - this.buttonSpacing;
1036
-
1037
- const finalWidth = Math.min(Math.max(contentWidth, this.canvasMinWidth), maxWidth);
1038
- const finalHeight = Math.min(Math.max(contentHeight, this.canvasMinHeight), maxHeight);
1039
-
1040
- console.log('[Canvas Position Debug] Moving canvas:', {
1041
- from: `${container.style.left}, ${container.style.top}`,
1042
- to: `${absoluteX}px, ${absoluteY}px`,
1043
- size: `${finalWidth}x${finalHeight}`
1044
- });
1045
-
1046
- container.style.left = `${absoluteX}px`;
1047
- container.style.top = `${absoluteY}px`;
1048
- container.style.width = `${finalWidth}px`;
1049
- container.style.height = `${finalHeight}px`;
1050
-
1051
- // Check stroke count after move
1052
- setTimeout(() => {
1053
- console.log('[Canvas Position Debug] After move stroke count:', this.penCanvas?.strokes?.size || 0);
1054
- }, 50);
1055
- }
1056
- }
1057
-
1058
- /**
1059
- * Download canvas as bitmap and transcribe
1060
- * @private
1061
- */
1062
- async _downloadCanvasAsBitmap() {
1063
- if (!this.penCanvas) {
1064
- return;
1065
- }
1066
- try {
1067
- // Get the canvas SVG element
1068
- const svgElement = this.penCanvas.svg;
1069
- if (!svgElement) {
1070
- return;
1071
- }
1072
-
1073
- // Create a canvas element for conversion
1074
- const canvas = document.createElement('canvas');
1075
- const ctx = canvas.getContext('2d');
1076
-
1077
- // Set canvas size to match SVG
1078
- const svgRect = svgElement.getBoundingClientRect();
1079
- canvas.width = svgRect.width;
1080
- canvas.height = svgRect.height;
1081
-
1082
- // Convert SVG to data URL
1083
- const svgData = new XMLSerializer().serializeToString(svgElement);
1084
- const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
1085
- const url = URL.createObjectURL(svgBlob);
1086
-
1087
- // Create an image from the SVG
1088
- const img = new Image();
1089
- img.onload = async () => {
1090
- // Draw the image to canvas
1091
- ctx.drawImage(img, 0, 0);
1092
-
1093
- // Convert to blob and transcribe (no download)
1094
- canvas.toBlob(async (blob) => {
1095
- URL.revokeObjectURL(url);
1096
- await this._transcribeCanvas(blob);
1097
- }, 'image/png');
1098
- };
1099
-
1100
- img.src = url;
1101
-
1102
- } catch (error) {
1103
- }
1104
- }
1105
-
1106
- /**
1107
- * Transcribe canvas content
1108
- * @private
1109
- */
1110
- async _transcribeCanvas(imageBlob) {
1111
- try {
1112
- // Import transcription service
1113
- const { omdTranscriptionService } = await import('./omdTranscriptionService.js');
1114
- // Create transcription service instance (no API keys needed - server handles them)
1115
- const transcriptionService = new omdTranscriptionService({
1116
- defaultProvider: 'gemini'
1117
- });
1118
-
1119
- // Check if service is available
1120
- if (!transcriptionService.isAvailable()) {
1121
- return;
1122
- }
1123
-
1124
- // Transcribe with fallback
1125
- const result = await transcriptionService.transcribeWithFallback(imageBlob, {
1126
- prompt: 'Transcribe this handwritten mathematical expression. Return ONLY the pure mathematical expression with no formatting, no LaTeX, no dollar signs, no explanations. Use ^ for powers (e.g., 3^2), use / for fractions (e.g., (2x+1)/(x-3)), use * for multiplication, use + and - for addition/subtraction. Return only the expression.'
1127
- });
1128
-
1129
- if (result.text) {
1130
- this._setSubmitButtonLoading(false);
1131
- this.flashValidation(true);
1132
- this.transcribedText = result.text;
1133
- if (this.onValidateCallback) {
1134
- this.onValidateCallback();
1135
- }
1136
- } else {
1137
- this._setSubmitButtonLoading(false);
1138
- }
1139
- } catch (error) {
1140
- this.flashValidation(false);
1141
- this._setSubmitButtonLoading(false);
1142
- }
1143
- }
1144
-
1145
- /**
1146
- * Set submit button loading state
1147
- * @private
1148
- */
1149
- _setSubmitButtonLoading(isLoading) {
1150
- if (!this.submitButton) return;
1151
-
1152
- if (isLoading) {
1153
- // Start blinking animation
1154
- this._startBlinkingAnimation();
1155
- } else {
1156
- // Stop blinking and restore original state
1157
- this._stopBlinkingAnimation();
1158
- this.submitButton.setText("✓");
1159
- this.submitButton.setFillColor('#2ECC71');
1160
- this.submitButton.setFontColor('white');
1161
- }
1162
- }
1163
-
1164
- /**
1165
- * Start blinking animation for submit button
1166
- * @private
1167
- */
1168
- _startBlinkingAnimation() {
1169
- if (!this.submitButton) return;
1170
-
1171
- let isOrange = true;
1172
- const blink = () => {
1173
- if (isOrange) {
1174
- this.submitButton.setFillColor('#FFA500'); // Orange
1175
- } else {
1176
- this.submitButton.setFillColor('#2ECC71'); // Green
1177
- }
1178
- isOrange = !isOrange;
1179
-
1180
- this.blinkAnimationId = setTimeout(blink, 300); // Blink every 300ms
1181
- };
1182
-
1183
- // Start blinking immediately
1184
- blink();
1185
- }
1186
-
1187
- /**
1188
- * Stop blinking animation for submit button
1189
- * @private
1190
- */
1191
- _stopBlinkingAnimation() {
1192
- if (this.blinkAnimationId) {
1193
- clearTimeout(this.blinkAnimationId);
1194
- this.blinkAnimationId = null;
1195
- }
1196
- }
1197
-
1198
- /**
1199
- * Reposition canvas relative to popup
1200
- */
1201
- repositionCanvasRelativeToPopup() {
1202
- this._updateCanvasPosition();
1203
- }
1
+ import { jsvgGroup, jsvgRect, jsvgButton, jsvgLayoutGroup, jsvgTextInput } from '@teachinglab/jsvg';
2
+ /**
3
+ * omdPopup - Handles popup creation and management for node overlays
4
+ */
5
+ export class omdPopup {
6
+ constructor(targetNode, parentElement, options = {}) {
7
+ this.targetNode = targetNode;
8
+ this.parentElement = parentElement;
9
+ this.options = {
10
+ editable: true,
11
+ animationDuration: 200,
12
+ ...options
13
+ };
14
+
15
+ // State
16
+ this.popup = null;
17
+ this.popupBackground = null;
18
+ this.popupTextInput = null;
19
+ this.penCanvas = null;
20
+ this.penCanvasCleanup = null;
21
+ this.currentMode = 'text'; // 'text' or 'pen'
22
+ this.popupAnimationId = null;
23
+
24
+ // Buttons
25
+ this.penButton = null;
26
+ this.textButton = null;
27
+ this.clearButton = null;
28
+ this.submitButton = null;
29
+
30
+ // Callbacks
31
+ this.onValidateCallback = null;
32
+ this.onClearCallback = null;
33
+
34
+ // Layout configuration - centralized variables
35
+ this.popupWidth = 400;
36
+ this.buttonSize = 18;
37
+ this.margin = 10;
38
+ this.buttonSpacing = 4;
39
+ this.canvasMinWidth = 100;
40
+ this.canvasMinHeight = 60;
41
+ this.popupHeightMultiplier = 2;
42
+ this.targetNodeDefaultHeight = 40;
43
+ this.canvasTopOffset = this.margin;
44
+ this.canvasLeftOffset = this.margin;
45
+ }
46
+
47
+ /**
48
+ * Creates and shows the popup
49
+ * @param {number} x - X position
50
+ * @param {number} y - Y position
51
+ * @returns {Promise} Promise that resolves when animation completes
52
+ */
53
+ show(x, y) {
54
+ if (this.popup) {
55
+ return this.hide().then(() => this.show(x, y));
56
+ }
57
+
58
+ this._createPopup();
59
+ this._positionPopup(x, y);
60
+ this.parentElement.addChild(this.popup);
61
+
62
+ // Start invisible and animate in
63
+ this.popup.setOpacity(0);
64
+ return this._animateOpacity(0, 1, this.options.animationDuration);
65
+ }
66
+
67
+ /**
68
+ * Hides the popup
69
+ * @returns {Promise} Promise that resolves when animation completes
70
+ */
71
+ hide() {
72
+ if (!this.popup) return Promise.resolve();
73
+
74
+ // Store original opacities for animation
75
+ const originalPopupOpacity = this.popup.opacity;
76
+ const originalTextInputOpacity = this.popupTextInput?.div?.style.opacity || '1';
77
+
78
+ // Ensure canvas and text input are visible for animation but preserve canvas drawing
79
+ if (this.penCanvas && this.penCanvas.container) {
80
+ this.penCanvas.container.style.display = 'block';
81
+ // Don't fade canvas opacity - keep strokes visible during popup hide
82
+ }
83
+ if (this.popupTextInput && this.popupTextInput.div) {
84
+ this.popupTextInput.div.style.display = 'flex';
85
+ this.popupTextInput.div.style.opacity = originalTextInputOpacity;
86
+ }
87
+
88
+ // Animate popup and canvas together
89
+ const duration = this.options.animationDuration || 300;
90
+ const startTime = performance.now();
91
+
92
+ const animate = (currentTime) => {
93
+ const elapsed = currentTime - startTime;
94
+ const progress = Math.min(elapsed / duration, 1);
95
+
96
+ // Easing function for smooth animation
97
+ const easeOut = 1 - Math.pow(1 - progress, 3);
98
+ const currentOpacity = originalPopupOpacity * (1 - easeOut);
99
+
100
+ // Animate popup
101
+ if (this.popup) {
102
+ this.popup.setOpacity(currentOpacity);
103
+ }
104
+
105
+ // Animate canvas container with same opacity curve
106
+ if (this.penCanvas && this.penCanvas.container) {
107
+ this.penCanvas.container.style.opacity = currentOpacity;
108
+ }
109
+
110
+ // Animate text input with proper opacity setting
111
+ if (this.popupTextInput && this.popupTextInput.div) {
112
+ this.popupTextInput.div.style.opacity = currentOpacity;
113
+ }
114
+
115
+ if (progress < 1) {
116
+ this.popupAnimationId = requestAnimationFrame(animate);
117
+ } else {
118
+ // Animation complete - hide and cleanup both popup and canvas
119
+ if (this.penCanvas && this.penCanvas.container) {
120
+ this.penCanvas.container.style.display = 'none';
121
+ this.penCanvas.container.style.opacity = '1'; // Reset for next show
122
+ }
123
+ if (this.popupTextInput && this.popupTextInput.div) {
124
+ this.popupTextInput.div.style.display = 'none';
125
+ }
126
+ this._cleanup();
127
+ }
128
+ };
129
+
130
+ this.popupAnimationId = requestAnimationFrame(animate);
131
+
132
+ return new Promise((resolve) => {
133
+ setTimeout(resolve, duration);
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Toggles popup visibility
139
+ * @param {number} x - X position for showing
140
+ * @param {number} y - Y position for showing
141
+ * @returns {Promise} Promise that resolves when animation completes
142
+ */
143
+ toggle(x, y) {
144
+ if (this.popup && this.popup.visible) {
145
+ return this.hide();
146
+ } else {
147
+ return this.show(x, y);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Sets the validation callback
153
+ * @param {Function} callback - Function to call for validation
154
+ */
155
+ setValidationCallback(callback) {
156
+ this.onValidateCallback = callback;
157
+ }
158
+
159
+ /**
160
+ * Sets the clear callback
161
+ * @param {Function} callback - Function to call when clearing
162
+ */
163
+ setClearCallback(callback) {
164
+ this.onClearCallback = callback;
165
+ }
166
+
167
+ /**
168
+ * Gets the current input value
169
+ * @returns {string} Current input value
170
+ */
171
+ getValue() {
172
+ // If we have transcribed text from pen mode, return it
173
+ if (this.transcribedText) {
174
+ const text = this.transcribedText;
175
+ // Clear the transcribed text after returning it
176
+ this.transcribedText = null;
177
+ return text;
178
+ }
179
+
180
+ if (this.currentMode === 'text' && this.popupTextInput) {
181
+ return this.popupTextInput.getText();
182
+ }
183
+ return '';
184
+ }
185
+
186
+ /**
187
+ * Sets the input value
188
+ * @param {string} value - Value to set
189
+ */
190
+ setValue(value) {
191
+ if (this.currentMode === 'text' && this.popupTextInput) {
192
+ this.popupTextInput.setText(value);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Switches between text and pen modes
198
+ * @param {string} mode - 'text' or 'pen'
199
+ */
200
+ switchToMode(mode) {
201
+ if (this.currentMode === mode || !this.popup) {
202
+ return;
203
+ }
204
+ this.currentMode = mode;
205
+ if (mode === 'pen') {
206
+ this._showPenMode();
207
+ } else {
208
+ this._showTextMode();
209
+ }
210
+ // Update button states
211
+ this._updateButtonStates();
212
+ }
213
+
214
+ /**
215
+ * Creates the popup structure
216
+ * @private
217
+ */
218
+ _createPopup() {
219
+ const popupHeight = (this.targetNode?.height || this.targetNodeDefaultHeight) * this.popupHeightMultiplier;
220
+
221
+ // Create popup container
222
+ this.popup = new jsvgLayoutGroup();
223
+
224
+ // Create popup background
225
+ this.popupBackground = new jsvgRect();
226
+ this.popupBackground.setWidthAndHeight(this.popupWidth, popupHeight);
227
+ this.popupBackground.setFillColor('white');
228
+ this.popupBackground.setStrokeColor('black');
229
+ this.popupBackground.setStrokeWidth(2);
230
+ this.popupBackground.setCornerRadius(8);
231
+ this.popup.addChild(this.popupBackground);
232
+
233
+ // Create buttons
234
+ this._createButtons(this.popupWidth, popupHeight, this.buttonSize, this.margin, this.buttonSpacing);
235
+
236
+ // Create text input (default mode)
237
+ this._createTextInput(this.popupWidth, popupHeight, this.margin);
238
+
239
+ // Set initial mode
240
+ this.currentMode = 'text';
241
+ this._updateButtonStates();
242
+ }
243
+
244
+ /**
245
+ * Creates the popup buttons
246
+ * @private
247
+ */
248
+ _createButtons(popupWidth, popupHeight, buttonSize, margin, buttonSpacing) {
249
+ const buttonX = popupWidth - buttonSize - margin;
250
+
251
+ // Clear button
252
+ const clearButtonY = popupHeight - (buttonSize * 2) - margin - buttonSpacing;
253
+ this.clearButton = new jsvgButton();
254
+ this.clearButton.setText("C");
255
+ this.clearButton.setWidthAndHeight(buttonSize, buttonSize);
256
+ this.clearButton.setFillColor('#E65423');
257
+ this.clearButton.setFontColor('white');
258
+ this.clearButton.buttonText.setFontWeight('bold');
259
+ this.clearButton.setPosition(buttonX, clearButtonY);
260
+ this.clearButton.setClickCallback(() => {
261
+ if (this.currentMode === 'pen' && this.penCanvas) {
262
+ // Clear the canvas
263
+ this.penCanvas.clear();
264
+ } else if (this.currentMode === 'text' && this.popupTextInput) {
265
+ // Clear the text input
266
+ this.popupTextInput.setText('');
267
+ }
268
+ // Also call the external clear callback if provided
269
+ if (this.onClearCallback) {
270
+ this.onClearCallback();
271
+ }
272
+ });
273
+ this.popup.addChild(this.clearButton);
274
+
275
+ // Submit button
276
+ const submitButtonY = popupHeight - buttonSize - margin;
277
+ this.submitButton = new jsvgButton();
278
+ this.submitButton.setText("✓");
279
+ this.submitButton.setWidthAndHeight(buttonSize, buttonSize);
280
+ this.submitButton.setFillColor('#2ECC71');
281
+ this.submitButton.setFontColor('white');
282
+ this.submitButton.buttonText.setFontWeight('bold');
283
+ this.submitButton.setPosition(buttonX, submitButtonY);
284
+ this.submitButton.setClickCallback(() => {
285
+ if (this.currentMode === 'pen' && this.penCanvas) {
286
+ // For pen mode, transcribe the canvas first
287
+ this._setSubmitButtonLoading(true);
288
+ this._downloadCanvasAsBitmap();
289
+ } else if (this.onValidateCallback) {
290
+ // For text mode, validate directly
291
+ this.onValidateCallback();
292
+ }
293
+ });
294
+ this.popup.addChild(this.submitButton);
295
+
296
+ // Mode buttons (P and T) - positioned in bottom left, aligned with other buttons
297
+ const leftMargin = margin;
298
+
299
+ // Text button (above pen button, aligned with clear button)
300
+ this.textButton = new jsvgButton();
301
+ this.textButton.setText("T");
302
+ this.textButton.setWidthAndHeight(buttonSize, buttonSize);
303
+ this.textButton.setFillColor('#28a745');
304
+ this.textButton.setFontColor('white');
305
+ this.textButton.buttonText.setFontWeight('bold');
306
+ this.textButton.setPosition(leftMargin, clearButtonY);
307
+ this.textButton.setClickCallback(() => {
308
+ this.switchToMode('text');
309
+ });
310
+ this.popup.addChild(this.textButton);
311
+
312
+ // Pen button (bottom, aligned with submit button)
313
+ this.penButton = new jsvgButton();
314
+ this.penButton.setText("");
315
+ this.penButton.setWidthAndHeight(buttonSize, buttonSize);
316
+ this.penButton.setFillColor('#007bff');
317
+ this.penButton.setPosition(leftMargin, submitButtonY);
318
+ this.penButton.setClickCallback(() => this.switchToMode('pen'));
319
+
320
+ // Add pencil icon (same as canvas toolbar)
321
+ const pencilIconSvg = `<svg width="12" height="12" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
322
+ <path d="M13.3658 4.68008C13.7041 4.34179 13.8943 3.88294 13.8943 3.40447C13.8944 2.926 13.7044 2.4671 13.3661 2.12872C13.0278 1.79035 12.5689 1.60022 12.0905 1.60016C11.612 1.6001 11.1531 1.79011 10.8147 2.1284L2.27329 10.6718C2.12469 10.8199 2.0148 11.0023 1.95329 11.203L1.10785 13.9882C1.09131 14.0436 1.09006 14.1024 1.10423 14.1584C1.11841 14.2144 1.14748 14.2655 1.18836 14.3063C1.22924 14.3471 1.28041 14.3761 1.33643 14.3902C1.39246 14.4043 1.45125 14.403 1.50657 14.3863L4.29249 13.5415C4.49292 13.4806 4.67532 13.3713 4.82369 13.2234L13.3658 4.68008Z" stroke="white" stroke-width="1.28" stroke-linecap="round" stroke-linejoin="round"/>
323
+ <path d="M9.41443 3.52039L11.9744 6.08039" stroke="white" stroke-width="1.28" stroke-linecap="round" stroke-linejoin="round"/>
324
+ </svg>`;
325
+ const dataURI = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(pencilIconSvg);
326
+ this.penButton.addImage(dataURI, 12, 12);
327
+
328
+ this.popup.addChild(this.penButton);
329
+ }
330
+
331
+ /**
332
+ * Creates the text input
333
+ * @private
334
+ */
335
+ _createTextInput(popupWidth, popupHeight, margin) {
336
+ const buttonAreaWidth = (this.buttonSize * 2) + (margin * 2) + this.margin;
337
+
338
+ // Text input covers the middle area between the left and right button areas
339
+ const inputWidth = popupWidth - buttonAreaWidth - (margin * 2);
340
+ const inputHeight = popupHeight - (margin * 2);
341
+ const inputX = this.buttonSize + (margin * 2);
342
+ const inputY = margin;
343
+
344
+ this.popupTextInput = new jsvgTextInput();
345
+ this.popupTextInput.setWidthAndHeight(inputWidth, inputHeight);
346
+ this.popupTextInput.setPosition(inputX, inputY);
347
+
348
+ // Make background transparent/invisible
349
+ this.popupTextInput.setFillColor('transparent');
350
+ this.popupTextInput.setStrokeColor('transparent');
351
+ this.popupTextInput.setStrokeWidth(0);
352
+ this.popupTextInput.setPlaceholderText("");
353
+
354
+ // Style the actual input for large centered text
355
+ if (this.popupTextInput.div) {
356
+ this.popupTextInput.div.style.border = 'none';
357
+ this.popupTextInput.div.style.outline = 'none';
358
+ this.popupTextInput.div.style.background = 'transparent';
359
+ this.popupTextInput.div.style.textAlign = 'center';
360
+ this.popupTextInput.div.style.fontSize = '48px';
361
+ this.popupTextInput.div.style.fontFamily = 'Albert Sans, Arial, sans-serif';
362
+ this.popupTextInput.div.style.resize = 'none';
363
+ this.popupTextInput.div.style.width = '100%';
364
+ this.popupTextInput.div.style.height = '100%';
365
+ this.popupTextInput.div.style.display = 'flex';
366
+ this.popupTextInput.div.style.alignItems = 'center';
367
+ this.popupTextInput.div.style.justifyContent = 'center';
368
+ this.popupTextInput.div.style.boxSizing = 'border-box';
369
+ }
370
+
371
+ // Add to popup in text mode
372
+ this.popup.addChild(this.popupTextInput);
373
+ }
374
+
375
+ /**
376
+ * Creates the pen canvas
377
+ * @private
378
+ */
379
+ _createPenCanvas() {
380
+ // Use class variables directly
381
+ const popupWidth = this.popupWidth;
382
+ const popupHeight = (this.targetNode?.height || this.targetNodeDefaultHeight) * this.popupHeightMultiplier;
383
+
384
+ // Calculate canvas dimensions based on popup size
385
+ const leftMargin = this.buttonSize;
386
+ const rightMargin = 2 * this.buttonSize + this.buttonSpacing; // Space for buttons
387
+
388
+ // Calculate canvas dimensions based on popup size
389
+ const canvasWidth = Math.max(popupWidth - leftMargin - rightMargin, this.canvasMinWidth);
390
+
391
+ // Calculate canvas height to fit in available space
392
+ const buttonAreaHeight = (this.buttonSize * 2) + this.buttonSpacing + (this.margin * 2);
393
+ const availableHeight = popupHeight - buttonAreaHeight;
394
+ const canvasHeight = Math.max(availableHeight, this.canvasMinHeight);
395
+ const canvasX = leftMargin;
396
+
397
+ // Try to create a regular HTML container first (fallback approach)
398
+ let canvasContainer;
399
+ let foreignObject = null;
400
+
401
+ // Check if we're in an SVG context
402
+ if (this.parentElement && this.parentElement.element &&
403
+ this.parentElement.element.namespaceURI === 'http://www.w3.org/2000/svg') {
404
+
405
+ // Create an SVG foreignObject to embed HTML canvas
406
+ foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
407
+ foreignObject.setAttribute('width', canvasWidth);
408
+ foreignObject.setAttribute('height', canvasHeight);
409
+ foreignObject.setAttribute('x', canvasX);
410
+ foreignObject.setAttribute('y', this.canvasTopOffset);
411
+
412
+ // Create the HTML container inside foreignObject
413
+ canvasContainer = document.createElement('div');
414
+ canvasContainer.style.width = `${canvasWidth}px`;
415
+ canvasContainer.style.height = `${canvasHeight}px`;
416
+ canvasContainer.style.border = '1px solid #ddd';
417
+ canvasContainer.style.borderRadius = '4px';
418
+ canvasContainer.style.backgroundColor = '#f8f9fa';
419
+ canvasContainer.style.margin = '0';
420
+ canvasContainer.style.padding = '0';
421
+
422
+ // Add the container to foreignObject
423
+ foreignObject.appendChild(canvasContainer);
424
+ } else {
425
+ // Create a regular HTML container
426
+ canvasContainer = document.createElement('div');
427
+ canvasContainer.style.width = `${canvasWidth}px`;
428
+ canvasContainer.style.height = `${canvasHeight}px`;
429
+ canvasContainer.style.position = 'absolute';
430
+ canvasContainer.style.left = `${canvasX}px`;
431
+ canvasContainer.style.top = `${this.canvasTopOffset}px`;
432
+ canvasContainer.style.border = 'none'; // No border
433
+ canvasContainer.style.borderRadius = '0px';
434
+ canvasContainer.style.backgroundColor = '#ffffff'; // White background
435
+ canvasContainer.style.zIndex = '1000';
436
+ canvasContainer.style.pointerEvents = 'auto';
437
+ canvasContainer.style.cursor = 'crosshair';
438
+ }
439
+
440
+ // Import our new modular canvas system
441
+ import('../../canvas/index.js').then(({ createCanvas }) => {
442
+ // Ensure we have valid positive dimensions
443
+ const finalWidth = canvasWidth;
444
+ const finalHeight = canvasHeight;
445
+
446
+ // Create the canvas using our new system
447
+ this.penCanvas = createCanvas(canvasContainer, {
448
+ width: finalWidth - this.buttonSize,
449
+ height: finalHeight,
450
+ showToolbar: false,
451
+ showGrid: false,
452
+ backgroundColor: '#ffffff', // White background
453
+ strokeWidth: 3,
454
+ strokeColor: '#000000'
455
+ });
456
+
457
+ // Store the container and foreignObject references for cleanup
458
+ this.penCanvas.container = canvasContainer;
459
+ this.penCanvas.foreignObject = foreignObject;
460
+
461
+ // Set pencil as the default tool
462
+ this.penCanvas.toolManager.setActiveTool('pencil');
463
+
464
+ // Cursor will be shown/hidden by EventManager based on mouse enter/leave
465
+
466
+ // Add event listeners for debugging
467
+ this.penCanvas.on('strokeStarted', (event) => {});
468
+ this.penCanvas.on('strokeCompleted', (event) => {});
469
+
470
+ // Add pointer event debugging
471
+ canvasContainer.addEventListener('pointerdown', (e) => {});
472
+
473
+
474
+
475
+ // Add click debugging
476
+ canvasContainer.addEventListener('click', (e) => {});
477
+
478
+ // Store cleanup function
479
+ this.penCanvasCleanup = () => {
480
+ if (this.penCanvas) {
481
+ this.penCanvas.destroy();
482
+ }
483
+ if (foreignObject && foreignObject.parentNode) {
484
+ foreignObject.parentNode.removeChild(foreignObject);
485
+ } else if (canvasContainer && canvasContainer.parentNode) {
486
+ canvasContainer.parentNode.removeChild(canvasContainer);
487
+ }
488
+ // Remove step visualizer listeners
489
+ this._removeStepVisualizerListeners();
490
+ };
491
+
492
+ // Set up step visualizer change detection
493
+ this._setupStepVisualizerListeners(); // If we're currently in pen mode, show the canvas
494
+ if (this.currentMode === 'pen' && this.popup) {
495
+ this._addCanvasToParent(foreignObject || canvasContainer);
496
+ }
497
+
498
+ }).catch(console.error);
499
+ }
500
+
501
+ /**
502
+ * Shows pen mode
503
+ * @private
504
+ */
505
+ _showPenMode() {
506
+ // Hide text input by making it invisible instead of removing it
507
+ if (this.popupTextInput && this.popupTextInput.div) {
508
+ this.popupTextInput.div.style.display = 'none';
509
+ this.popupTextInput.div.style.visibility = 'hidden';
510
+ }
511
+
512
+ // Create pen canvas if it doesn't exist
513
+ if (!this.penCanvas) {
514
+ this._createPenCanvas();
515
+ } else {
516
+ // Show existing canvas and ensure it's fully opaque
517
+ const element = this.penCanvas.foreignObject || this.penCanvas.container;
518
+ if (element) {
519
+ element.style.display = 'block';
520
+ element.style.opacity = '1'; // Ensure full opacity for stroke visibility
521
+ }
522
+ this._addCanvasToParent();
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Shows text mode
528
+ * @private
529
+ */
530
+ _showTextMode() {
531
+ // Hide pen canvas by removing its element
532
+ if (this.penCanvas) {
533
+ const element = this.penCanvas.foreignObject || this.penCanvas.container;
534
+ if (element && element.parentNode) {
535
+ element.parentNode.removeChild(element);
536
+ }
537
+ }
538
+
539
+ // Show text input and ensure it's visible and focused
540
+ if (this.popupTextInput && this.popup) {
541
+ // Make sure text input is visible
542
+ if (this.popupTextInput.div) {
543
+ this.popupTextInput.div.style.display = 'flex';
544
+ this.popupTextInput.div.style.visibility = 'visible';
545
+ this.popupTextInput.div.style.opacity = '1';
546
+ }
547
+
548
+ // Add to popup (jsvgLayoutGroup handles duplicates automatically)
549
+ this.popup.addChild(this.popupTextInput);
550
+
551
+ // Focus the text input after a short delay to ensure it's rendered
552
+ setTimeout(() => {
553
+ if (this.popupTextInput.div) {
554
+ this.popupTextInput.div.focus();
555
+ }
556
+ }, 100);
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Adds canvas to parent element (same level as popup)
562
+ * @private
563
+ */
564
+ _addCanvasToParent(element = null) {
565
+ // If no element provided and canvas already exists, re-add it to parent
566
+ if (!element && this.penCanvas && this.penCanvas.container) {
567
+ // Re-add the canvas container to the parent
568
+ if (this.penCanvas.container.parentNode) {
569
+ this.penCanvas.container.parentNode.removeChild(this.penCanvas.container);
570
+ }
571
+ document.body.appendChild(this.penCanvas.container);
572
+ this._updateCanvasPosition();
573
+ return;
574
+ }
575
+
576
+ if (this.penCanvas && this.popup && this.parentElement) {
577
+ const popupX = this.popup.xpos || 0;
578
+ const popupY = this.popup.ypos || 0;
579
+
580
+ // If we have an element to add
581
+ if (element) {
582
+ // Check if it's a foreignObject (SVG) or regular HTML element
583
+ if (element.namespaceURI === 'http://www.w3.org/2000/svg') {
584
+ // SVG foreignObject
585
+ element.setAttribute('x', popupX + this.canvasLeftOffset);
586
+ element.setAttribute('y', popupY + this.canvasTopOffset);
587
+
588
+ // Add the foreignObject to the parent element
589
+ if (this.parentElement.element) {
590
+ this.parentElement.element.appendChild(element);
591
+ } else if (this.parentElement.appendChild) {
592
+ this.parentElement.appendChild(element);
593
+ }
594
+ } else {
595
+ // Regular HTML element
596
+ const popupRect = this.popup.svgObject ? this.popup.svgObject.getBoundingClientRect() : null;
597
+
598
+ if (popupRect) {
599
+ // Calculate position to match popup exactly
600
+ const leftButtonArea = this.buttonSize + (this.margin * 2);
601
+ const rightButtonArea = (this.buttonSize * 2) + (this.margin * 2) + this.margin;
602
+
603
+ // Position canvas in the middle content area
604
+ const absoluteX = popupRect.left + leftButtonArea;
605
+ const absoluteY = popupRect.top + this.margin;
606
+
607
+ element.style.left = `${absoluteX}px`;
608
+ element.style.top = `${absoluteY}px`;
609
+ element.style.position = 'fixed';
610
+ element.style.zIndex = '9999';
611
+ element.style.pointerEvents = 'auto';
612
+
613
+ // Set canvas size to match popup content area
614
+ const contentWidth = popupRect.width - leftButtonArea - rightButtonArea;
615
+ element.style.width = `${contentWidth}px`;
616
+
617
+ // Store popup reference for resize handling
618
+ this.penCanvas.popupRect = popupRect;
619
+
620
+ // Add resize observer to track popup changes
621
+ this._setupResizeObserver();
622
+
623
+ } else {
624
+ // Fallback positioning
625
+ const absoluteX = (window.innerWidth - 280) / 2;
626
+ const absoluteY = (window.innerHeight - 60) / 2;
627
+
628
+ element.style.left = `${absoluteX}px`;
629
+ element.style.top = `${absoluteY}px`;
630
+ element.style.position = 'fixed';
631
+ element.style.zIndex = '9999';
632
+ element.style.pointerEvents = 'auto';
633
+ }
634
+
635
+ // Add to document body for HTML approach
636
+ document.body.appendChild(element);
637
+ }
638
+ }
639
+ }
640
+ }
641
+
642
+ /**
643
+ * Updates button visual states based on current mode
644
+ * @private
645
+ */
646
+ _updateButtonStates() {
647
+ if (!this.penButton || !this.textButton) return;
648
+
649
+ if (this.currentMode === 'pen') {
650
+ this.penButton.setFillColor('#2980B9'); // Darker blue for active
651
+ this.textButton.setFillColor('#9B59B6'); // Normal purple
652
+ } else {
653
+ this.penButton.setFillColor('#3498DB'); // Normal blue
654
+ this.textButton.setFillColor('#8E44AD'); // Darker purple for active
655
+ }
656
+ }
657
+
658
+ /**
659
+ * Positions the popup
660
+ * @private
661
+ */
662
+ _positionPopup(x, y) {
663
+ if (!this.popup) return;
664
+ this.popup.setPosition(x, y);
665
+ }
666
+
667
+ /**
668
+ * Animates popup opacity
669
+ * @private
670
+ */
671
+ _animateOpacity(fromOpacity, toOpacity, duration) {
672
+ return new Promise((resolve) => {
673
+ if (!this.popup) {
674
+ resolve();
675
+ return;
676
+ }
677
+
678
+ if (this.popupAnimationId) {
679
+ cancelAnimationFrame(this.popupAnimationId);
680
+ }
681
+
682
+ const startTime = performance.now();
683
+ const deltaOpacity = toOpacity - fromOpacity;
684
+
685
+ const animate = (currentTime) => {
686
+ const elapsed = currentTime - startTime;
687
+ const progress = Math.min(elapsed / duration, 1);
688
+ const easedProgress = 1 - Math.pow(1 - progress, 3);
689
+ const currentOpacity = fromOpacity + (deltaOpacity * easedProgress);
690
+
691
+ // Animate popup
692
+ this.popup.setOpacity(currentOpacity);
693
+
694
+ // Animate canvas with same opacity if it exists
695
+ if (this.penCanvas && this.penCanvas.container && this.currentMode === 'pen') {
696
+ this.penCanvas.container.style.opacity = currentOpacity;
697
+ }
698
+
699
+ if (progress < 1) {
700
+ this.popupAnimationId = requestAnimationFrame(animate);
701
+ } else {
702
+ this.popupAnimationId = null;
703
+ // Ensure canvas is fully opaque when animation completes
704
+ if (this.penCanvas && this.penCanvas.container && this.currentMode === 'pen') {
705
+ this.penCanvas.container.style.opacity = '1';
706
+ }
707
+ resolve();
708
+ }
709
+ };
710
+
711
+ this.popupAnimationId = requestAnimationFrame(animate);
712
+ });
713
+ }
714
+
715
+ /**
716
+ * Flashes the popup background to indicate validation result
717
+ * @param {boolean} isValid - Whether validation was successful
718
+ */
719
+ flashValidation(isValid) {
720
+ if (!this.popupBackground) return;
721
+
722
+ const flashColor = isValid ? '#E8F5E8' : '#FFE6E6';
723
+ this._flashAllElements(flashColor);
724
+ }
725
+
726
+ /**
727
+ * Flash all elements with the same color and return to white
728
+ * @private
729
+ */
730
+ _flashAllElements(flashColor) {
731
+ // Flash popup background
732
+ this.popupBackground.setFillColor(flashColor);
733
+
734
+ // Flash canvas container
735
+ if (this.penCanvas && this.penCanvas.container) {
736
+ this.penCanvas.container.style.backgroundColor = flashColor;
737
+ }
738
+
739
+ // Flash canvas drawing area
740
+ if (this.penCanvas && this.penCanvas.svg) {
741
+ this.penCanvas.svg.style.backgroundColor = flashColor;
742
+ }
743
+
744
+ // Return all elements to white after 300ms
745
+ setTimeout(() => {
746
+ this.popupBackground.setFillColor('white');
747
+
748
+ if (this.penCanvas && this.penCanvas.container) {
749
+ this.penCanvas.container.style.backgroundColor = 'white';
750
+ }
751
+
752
+ if (this.penCanvas && this.penCanvas.svg) {
753
+ this.penCanvas.svg.style.backgroundColor = 'white';
754
+ }
755
+ }, 300);
756
+ }
757
+
758
+ /**
759
+ * Checks if two mathematical expressions are equivalent
760
+ * @param {string} expr1 - First expression
761
+ * @param {string} expr2 - Second expression
762
+ * @returns {boolean} True if expressions are mathematically equivalent
763
+ */
764
+ areExpressionsEquivalent(expr1, expr2) {
765
+ // Robust equivalence: compare evaluated results for random variable assignments
766
+ if (!window.math || !window.math.simplify || !window.math.parse) {
767
+ return false;
768
+ }
769
+ try {
770
+ const expr1Trimmed = expr1.trim();
771
+ const expr2Trimmed = expr2.trim();
772
+ const node1 = window.math.simplify(expr1Trimmed);
773
+ const node2 = window.math.simplify(expr2Trimmed);
774
+
775
+ // If ASTs match, return true
776
+ if (node1.equals(node2)) return true;
777
+
778
+ // Otherwise, compare evaluated results for random variable assignments
779
+ // Find all variable names
780
+ const getVars = expr => {
781
+ const node = window.math.parse(expr);
782
+ const vars = new Set();
783
+ node.traverse(n => {
784
+ if (n.isSymbolNode) vars.add(n.name);
785
+ });
786
+ return Array.from(vars);
787
+ };
788
+ const vars = Array.from(new Set([...getVars(expr1Trimmed), ...getVars(expr2Trimmed)]));
789
+ if (vars.length === 0) {
790
+ // No variables, just compare evaluated results
791
+ return node1.evaluate() === node2.evaluate();
792
+ }
793
+ // Try several random assignments
794
+ for (let i = 0; i < 100; i++) {
795
+ const scope = {};
796
+ for (const v of vars) {
797
+ scope[v] = Math.floor(Math.random() * 1000 + 1); // random int 1-10
798
+ }
799
+ const val1 = node1.evaluate(scope);
800
+ const val2 = node2.evaluate(scope);
801
+ if (Math.abs(val1 - val2) > 1e-9) return false;
802
+ }
803
+ return true;
804
+ } catch (e) {
805
+ return false;
806
+ }
807
+ }
808
+
809
+ /**
810
+ * Cleanup popup and all associated elements
811
+ * @private
812
+ */
813
+ _cleanup() {
814
+ if (this.popup && this.parentElement) {
815
+ this.parentElement.removeChild(this.popup);
816
+ }
817
+
818
+ // Remove pen canvas from parent if it exists
819
+ if (this.penCanvas) {
820
+ // Remove both foreignObject and container if they exist
821
+ if (this.penCanvas.foreignObject && this.penCanvas.foreignObject.parentNode) {
822
+ this.penCanvas.foreignObject.parentNode.removeChild(this.penCanvas.foreignObject);
823
+ }
824
+
825
+ if (this.penCanvas.container && this.penCanvas.container.parentNode) {
826
+ this.penCanvas.container.parentNode.removeChild(this.penCanvas.container);
827
+ }
828
+ }
829
+
830
+ // Clean up pen canvas
831
+ if (this.penCanvasCleanup) {
832
+ this.penCanvasCleanup();
833
+ this.penCanvasCleanup = null;
834
+ }
835
+
836
+ // Clean up resize observer
837
+ if (this.resizeObserver) {
838
+ this.resizeObserver.disconnect();
839
+ this.resizeObserver = null;
840
+ }
841
+
842
+ // Clean up step visualizer listeners
843
+ this._removeStepVisualizerListeners();
844
+
845
+ // Clean up animation
846
+ if (this.popupAnimationId) {
847
+ cancelAnimationFrame(this.popupAnimationId);
848
+ this.popupAnimationId = null;
849
+ }
850
+
851
+ // Reset all references
852
+ this.popup = null;
853
+ this.popupBackground = null;
854
+ this.popupTextInput = null;
855
+ this.penCanvas = null;
856
+ this.penButton = null;
857
+ this.textButton = null;
858
+ this.clearButton = null;
859
+ this.submitButton = null;
860
+ }
861
+
862
+ /**
863
+ * Destroys the popup completely
864
+ */
865
+ destroy() {
866
+ return this.hide();
867
+ }
868
+
869
+ /**
870
+ * Debug function to test canvas positioning
871
+ */
872
+ debugCanvasPosition() {
873
+ if (this.penCanvas && this.penCanvas.container) {
874
+ const container = this.penCanvas.container;
875
+ // Flash the border to make it visible
876
+ container.style.border = '3px solid #00ff00';
877
+ setTimeout(() => {
878
+ container.style.border = '2px solid #ff0000';
879
+ }, 1000);
880
+ }
881
+ }
882
+
883
+ /**
884
+ * Reposition canvas to center of screen
885
+ */
886
+ centerCanvas() {
887
+ if (this.penCanvas && this.penCanvas.container) {
888
+ const container = this.penCanvas.container;
889
+ const centerX = (window.innerWidth - 280) / 2;
890
+ const centerY = (window.innerHeight - 60) / 2;
891
+ container.style.left = `${centerX}px`;
892
+ container.style.top = `${centerY}px`;
893
+ }
894
+ }
895
+
896
+ /**
897
+ * Setup resize observer to track popup changes
898
+ * @private
899
+ */
900
+ _setupResizeObserver() {
901
+ if (!this.penCanvas || !this.popup || !this.popup.svgObject) return;
902
+
903
+ // Create a resize observer to track popup changes
904
+ this.resizeObserver = new ResizeObserver((entries) => {
905
+ for (const entry of entries) {
906
+ this._updateCanvasPosition();
907
+ }
908
+ });
909
+
910
+ // Observe the popup element
911
+ this.resizeObserver.observe(this.popup.svgObject);
912
+
913
+ // Also observe the parent element for position changes
914
+ if (this.parentElement && this.parentElement.svgObject) {
915
+ this.resizeObserver.observe(this.parentElement.svgObject);
916
+ }
917
+
918
+ // Also observe the document body for zoom changes
919
+ this.resizeObserver.observe(document.body);
920
+ }
921
+
922
+ /**
923
+ * Setup step visualizer listeners to track expand/collapse
924
+ * @private
925
+ */
926
+ _setupStepVisualizerListeners() {
927
+ if (!this.penCanvas) return;
928
+
929
+ // Listen for step visualizer events that might change layout
930
+ this._stepVisualizerUpdateHandler = () => {
931
+ console.log('[Step Visualizer Debug] Layout change detected, updating canvas position');
932
+ // Small delay to allow layout to settle
933
+ setTimeout(() => this._updateCanvasPosition(), 50);
934
+ };
935
+
936
+ // Listen for various events that might indicate step visualizer changes
937
+ document.addEventListener('click', this._stepVisualizerUpdateHandler);
938
+ window.addEventListener('resize', this._stepVisualizerUpdateHandler);
939
+
940
+ // Listen for custom step visualizer events if they exist
941
+ if (this.parentElement && this.parentElement.element) {
942
+ this.parentElement.element.addEventListener('stepVisualizerChanged', this._stepVisualizerUpdateHandler);
943
+ }
944
+
945
+ // Set up mutation observer to detect DOM changes in step visualizer
946
+ if (this.parentElement && this.parentElement.element) {
947
+ this._mutationObserver = new MutationObserver((mutations) => {
948
+ let shouldUpdate = false;
949
+ for (const mutation of mutations) {
950
+ // Check if any changes might affect layout
951
+ if (mutation.type === 'attributes' &&
952
+ (mutation.attributeName === 'style' ||
953
+ mutation.attributeName === 'class' ||
954
+ mutation.attributeName === 'transform')) {
955
+ console.log('[Step Visualizer Debug] Mutation detected:', mutation.attributeName, mutation.target);
956
+ shouldUpdate = true;
957
+ break;
958
+ }
959
+ if (mutation.type === 'childList' &&
960
+ (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) {
961
+ console.log('[Step Visualizer Debug] Child list mutation detected');
962
+ shouldUpdate = true;
963
+ break;
964
+ }
965
+ }
966
+ if (shouldUpdate) {
967
+ setTimeout(() => this._updateCanvasPosition(), 10);
968
+ }
969
+ });
970
+
971
+ // Observe the parent element and its children
972
+ this._mutationObserver.observe(this.parentElement.element, {
973
+ childList: true,
974
+ attributes: true,
975
+ subtree: true,
976
+ attributeFilter: ['style', 'class', 'transform', 'viewBox']
977
+ });
978
+ }
979
+ }
980
+
981
+ /**
982
+ * Remove step visualizer listeners
983
+ * @private
984
+ */
985
+ _removeStepVisualizerListeners() {
986
+ if (this._stepVisualizerUpdateHandler) {
987
+ document.removeEventListener('click', this._stepVisualizerUpdateHandler);
988
+ window.removeEventListener('resize', this._stepVisualizerUpdateHandler);
989
+
990
+ if (this.parentElement && this.parentElement.element) {
991
+ this.parentElement.element.removeEventListener('stepVisualizerChanged', this._stepVisualizerUpdateHandler);
992
+ }
993
+
994
+ this._stepVisualizerUpdateHandler = null;
995
+ }
996
+
997
+ if (this._mutationObserver) {
998
+ this._mutationObserver.disconnect();
999
+ this._mutationObserver = null;
1000
+ }
1001
+ }
1002
+
1003
+ /**
1004
+ * Update canvas position to match popup
1005
+ * @private
1006
+ */
1007
+ _updateCanvasPosition() {
1008
+ if (!this.penCanvas || !this.penCanvas.container || !this.popup) return;
1009
+
1010
+ const container = this.penCanvas.container;
1011
+ const popupRect = this.popup.svgObject ? this.popup.svgObject.getBoundingClientRect() : null;
1012
+
1013
+ console.log('[Canvas Position Debug] Update triggered:', {
1014
+ hasCanvas: !!this.penCanvas,
1015
+ hasContainer: !!container,
1016
+ strokeCount: this.penCanvas?.strokes?.size || 0,
1017
+ popupRect: popupRect ? `${popupRect.width}x${popupRect.height}` : 'null'
1018
+ });
1019
+
1020
+ if (popupRect && popupRect.width > 0 && popupRect.height > 0) {
1021
+ // Calculate button areas based on popup dimensions
1022
+ const leftButtonArea = this.buttonSize + (this.margin * 2);
1023
+ const rightButtonArea = (this.buttonSize * 2) + (this.margin * 2) + this.margin;
1024
+
1025
+ // Calculate content area within popup
1026
+ const contentWidth = Math.max(popupRect.width - leftButtonArea - rightButtonArea, this.canvasMinWidth);
1027
+ const contentHeight = Math.max(popupRect.height - (this.margin * 2) - (this.buttonSize * 2) - this.buttonSpacing, this.canvasMinHeight);
1028
+
1029
+ // Position canvas within popup bounds
1030
+ const absoluteX = popupRect.left + leftButtonArea;
1031
+ const absoluteY = popupRect.top + this.margin;
1032
+
1033
+ // Ensure canvas doesn't exceed popup bounds
1034
+ const maxWidth = popupRect.width - leftButtonArea - rightButtonArea;
1035
+ const maxHeight = popupRect.height - (this.margin * 2) - (this.buttonSize * 2) - this.buttonSpacing;
1036
+
1037
+ const finalWidth = Math.min(Math.max(contentWidth, this.canvasMinWidth), maxWidth);
1038
+ const finalHeight = Math.min(Math.max(contentHeight, this.canvasMinHeight), maxHeight);
1039
+
1040
+ console.log('[Canvas Position Debug] Moving canvas:', {
1041
+ from: `${container.style.left}, ${container.style.top}`,
1042
+ to: `${absoluteX}px, ${absoluteY}px`,
1043
+ size: `${finalWidth}x${finalHeight}`
1044
+ });
1045
+
1046
+ container.style.left = `${absoluteX}px`;
1047
+ container.style.top = `${absoluteY}px`;
1048
+ container.style.width = `${finalWidth}px`;
1049
+ container.style.height = `${finalHeight}px`;
1050
+
1051
+ // Check stroke count after move
1052
+ setTimeout(() => {
1053
+ console.log('[Canvas Position Debug] After move stroke count:', this.penCanvas?.strokes?.size || 0);
1054
+ }, 50);
1055
+ }
1056
+ }
1057
+
1058
+ /**
1059
+ * Download canvas as bitmap and transcribe
1060
+ * @private
1061
+ */
1062
+ async _downloadCanvasAsBitmap() {
1063
+ if (!this.penCanvas) {
1064
+ return;
1065
+ }
1066
+ try {
1067
+ // Get the canvas SVG element
1068
+ const svgElement = this.penCanvas.svg;
1069
+ if (!svgElement) {
1070
+ return;
1071
+ }
1072
+
1073
+ // Create a canvas element for conversion
1074
+ const canvas = document.createElement('canvas');
1075
+ const ctx = canvas.getContext('2d');
1076
+
1077
+ // Set canvas size to match SVG
1078
+ const svgRect = svgElement.getBoundingClientRect();
1079
+ canvas.width = svgRect.width;
1080
+ canvas.height = svgRect.height;
1081
+
1082
+ // Convert SVG to data URL
1083
+ const svgData = new XMLSerializer().serializeToString(svgElement);
1084
+ const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
1085
+ const url = URL.createObjectURL(svgBlob);
1086
+
1087
+ // Create an image from the SVG
1088
+ const img = new Image();
1089
+ img.onload = async () => {
1090
+ // Draw the image to canvas
1091
+ ctx.drawImage(img, 0, 0);
1092
+
1093
+ // Convert to blob and transcribe (no download)
1094
+ canvas.toBlob(async (blob) => {
1095
+ URL.revokeObjectURL(url);
1096
+ await this._transcribeCanvas(blob);
1097
+ }, 'image/png');
1098
+ };
1099
+
1100
+ img.src = url;
1101
+
1102
+ } catch (error) {
1103
+ }
1104
+ }
1105
+
1106
+ /**
1107
+ * Transcribe canvas content
1108
+ * @private
1109
+ */
1110
+ async _transcribeCanvas(imageBlob) {
1111
+ try {
1112
+ // Import transcription service
1113
+ const { omdTranscriptionService } = await import('./omdTranscriptionService.js');
1114
+ // Create transcription service instance (no API keys needed - server handles them)
1115
+ const transcriptionService = new omdTranscriptionService({
1116
+ defaultProvider: 'gemini'
1117
+ });
1118
+
1119
+ // Check if service is available
1120
+ if (!transcriptionService.isAvailable()) {
1121
+ return;
1122
+ }
1123
+
1124
+ // Transcribe with fallback
1125
+ const result = await transcriptionService.transcribeWithFallback(imageBlob, {
1126
+ prompt: 'Transcribe this handwritten mathematical expression. Return ONLY the pure mathematical expression with no formatting, no LaTeX, no dollar signs, no explanations. Use ^ for powers (e.g., 3^2), use / for fractions (e.g., (2x+1)/(x-3)), use * for multiplication, use + and - for addition/subtraction. Return only the expression.'
1127
+ });
1128
+
1129
+ if (result.text) {
1130
+ this._setSubmitButtonLoading(false);
1131
+ this.flashValidation(true);
1132
+ this.transcribedText = result.text;
1133
+ if (this.onValidateCallback) {
1134
+ this.onValidateCallback();
1135
+ }
1136
+ } else {
1137
+ this._setSubmitButtonLoading(false);
1138
+ }
1139
+ } catch (error) {
1140
+ this.flashValidation(false);
1141
+ this._setSubmitButtonLoading(false);
1142
+ }
1143
+ }
1144
+
1145
+ /**
1146
+ * Set submit button loading state
1147
+ * @private
1148
+ */
1149
+ _setSubmitButtonLoading(isLoading) {
1150
+ if (!this.submitButton) return;
1151
+
1152
+ if (isLoading) {
1153
+ // Start blinking animation
1154
+ this._startBlinkingAnimation();
1155
+ } else {
1156
+ // Stop blinking and restore original state
1157
+ this._stopBlinkingAnimation();
1158
+ this.submitButton.setText("✓");
1159
+ this.submitButton.setFillColor('#2ECC71');
1160
+ this.submitButton.setFontColor('white');
1161
+ }
1162
+ }
1163
+
1164
+ /**
1165
+ * Start blinking animation for submit button
1166
+ * @private
1167
+ */
1168
+ _startBlinkingAnimation() {
1169
+ if (!this.submitButton) return;
1170
+
1171
+ let isOrange = true;
1172
+ const blink = () => {
1173
+ if (isOrange) {
1174
+ this.submitButton.setFillColor('#FFA500'); // Orange
1175
+ } else {
1176
+ this.submitButton.setFillColor('#2ECC71'); // Green
1177
+ }
1178
+ isOrange = !isOrange;
1179
+
1180
+ this.blinkAnimationId = setTimeout(blink, 300); // Blink every 300ms
1181
+ };
1182
+
1183
+ // Start blinking immediately
1184
+ blink();
1185
+ }
1186
+
1187
+ /**
1188
+ * Stop blinking animation for submit button
1189
+ * @private
1190
+ */
1191
+ _stopBlinkingAnimation() {
1192
+ if (this.blinkAnimationId) {
1193
+ clearTimeout(this.blinkAnimationId);
1194
+ this.blinkAnimationId = null;
1195
+ }
1196
+ }
1197
+
1198
+ /**
1199
+ * Reposition canvas relative to popup
1200
+ */
1201
+ repositionCanvasRelativeToPopup() {
1202
+ this._updateCanvasPosition();
1203
+ }
1204
1204
  }