@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.
Files changed (144) hide show
  1. package/README.md +138 -0
  2. package/canvas/core/canvasConfig.js +203 -0
  3. package/canvas/core/omdCanvas.js +475 -0
  4. package/canvas/drawing/segment.js +168 -0
  5. package/canvas/drawing/stroke.js +386 -0
  6. package/canvas/events/eventManager.js +435 -0
  7. package/canvas/events/pointerEventHandler.js +263 -0
  8. package/canvas/features/focusFrameManager.js +287 -0
  9. package/canvas/index.js +49 -0
  10. package/canvas/tools/eraserTool.js +322 -0
  11. package/canvas/tools/pencilTool.js +319 -0
  12. package/canvas/tools/selectTool.js +457 -0
  13. package/canvas/tools/tool.js +223 -0
  14. package/canvas/tools/toolManager.js +394 -0
  15. package/canvas/ui/cursor.js +438 -0
  16. package/canvas/ui/toolbar.js +304 -0
  17. package/canvas/utils/boundingBox.js +378 -0
  18. package/canvas/utils/mathUtils.js +259 -0
  19. package/docs/api/configuration-options.md +104 -0
  20. package/docs/api/eventManager.md +68 -0
  21. package/docs/api/focusFrameManager.md +150 -0
  22. package/docs/api/index.md +91 -0
  23. package/docs/api/main.md +58 -0
  24. package/docs/api/omdBinaryExpressionNode.md +227 -0
  25. package/docs/api/omdCanvas.md +142 -0
  26. package/docs/api/omdConfigManager.md +192 -0
  27. package/docs/api/omdConstantNode.md +117 -0
  28. package/docs/api/omdDisplay.md +121 -0
  29. package/docs/api/omdEquationNode.md +161 -0
  30. package/docs/api/omdEquationSequenceNode.md +301 -0
  31. package/docs/api/omdEquationStack.md +139 -0
  32. package/docs/api/omdFunctionNode.md +141 -0
  33. package/docs/api/omdGroupNode.md +182 -0
  34. package/docs/api/omdHelpers.md +96 -0
  35. package/docs/api/omdLeafNode.md +163 -0
  36. package/docs/api/omdNode.md +101 -0
  37. package/docs/api/omdOperationDisplayNode.md +139 -0
  38. package/docs/api/omdOperatorNode.md +127 -0
  39. package/docs/api/omdParenthesisNode.md +122 -0
  40. package/docs/api/omdPopup.md +117 -0
  41. package/docs/api/omdPowerNode.md +127 -0
  42. package/docs/api/omdRationalNode.md +128 -0
  43. package/docs/api/omdSequenceNode.md +128 -0
  44. package/docs/api/omdSimplification.md +110 -0
  45. package/docs/api/omdSqrtNode.md +79 -0
  46. package/docs/api/omdStepVisualizer.md +115 -0
  47. package/docs/api/omdStepVisualizerHighlighting.md +61 -0
  48. package/docs/api/omdStepVisualizerInteractiveSteps.md +129 -0
  49. package/docs/api/omdStepVisualizerLayout.md +60 -0
  50. package/docs/api/omdStepVisualizerNodeUtils.md +140 -0
  51. package/docs/api/omdStepVisualizerTextBoxes.md +68 -0
  52. package/docs/api/omdToolbar.md +102 -0
  53. package/docs/api/omdTranscriptionService.md +76 -0
  54. package/docs/api/omdTreeDiff.md +134 -0
  55. package/docs/api/omdUnaryExpressionNode.md +174 -0
  56. package/docs/api/omdUtilities.md +70 -0
  57. package/docs/api/omdVariableNode.md +148 -0
  58. package/docs/api/selectTool.md +74 -0
  59. package/docs/api/simplificationEngine.md +98 -0
  60. package/docs/api/simplificationRules.md +77 -0
  61. package/docs/api/simplificationUtils.md +64 -0
  62. package/docs/api/transcribe.md +43 -0
  63. package/docs/api-reference.md +85 -0
  64. package/docs/index.html +454 -0
  65. package/docs/user-guide.md +9 -0
  66. package/index.js +67 -0
  67. package/omd/config/omdConfigManager.js +267 -0
  68. package/omd/core/index.js +150 -0
  69. package/omd/core/omdEquationStack.js +347 -0
  70. package/omd/core/omdUtilities.js +115 -0
  71. package/omd/display/omdDisplay.js +443 -0
  72. package/omd/display/omdToolbar.js +502 -0
  73. package/omd/nodes/omdBinaryExpressionNode.js +460 -0
  74. package/omd/nodes/omdConstantNode.js +142 -0
  75. package/omd/nodes/omdEquationNode.js +1223 -0
  76. package/omd/nodes/omdEquationSequenceNode.js +1273 -0
  77. package/omd/nodes/omdFunctionNode.js +352 -0
  78. package/omd/nodes/omdGroupNode.js +68 -0
  79. package/omd/nodes/omdLeafNode.js +77 -0
  80. package/omd/nodes/omdNode.js +557 -0
  81. package/omd/nodes/omdOperationDisplayNode.js +322 -0
  82. package/omd/nodes/omdOperatorNode.js +109 -0
  83. package/omd/nodes/omdParenthesisNode.js +293 -0
  84. package/omd/nodes/omdPowerNode.js +236 -0
  85. package/omd/nodes/omdRationalNode.js +295 -0
  86. package/omd/nodes/omdSqrtNode.js +308 -0
  87. package/omd/nodes/omdUnaryExpressionNode.js +178 -0
  88. package/omd/nodes/omdVariableNode.js +123 -0
  89. package/omd/simplification/omdSimplification.js +171 -0
  90. package/omd/simplification/omdSimplificationEngine.js +886 -0
  91. package/omd/simplification/package.json +6 -0
  92. package/omd/simplification/rules/binaryRules.js +1037 -0
  93. package/omd/simplification/rules/functionRules.js +111 -0
  94. package/omd/simplification/rules/index.js +48 -0
  95. package/omd/simplification/rules/parenthesisRules.js +19 -0
  96. package/omd/simplification/rules/powerRules.js +143 -0
  97. package/omd/simplification/rules/rationalRules.js +475 -0
  98. package/omd/simplification/rules/sqrtRules.js +48 -0
  99. package/omd/simplification/rules/unaryRules.js +37 -0
  100. package/omd/simplification/simplificationRules.js +32 -0
  101. package/omd/simplification/simplificationUtils.js +1056 -0
  102. package/omd/step-visualizer/omdStepVisualizer.js +597 -0
  103. package/omd/step-visualizer/omdStepVisualizerHighlighting.js +206 -0
  104. package/omd/step-visualizer/omdStepVisualizerLayout.js +245 -0
  105. package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +163 -0
  106. package/omd/utils/omdNodeOverlay.js +638 -0
  107. package/omd/utils/omdPopup.js +1084 -0
  108. package/omd/utils/omdStepVisualizerInteractiveSteps.js +491 -0
  109. package/omd/utils/omdStepVisualizerNodeUtils.js +268 -0
  110. package/omd/utils/omdTranscriptionService.js +125 -0
  111. package/omd/utils/omdTreeDiff.js +734 -0
  112. package/package.json +46 -0
  113. package/src/index.js +62 -0
  114. package/src/json-schemas.md +109 -0
  115. package/src/omd-json-samples.js +115 -0
  116. package/src/omd.js +109 -0
  117. package/src/omdApp.js +391 -0
  118. package/src/omdAppCanvas.js +336 -0
  119. package/src/omdBalanceHanger.js +172 -0
  120. package/src/omdColor.js +13 -0
  121. package/src/omdCoordinatePlane.js +467 -0
  122. package/src/omdEquation.js +125 -0
  123. package/src/omdExpression.js +104 -0
  124. package/src/omdFunction.js +113 -0
  125. package/src/omdMetaExpression.js +287 -0
  126. package/src/omdNaturalExpression.js +564 -0
  127. package/src/omdNode.js +384 -0
  128. package/src/omdNumber.js +53 -0
  129. package/src/omdNumberLine.js +107 -0
  130. package/src/omdNumberTile.js +119 -0
  131. package/src/omdOperator.js +73 -0
  132. package/src/omdPowerExpression.js +92 -0
  133. package/src/omdProblem.js +55 -0
  134. package/src/omdRatioChart.js +232 -0
  135. package/src/omdRationalExpression.js +115 -0
  136. package/src/omdSampleData.js +215 -0
  137. package/src/omdShapes.js +476 -0
  138. package/src/omdSpinner.js +148 -0
  139. package/src/omdString.js +39 -0
  140. package/src/omdTable.js +369 -0
  141. package/src/omdTapeDiagram.js +245 -0
  142. package/src/omdTerm.js +92 -0
  143. package/src/omdTileEquation.js +349 -0
  144. 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
+ }