@teachinglab/omd 0.6.1 → 0.6.3
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 +606 -547
- package/omd/core/omdUtilities.js +113 -113
- package/omd/display/omdDisplay.js +1045 -963
- 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 -57
- 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
|
@@ -1,638 +1,638 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* omdNodeOverlay - A utility class for covering up nodes with overlay elements
|
|
3
|
-
*
|
|
4
|
-
* This class creates a visual overlay that can be positioned over any node
|
|
5
|
-
* to hide or mask it. Useful for step-by-step reveals, progressive disclosure,
|
|
6
|
-
* or highlighting specific parts of expressions.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { omdPopup } from './omdPopup.js';
|
|
10
|
-
import { jsvgGroup, jsvgRect } from '@teachinglab/jsvg';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* A flexible overlay system for covering nodes with customizable elements
|
|
14
|
-
* @extends jsvgGroup
|
|
15
|
-
*/
|
|
16
|
-
export class omdNodeOverlay extends jsvgGroup {
|
|
17
|
-
/**
|
|
18
|
-
* Creates a new node overlay
|
|
19
|
-
* @param {Object} options - Configuration options
|
|
20
|
-
* @param {string} options.fillColor - Fill color for the overlay (default: 'white')
|
|
21
|
-
* @param {string} options.strokeColor - Stroke color for the overlay (default: 'black')
|
|
22
|
-
* @param {number} options.strokeWidth - Stroke width (default: 2)
|
|
23
|
-
* @param {number} options.opacity - Opacity of the overlay (default: 1.0)
|
|
24
|
-
* @param {number} options.padding - Padding around the target node (default: 0)
|
|
25
|
-
* @param {number} options.cornerRadius - Corner radius for rounded rectangles (default: 4)
|
|
26
|
-
* @param {string} options.overlayType - Type of overlay: 'rectangle', 'ellipse', 'text' (default: 'rectangle')
|
|
27
|
-
* @param {boolean} options.animated - Whether to animate show/hide transitions (default: false)
|
|
28
|
-
* @param {string} options.text - Initial text for text overlays (default: '')
|
|
29
|
-
* @param {boolean} options.editable - Whether text overlay should be editable (default: false)
|
|
30
|
-
*/
|
|
31
|
-
constructor(options = {}) {
|
|
32
|
-
super();
|
|
33
|
-
|
|
34
|
-
this.options = {
|
|
35
|
-
fillColor: 'white',
|
|
36
|
-
strokeColor: 'black',
|
|
37
|
-
strokeWidth: 2,
|
|
38
|
-
opacity: 1.0,
|
|
39
|
-
padding: 0,
|
|
40
|
-
cornerRadius: 4,
|
|
41
|
-
overlayType: 'rectangle',
|
|
42
|
-
animated: false,
|
|
43
|
-
animationDuration: 300,
|
|
44
|
-
text: '',
|
|
45
|
-
editable: false,
|
|
46
|
-
...options
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
this.targetNode = null;
|
|
50
|
-
this.overlayElement = null;
|
|
51
|
-
this.textElement = null;
|
|
52
|
-
this.isVisible = false;
|
|
53
|
-
this.animationId = null;
|
|
54
|
-
this.clickLayer = null;
|
|
55
|
-
this.savedHandlers = [];
|
|
56
|
-
this.omdPopup = null;
|
|
57
|
-
|
|
58
|
-
this._createOverlayElement();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Creates the actual overlay element based on the overlay type
|
|
63
|
-
* @private
|
|
64
|
-
*/
|
|
65
|
-
_createOverlayElement() {
|
|
66
|
-
switch (this.options.overlayType) {
|
|
67
|
-
case 'rectangle':
|
|
68
|
-
this.overlayElement = new jsvgRect();
|
|
69
|
-
if (this.options.cornerRadius > 0) {
|
|
70
|
-
this.overlayElement.setCornerRadius(this.options.cornerRadius);
|
|
71
|
-
}
|
|
72
|
-
break;
|
|
73
|
-
case 'ellipse':
|
|
74
|
-
this.overlayElement = new jsvgEllipse();
|
|
75
|
-
break;
|
|
76
|
-
case 'text':
|
|
77
|
-
this._createTextOverlay();
|
|
78
|
-
return;
|
|
79
|
-
default:
|
|
80
|
-
this.overlayElement = new jsvgRect();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
this.overlayElement.setFillColor(this.options.fillColor);
|
|
84
|
-
this.overlayElement.setStrokeColor(this.options.strokeColor);
|
|
85
|
-
this.overlayElement.setStrokeWidth(this.options.strokeWidth);
|
|
86
|
-
this.overlayElement.setOpacity(this.options.opacity);
|
|
87
|
-
this.addChild(this.overlayElement);
|
|
88
|
-
this.setOpacity(0);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Creates a text overlay with background and editable text
|
|
93
|
-
* @private
|
|
94
|
-
*/
|
|
95
|
-
_createTextOverlay() {
|
|
96
|
-
// Create background rectangle
|
|
97
|
-
this.overlayElement = new jsvgRect();
|
|
98
|
-
if (this.options.cornerRadius > 0) {
|
|
99
|
-
this.overlayElement.setCornerRadius(this.options.cornerRadius);
|
|
100
|
-
}
|
|
101
|
-
this.overlayElement.setFillColor(this.options.fillColor);
|
|
102
|
-
this.overlayElement.setStrokeColor(this.options.strokeColor);
|
|
103
|
-
this.overlayElement.setStrokeWidth(this.options.strokeWidth);
|
|
104
|
-
this.overlayElement.setOpacity(this.options.opacity);
|
|
105
|
-
this.addChild(this.overlayElement);
|
|
106
|
-
|
|
107
|
-
// Create a click-only layer for editable overlays
|
|
108
|
-
if (this.options.editable) {
|
|
109
|
-
this.clickLayer = new jsvgRect();
|
|
110
|
-
this.clickLayer.setWidthAndHeight(this.overlayElement.width, this.overlayElement.height);
|
|
111
|
-
this.clickLayer.setPosition(0, 0);
|
|
112
|
-
this.clickLayer.setFillColor('transparent');
|
|
113
|
-
this.clickLayer.setStrokeColor('transparent');
|
|
114
|
-
this.addChild(this.clickLayer);
|
|
115
|
-
|
|
116
|
-
this.clickLayer.setClickCallback(() => {
|
|
117
|
-
this.togglePopup();
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// For editable overlays, create a popup instead of inline text
|
|
122
|
-
if (this.options.editable) {
|
|
123
|
-
this.textElement = null;
|
|
124
|
-
} else {
|
|
125
|
-
// For static text overlays, use jsvgTextLine
|
|
126
|
-
this.textElement = new jsvgTextLine();
|
|
127
|
-
this.textElement.setText(this.options.text);
|
|
128
|
-
this.textElement.setTextAnchor('middle');
|
|
129
|
-
this.textElement.svgObject.setAttribute('dominant-baseline', 'middle');
|
|
130
|
-
this.addChild(this.textElement);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
this.setOpacity(0);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Targets a specific node to overlay
|
|
138
|
-
* @param {omdNode} node - The node to cover
|
|
139
|
-
* @returns {omdNodeOverlay} This overlay for method chaining
|
|
140
|
-
*/
|
|
141
|
-
coverNode(node) {
|
|
142
|
-
if (!node) {
|
|
143
|
-
console.warn('omdNodeOverlay: Cannot cover null or undefined node');
|
|
144
|
-
return this;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
this.targetNode = node;
|
|
148
|
-
|
|
149
|
-
// Save and override parent mouse handlers to prevent highlighting
|
|
150
|
-
this.savedHandlers = [];
|
|
151
|
-
let current = this.targetNode;
|
|
152
|
-
while (current) {
|
|
153
|
-
if (current.svgObject && typeof current.svgObject.onmouseenter === 'function') {
|
|
154
|
-
this.savedHandlers.push({
|
|
155
|
-
element: current.svgObject,
|
|
156
|
-
onmouseenter: current.svgObject.onmouseenter,
|
|
157
|
-
onmouseleave: current.svgObject.onmouseleave,
|
|
158
|
-
});
|
|
159
|
-
current.svgObject.onmouseenter = null;
|
|
160
|
-
current.svgObject.onmouseleave = null;
|
|
161
|
-
}
|
|
162
|
-
current = current.parent;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Add the overlay as a child of the target node's parent
|
|
166
|
-
if (this.targetNode.parent) {
|
|
167
|
-
node.parent.addChild(this);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
this._updateOverlayPosition();
|
|
171
|
-
return this;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Manually sets the overlay position and size
|
|
176
|
-
* @param {number} x - X position
|
|
177
|
-
* @param {number} y - Y position
|
|
178
|
-
* @param {number} width - Width of the overlay
|
|
179
|
-
* @param {number} height - Height of the overlay
|
|
180
|
-
* @param {boolean} clearTargetNode - Whether to clear the target node
|
|
181
|
-
* @returns {omdNodeOverlay} This overlay for method chaining
|
|
182
|
-
*/
|
|
183
|
-
setCoverArea(x, y, width, height, clearTargetNode = true) {
|
|
184
|
-
if (clearTargetNode) {
|
|
185
|
-
this.targetNode = null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const padding = this.options.padding || 0;
|
|
189
|
-
const adjustedX = x - padding;
|
|
190
|
-
const adjustedY = y - padding;
|
|
191
|
-
const adjustedWidth = width + (padding * 2);
|
|
192
|
-
const adjustedHeight = height + (padding * 2);
|
|
193
|
-
|
|
194
|
-
this.setPosition(adjustedX, adjustedY);
|
|
195
|
-
this.overlayElement.setWidthAndHeight(adjustedWidth, adjustedHeight);
|
|
196
|
-
|
|
197
|
-
// Update click layer size to match overlay
|
|
198
|
-
if (this.clickLayer) {
|
|
199
|
-
this.clickLayer.setWidthAndHeight(adjustedWidth, adjustedHeight);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Position text element for text overlays
|
|
203
|
-
if (this.options.overlayType === 'text' && this.textElement) {
|
|
204
|
-
this.textElement.setPosition(adjustedWidth / 2, adjustedHeight / 2);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return this;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Updates the overlay position to match the target node
|
|
212
|
-
* @private
|
|
213
|
-
*/
|
|
214
|
-
_updateOverlayPosition() {
|
|
215
|
-
if (!this.targetNode) return;
|
|
216
|
-
|
|
217
|
-
// Get the position relative to the overlay's parent
|
|
218
|
-
let nodePos;
|
|
219
|
-
if (this.parent === this.targetNode.parent) {
|
|
220
|
-
nodePos = {
|
|
221
|
-
x: this.targetNode.xpos || this.targetNode.x || 0,
|
|
222
|
-
y: this.targetNode.ypos || this.targetNode.y || 0
|
|
223
|
-
};
|
|
224
|
-
} else {
|
|
225
|
-
nodePos = this._getGlobalPosition(this.targetNode);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
let nodeWidth = this.targetNode.width || 0;
|
|
229
|
-
let nodeHeight = this.targetNode.height || 0;
|
|
230
|
-
|
|
231
|
-
// Prefer backRect dimensions if available
|
|
232
|
-
if (this.targetNode.backRect) {
|
|
233
|
-
nodeWidth = this.targetNode.backRect.width || nodeWidth;
|
|
234
|
-
nodeHeight = this.targetNode.backRect.height || nodeHeight;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
this.setCoverArea(nodePos.x, nodePos.y, nodeWidth, nodeHeight, false);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Attempts to match visual properties of the target node
|
|
244
|
-
* @private
|
|
245
|
-
*/
|
|
246
|
-
_matchNodeProperties() {
|
|
247
|
-
if (!this.targetNode) return;
|
|
248
|
-
|
|
249
|
-
// Try to match corner radius for rectangles
|
|
250
|
-
if (this.options.overlayType === 'rectangle' && this.overlayElement.setCornerRadius) {
|
|
251
|
-
let cornerRadius = 0;
|
|
252
|
-
|
|
253
|
-
// First, check if the node has a backRect (common in OMD nodes)
|
|
254
|
-
if (this.targetNode.backRect && this.targetNode.backRect.cornerRadius !== undefined) {
|
|
255
|
-
cornerRadius = this.targetNode.backRect.cornerRadius;
|
|
256
|
-
}
|
|
257
|
-
// Fallback: check the main SVG object for rx/ry attributes
|
|
258
|
-
else if (this.targetNode.svgObject) {
|
|
259
|
-
const rx = this.targetNode.svgObject.getAttribute('rx');
|
|
260
|
-
const ry = this.targetNode.svgObject.getAttribute('ry');
|
|
261
|
-
cornerRadius = parseFloat(rx || ry || 0);
|
|
262
|
-
}
|
|
263
|
-
// Another fallback: check if the SVG object is a rect with corner radius
|
|
264
|
-
else if (this.targetNode.svgObject && this.targetNode.svgObject.tagName === 'rect') {
|
|
265
|
-
const rx = this.targetNode.svgObject.getAttribute('rx');
|
|
266
|
-
const ry = this.targetNode.svgObject.getAttribute('ry');
|
|
267
|
-
cornerRadius = parseFloat(rx || ry || 0);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Apply the corner radius if found
|
|
271
|
-
if (cornerRadius > 0) {
|
|
272
|
-
this.overlayElement.setCornerRadius(cornerRadius);
|
|
273
|
-
this.options.cornerRadius = cornerRadius;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Match font properties for text overlays
|
|
278
|
-
if (this.options.overlayType === 'text' && this.textElement) {
|
|
279
|
-
this._matchFontProperties();
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Try to match stroke width if target has stroke
|
|
283
|
-
if (this.targetNode.svgObject && !this.options.preserveStrokeWidth) {
|
|
284
|
-
const targetStrokeWidth = this.targetNode.svgObject.getAttribute('stroke-width');
|
|
285
|
-
if (targetStrokeWidth) {
|
|
286
|
-
const strokeWidth = parseFloat(targetStrokeWidth);
|
|
287
|
-
if (strokeWidth > 0) {
|
|
288
|
-
this.overlayElement.setStrokeWidth(strokeWidth);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Matches font properties from the target node
|
|
296
|
-
* @private
|
|
297
|
-
*/
|
|
298
|
-
_matchFontProperties() {
|
|
299
|
-
if (!this.targetNode || !this.textElement) return;
|
|
300
|
-
|
|
301
|
-
// Get font size from target node
|
|
302
|
-
let fontSize = 32; // Default fallback
|
|
303
|
-
if (this.targetNode.getFontSize && typeof this.targetNode.getFontSize === 'function') {
|
|
304
|
-
fontSize = this.targetNode.getFontSize();
|
|
305
|
-
} else if (this.targetNode.fontSize) {
|
|
306
|
-
fontSize = this.targetNode.fontSize;
|
|
307
|
-
}
|
|
308
|
-
this.textElement.setFontSize(fontSize);
|
|
309
|
-
|
|
310
|
-
// Try to match font family and color from target node's text element
|
|
311
|
-
if (this.targetNode.textElement) {
|
|
312
|
-
const targetTextSvg = this.targetNode.textElement.svgObject;
|
|
313
|
-
if (targetTextSvg) {
|
|
314
|
-
// Match font family
|
|
315
|
-
const fontFamily = targetTextSvg.style.fontFamily || 'Arial, Helvetica, sans-serif';
|
|
316
|
-
this.textElement.setFontFamily(fontFamily);
|
|
317
|
-
|
|
318
|
-
// Match font color - handle both jsvgTextLine and jsvgTextInput
|
|
319
|
-
const fontColor = targetTextSvg.style.fill || targetTextSvg.getAttribute('fill') || 'black';
|
|
320
|
-
if (this.textElement.setFontColor) {
|
|
321
|
-
this.textElement.setFontColor(fontColor);
|
|
322
|
-
} else if (this.textElement.div) {
|
|
323
|
-
// For jsvgTextInput, set color on the div
|
|
324
|
-
this.textElement.div.style.color = fontColor;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Match font weight if available
|
|
328
|
-
const fontWeight = targetTextSvg.style.fontWeight;
|
|
329
|
-
if (fontWeight && this.textElement.setFontWeight) {
|
|
330
|
-
this.textElement.setFontWeight(fontWeight);
|
|
331
|
-
} else if (fontWeight && this.textElement.div) {
|
|
332
|
-
// For jsvgTextInput, set font weight on the div
|
|
333
|
-
this.textElement.div.style.fontWeight = fontWeight;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Calculates the global position of a node
|
|
341
|
-
* @param {omdNode} node - The node to get position for
|
|
342
|
-
* @returns {Object} Object with x and y coordinates
|
|
343
|
-
* @private
|
|
344
|
-
*/
|
|
345
|
-
_getGlobalPosition(node) {
|
|
346
|
-
// Try to use SVG bounding box for accurate positioning
|
|
347
|
-
if (node.svgObject && node.svgObject.getCTM) {
|
|
348
|
-
try {
|
|
349
|
-
const ctm = node.svgObject.getCTM();
|
|
350
|
-
if (ctm) {
|
|
351
|
-
return { x: ctm.e, y: ctm.f };
|
|
352
|
-
}
|
|
353
|
-
} catch (e) {
|
|
354
|
-
// Fall back to manual calculation
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
let x = node.xpos || node.x || 0;
|
|
359
|
-
let y = node.ypos || node.y || 0;
|
|
360
|
-
|
|
361
|
-
// Walk up the parent chain
|
|
362
|
-
let parent = node.parent;
|
|
363
|
-
while (parent && parent.xpos !== undefined) {
|
|
364
|
-
x += parent.xpos || parent.x || 0;
|
|
365
|
-
y += parent.ypos || parent.y || 0;
|
|
366
|
-
parent = parent.parent;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return { x, y };
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Creates the popup instance
|
|
374
|
-
* @private
|
|
375
|
-
*/
|
|
376
|
-
_createPopup() {
|
|
377
|
-
if (!this.options.editable || this.omdPopup) return;
|
|
378
|
-
|
|
379
|
-
// Create the popup instance
|
|
380
|
-
this.omdPopup = new omdPopup(this.targetNode, this.parent, {
|
|
381
|
-
animationDuration: this.options.animationDuration
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
// Set initial text value
|
|
385
|
-
this.omdPopup.setValue(this.options.text);
|
|
386
|
-
|
|
387
|
-
// Set up validation callback
|
|
388
|
-
this.omdPopup.setValidationCallback(() => {
|
|
389
|
-
const currentText = this.omdPopup.getValue();
|
|
390
|
-
|
|
391
|
-
if (this.targetNode) {
|
|
392
|
-
const nodeText = this.targetNode.text || this.targetNode.toString() || "";
|
|
393
|
-
|
|
394
|
-
if (this.omdPopup.areExpressionsEquivalent(currentText.trim(), nodeText.trim())) {
|
|
395
|
-
// Show success and destroy overlay
|
|
396
|
-
this.omdPopup.flashValidation(true);
|
|
397
|
-
setTimeout(() => {
|
|
398
|
-
this.destroy();
|
|
399
|
-
}, 500);
|
|
400
|
-
} else {
|
|
401
|
-
// Show error
|
|
402
|
-
this.omdPopup.flashValidation(false);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
// Set up clear callback
|
|
408
|
-
this.omdPopup.setClearCallback(() => {
|
|
409
|
-
this.omdPopup.setValue('');
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Toggles the popup visibility
|
|
415
|
-
* @returns {Promise} Promise that resolves when animation completes
|
|
416
|
-
*/
|
|
417
|
-
togglePopup() {
|
|
418
|
-
if (!this.options.editable) return Promise.resolve();
|
|
419
|
-
|
|
420
|
-
if (!this.omdPopup) {
|
|
421
|
-
this._createPopup();
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const overlayX = this.xpos || this.x || 0;
|
|
425
|
-
const overlayY = this.ypos || this.y || 0;
|
|
426
|
-
const overlayWidth = this.overlayElement.width || this.targetNode.width || 100;
|
|
427
|
-
const overlayHeight = this.overlayElement.height || this.targetNode.height || 40;
|
|
428
|
-
const popupWidth = 400;
|
|
429
|
-
|
|
430
|
-
const popupX = overlayX + (overlayWidth / 2) - (popupWidth / 2);
|
|
431
|
-
const popupY = overlayY + overlayHeight + 10;
|
|
432
|
-
|
|
433
|
-
return this.omdPopup.toggle(popupX, popupY);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Hides and removes the popup
|
|
446
|
-
* @private
|
|
447
|
-
*/
|
|
448
|
-
_hidePopup() {
|
|
449
|
-
if (this.omdPopup) {
|
|
450
|
-
this.omdPopup.destroy();
|
|
451
|
-
this.omdPopup = null;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Shows the overlay
|
|
457
|
-
* @param {boolean} animated - Whether to animate the show
|
|
458
|
-
* @returns {Promise} Promise that resolves when animation completes
|
|
459
|
-
*/
|
|
460
|
-
show(animated = this.options.animated) {
|
|
461
|
-
return new Promise((resolve) => {
|
|
462
|
-
if (this.isVisible) {
|
|
463
|
-
resolve();
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
this.isVisible = true;
|
|
468
|
-
|
|
469
|
-
if (animated) {
|
|
470
|
-
this._animateOpacity(0, this.options.opacity, this.options.animationDuration)
|
|
471
|
-
.then(resolve);
|
|
472
|
-
} else {
|
|
473
|
-
this.setOpacity(this.options.opacity);
|
|
474
|
-
resolve();
|
|
475
|
-
}
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Animates the opacity from one value to another
|
|
481
|
-
* @private
|
|
482
|
-
*/
|
|
483
|
-
_animateOpacity(fromOpacity, toOpacity, duration) {
|
|
484
|
-
return new Promise((resolve) => {
|
|
485
|
-
if (this.animationId) {
|
|
486
|
-
cancelAnimationFrame(this.animationId);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const startTime = performance.now();
|
|
490
|
-
const deltaOpacity = toOpacity - fromOpacity;
|
|
491
|
-
|
|
492
|
-
const animate = (currentTime) => {
|
|
493
|
-
const elapsed = currentTime - startTime;
|
|
494
|
-
const progress = Math.min(elapsed / duration, 1);
|
|
495
|
-
const easedProgress = 1 - Math.pow(1 - progress, 3);
|
|
496
|
-
const currentOpacity = fromOpacity + (deltaOpacity * easedProgress);
|
|
497
|
-
|
|
498
|
-
this.setOpacity(currentOpacity);
|
|
499
|
-
|
|
500
|
-
if (progress < 1) {
|
|
501
|
-
this.animationId = requestAnimationFrame(animate);
|
|
502
|
-
} else {
|
|
503
|
-
this.animationId = null;
|
|
504
|
-
resolve();
|
|
505
|
-
}
|
|
506
|
-
};
|
|
507
|
-
|
|
508
|
-
this.animationId = requestAnimationFrame(animate);
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* Removes the overlay and cleans up resources
|
|
514
|
-
* @param {boolean} animated - Whether to animate the destruction
|
|
515
|
-
* @returns {Promise} Promise that resolves when destruction is complete
|
|
516
|
-
*/
|
|
517
|
-
destroy(animated = this.options.animated) {
|
|
518
|
-
return new Promise((resolve) => {
|
|
519
|
-
if (animated && this.isVisible) {
|
|
520
|
-
this._animateOpacity(this.options.opacity, 0, this.options.animationDuration)
|
|
521
|
-
.then(() => {
|
|
522
|
-
this._performDestroy();
|
|
523
|
-
resolve();
|
|
524
|
-
});
|
|
525
|
-
} else {
|
|
526
|
-
this._performDestroy();
|
|
527
|
-
resolve();
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/**
|
|
533
|
-
* Performs the actual destruction without animation
|
|
534
|
-
* @private
|
|
535
|
-
*/
|
|
536
|
-
_performDestroy() {
|
|
537
|
-
if (this.animationId) {
|
|
538
|
-
cancelAnimationFrame(this.animationId);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Restore original mouse handlers
|
|
542
|
-
this.savedHandlers.forEach(({ element, onmouseenter, onmouseleave }) => {
|
|
543
|
-
if (element) {
|
|
544
|
-
element.onmouseenter = onmouseenter;
|
|
545
|
-
element.onmouseleave = onmouseleave;
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
this._hidePopup();
|
|
550
|
-
|
|
551
|
-
this.targetNode = null;
|
|
552
|
-
this.isVisible = false;
|
|
553
|
-
|
|
554
|
-
if (this.parent) {
|
|
555
|
-
this.parent.removeChild(this);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// === ESSENTIAL GETTERS/SETTERS ===
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* Checks if the overlay is currently visible
|
|
563
|
-
*/
|
|
564
|
-
getIsVisible() {
|
|
565
|
-
return this.isVisible;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* Gets the currently targeted node
|
|
570
|
-
*/
|
|
571
|
-
getTargetNode() {
|
|
572
|
-
return this.targetNode;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Sets the text content for text overlays
|
|
577
|
-
*/
|
|
578
|
-
setText(text) {
|
|
579
|
-
if (this.textElement) {
|
|
580
|
-
this.textElement.setText(text);
|
|
581
|
-
this.options.text = text;
|
|
582
|
-
}
|
|
583
|
-
return this;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Gets the text content from text overlays
|
|
588
|
-
*/
|
|
589
|
-
getText() {
|
|
590
|
-
if (this.textElement) {
|
|
591
|
-
return this.textElement.getText();
|
|
592
|
-
}
|
|
593
|
-
return this.options.text;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* Factory function for creating common overlay presets
|
|
599
|
-
*/
|
|
600
|
-
export class omdNodeOverlayPresets {
|
|
601
|
-
/**
|
|
602
|
-
* Creates a simple white overlay for hiding content
|
|
603
|
-
*/
|
|
604
|
-
static createHidingOverlay(enableTextInput = false, initialText = null) {
|
|
605
|
-
const config = {
|
|
606
|
-
fillColor: 'white',
|
|
607
|
-
strokeColor: 'black',
|
|
608
|
-
strokeWidth: 2,
|
|
609
|
-
padding: 1,
|
|
610
|
-
animated: true
|
|
611
|
-
};
|
|
612
|
-
|
|
613
|
-
if (enableTextInput) {
|
|
614
|
-
config.overlayType = 'text';
|
|
615
|
-
config.text = initialText || '';
|
|
616
|
-
config.editable = true;
|
|
617
|
-
config.fillColor = 'white';
|
|
618
|
-
config.strokeColor = 'black';
|
|
619
|
-
config.strokeWidth = 2;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
return new omdNodeOverlay(config);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
/**
|
|
626
|
-
* Creates a highlighted border overlay for feedback
|
|
627
|
-
*/
|
|
628
|
-
static createHighlightOverlay(color = '#ffff00') {
|
|
629
|
-
return new omdNodeOverlay({
|
|
630
|
-
fillColor: 'none',
|
|
631
|
-
strokeColor: color,
|
|
632
|
-
strokeWidth: 3,
|
|
633
|
-
padding: 2,
|
|
634
|
-
cornerRadius: 4,
|
|
635
|
-
animated: true
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* omdNodeOverlay - A utility class for covering up nodes with overlay elements
|
|
3
|
+
*
|
|
4
|
+
* This class creates a visual overlay that can be positioned over any node
|
|
5
|
+
* to hide or mask it. Useful for step-by-step reveals, progressive disclosure,
|
|
6
|
+
* or highlighting specific parts of expressions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { omdPopup } from './omdPopup.js';
|
|
10
|
+
import { jsvgGroup, jsvgRect } from '@teachinglab/jsvg';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A flexible overlay system for covering nodes with customizable elements
|
|
14
|
+
* @extends jsvgGroup
|
|
15
|
+
*/
|
|
16
|
+
export class omdNodeOverlay extends jsvgGroup {
|
|
17
|
+
/**
|
|
18
|
+
* Creates a new node overlay
|
|
19
|
+
* @param {Object} options - Configuration options
|
|
20
|
+
* @param {string} options.fillColor - Fill color for the overlay (default: 'white')
|
|
21
|
+
* @param {string} options.strokeColor - Stroke color for the overlay (default: 'black')
|
|
22
|
+
* @param {number} options.strokeWidth - Stroke width (default: 2)
|
|
23
|
+
* @param {number} options.opacity - Opacity of the overlay (default: 1.0)
|
|
24
|
+
* @param {number} options.padding - Padding around the target node (default: 0)
|
|
25
|
+
* @param {number} options.cornerRadius - Corner radius for rounded rectangles (default: 4)
|
|
26
|
+
* @param {string} options.overlayType - Type of overlay: 'rectangle', 'ellipse', 'text' (default: 'rectangle')
|
|
27
|
+
* @param {boolean} options.animated - Whether to animate show/hide transitions (default: false)
|
|
28
|
+
* @param {string} options.text - Initial text for text overlays (default: '')
|
|
29
|
+
* @param {boolean} options.editable - Whether text overlay should be editable (default: false)
|
|
30
|
+
*/
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
super();
|
|
33
|
+
|
|
34
|
+
this.options = {
|
|
35
|
+
fillColor: 'white',
|
|
36
|
+
strokeColor: 'black',
|
|
37
|
+
strokeWidth: 2,
|
|
38
|
+
opacity: 1.0,
|
|
39
|
+
padding: 0,
|
|
40
|
+
cornerRadius: 4,
|
|
41
|
+
overlayType: 'rectangle',
|
|
42
|
+
animated: false,
|
|
43
|
+
animationDuration: 300,
|
|
44
|
+
text: '',
|
|
45
|
+
editable: false,
|
|
46
|
+
...options
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
this.targetNode = null;
|
|
50
|
+
this.overlayElement = null;
|
|
51
|
+
this.textElement = null;
|
|
52
|
+
this.isVisible = false;
|
|
53
|
+
this.animationId = null;
|
|
54
|
+
this.clickLayer = null;
|
|
55
|
+
this.savedHandlers = [];
|
|
56
|
+
this.omdPopup = null;
|
|
57
|
+
|
|
58
|
+
this._createOverlayElement();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Creates the actual overlay element based on the overlay type
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
_createOverlayElement() {
|
|
66
|
+
switch (this.options.overlayType) {
|
|
67
|
+
case 'rectangle':
|
|
68
|
+
this.overlayElement = new jsvgRect();
|
|
69
|
+
if (this.options.cornerRadius > 0) {
|
|
70
|
+
this.overlayElement.setCornerRadius(this.options.cornerRadius);
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
case 'ellipse':
|
|
74
|
+
this.overlayElement = new jsvgEllipse();
|
|
75
|
+
break;
|
|
76
|
+
case 'text':
|
|
77
|
+
this._createTextOverlay();
|
|
78
|
+
return;
|
|
79
|
+
default:
|
|
80
|
+
this.overlayElement = new jsvgRect();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.overlayElement.setFillColor(this.options.fillColor);
|
|
84
|
+
this.overlayElement.setStrokeColor(this.options.strokeColor);
|
|
85
|
+
this.overlayElement.setStrokeWidth(this.options.strokeWidth);
|
|
86
|
+
this.overlayElement.setOpacity(this.options.opacity);
|
|
87
|
+
this.addChild(this.overlayElement);
|
|
88
|
+
this.setOpacity(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Creates a text overlay with background and editable text
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
_createTextOverlay() {
|
|
96
|
+
// Create background rectangle
|
|
97
|
+
this.overlayElement = new jsvgRect();
|
|
98
|
+
if (this.options.cornerRadius > 0) {
|
|
99
|
+
this.overlayElement.setCornerRadius(this.options.cornerRadius);
|
|
100
|
+
}
|
|
101
|
+
this.overlayElement.setFillColor(this.options.fillColor);
|
|
102
|
+
this.overlayElement.setStrokeColor(this.options.strokeColor);
|
|
103
|
+
this.overlayElement.setStrokeWidth(this.options.strokeWidth);
|
|
104
|
+
this.overlayElement.setOpacity(this.options.opacity);
|
|
105
|
+
this.addChild(this.overlayElement);
|
|
106
|
+
|
|
107
|
+
// Create a click-only layer for editable overlays
|
|
108
|
+
if (this.options.editable) {
|
|
109
|
+
this.clickLayer = new jsvgRect();
|
|
110
|
+
this.clickLayer.setWidthAndHeight(this.overlayElement.width, this.overlayElement.height);
|
|
111
|
+
this.clickLayer.setPosition(0, 0);
|
|
112
|
+
this.clickLayer.setFillColor('transparent');
|
|
113
|
+
this.clickLayer.setStrokeColor('transparent');
|
|
114
|
+
this.addChild(this.clickLayer);
|
|
115
|
+
|
|
116
|
+
this.clickLayer.setClickCallback(() => {
|
|
117
|
+
this.togglePopup();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// For editable overlays, create a popup instead of inline text
|
|
122
|
+
if (this.options.editable) {
|
|
123
|
+
this.textElement = null;
|
|
124
|
+
} else {
|
|
125
|
+
// For static text overlays, use jsvgTextLine
|
|
126
|
+
this.textElement = new jsvgTextLine();
|
|
127
|
+
this.textElement.setText(this.options.text);
|
|
128
|
+
this.textElement.setTextAnchor('middle');
|
|
129
|
+
this.textElement.svgObject.setAttribute('dominant-baseline', 'middle');
|
|
130
|
+
this.addChild(this.textElement);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.setOpacity(0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Targets a specific node to overlay
|
|
138
|
+
* @param {omdNode} node - The node to cover
|
|
139
|
+
* @returns {omdNodeOverlay} This overlay for method chaining
|
|
140
|
+
*/
|
|
141
|
+
coverNode(node) {
|
|
142
|
+
if (!node) {
|
|
143
|
+
console.warn('omdNodeOverlay: Cannot cover null or undefined node');
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.targetNode = node;
|
|
148
|
+
|
|
149
|
+
// Save and override parent mouse handlers to prevent highlighting
|
|
150
|
+
this.savedHandlers = [];
|
|
151
|
+
let current = this.targetNode;
|
|
152
|
+
while (current) {
|
|
153
|
+
if (current.svgObject && typeof current.svgObject.onmouseenter === 'function') {
|
|
154
|
+
this.savedHandlers.push({
|
|
155
|
+
element: current.svgObject,
|
|
156
|
+
onmouseenter: current.svgObject.onmouseenter,
|
|
157
|
+
onmouseleave: current.svgObject.onmouseleave,
|
|
158
|
+
});
|
|
159
|
+
current.svgObject.onmouseenter = null;
|
|
160
|
+
current.svgObject.onmouseleave = null;
|
|
161
|
+
}
|
|
162
|
+
current = current.parent;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Add the overlay as a child of the target node's parent
|
|
166
|
+
if (this.targetNode.parent) {
|
|
167
|
+
node.parent.addChild(this);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this._updateOverlayPosition();
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Manually sets the overlay position and size
|
|
176
|
+
* @param {number} x - X position
|
|
177
|
+
* @param {number} y - Y position
|
|
178
|
+
* @param {number} width - Width of the overlay
|
|
179
|
+
* @param {number} height - Height of the overlay
|
|
180
|
+
* @param {boolean} clearTargetNode - Whether to clear the target node
|
|
181
|
+
* @returns {omdNodeOverlay} This overlay for method chaining
|
|
182
|
+
*/
|
|
183
|
+
setCoverArea(x, y, width, height, clearTargetNode = true) {
|
|
184
|
+
if (clearTargetNode) {
|
|
185
|
+
this.targetNode = null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const padding = this.options.padding || 0;
|
|
189
|
+
const adjustedX = x - padding;
|
|
190
|
+
const adjustedY = y - padding;
|
|
191
|
+
const adjustedWidth = width + (padding * 2);
|
|
192
|
+
const adjustedHeight = height + (padding * 2);
|
|
193
|
+
|
|
194
|
+
this.setPosition(adjustedX, adjustedY);
|
|
195
|
+
this.overlayElement.setWidthAndHeight(adjustedWidth, adjustedHeight);
|
|
196
|
+
|
|
197
|
+
// Update click layer size to match overlay
|
|
198
|
+
if (this.clickLayer) {
|
|
199
|
+
this.clickLayer.setWidthAndHeight(adjustedWidth, adjustedHeight);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Position text element for text overlays
|
|
203
|
+
if (this.options.overlayType === 'text' && this.textElement) {
|
|
204
|
+
this.textElement.setPosition(adjustedWidth / 2, adjustedHeight / 2);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Updates the overlay position to match the target node
|
|
212
|
+
* @private
|
|
213
|
+
*/
|
|
214
|
+
_updateOverlayPosition() {
|
|
215
|
+
if (!this.targetNode) return;
|
|
216
|
+
|
|
217
|
+
// Get the position relative to the overlay's parent
|
|
218
|
+
let nodePos;
|
|
219
|
+
if (this.parent === this.targetNode.parent) {
|
|
220
|
+
nodePos = {
|
|
221
|
+
x: this.targetNode.xpos || this.targetNode.x || 0,
|
|
222
|
+
y: this.targetNode.ypos || this.targetNode.y || 0
|
|
223
|
+
};
|
|
224
|
+
} else {
|
|
225
|
+
nodePos = this._getGlobalPosition(this.targetNode);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let nodeWidth = this.targetNode.width || 0;
|
|
229
|
+
let nodeHeight = this.targetNode.height || 0;
|
|
230
|
+
|
|
231
|
+
// Prefer backRect dimensions if available
|
|
232
|
+
if (this.targetNode.backRect) {
|
|
233
|
+
nodeWidth = this.targetNode.backRect.width || nodeWidth;
|
|
234
|
+
nodeHeight = this.targetNode.backRect.height || nodeHeight;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.setCoverArea(nodePos.x, nodePos.y, nodeWidth, nodeHeight, false);
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Attempts to match visual properties of the target node
|
|
244
|
+
* @private
|
|
245
|
+
*/
|
|
246
|
+
_matchNodeProperties() {
|
|
247
|
+
if (!this.targetNode) return;
|
|
248
|
+
|
|
249
|
+
// Try to match corner radius for rectangles
|
|
250
|
+
if (this.options.overlayType === 'rectangle' && this.overlayElement.setCornerRadius) {
|
|
251
|
+
let cornerRadius = 0;
|
|
252
|
+
|
|
253
|
+
// First, check if the node has a backRect (common in OMD nodes)
|
|
254
|
+
if (this.targetNode.backRect && this.targetNode.backRect.cornerRadius !== undefined) {
|
|
255
|
+
cornerRadius = this.targetNode.backRect.cornerRadius;
|
|
256
|
+
}
|
|
257
|
+
// Fallback: check the main SVG object for rx/ry attributes
|
|
258
|
+
else if (this.targetNode.svgObject) {
|
|
259
|
+
const rx = this.targetNode.svgObject.getAttribute('rx');
|
|
260
|
+
const ry = this.targetNode.svgObject.getAttribute('ry');
|
|
261
|
+
cornerRadius = parseFloat(rx || ry || 0);
|
|
262
|
+
}
|
|
263
|
+
// Another fallback: check if the SVG object is a rect with corner radius
|
|
264
|
+
else if (this.targetNode.svgObject && this.targetNode.svgObject.tagName === 'rect') {
|
|
265
|
+
const rx = this.targetNode.svgObject.getAttribute('rx');
|
|
266
|
+
const ry = this.targetNode.svgObject.getAttribute('ry');
|
|
267
|
+
cornerRadius = parseFloat(rx || ry || 0);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Apply the corner radius if found
|
|
271
|
+
if (cornerRadius > 0) {
|
|
272
|
+
this.overlayElement.setCornerRadius(cornerRadius);
|
|
273
|
+
this.options.cornerRadius = cornerRadius;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Match font properties for text overlays
|
|
278
|
+
if (this.options.overlayType === 'text' && this.textElement) {
|
|
279
|
+
this._matchFontProperties();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Try to match stroke width if target has stroke
|
|
283
|
+
if (this.targetNode.svgObject && !this.options.preserveStrokeWidth) {
|
|
284
|
+
const targetStrokeWidth = this.targetNode.svgObject.getAttribute('stroke-width');
|
|
285
|
+
if (targetStrokeWidth) {
|
|
286
|
+
const strokeWidth = parseFloat(targetStrokeWidth);
|
|
287
|
+
if (strokeWidth > 0) {
|
|
288
|
+
this.overlayElement.setStrokeWidth(strokeWidth);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Matches font properties from the target node
|
|
296
|
+
* @private
|
|
297
|
+
*/
|
|
298
|
+
_matchFontProperties() {
|
|
299
|
+
if (!this.targetNode || !this.textElement) return;
|
|
300
|
+
|
|
301
|
+
// Get font size from target node
|
|
302
|
+
let fontSize = 32; // Default fallback
|
|
303
|
+
if (this.targetNode.getFontSize && typeof this.targetNode.getFontSize === 'function') {
|
|
304
|
+
fontSize = this.targetNode.getFontSize();
|
|
305
|
+
} else if (this.targetNode.fontSize) {
|
|
306
|
+
fontSize = this.targetNode.fontSize;
|
|
307
|
+
}
|
|
308
|
+
this.textElement.setFontSize(fontSize);
|
|
309
|
+
|
|
310
|
+
// Try to match font family and color from target node's text element
|
|
311
|
+
if (this.targetNode.textElement) {
|
|
312
|
+
const targetTextSvg = this.targetNode.textElement.svgObject;
|
|
313
|
+
if (targetTextSvg) {
|
|
314
|
+
// Match font family
|
|
315
|
+
const fontFamily = targetTextSvg.style.fontFamily || 'Arial, Helvetica, sans-serif';
|
|
316
|
+
this.textElement.setFontFamily(fontFamily);
|
|
317
|
+
|
|
318
|
+
// Match font color - handle both jsvgTextLine and jsvgTextInput
|
|
319
|
+
const fontColor = targetTextSvg.style.fill || targetTextSvg.getAttribute('fill') || 'black';
|
|
320
|
+
if (this.textElement.setFontColor) {
|
|
321
|
+
this.textElement.setFontColor(fontColor);
|
|
322
|
+
} else if (this.textElement.div) {
|
|
323
|
+
// For jsvgTextInput, set color on the div
|
|
324
|
+
this.textElement.div.style.color = fontColor;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Match font weight if available
|
|
328
|
+
const fontWeight = targetTextSvg.style.fontWeight;
|
|
329
|
+
if (fontWeight && this.textElement.setFontWeight) {
|
|
330
|
+
this.textElement.setFontWeight(fontWeight);
|
|
331
|
+
} else if (fontWeight && this.textElement.div) {
|
|
332
|
+
// For jsvgTextInput, set font weight on the div
|
|
333
|
+
this.textElement.div.style.fontWeight = fontWeight;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Calculates the global position of a node
|
|
341
|
+
* @param {omdNode} node - The node to get position for
|
|
342
|
+
* @returns {Object} Object with x and y coordinates
|
|
343
|
+
* @private
|
|
344
|
+
*/
|
|
345
|
+
_getGlobalPosition(node) {
|
|
346
|
+
// Try to use SVG bounding box for accurate positioning
|
|
347
|
+
if (node.svgObject && node.svgObject.getCTM) {
|
|
348
|
+
try {
|
|
349
|
+
const ctm = node.svgObject.getCTM();
|
|
350
|
+
if (ctm) {
|
|
351
|
+
return { x: ctm.e, y: ctm.f };
|
|
352
|
+
}
|
|
353
|
+
} catch (e) {
|
|
354
|
+
// Fall back to manual calculation
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let x = node.xpos || node.x || 0;
|
|
359
|
+
let y = node.ypos || node.y || 0;
|
|
360
|
+
|
|
361
|
+
// Walk up the parent chain
|
|
362
|
+
let parent = node.parent;
|
|
363
|
+
while (parent && parent.xpos !== undefined) {
|
|
364
|
+
x += parent.xpos || parent.x || 0;
|
|
365
|
+
y += parent.ypos || parent.y || 0;
|
|
366
|
+
parent = parent.parent;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return { x, y };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Creates the popup instance
|
|
374
|
+
* @private
|
|
375
|
+
*/
|
|
376
|
+
_createPopup() {
|
|
377
|
+
if (!this.options.editable || this.omdPopup) return;
|
|
378
|
+
|
|
379
|
+
// Create the popup instance
|
|
380
|
+
this.omdPopup = new omdPopup(this.targetNode, this.parent, {
|
|
381
|
+
animationDuration: this.options.animationDuration
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Set initial text value
|
|
385
|
+
this.omdPopup.setValue(this.options.text);
|
|
386
|
+
|
|
387
|
+
// Set up validation callback
|
|
388
|
+
this.omdPopup.setValidationCallback(() => {
|
|
389
|
+
const currentText = this.omdPopup.getValue();
|
|
390
|
+
|
|
391
|
+
if (this.targetNode) {
|
|
392
|
+
const nodeText = this.targetNode.text || this.targetNode.toString() || "";
|
|
393
|
+
|
|
394
|
+
if (this.omdPopup.areExpressionsEquivalent(currentText.trim(), nodeText.trim())) {
|
|
395
|
+
// Show success and destroy overlay
|
|
396
|
+
this.omdPopup.flashValidation(true);
|
|
397
|
+
setTimeout(() => {
|
|
398
|
+
this.destroy();
|
|
399
|
+
}, 500);
|
|
400
|
+
} else {
|
|
401
|
+
// Show error
|
|
402
|
+
this.omdPopup.flashValidation(false);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Set up clear callback
|
|
408
|
+
this.omdPopup.setClearCallback(() => {
|
|
409
|
+
this.omdPopup.setValue('');
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Toggles the popup visibility
|
|
415
|
+
* @returns {Promise} Promise that resolves when animation completes
|
|
416
|
+
*/
|
|
417
|
+
togglePopup() {
|
|
418
|
+
if (!this.options.editable) return Promise.resolve();
|
|
419
|
+
|
|
420
|
+
if (!this.omdPopup) {
|
|
421
|
+
this._createPopup();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const overlayX = this.xpos || this.x || 0;
|
|
425
|
+
const overlayY = this.ypos || this.y || 0;
|
|
426
|
+
const overlayWidth = this.overlayElement.width || this.targetNode.width || 100;
|
|
427
|
+
const overlayHeight = this.overlayElement.height || this.targetNode.height || 40;
|
|
428
|
+
const popupWidth = 400;
|
|
429
|
+
|
|
430
|
+
const popupX = overlayX + (overlayWidth / 2) - (popupWidth / 2);
|
|
431
|
+
const popupY = overlayY + overlayHeight + 10;
|
|
432
|
+
|
|
433
|
+
return this.omdPopup.toggle(popupX, popupY);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Hides and removes the popup
|
|
446
|
+
* @private
|
|
447
|
+
*/
|
|
448
|
+
_hidePopup() {
|
|
449
|
+
if (this.omdPopup) {
|
|
450
|
+
this.omdPopup.destroy();
|
|
451
|
+
this.omdPopup = null;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Shows the overlay
|
|
457
|
+
* @param {boolean} animated - Whether to animate the show
|
|
458
|
+
* @returns {Promise} Promise that resolves when animation completes
|
|
459
|
+
*/
|
|
460
|
+
show(animated = this.options.animated) {
|
|
461
|
+
return new Promise((resolve) => {
|
|
462
|
+
if (this.isVisible) {
|
|
463
|
+
resolve();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
this.isVisible = true;
|
|
468
|
+
|
|
469
|
+
if (animated) {
|
|
470
|
+
this._animateOpacity(0, this.options.opacity, this.options.animationDuration)
|
|
471
|
+
.then(resolve);
|
|
472
|
+
} else {
|
|
473
|
+
this.setOpacity(this.options.opacity);
|
|
474
|
+
resolve();
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Animates the opacity from one value to another
|
|
481
|
+
* @private
|
|
482
|
+
*/
|
|
483
|
+
_animateOpacity(fromOpacity, toOpacity, duration) {
|
|
484
|
+
return new Promise((resolve) => {
|
|
485
|
+
if (this.animationId) {
|
|
486
|
+
cancelAnimationFrame(this.animationId);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const startTime = performance.now();
|
|
490
|
+
const deltaOpacity = toOpacity - fromOpacity;
|
|
491
|
+
|
|
492
|
+
const animate = (currentTime) => {
|
|
493
|
+
const elapsed = currentTime - startTime;
|
|
494
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
495
|
+
const easedProgress = 1 - Math.pow(1 - progress, 3);
|
|
496
|
+
const currentOpacity = fromOpacity + (deltaOpacity * easedProgress);
|
|
497
|
+
|
|
498
|
+
this.setOpacity(currentOpacity);
|
|
499
|
+
|
|
500
|
+
if (progress < 1) {
|
|
501
|
+
this.animationId = requestAnimationFrame(animate);
|
|
502
|
+
} else {
|
|
503
|
+
this.animationId = null;
|
|
504
|
+
resolve();
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
this.animationId = requestAnimationFrame(animate);
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Removes the overlay and cleans up resources
|
|
514
|
+
* @param {boolean} animated - Whether to animate the destruction
|
|
515
|
+
* @returns {Promise} Promise that resolves when destruction is complete
|
|
516
|
+
*/
|
|
517
|
+
destroy(animated = this.options.animated) {
|
|
518
|
+
return new Promise((resolve) => {
|
|
519
|
+
if (animated && this.isVisible) {
|
|
520
|
+
this._animateOpacity(this.options.opacity, 0, this.options.animationDuration)
|
|
521
|
+
.then(() => {
|
|
522
|
+
this._performDestroy();
|
|
523
|
+
resolve();
|
|
524
|
+
});
|
|
525
|
+
} else {
|
|
526
|
+
this._performDestroy();
|
|
527
|
+
resolve();
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Performs the actual destruction without animation
|
|
534
|
+
* @private
|
|
535
|
+
*/
|
|
536
|
+
_performDestroy() {
|
|
537
|
+
if (this.animationId) {
|
|
538
|
+
cancelAnimationFrame(this.animationId);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Restore original mouse handlers
|
|
542
|
+
this.savedHandlers.forEach(({ element, onmouseenter, onmouseleave }) => {
|
|
543
|
+
if (element) {
|
|
544
|
+
element.onmouseenter = onmouseenter;
|
|
545
|
+
element.onmouseleave = onmouseleave;
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
this._hidePopup();
|
|
550
|
+
|
|
551
|
+
this.targetNode = null;
|
|
552
|
+
this.isVisible = false;
|
|
553
|
+
|
|
554
|
+
if (this.parent) {
|
|
555
|
+
this.parent.removeChild(this);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// === ESSENTIAL GETTERS/SETTERS ===
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Checks if the overlay is currently visible
|
|
563
|
+
*/
|
|
564
|
+
getIsVisible() {
|
|
565
|
+
return this.isVisible;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Gets the currently targeted node
|
|
570
|
+
*/
|
|
571
|
+
getTargetNode() {
|
|
572
|
+
return this.targetNode;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Sets the text content for text overlays
|
|
577
|
+
*/
|
|
578
|
+
setText(text) {
|
|
579
|
+
if (this.textElement) {
|
|
580
|
+
this.textElement.setText(text);
|
|
581
|
+
this.options.text = text;
|
|
582
|
+
}
|
|
583
|
+
return this;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Gets the text content from text overlays
|
|
588
|
+
*/
|
|
589
|
+
getText() {
|
|
590
|
+
if (this.textElement) {
|
|
591
|
+
return this.textElement.getText();
|
|
592
|
+
}
|
|
593
|
+
return this.options.text;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Factory function for creating common overlay presets
|
|
599
|
+
*/
|
|
600
|
+
export class omdNodeOverlayPresets {
|
|
601
|
+
/**
|
|
602
|
+
* Creates a simple white overlay for hiding content
|
|
603
|
+
*/
|
|
604
|
+
static createHidingOverlay(enableTextInput = false, initialText = null) {
|
|
605
|
+
const config = {
|
|
606
|
+
fillColor: 'white',
|
|
607
|
+
strokeColor: 'black',
|
|
608
|
+
strokeWidth: 2,
|
|
609
|
+
padding: 1,
|
|
610
|
+
animated: true
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
if (enableTextInput) {
|
|
614
|
+
config.overlayType = 'text';
|
|
615
|
+
config.text = initialText || '';
|
|
616
|
+
config.editable = true;
|
|
617
|
+
config.fillColor = 'white';
|
|
618
|
+
config.strokeColor = 'black';
|
|
619
|
+
config.strokeWidth = 2;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return new omdNodeOverlay(config);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Creates a highlighted border overlay for feedback
|
|
627
|
+
*/
|
|
628
|
+
static createHighlightOverlay(color = '#ffff00') {
|
|
629
|
+
return new omdNodeOverlay({
|
|
630
|
+
fillColor: 'none',
|
|
631
|
+
strokeColor: color,
|
|
632
|
+
strokeWidth: 3,
|
|
633
|
+
padding: 2,
|
|
634
|
+
cornerRadius: 4,
|
|
635
|
+
animated: true
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|