@teachinglab/omd 0.1.0
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 +138 -0
- package/canvas/core/canvasConfig.js +203 -0
- package/canvas/core/omdCanvas.js +475 -0
- package/canvas/drawing/segment.js +168 -0
- package/canvas/drawing/stroke.js +386 -0
- package/canvas/events/eventManager.js +435 -0
- package/canvas/events/pointerEventHandler.js +263 -0
- package/canvas/features/focusFrameManager.js +287 -0
- package/canvas/index.js +49 -0
- package/canvas/tools/eraserTool.js +322 -0
- package/canvas/tools/pencilTool.js +319 -0
- package/canvas/tools/selectTool.js +457 -0
- package/canvas/tools/tool.js +223 -0
- package/canvas/tools/toolManager.js +394 -0
- package/canvas/ui/cursor.js +438 -0
- package/canvas/ui/toolbar.js +304 -0
- package/canvas/utils/boundingBox.js +378 -0
- package/canvas/utils/mathUtils.js +259 -0
- package/docs/api/configuration-options.md +104 -0
- package/docs/api/eventManager.md +68 -0
- package/docs/api/focusFrameManager.md +150 -0
- package/docs/api/index.md +91 -0
- package/docs/api/main.md +58 -0
- package/docs/api/omdBinaryExpressionNode.md +227 -0
- package/docs/api/omdCanvas.md +142 -0
- package/docs/api/omdConfigManager.md +192 -0
- package/docs/api/omdConstantNode.md +117 -0
- package/docs/api/omdDisplay.md +121 -0
- package/docs/api/omdEquationNode.md +161 -0
- package/docs/api/omdEquationSequenceNode.md +301 -0
- package/docs/api/omdEquationStack.md +139 -0
- package/docs/api/omdFunctionNode.md +141 -0
- package/docs/api/omdGroupNode.md +182 -0
- package/docs/api/omdHelpers.md +96 -0
- package/docs/api/omdLeafNode.md +163 -0
- package/docs/api/omdNode.md +101 -0
- package/docs/api/omdOperationDisplayNode.md +139 -0
- package/docs/api/omdOperatorNode.md +127 -0
- package/docs/api/omdParenthesisNode.md +122 -0
- package/docs/api/omdPopup.md +117 -0
- package/docs/api/omdPowerNode.md +127 -0
- package/docs/api/omdRationalNode.md +128 -0
- package/docs/api/omdSequenceNode.md +128 -0
- package/docs/api/omdSimplification.md +110 -0
- package/docs/api/omdSqrtNode.md +79 -0
- package/docs/api/omdStepVisualizer.md +115 -0
- package/docs/api/omdStepVisualizerHighlighting.md +61 -0
- package/docs/api/omdStepVisualizerInteractiveSteps.md +129 -0
- package/docs/api/omdStepVisualizerLayout.md +60 -0
- package/docs/api/omdStepVisualizerNodeUtils.md +140 -0
- package/docs/api/omdStepVisualizerTextBoxes.md +68 -0
- package/docs/api/omdToolbar.md +102 -0
- package/docs/api/omdTranscriptionService.md +76 -0
- package/docs/api/omdTreeDiff.md +134 -0
- package/docs/api/omdUnaryExpressionNode.md +174 -0
- package/docs/api/omdUtilities.md +70 -0
- package/docs/api/omdVariableNode.md +148 -0
- package/docs/api/selectTool.md +74 -0
- package/docs/api/simplificationEngine.md +98 -0
- package/docs/api/simplificationRules.md +77 -0
- package/docs/api/simplificationUtils.md +64 -0
- package/docs/api/transcribe.md +43 -0
- package/docs/api-reference.md +85 -0
- package/docs/index.html +454 -0
- package/docs/user-guide.md +9 -0
- package/index.js +67 -0
- package/omd/config/omdConfigManager.js +267 -0
- package/omd/core/index.js +150 -0
- package/omd/core/omdEquationStack.js +347 -0
- package/omd/core/omdUtilities.js +115 -0
- package/omd/display/omdDisplay.js +443 -0
- package/omd/display/omdToolbar.js +502 -0
- package/omd/nodes/omdBinaryExpressionNode.js +460 -0
- package/omd/nodes/omdConstantNode.js +142 -0
- package/omd/nodes/omdEquationNode.js +1223 -0
- package/omd/nodes/omdEquationSequenceNode.js +1273 -0
- package/omd/nodes/omdFunctionNode.js +352 -0
- package/omd/nodes/omdGroupNode.js +68 -0
- package/omd/nodes/omdLeafNode.js +77 -0
- package/omd/nodes/omdNode.js +557 -0
- package/omd/nodes/omdOperationDisplayNode.js +322 -0
- package/omd/nodes/omdOperatorNode.js +109 -0
- package/omd/nodes/omdParenthesisNode.js +293 -0
- package/omd/nodes/omdPowerNode.js +236 -0
- package/omd/nodes/omdRationalNode.js +295 -0
- package/omd/nodes/omdSqrtNode.js +308 -0
- package/omd/nodes/omdUnaryExpressionNode.js +178 -0
- package/omd/nodes/omdVariableNode.js +123 -0
- package/omd/simplification/omdSimplification.js +171 -0
- package/omd/simplification/omdSimplificationEngine.js +886 -0
- package/omd/simplification/package.json +6 -0
- package/omd/simplification/rules/binaryRules.js +1037 -0
- package/omd/simplification/rules/functionRules.js +111 -0
- package/omd/simplification/rules/index.js +48 -0
- package/omd/simplification/rules/parenthesisRules.js +19 -0
- package/omd/simplification/rules/powerRules.js +143 -0
- package/omd/simplification/rules/rationalRules.js +475 -0
- package/omd/simplification/rules/sqrtRules.js +48 -0
- package/omd/simplification/rules/unaryRules.js +37 -0
- package/omd/simplification/simplificationRules.js +32 -0
- package/omd/simplification/simplificationUtils.js +1056 -0
- package/omd/step-visualizer/omdStepVisualizer.js +597 -0
- package/omd/step-visualizer/omdStepVisualizerHighlighting.js +206 -0
- package/omd/step-visualizer/omdStepVisualizerLayout.js +245 -0
- package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +163 -0
- package/omd/utils/omdNodeOverlay.js +638 -0
- package/omd/utils/omdPopup.js +1084 -0
- package/omd/utils/omdStepVisualizerInteractiveSteps.js +491 -0
- package/omd/utils/omdStepVisualizerNodeUtils.js +268 -0
- package/omd/utils/omdTranscriptionService.js +125 -0
- package/omd/utils/omdTreeDiff.js +734 -0
- package/package.json +46 -0
- package/src/index.js +62 -0
- package/src/json-schemas.md +109 -0
- package/src/omd-json-samples.js +115 -0
- package/src/omd.js +109 -0
- package/src/omdApp.js +391 -0
- package/src/omdAppCanvas.js +336 -0
- package/src/omdBalanceHanger.js +172 -0
- package/src/omdColor.js +13 -0
- package/src/omdCoordinatePlane.js +467 -0
- package/src/omdEquation.js +125 -0
- package/src/omdExpression.js +104 -0
- package/src/omdFunction.js +113 -0
- package/src/omdMetaExpression.js +287 -0
- package/src/omdNaturalExpression.js +564 -0
- package/src/omdNode.js +384 -0
- package/src/omdNumber.js +53 -0
- package/src/omdNumberLine.js +107 -0
- package/src/omdNumberTile.js +119 -0
- package/src/omdOperator.js +73 -0
- package/src/omdPowerExpression.js +92 -0
- package/src/omdProblem.js +55 -0
- package/src/omdRatioChart.js +232 -0
- package/src/omdRationalExpression.js +115 -0
- package/src/omdSampleData.js +215 -0
- package/src/omdShapes.js +476 -0
- package/src/omdSpinner.js +148 -0
- package/src/omdString.js +39 -0
- package/src/omdTable.js +369 -0
- package/src/omdTapeDiagram.js +245 -0
- package/src/omdTerm.js +92 -0
- package/src/omdTileEquation.js +349 -0
- package/src/omdVariable.js +51 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import {omdColor} from '../../src/omdColor.js';
|
|
2
|
+
|
|
3
|
+
export class Toolbar {
|
|
4
|
+
/**
|
|
5
|
+
* @param {OMDCanvas} canvas - Canvas instance
|
|
6
|
+
*/
|
|
7
|
+
constructor(canvas) {
|
|
8
|
+
this.canvas = canvas;
|
|
9
|
+
this.buttons = new Map();
|
|
10
|
+
this.activeButton = null;
|
|
11
|
+
this.omdColor = omdColor; // Use omdColor for consistent styling
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
// Create toolbar element
|
|
16
|
+
this._createElement();
|
|
17
|
+
|
|
18
|
+
// Create tool buttons
|
|
19
|
+
this._createToolButtons();
|
|
20
|
+
|
|
21
|
+
// Listen for tool changes
|
|
22
|
+
this.canvas.on('toolChanged', (event) => {
|
|
23
|
+
this._updateActiveButton(event.detail.name);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create the toolbar SVG element
|
|
31
|
+
* @private
|
|
32
|
+
*/
|
|
33
|
+
_createElement() {
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
// Create a jsvgGroup for the toolbar
|
|
37
|
+
this.toolbarGroup = new jsvgGroup();
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
// Create background rectangle
|
|
41
|
+
this.background = new jsvgRect();
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
// Initial size, will be updated after buttons are created
|
|
45
|
+
this.background.setWidthAndHeight(100, 54);
|
|
46
|
+
this.background.setCornerRadius(27); // Pill shape
|
|
47
|
+
this.background.setFillColor(this.omdColor.mediumGray); // Modern dark, semi-transparent
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
// Debug the background SVG object
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
this.toolbarGroup.addChild(this.background);
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
// Position the toolbar at bottom center
|
|
59
|
+
this._updatePosition();``
|
|
60
|
+
|
|
61
|
+
// Add to main SVG so it is rendered
|
|
62
|
+
this.canvas.svg.appendChild(this.toolbarGroup.svgObject);
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
// Check if the SVG is actually in the DOM
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
// Check the actual SVG content
|
|
70
|
+
console.log('Toolbar SVG innerHTML:', this.toolbarGroup.svgObject.innerHTML);
|
|
71
|
+
console.log('Background SVG object:', this.background.svgObject);
|
|
72
|
+
console.log('Background SVG innerHTML:', this.background.svgObject.outerHTML);
|
|
73
|
+
|
|
74
|
+
// Check if the background is visible
|
|
75
|
+
console.log('Background fill color:', this.background.svgObject.getAttribute('fill'));
|
|
76
|
+
console.log('Background width/height:', this.background.svgObject.getAttribute('width'), this.background.svgObject.getAttribute('height'));
|
|
77
|
+
}, 100);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Update toolbar position to bottom center
|
|
82
|
+
* @private
|
|
83
|
+
*/
|
|
84
|
+
_updatePosition() {
|
|
85
|
+
const canvasRect = this.canvas.container.getBoundingClientRect();
|
|
86
|
+
const toolbarWidth = this.background.width;
|
|
87
|
+
const toolbarHeight = this.background.height;
|
|
88
|
+
// Bottom center, 24px from bottom
|
|
89
|
+
const x = (canvasRect.width - toolbarWidth) / 2;
|
|
90
|
+
const y = canvasRect.height - toolbarHeight - 24;
|
|
91
|
+
|
|
92
|
+
// Ensure toolbar stays within canvas bounds
|
|
93
|
+
const clampedX = Math.max(0, Math.min(x, canvasRect.width - toolbarWidth));
|
|
94
|
+
const clampedY = Math.max(0, Math.min(y, canvasRect.height - toolbarHeight));
|
|
95
|
+
this.toolbarGroup.setPosition(x, y);
|
|
96
|
+
|
|
97
|
+
// Debug the SVG object
|
|
98
|
+
console.log('Toolbar group SVG object:', this.toolbarGroup.svgObject);
|
|
99
|
+
console.log('Toolbar group SVG object style:', this.toolbarGroup.svgObject.style);
|
|
100
|
+
console.log('Toolbar group SVG object parent:', this.toolbarGroup.svgObject.parentNode);
|
|
101
|
+
|
|
102
|
+
// Set z-index to ensure it's on top
|
|
103
|
+
this.toolbarGroup.svgObject.style.zIndex = '1000';
|
|
104
|
+
this.toolbarGroup.svgObject.style.pointerEvents = 'auto';
|
|
105
|
+
|
|
106
|
+
// Fix the viewBox to include the toolbar position
|
|
107
|
+
const bgWidth = this.background.width;
|
|
108
|
+
const bgHeight = this.background.height;
|
|
109
|
+
const viewBoxX = x;
|
|
110
|
+
const viewBoxY = y;
|
|
111
|
+
const viewBoxWidth = Math.max(500, x + bgWidth);
|
|
112
|
+
const viewBoxHeight = Math.max(500, y + bgHeight);
|
|
113
|
+
|
|
114
|
+
this.toolbarGroup.svgObject.setAttribute('viewBox', `${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}`);
|
|
115
|
+
console.log('Updated viewBox to:', `${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}`);
|
|
116
|
+
|
|
117
|
+
// Don't set x/y attributes - let setPosition handle it via transform
|
|
118
|
+
// this.toolbarGroup.svgObject.setAttribute('x', x);
|
|
119
|
+
// this.toolbarGroup.svgObject.setAttribute('y', y);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Create tool buttons
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
_createToolButtons() {
|
|
127
|
+
const tools = this.canvas.toolManager.getAllToolMetadata();
|
|
128
|
+
console.log('Creating tool buttons for tools:', tools);
|
|
129
|
+
|
|
130
|
+
const buttonSize = 48;
|
|
131
|
+
const spacing = 8;
|
|
132
|
+
const padding = 6;
|
|
133
|
+
let xPos = padding;
|
|
134
|
+
const yPos = padding;
|
|
135
|
+
tools.forEach(toolMeta => {
|
|
136
|
+
const button = this._createJsvgButton(toolMeta, buttonSize);
|
|
137
|
+
button.setPosition(xPos, yPos);
|
|
138
|
+
this.toolbarGroup.addChild(button);
|
|
139
|
+
this.buttons.set(toolMeta.name, button);
|
|
140
|
+
xPos += buttonSize + spacing;
|
|
141
|
+
});
|
|
142
|
+
// Remove last spacing
|
|
143
|
+
const totalWidth = xPos - spacing + padding;
|
|
144
|
+
const totalHeight = buttonSize + 2 * padding;
|
|
145
|
+
this.background.setWidthAndHeight(totalWidth, totalHeight);
|
|
146
|
+
this.background.setCornerRadius(totalHeight / 2);
|
|
147
|
+
// Reposition after sizing
|
|
148
|
+
this._updatePosition();
|
|
149
|
+
console.log('Toolbar background width set to:', totalWidth);
|
|
150
|
+
|
|
151
|
+
// Set initial active button
|
|
152
|
+
const activeTool = this.canvas.toolManager.getActiveTool();
|
|
153
|
+
if (activeTool) {
|
|
154
|
+
this._updateActiveButton(activeTool.name);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Create individual tool button using jsvgButton
|
|
160
|
+
* @private
|
|
161
|
+
*/
|
|
162
|
+
_createJsvgButton(toolMeta, size = 48) {
|
|
163
|
+
console.log('Creating jsvgButton for tool:', toolMeta.name);
|
|
164
|
+
|
|
165
|
+
const button = new jsvgButton();
|
|
166
|
+
button.setWidthAndHeight(size, size);
|
|
167
|
+
button.setCornerRadius(size / 2); // Make it circular
|
|
168
|
+
button.setFillColor('white');
|
|
169
|
+
|
|
170
|
+
// Remove any default text from the button group (if present)
|
|
171
|
+
// jsvgButton may add a <text> element by default; remove it
|
|
172
|
+
button.setText(''); // Clear any default text
|
|
173
|
+
|
|
174
|
+
// Set the icon SVG
|
|
175
|
+
const iconSvg = this._getToolIconSvg(toolMeta.name);
|
|
176
|
+
if (iconSvg) {
|
|
177
|
+
const dataURI = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(iconSvg);
|
|
178
|
+
button.addImage(dataURI, size * 0.5, size * 0.5); // Icon at 50% of button size
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Set click callback
|
|
182
|
+
button.setClickCallback(() => {
|
|
183
|
+
this.canvas.toolManager.setActiveTool(toolMeta.name);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Store tool metadata
|
|
187
|
+
button.toolMeta = toolMeta;
|
|
188
|
+
return button;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get SVG icon for tool
|
|
193
|
+
* @param {string} toolName - Tool name
|
|
194
|
+
* @returns {string} SVG string
|
|
195
|
+
* @private
|
|
196
|
+
*/
|
|
197
|
+
_getToolIconSvg(toolName) {
|
|
198
|
+
const icons = {
|
|
199
|
+
'pencil': `<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
200
|
+
<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="black" stroke-width="1.28" stroke-linecap="round" stroke-linejoin="round"/>
|
|
201
|
+
<path d="M9.41443 3.52039L11.9744 6.08039" stroke="black" stroke-width="1.28" stroke-linecap="round" stroke-linejoin="round"/>
|
|
202
|
+
</svg>`,
|
|
203
|
+
'eraser': `<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
204
|
+
<path d="M13.2591 12.76H4.93909C4.77032 12.7604 4.60314 12.7274 4.44717 12.663C4.29121 12.5985 4.14953 12.5038 4.03029 12.3844L1.47413 9.825C1.23417 9.58496 1.09937 9.25945 1.09937 8.92004C1.09937 8.58063 1.23417 8.25511 1.47413 8.01508L7.87413 1.61508C7.993 1.49616 8.13413 1.40183 8.28946 1.33747C8.44479 1.27312 8.61128 1.23999 8.77941 1.23999C8.94755 1.23999 9.11404 1.27312 9.26937 1.33747C9.4247 1.40183 9.56583 1.49616 9.68469 1.61508L13.5241 5.45508C13.764 5.69511 13.8988 6.02063 13.8988 6.36004C13.8988 6.69945 13.764 7.02496 13.5241 7.265L8.03285 12.76" stroke="black" stroke-width="1.28" stroke-linecap="round" stroke-linejoin="round"/>
|
|
205
|
+
<path d="M3.07159 6.41772L8.72151 12.0676" stroke="black" stroke-width="1.28" stroke-linecap="round" stroke-linejoin="round"/>
|
|
206
|
+
</svg>`,
|
|
207
|
+
'select': `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
208
|
+
<path d="M1.63448 2.04462C1.60922 1.98633 1.60208 1.92179 1.61397 1.85938C1.62585 1.79697 1.65623 1.73958 1.70116 1.69466C1.74608 1.64973 1.80347 1.61935 1.86588 1.60747C1.92829 1.59558 1.99283 1.60272 2.05112 1.62798L12.2911 5.78798C12.3534 5.81335 12.4061 5.85768 12.4417 5.91469C12.4774 5.9717 12.4941 6.03849 12.4897 6.10557C12.4852 6.17266 12.4597 6.23663 12.4169 6.28842C12.374 6.3402 12.3159 6.37717 12.2508 6.39406L8.33144 7.40526C8.11 7.46219 7.90784 7.5774 7.74599 7.73891C7.58415 7.90042 7.46852 8.10234 7.41112 8.32366L6.40056 12.2443C6.38367 12.3094 6.3467 12.3675 6.29492 12.4104C6.24313 12.4532 6.17916 12.4787 6.11207 12.4832C6.04499 12.4876 5.9782 12.4709 5.92119 12.4352C5.86419 12.3996 5.81985 12.3469 5.79448 12.2846L1.63448 2.04462Z" stroke="black" stroke-width="1.28" stroke-linecap="round" stroke-linejoin="round"/>
|
|
209
|
+
</svg>`
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return icons[toolName] || icons['pencil'];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Update active button styling
|
|
217
|
+
* @param {string} toolName - Active tool name
|
|
218
|
+
* @private
|
|
219
|
+
*/
|
|
220
|
+
_updateActiveButton(toolName) {
|
|
221
|
+
// Reset all buttons
|
|
222
|
+
this.buttons.forEach(button => {
|
|
223
|
+
button.setFillColor('white');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Highlight active button
|
|
227
|
+
const activeButton = this.buttons.get(toolName);
|
|
228
|
+
if (activeButton) {
|
|
229
|
+
activeButton.setFillColor(this.omdColor.lightGray);
|
|
230
|
+
this.activeButton = activeButton;
|
|
231
|
+
} else {
|
|
232
|
+
this.activeButton = null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Add custom button to toolbar
|
|
238
|
+
* @param {Object} config - Button configuration
|
|
239
|
+
* @param {string} config.id - Button ID
|
|
240
|
+
* @param {string} config.icon - SVG icon string
|
|
241
|
+
* @param {Function} config.callback - Click callback
|
|
242
|
+
* @param {string} [config.tooltip] - Tooltip text
|
|
243
|
+
*/
|
|
244
|
+
addButton(config) {
|
|
245
|
+
const button = this._createJsvgButton({ name: config.id }, 48);
|
|
246
|
+
|
|
247
|
+
// Set icon
|
|
248
|
+
if (config.icon) {
|
|
249
|
+
const dataURI = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(config.icon);
|
|
250
|
+
button.addImage(dataURI, 24, 24);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Set click callback
|
|
254
|
+
if (config.callback) {
|
|
255
|
+
button.setClickCallback(config.callback);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Add to toolbar group
|
|
259
|
+
this.toolbarGroup.addChild(button);
|
|
260
|
+
|
|
261
|
+
// Store button
|
|
262
|
+
this.buttons.set(config.id, button);
|
|
263
|
+
|
|
264
|
+
return button;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Remove button from toolbar
|
|
269
|
+
* @param {string} buttonId - Button ID to remove
|
|
270
|
+
*/
|
|
271
|
+
removeButton(buttonId) {
|
|
272
|
+
const button = this.buttons.get(buttonId);
|
|
273
|
+
if (button) {
|
|
274
|
+
this.toolbarGroup.removeChild(button);
|
|
275
|
+
this.buttons.delete(buttonId);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Show toolbar
|
|
281
|
+
*/
|
|
282
|
+
show() {
|
|
283
|
+
this.toolbarGroup.svgObject.style.display = 'block';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Hide toolbar
|
|
288
|
+
*/
|
|
289
|
+
hide() {
|
|
290
|
+
this.toolbarGroup.svgObject.style.display = 'none';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Destroy toolbar
|
|
297
|
+
*/
|
|
298
|
+
destroy() {
|
|
299
|
+
if (this.toolbarGroup.svgObject.parentNode) {
|
|
300
|
+
this.toolbarGroup.svgObject.parentNode.removeChild(this.toolbarGroup.svgObject);
|
|
301
|
+
}
|
|
302
|
+
this.buttons.clear();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
export class BoundingBox {
|
|
2
|
+
/**
|
|
3
|
+
* @param {number} [x=0] - X coordinate
|
|
4
|
+
* @param {number} [y=0] - Y coordinate
|
|
5
|
+
* @param {number} [width=0] - Width
|
|
6
|
+
* @param {number} [height=0] - Height
|
|
7
|
+
*/
|
|
8
|
+
constructor(x = 0, y = 0, width = 0, height = 0) {
|
|
9
|
+
this.x = x;
|
|
10
|
+
this.y = y;
|
|
11
|
+
this.width = width;
|
|
12
|
+
this.height = height;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Set bounding box values
|
|
17
|
+
* @param {number} x - X coordinate
|
|
18
|
+
* @param {number} y - Y coordinate
|
|
19
|
+
* @param {number} width - Width
|
|
20
|
+
* @param {number} height - Height
|
|
21
|
+
*/
|
|
22
|
+
set(x, y, width, height) {
|
|
23
|
+
this.x = x;
|
|
24
|
+
this.y = y;
|
|
25
|
+
this.width = width;
|
|
26
|
+
this.height = height;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get left edge
|
|
31
|
+
* @returns {number} Left X coordinate
|
|
32
|
+
*/
|
|
33
|
+
get left() {
|
|
34
|
+
return this.x;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get right edge
|
|
39
|
+
* @returns {number} Right X coordinate
|
|
40
|
+
*/
|
|
41
|
+
get right() {
|
|
42
|
+
return this.x + this.width;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get top edge
|
|
47
|
+
* @returns {number} Top Y coordinate
|
|
48
|
+
*/
|
|
49
|
+
get top() {
|
|
50
|
+
return this.y;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get bottom edge
|
|
55
|
+
* @returns {number} Bottom Y coordinate
|
|
56
|
+
*/
|
|
57
|
+
get bottom() {
|
|
58
|
+
return this.y + this.height;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get center X coordinate
|
|
63
|
+
* @returns {number} Center X
|
|
64
|
+
*/
|
|
65
|
+
get centerX() {
|
|
66
|
+
return this.x + this.width / 2;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get center Y coordinate
|
|
71
|
+
* @returns {number} Center Y
|
|
72
|
+
*/
|
|
73
|
+
get centerY() {
|
|
74
|
+
return this.y + this.height / 2;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get center point
|
|
79
|
+
* @returns {Object} {x, y} center coordinates
|
|
80
|
+
*/
|
|
81
|
+
get center() {
|
|
82
|
+
return {
|
|
83
|
+
x: this.centerX,
|
|
84
|
+
y: this.centerY
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if point is inside bounding box
|
|
90
|
+
* @param {number} x - X coordinate
|
|
91
|
+
* @param {number} y - Y coordinate
|
|
92
|
+
* @param {number} [tolerance=0] - Tolerance for edge cases
|
|
93
|
+
* @returns {boolean} True if point is inside
|
|
94
|
+
*/
|
|
95
|
+
containsPoint(x, y, tolerance = 0) {
|
|
96
|
+
return x >= this.left - tolerance &&
|
|
97
|
+
x <= this.right + tolerance &&
|
|
98
|
+
y >= this.top - tolerance &&
|
|
99
|
+
y <= this.bottom + tolerance;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if this bounding box intersects with another
|
|
104
|
+
* @param {BoundingBox} other - Other bounding box
|
|
105
|
+
* @returns {boolean} True if boxes intersect
|
|
106
|
+
*/
|
|
107
|
+
intersects(other) {
|
|
108
|
+
return !(this.right < other.left ||
|
|
109
|
+
other.right < this.left ||
|
|
110
|
+
this.bottom < other.top ||
|
|
111
|
+
other.bottom < this.top);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if this bounding box completely contains another
|
|
116
|
+
* @param {BoundingBox} other - Other bounding box
|
|
117
|
+
* @returns {boolean} True if this box contains the other
|
|
118
|
+
*/
|
|
119
|
+
contains(other) {
|
|
120
|
+
return this.left <= other.left &&
|
|
121
|
+
this.right >= other.right &&
|
|
122
|
+
this.top <= other.top &&
|
|
123
|
+
this.bottom >= other.bottom;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Expand bounding box to include a point
|
|
128
|
+
* @param {number} x - X coordinate
|
|
129
|
+
* @param {number} y - Y coordinate
|
|
130
|
+
*/
|
|
131
|
+
expandToIncludePoint(x, y) {
|
|
132
|
+
if (this.width === 0 && this.height === 0) {
|
|
133
|
+
// First point
|
|
134
|
+
this.x = x;
|
|
135
|
+
this.y = y;
|
|
136
|
+
this.width = 0;
|
|
137
|
+
this.height = 0;
|
|
138
|
+
} else {
|
|
139
|
+
const newLeft = Math.min(this.left, x);
|
|
140
|
+
const newTop = Math.min(this.top, y);
|
|
141
|
+
const newRight = Math.max(this.right, x);
|
|
142
|
+
const newBottom = Math.max(this.bottom, y);
|
|
143
|
+
|
|
144
|
+
this.x = newLeft;
|
|
145
|
+
this.y = newTop;
|
|
146
|
+
this.width = newRight - newLeft;
|
|
147
|
+
this.height = newBottom - newTop;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Expand bounding box to include another bounding box
|
|
153
|
+
* @param {BoundingBox} other - Other bounding box
|
|
154
|
+
*/
|
|
155
|
+
expandToIncludeBox(other) {
|
|
156
|
+
if (other.width === 0 && other.height === 0) return;
|
|
157
|
+
|
|
158
|
+
if (this.width === 0 && this.height === 0) {
|
|
159
|
+
this.set(other.x, other.y, other.width, other.height);
|
|
160
|
+
} else {
|
|
161
|
+
const newLeft = Math.min(this.left, other.left);
|
|
162
|
+
const newTop = Math.min(this.top, other.top);
|
|
163
|
+
const newRight = Math.max(this.right, other.right);
|
|
164
|
+
const newBottom = Math.max(this.bottom, other.bottom);
|
|
165
|
+
|
|
166
|
+
this.x = newLeft;
|
|
167
|
+
this.y = newTop;
|
|
168
|
+
this.width = newRight - newLeft;
|
|
169
|
+
this.height = newBottom - newTop;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get intersection with another bounding box
|
|
175
|
+
* @param {BoundingBox} other - Other bounding box
|
|
176
|
+
* @returns {BoundingBox|null} Intersection box or null if no intersection
|
|
177
|
+
*/
|
|
178
|
+
getIntersection(other) {
|
|
179
|
+
if (!this.intersects(other)) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const left = Math.max(this.left, other.left);
|
|
184
|
+
const top = Math.max(this.top, other.top);
|
|
185
|
+
const right = Math.min(this.right, other.right);
|
|
186
|
+
const bottom = Math.min(this.bottom, other.bottom);
|
|
187
|
+
|
|
188
|
+
return new BoundingBox(left, top, right - left, bottom - top);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get union with another bounding box
|
|
193
|
+
* @param {BoundingBox} other - Other bounding box
|
|
194
|
+
* @returns {BoundingBox} Union bounding box
|
|
195
|
+
*/
|
|
196
|
+
getUnion(other) {
|
|
197
|
+
if (this.width === 0 && this.height === 0) {
|
|
198
|
+
return other.clone();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (other.width === 0 && other.height === 0) {
|
|
202
|
+
return this.clone();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const left = Math.min(this.left, other.left);
|
|
206
|
+
const top = Math.min(this.top, other.top);
|
|
207
|
+
const right = Math.max(this.right, other.right);
|
|
208
|
+
const bottom = Math.max(this.bottom, other.bottom);
|
|
209
|
+
|
|
210
|
+
return new BoundingBox(left, top, right - left, bottom - top);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Inflate (expand) bounding box by amount
|
|
215
|
+
* @param {number} amount - Amount to inflate (positive to expand, negative to shrink)
|
|
216
|
+
*/
|
|
217
|
+
inflate(amount) {
|
|
218
|
+
this.x -= amount;
|
|
219
|
+
this.y -= amount;
|
|
220
|
+
this.width += amount * 2;
|
|
221
|
+
this.height += amount * 2;
|
|
222
|
+
|
|
223
|
+
// Ensure width/height don't go negative
|
|
224
|
+
this.width = Math.max(0, this.width);
|
|
225
|
+
this.height = Math.max(0, this.height);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Move bounding box by offset
|
|
230
|
+
* @param {number} dx - X offset
|
|
231
|
+
* @param {number} dy - Y offset
|
|
232
|
+
*/
|
|
233
|
+
move(dx, dy) {
|
|
234
|
+
this.x += dx;
|
|
235
|
+
this.y += dy;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Scale bounding box by factor
|
|
240
|
+
* @param {number} scale - Scale factor
|
|
241
|
+
* @param {number} [originX] - Scale origin X (defaults to center)
|
|
242
|
+
* @param {number} [originY] - Scale origin Y (defaults to center)
|
|
243
|
+
*/
|
|
244
|
+
scale(scale, originX = this.centerX, originY = this.centerY) {
|
|
245
|
+
const newWidth = this.width * scale;
|
|
246
|
+
const newHeight = this.height * scale;
|
|
247
|
+
|
|
248
|
+
this.x = originX - (originX - this.x) * scale;
|
|
249
|
+
this.y = originY - (originY - this.y) * scale;
|
|
250
|
+
this.width = newWidth;
|
|
251
|
+
this.height = newHeight;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get area of bounding box
|
|
256
|
+
* @returns {number} Area
|
|
257
|
+
*/
|
|
258
|
+
getArea() {
|
|
259
|
+
return this.width * this.height;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Get perimeter of bounding box
|
|
264
|
+
* @returns {number} Perimeter
|
|
265
|
+
*/
|
|
266
|
+
getPerimeter() {
|
|
267
|
+
return 2 * (this.width + this.height);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Check if bounding box is empty (zero area)
|
|
272
|
+
* @returns {boolean} True if empty
|
|
273
|
+
*/
|
|
274
|
+
isEmpty() {
|
|
275
|
+
return this.width <= 0 || this.height <= 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Check if bounding box is valid
|
|
280
|
+
* @returns {boolean} True if valid
|
|
281
|
+
*/
|
|
282
|
+
isValid() {
|
|
283
|
+
return !isNaN(this.x) && !isNaN(this.y) &&
|
|
284
|
+
!isNaN(this.width) && !isNaN(this.height) &&
|
|
285
|
+
this.width >= 0 && this.height >= 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Calculate distance from point to bounding box
|
|
290
|
+
* @param {number} x - X coordinate
|
|
291
|
+
* @param {number} y - Y coordinate
|
|
292
|
+
* @returns {number} Distance (0 if point is inside)
|
|
293
|
+
*/
|
|
294
|
+
distanceToPoint(x, y) {
|
|
295
|
+
if (this.containsPoint(x, y)) {
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const dx = Math.max(0, Math.max(this.left - x, x - this.right));
|
|
300
|
+
const dy = Math.max(0, Math.max(this.top - y, y - this.bottom));
|
|
301
|
+
|
|
302
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get corners of bounding box
|
|
307
|
+
* @returns {Array<Object>} Array of {x, y} corner points
|
|
308
|
+
*/
|
|
309
|
+
getCorners() {
|
|
310
|
+
return [
|
|
311
|
+
{ x: this.left, y: this.top }, // Top-left
|
|
312
|
+
{ x: this.right, y: this.top }, // Top-right
|
|
313
|
+
{ x: this.right, y: this.bottom }, // Bottom-right
|
|
314
|
+
{ x: this.left, y: this.bottom } // Bottom-left
|
|
315
|
+
];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Create a copy of this bounding box
|
|
320
|
+
* @returns {BoundingBox} New bounding box instance
|
|
321
|
+
*/
|
|
322
|
+
clone() {
|
|
323
|
+
return new BoundingBox(this.x, this.y, this.width, this.height);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get string representation
|
|
328
|
+
* @returns {string} String representation
|
|
329
|
+
*/
|
|
330
|
+
toString() {
|
|
331
|
+
return `BoundingBox(${this.x}, ${this.y}, ${this.width}, ${this.height})`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Convert to JSON object
|
|
336
|
+
* @returns {Object} JSON representation
|
|
337
|
+
*/
|
|
338
|
+
toJSON() {
|
|
339
|
+
return {
|
|
340
|
+
x: this.x,
|
|
341
|
+
y: this.y,
|
|
342
|
+
width: this.width,
|
|
343
|
+
height: this.height
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Create bounding box from JSON object
|
|
349
|
+
* @param {Object} json - JSON representation
|
|
350
|
+
* @returns {BoundingBox} New bounding box instance
|
|
351
|
+
*/
|
|
352
|
+
static fromJSON(json) {
|
|
353
|
+
return new BoundingBox(json.x, json.y, json.width, json.height);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Create bounding box from array of points
|
|
358
|
+
* @param {Array<Object>} points - Array of {x, y} points
|
|
359
|
+
* @returns {BoundingBox} Bounding box containing all points
|
|
360
|
+
*/
|
|
361
|
+
static fromPoints(points) {
|
|
362
|
+
if (points.length === 0) {
|
|
363
|
+
return new BoundingBox();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let minX = Infinity, minY = Infinity;
|
|
367
|
+
let maxX = -Infinity, maxY = -Infinity;
|
|
368
|
+
|
|
369
|
+
points.forEach(point => {
|
|
370
|
+
minX = Math.min(minX, point.x);
|
|
371
|
+
minY = Math.min(minY, point.y);
|
|
372
|
+
maxX = Math.max(maxX, point.x);
|
|
373
|
+
maxY = Math.max(maxY, point.y);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
return new BoundingBox(minX, minY, maxX - minX, maxY - minY);
|
|
377
|
+
}
|
|
378
|
+
}
|