@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.
- package/README.md +257 -251
- package/README.old.md +137 -137
- package/canvas/core/canvasConfig.js +202 -202
- package/canvas/drawing/segment.js +167 -167
- package/canvas/drawing/stroke.js +385 -385
- package/canvas/events/eventManager.js +444 -444
- package/canvas/events/pointerEventHandler.js +262 -262
- package/canvas/index.js +48 -48
- package/canvas/tools/PointerTool.js +71 -71
- package/canvas/tools/tool.js +222 -222
- package/canvas/utils/boundingBox.js +377 -377
- package/canvas/utils/mathUtils.js +258 -258
- package/docs/api/configuration-options.md +198 -198
- package/docs/api/eventManager.md +82 -82
- package/docs/api/focusFrameManager.md +144 -144
- package/docs/api/index.md +105 -105
- package/docs/api/main.md +62 -62
- package/docs/api/omdBinaryExpressionNode.md +86 -86
- package/docs/api/omdCanvas.md +83 -83
- package/docs/api/omdConfigManager.md +112 -112
- package/docs/api/omdConstantNode.md +52 -52
- package/docs/api/omdDisplay.md +87 -87
- package/docs/api/omdEquationNode.md +174 -174
- package/docs/api/omdEquationSequenceNode.md +258 -258
- package/docs/api/omdEquationStack.md +192 -192
- package/docs/api/omdFunctionNode.md +82 -82
- package/docs/api/omdGroupNode.md +78 -78
- package/docs/api/omdHelpers.md +87 -87
- package/docs/api/omdLeafNode.md +85 -85
- package/docs/api/omdNode.md +201 -201
- package/docs/api/omdOperationDisplayNode.md +117 -117
- package/docs/api/omdOperatorNode.md +91 -91
- package/docs/api/omdParenthesisNode.md +133 -133
- package/docs/api/omdPopup.md +191 -191
- package/docs/api/omdPowerNode.md +131 -131
- package/docs/api/omdRationalNode.md +144 -144
- package/docs/api/omdSequenceNode.md +128 -128
- package/docs/api/omdSimplification.md +78 -78
- package/docs/api/omdSqrtNode.md +144 -144
- package/docs/api/omdStepVisualizer.md +146 -146
- package/docs/api/omdStepVisualizerHighlighting.md +65 -65
- package/docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
- package/docs/api/omdStepVisualizerLayout.md +70 -70
- package/docs/api/omdStepVisualizerNodeUtils.md +140 -140
- package/docs/api/omdStepVisualizerTextBoxes.md +76 -76
- package/docs/api/omdToolbar.md +130 -130
- package/docs/api/omdTranscriptionService.md +95 -95
- package/docs/api/omdTreeDiff.md +169 -169
- package/docs/api/omdUnaryExpressionNode.md +137 -137
- package/docs/api/omdUtilities.md +82 -82
- package/docs/api/omdVariableNode.md +123 -123
- package/docs/api/selectTool.md +74 -74
- package/docs/api/simplificationEngine.md +97 -97
- package/docs/api/simplificationRules.md +76 -76
- package/docs/api/simplificationUtils.md +64 -64
- package/docs/api/transcribe.md +43 -43
- package/docs/api-reference.md +85 -85
- package/docs/index.html +453 -453
- package/docs/index.md +38 -38
- package/docs/omd-objects.md +258 -258
- package/index.js +79 -79
- package/jsvg/index.js +3 -0
- package/jsvg/jsvg.js +898 -898
- package/jsvg/jsvgComponents.js +357 -358
- package/npm-docs/DOCUMENTATION_SUMMARY.md +220 -220
- package/npm-docs/README.md +251 -251
- package/npm-docs/api/api-reference.md +85 -85
- package/npm-docs/api/configuration-options.md +198 -198
- package/npm-docs/api/eventManager.md +82 -82
- package/npm-docs/api/expression-nodes.md +561 -561
- package/npm-docs/api/focusFrameManager.md +144 -144
- package/npm-docs/api/index.md +105 -105
- package/npm-docs/api/main.md +62 -62
- package/npm-docs/api/omdBinaryExpressionNode.md +86 -86
- package/npm-docs/api/omdCanvas.md +83 -83
- package/npm-docs/api/omdConfigManager.md +112 -112
- package/npm-docs/api/omdConstantNode.md +52 -52
- package/npm-docs/api/omdDisplay.md +87 -87
- package/npm-docs/api/omdEquationNode.md +174 -174
- package/npm-docs/api/omdEquationSequenceNode.md +258 -258
- package/npm-docs/api/omdEquationStack.md +192 -192
- package/npm-docs/api/omdFunctionNode.md +82 -82
- package/npm-docs/api/omdGroupNode.md +78 -78
- package/npm-docs/api/omdHelpers.md +87 -87
- package/npm-docs/api/omdLeafNode.md +85 -85
- package/npm-docs/api/omdNode.md +201 -201
- package/npm-docs/api/omdOperationDisplayNode.md +117 -117
- package/npm-docs/api/omdOperatorNode.md +91 -91
- package/npm-docs/api/omdParenthesisNode.md +133 -133
- package/npm-docs/api/omdPopup.md +191 -191
- package/npm-docs/api/omdPowerNode.md +131 -131
- package/npm-docs/api/omdRationalNode.md +144 -144
- package/npm-docs/api/omdSequenceNode.md +128 -128
- package/npm-docs/api/omdSimplification.md +78 -78
- package/npm-docs/api/omdSqrtNode.md +144 -144
- package/npm-docs/api/omdStepVisualizer.md +146 -146
- package/npm-docs/api/omdStepVisualizerHighlighting.md +65 -65
- package/npm-docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
- package/npm-docs/api/omdStepVisualizerLayout.md +70 -70
- package/npm-docs/api/omdStepVisualizerNodeUtils.md +140 -140
- package/npm-docs/api/omdStepVisualizerTextBoxes.md +76 -76
- package/npm-docs/api/omdToolbar.md +130 -130
- package/npm-docs/api/omdTranscriptionService.md +95 -95
- package/npm-docs/api/omdTreeDiff.md +169 -169
- package/npm-docs/api/omdUnaryExpressionNode.md +137 -137
- package/npm-docs/api/omdUtilities.md +82 -82
- package/npm-docs/api/omdVariableNode.md +123 -123
- package/npm-docs/api/selectTool.md +74 -74
- package/npm-docs/api/simplificationEngine.md +97 -97
- package/npm-docs/api/simplificationRules.md +76 -76
- package/npm-docs/api/simplificationUtils.md +64 -64
- package/npm-docs/api/transcribe.md +43 -43
- package/npm-docs/guides/equations.md +854 -854
- package/npm-docs/guides/factory-functions.md +354 -354
- package/npm-docs/guides/getting-started.md +318 -318
- package/npm-docs/guides/quick-examples.md +525 -525
- package/npm-docs/guides/visualizations.md +682 -682
- package/npm-docs/index.html +12 -0
- package/npm-docs/json-schemas.md +826 -826
- package/omd/config/omdConfigManager.js +279 -267
- package/omd/core/index.js +158 -158
- package/omd/core/omdEquationStack.js +546 -546
- package/omd/core/omdUtilities.js +113 -113
- package/omd/display/omdDisplay.js +969 -962
- package/omd/display/omdToolbar.js +501 -501
- package/omd/nodes/omdBinaryExpressionNode.js +459 -459
- package/omd/nodes/omdConstantNode.js +141 -141
- package/omd/nodes/omdEquationNode.js +1327 -1327
- package/omd/nodes/omdFunctionNode.js +351 -351
- package/omd/nodes/omdGroupNode.js +67 -67
- package/omd/nodes/omdLeafNode.js +76 -76
- package/omd/nodes/omdNode.js +556 -556
- package/omd/nodes/omdOperationDisplayNode.js +321 -321
- package/omd/nodes/omdOperatorNode.js +108 -108
- package/omd/nodes/omdParenthesisNode.js +292 -292
- package/omd/nodes/omdPowerNode.js +235 -235
- package/omd/nodes/omdRationalNode.js +295 -295
- package/omd/nodes/omdSqrtNode.js +307 -307
- package/omd/nodes/omdUnaryExpressionNode.js +227 -227
- package/omd/nodes/omdVariableNode.js +122 -122
- package/omd/simplification/omdSimplification.js +140 -140
- package/omd/simplification/omdSimplificationEngine.js +887 -887
- package/omd/simplification/package.json +5 -5
- package/omd/simplification/rules/binaryRules.js +1037 -1037
- package/omd/simplification/rules/functionRules.js +111 -111
- package/omd/simplification/rules/index.js +48 -48
- package/omd/simplification/rules/parenthesisRules.js +19 -19
- package/omd/simplification/rules/powerRules.js +143 -143
- package/omd/simplification/rules/rationalRules.js +725 -725
- package/omd/simplification/rules/sqrtRules.js +48 -48
- package/omd/simplification/rules/unaryRules.js +37 -37
- package/omd/simplification/simplificationRules.js +31 -31
- package/omd/simplification/simplificationUtils.js +1055 -1055
- package/omd/step-visualizer/omdStepVisualizer.js +947 -947
- package/omd/step-visualizer/omdStepVisualizerHighlighting.js +246 -246
- package/omd/step-visualizer/omdStepVisualizerLayout.js +892 -892
- package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +200 -200
- package/omd/utils/aiNextEquationStep.js +106 -106
- package/omd/utils/omdNodeOverlay.js +638 -638
- package/omd/utils/omdPopup.js +1203 -1203
- package/omd/utils/omdStepVisualizerInteractiveSteps.js +684 -684
- package/omd/utils/omdStepVisualizerNodeUtils.js +267 -267
- package/omd/utils/omdTranscriptionService.js +123 -123
- package/omd/utils/omdTreeDiff.js +733 -733
- package/package.json +59 -56
- package/readme.html +184 -120
- package/src/index.js +74 -74
- package/src/json-schemas.md +576 -576
- package/src/omd-json-samples.js +147 -147
- package/src/omdApp.js +391 -391
- package/src/omdAppCanvas.js +335 -335
- package/src/omdBalanceHanger.js +199 -199
- package/src/omdColor.js +13 -13
- package/src/omdCoordinatePlane.js +541 -541
- package/src/omdExpression.js +115 -115
- package/src/omdFactory.js +150 -150
- package/src/omdFunction.js +114 -114
- package/src/omdMetaExpression.js +290 -290
- package/src/omdNaturalExpression.js +563 -563
- package/src/omdNode.js +383 -383
- package/src/omdNumber.js +52 -52
- package/src/omdNumberLine.js +114 -112
- package/src/omdNumberTile.js +118 -118
- package/src/omdOperator.js +72 -72
- package/src/omdPowerExpression.js +91 -91
- package/src/omdProblem.js +259 -259
- package/src/omdRatioChart.js +251 -251
- package/src/omdRationalExpression.js +114 -114
- package/src/omdSampleData.js +215 -215
- package/src/omdShapes.js +512 -512
- package/src/omdSpinner.js +151 -151
- package/src/omdString.js +49 -49
- package/src/omdTable.js +498 -498
- package/src/omdTapeDiagram.js +244 -244
- package/src/omdTerm.js +91 -91
- package/src/omdTileEquation.js +349 -349
- package/src/omdUtils.js +84 -84
- package/src/omdVariable.js +51 -51
package/omd/utils/omdPopup.js
CHANGED
|
@@ -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
|
}
|