@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,443 @@
1
+ import { omdEquationNode } from '../nodes/omdEquationNode.js';
2
+ import { omdStepVisualizer } from '../step-visualizer/omdStepVisualizer.js';
3
+ import { getNodeForAST } from '../core/omdUtilities.js';
4
+ import { jsvgContainer } from '@teachinglab/jsvg';
5
+
6
+ /**
7
+ * OMD Renderer - Handles rendering of mathematical expressions
8
+ * This class provides a cleaner API for rendering expressions without
9
+ * being tied to specific DOM elements or UI concerns.
10
+ */
11
+ export class omdDisplay {
12
+ constructor(container, options = {}) {
13
+ this.container = container;
14
+ this.options = {
15
+ fontSize: 32,
16
+ centerContent: true,
17
+ topMargin: 40,
18
+ bottomMargin: 16,
19
+ fitToContent: false, // Only fit to content when explicitly requested
20
+ autoScale: true, // Automatically scale content to fit container
21
+ maxScale: 1, // Do not upscale beyond 1 by default
22
+ edgePadding: 16, // Horizontal padding from edges when scaling
23
+ ...options
24
+ };
25
+
26
+ // Create SVG container
27
+ this.svg = new jsvgContainer();
28
+ this.node = null;
29
+
30
+ // Set up the SVG
31
+ this._setupSVG();
32
+ }
33
+
34
+ _setupSVG() {
35
+ const width = this.container.offsetWidth || 800;
36
+ const height = this.container.offsetHeight || 600;
37
+
38
+ this.svg.setViewbox(width, height);
39
+ this.svg.svgObject.style.verticalAlign = "middle";
40
+ // Enable internal scrolling via native SVG scrolling if content overflows
41
+ this.svg.svgObject.style.overflow = 'hidden';
42
+ this.container.appendChild(this.svg.svgObject);
43
+
44
+ // Handle resize
45
+ if (window.ResizeObserver) {
46
+ this.resizeObserver = new ResizeObserver(() => {
47
+ this._handleResize();
48
+ });
49
+ this.resizeObserver.observe(this.container);
50
+ }
51
+ }
52
+
53
+ _handleResize() {
54
+ const width = this.container.offsetWidth;
55
+ const height = this.container.offsetHeight;
56
+ this.svg.setViewbox(width, height);
57
+
58
+ if (this.options.centerContent && this.node) {
59
+ this.centerNode();
60
+ }
61
+
62
+ // Reposition overlay toolbar (if any) on resize
63
+ this._repositionOverlayToolbar();
64
+ }
65
+
66
+ centerNode() {
67
+ if (!this.node) return;
68
+ const containerWidth = this.container.offsetWidth || 0;
69
+ const containerHeight = this.container.offsetHeight || 0;
70
+
71
+ // Determine actual content size (prefer sequence/current step when available)
72
+ let contentWidth = this.node.width || 0;
73
+ let contentHeight = this.node.height || 0;
74
+ if (this.node.getSequence) {
75
+ const seq = this.node.getSequence();
76
+ if (seq) {
77
+ if (seq.width && seq.height) {
78
+ contentWidth = seq.width;
79
+ contentHeight = seq.height;
80
+ }
81
+ if (seq.getCurrentStep) {
82
+ const step = seq.getCurrentStep();
83
+ if (step && step.width && step.height) {
84
+ contentWidth = Math.max(contentWidth, step.width);
85
+ contentHeight = Math.max(contentHeight, step.height);
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ // Compute scale to keep within bounds
92
+ let scale = 1;
93
+ if (this.options.autoScale && contentWidth > 0 && contentHeight > 0) {
94
+ const hPad = this.options.edgePadding || 0;
95
+ const vPadTop = this.options.topMargin || 0;
96
+ const vPadBottom = this.options.bottomMargin || 0;
97
+ // Reserve extra space for overlay toolbar if needed
98
+ let reserveBottom = vPadBottom;
99
+ if (this.node && typeof this.node.isToolbarOverlay === 'function' && this.node.isToolbarOverlay()) {
100
+ const tH = (typeof this.node.getToolbarVisualHeight === 'function') ? this.node.getToolbarVisualHeight() : 0;
101
+ reserveBottom += (tH + (this.node.getOverlayPadding ? this.node.getOverlayPadding() : 16));
102
+ }
103
+ const availW = Math.max(0, containerWidth - hPad * 2);
104
+ const availH = Math.max(0, containerHeight - (vPadTop + reserveBottom));
105
+ const sx = availW > 0 ? (availW / contentWidth) : 1;
106
+ const sy = availH > 0 ? (availH / contentHeight) : 1;
107
+ const maxScale = (typeof this.options.maxScale === 'number') ? this.options.maxScale : 1;
108
+ scale = Math.min(sx, sy, maxScale);
109
+ if (!isFinite(scale) || scale <= 0) scale = 1;
110
+ }
111
+
112
+ // Apply scale
113
+ if (typeof this.node.setScale === 'function') {
114
+ this.node.setScale(scale);
115
+ }
116
+
117
+ // Compute X so that equals anchor (if present) is centered after scaling
118
+ let x;
119
+ if (this.node.type === 'omdEquationSequenceNode' && this.node.alignPointX !== undefined) {
120
+ const screenCenterX = containerWidth / 2;
121
+ x = screenCenterX - (this.node.alignPointX * scale);
122
+ } else {
123
+ const scaledWidth = (this.node.width || contentWidth) * scale;
124
+ x = (containerWidth - scaledWidth) / 2;
125
+ }
126
+
127
+ // Y is top margin; scaled content will grow downward
128
+ this.node.setPosition(x, this.options.topMargin);
129
+
130
+ // Reposition overlay toolbar (if any)
131
+ this._repositionOverlayToolbar();
132
+
133
+ // If content still exceeds available height (even after scaling), enable container scroll
134
+ const totalNeededH = (contentHeight * scale) + (this.options.topMargin || 0) + (this.options.bottomMargin || 0);
135
+ if (totalNeededH > containerHeight) {
136
+ // Let the host scroll vertically; keep horizontal overflow hidden to avoid layout shift
137
+ this.container.style.overflowY = 'auto';
138
+ this.container.style.overflowX = 'hidden';
139
+ } else {
140
+ this.container.style.overflow = 'hidden';
141
+ }
142
+ }
143
+
144
+ fitToContent() {
145
+ if (!this.node) {
146
+ console.log('No node to fit');
147
+ return;
148
+ }
149
+
150
+ // Try to get actual rendered dimensions
151
+ let actualWidth = 0;
152
+ let actualHeight = 0;
153
+
154
+ // Get both sequence and current step dimensions
155
+ let sequenceWidth = 0, sequenceHeight = 0;
156
+ let stepWidth = 0, stepHeight = 0;
157
+
158
+ if (this.node.getSequence) {
159
+ const sequence = this.node.getSequence();
160
+ if (sequence && sequence.width && sequence.height) {
161
+ sequenceWidth = sequence.width;
162
+ sequenceHeight = sequence.height;
163
+ console.log('Sequence dimensions:', sequenceWidth, 'x', sequenceHeight);
164
+
165
+ // Check current step dimensions too
166
+ if (sequence.getCurrentStep) {
167
+ const currentStep = sequence.getCurrentStep();
168
+ if (currentStep && currentStep.width && currentStep.height) {
169
+ stepWidth = currentStep.width;
170
+ stepHeight = currentStep.height;
171
+ console.log('Current step dimensions:', stepWidth, 'x', stepHeight);
172
+ }
173
+ }
174
+
175
+ // Use the larger of sequence or step dimensions
176
+ actualWidth = Math.max(sequenceWidth, stepWidth);
177
+ actualHeight = Math.max(sequenceHeight, stepHeight);
178
+ console.log('Using maximum dimensions:', actualWidth, 'x', actualHeight);
179
+ }
180
+ }
181
+
182
+ // Fallback to node dimensions only if sequence/step dimensions aren't available
183
+ if ((actualWidth === 0 || actualHeight === 0) && this.node.width && this.node.height) {
184
+ actualWidth = this.node.width;
185
+ actualHeight = this.node.height;
186
+ console.log('Using node dimensions:', actualWidth, 'x', actualHeight);
187
+ }
188
+
189
+ // Fallback dimensions
190
+ if (actualWidth === 0 || actualHeight === 0) {
191
+ actualWidth = 200;
192
+ actualHeight = 60;
193
+ console.log('Using fallback dimensions:', actualWidth, 'x', actualHeight);
194
+ }
195
+
196
+ const padding = 10; // More comfortable padding to match user expectation
197
+ const newWidth = actualWidth + (padding * 2);
198
+ const newHeight = actualHeight + (padding * 2);
199
+
200
+ console.log('Setting container to:', newWidth, 'x', newHeight);
201
+
202
+ // Position the content at the minimal padding offset FIRST
203
+ if (this.node && this.node.setPosition) {
204
+ this.node.setPosition(padding, padding);
205
+ console.log('Positioning content at:', padding, padding);
206
+ }
207
+
208
+ // Update SVG dimensions with viewBox starting from 0,0 since we repositioned content
209
+ this.svg.setViewbox(newWidth, newHeight);
210
+ this.svg.setWidthAndHeight(newWidth, newHeight);
211
+
212
+ // Update container
213
+ this.container.style.width = `${newWidth}px`;
214
+ this.container.style.height = `${newHeight}px`;
215
+ }
216
+
217
+ /**
218
+ * Renders a mathematical expression or equation
219
+ * @param {string|omdNode} expression - Expression string or node
220
+ * @returns {omdNode} The rendered node
221
+ */
222
+ render(expression) {
223
+ // Clear previous node
224
+ if (this.node) {
225
+ this.svg.removeChild(this.node);
226
+ }
227
+
228
+ // Create node from expression
229
+ if (typeof expression === 'string') {
230
+ if (expression.includes(';')) {
231
+ // Multiple equations
232
+ const equationStrings = expression.split(';').filter(s => s.trim() !== '');
233
+ const steps = equationStrings.map(str => omdEquationNode.fromString(str));
234
+ this.node = new omdStepVisualizer(steps);
235
+ } else {
236
+ // Single expression or equation
237
+ if (expression.includes('=')) {
238
+ const firstStep = omdEquationNode.fromString(expression);
239
+ this.node = new omdStepVisualizer([firstStep]);
240
+ } else {
241
+ // Create node directly from expression
242
+ const parsedAST = math.parse(expression);
243
+ const NodeClass = getNodeForAST(parsedAST);
244
+ const firstStep = new NodeClass(parsedAST);
245
+ this.node = new omdStepVisualizer([firstStep]);
246
+ }
247
+ }
248
+ } else {
249
+ // Assume it's already a node
250
+ this.node = expression;
251
+ }
252
+
253
+ // Initialize and render
254
+ const sequence = this.node.getSequence ? this.node.getSequence() : null;
255
+ if (sequence) {
256
+ sequence.setFontSize(this.options.fontSize);
257
+ // Apply filtering based on filterLevel
258
+ sequence.updateStepsVisibility(step => (step.stepMark ?? 0) === sequence.getFilterLevel());
259
+ }
260
+ this.svg.addChild(this.node);
261
+
262
+ // Apply any stored font settings
263
+ if (this.options.fontFamily) {
264
+ // Small delay to ensure SVG elements are fully rendered
265
+ setTimeout(() => {
266
+ this.setFont(this.options.fontFamily, this.options.fontWeight || '400');
267
+ }, 10);
268
+ }
269
+
270
+ // Only use fitToContent for tight sizing when explicitly requested
271
+ if (this.options.fitToContent) {
272
+ this.fitToContent();
273
+ } else if (this.options.centerContent) {
274
+ this.centerNode();
275
+ }
276
+ // Ensure overlay toolbar is positioned initially
277
+ this._repositionOverlayToolbar();
278
+
279
+ // Provide a default global refresh function if not present
280
+ if (typeof window !== 'undefined' && !window.refreshDisplayAndFilters) {
281
+ window.refreshDisplayAndFilters = () => {
282
+ try {
283
+ const node = this.getCurrentNode();
284
+ const sequence = node?.getSequence ? node.getSequence() : null;
285
+ if (sequence) {
286
+ if (typeof sequence.simplifyAll === 'function') {
287
+ sequence.simplifyAll();
288
+ }
289
+ if (typeof sequence.updateStepsVisibility === 'function') {
290
+ sequence.updateStepsVisibility(step => (step.stepMark ?? 0) === 0);
291
+ }
292
+ if (typeof node.updateLayout === 'function') {
293
+ node.updateLayout();
294
+ }
295
+ }
296
+ if (this.options.centerContent) {
297
+ this.centerNode();
298
+ }
299
+ } catch (e) {
300
+ // no-op
301
+ }
302
+ };
303
+ }
304
+
305
+ return this.node;
306
+ }
307
+
308
+ /**
309
+ * Updates the display with a new node
310
+ * @param {omdNode} newNode - The new node to display
311
+ */
312
+ update(newNode) {
313
+ if (this.node) {
314
+ this.svg.removeChild(this.node);
315
+ }
316
+
317
+ this.node = newNode;
318
+ this.node.setFontSize(this.options.fontSize);
319
+ this.node.initialize();
320
+ this.svg.addChild(this.node);
321
+
322
+ if (this.options.centerContent) {
323
+ this.centerNode();
324
+ }
325
+ // Ensure overlay toolbar is positioned on updates
326
+ this._repositionOverlayToolbar();
327
+ }
328
+
329
+ /**
330
+ * Gets the current node
331
+ * @returns {omdNode|null} The current node
332
+ */
333
+ getCurrentNode() {
334
+ return this.node;
335
+ }
336
+
337
+ /**
338
+ * Repositions overlay toolbar if current node supports it
339
+ * @private
340
+ */
341
+ _repositionOverlayToolbar() {
342
+ const rect = this.container.getBoundingClientRect();
343
+ const paddingTop = parseFloat(getComputedStyle(this.container).paddingTop || '0');
344
+ const paddingBottom = parseFloat(getComputedStyle(this.container).paddingBottom || '0');
345
+ const paddingLeft = parseFloat(getComputedStyle(this.container).paddingLeft || '0');
346
+ const paddingRight = parseFloat(getComputedStyle(this.container).paddingRight || '0');
347
+ const containerWidth = (rect.width - paddingLeft - paddingRight) || this.container.clientWidth || 0;
348
+ const containerHeight = (rect.height - paddingTop - paddingBottom) || this.container.clientHeight || 0;
349
+ const node = this.node;
350
+ if (!node) return;
351
+ const hasOverlayApi = typeof node.isToolbarOverlay === 'function' && typeof node.positionToolbarOverlay === 'function';
352
+ if (hasOverlayApi && node.isToolbarOverlay()) {
353
+ node.positionToolbarOverlay(containerWidth, containerHeight, 16);
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Sets the font size
359
+ * @param {number} size - The font size
360
+ */
361
+ setFontSize(size) {
362
+ this.options.fontSize = size;
363
+ if (this.node) {
364
+ // Apply font size - handle different node types
365
+ if (this.node.getSequence && typeof this.node.getSequence === 'function') {
366
+ // For omdEquationStack, set font size on the sequence
367
+ this.node.getSequence().setFontSize(size);
368
+ } else if (this.node.setFontSize && typeof this.node.setFontSize === 'function') {
369
+ // For regular nodes with setFontSize method
370
+ this.node.setFontSize(size);
371
+ }
372
+ this.node.initialize();
373
+ if (this.options.centerContent) {
374
+ this.centerNode();
375
+ }
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Sets the font family for all elements in the display
381
+ * @param {string} fontFamily - CSS font-family string (e.g., '"Shantell Sans", cursive')
382
+ * @param {string} fontWeight - CSS font-weight (default: '400')
383
+ */
384
+ setFont(fontFamily, fontWeight = '400') {
385
+ if (this.svg?.svgObject) {
386
+ const applyFont = (element) => {
387
+ if (element.style) {
388
+ element.style.fontFamily = fontFamily;
389
+ element.style.fontWeight = fontWeight;
390
+ }
391
+ // Recursively apply to all children
392
+ Array.from(element.children || []).forEach(applyFont);
393
+ };
394
+
395
+ // Apply font to the entire SVG
396
+ applyFont(this.svg.svgObject);
397
+
398
+ // Store font settings for future use
399
+ this.options.fontFamily = fontFamily;
400
+ this.options.fontWeight = fontWeight;
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Clears the display
406
+ */
407
+ clear() {
408
+ if (this.node) {
409
+ this.svg.removeChild(this.node);
410
+ this.node = null;
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Destroys the renderer and cleans up resources
416
+ */
417
+ destroy() {
418
+ this.clear();
419
+ if (this.resizeObserver) {
420
+ this.resizeObserver.disconnect();
421
+ }
422
+ if (this.container.contains(this.svg.svgObject)) {
423
+ this.container.removeChild(this.svg.svgObject);
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Repositions overlay toolbar if current node supports it
429
+ * @private
430
+ */
431
+ _repositionOverlayToolbar() {
432
+ // Use same width calculation as centering to ensure consistency
433
+ const containerWidth = this.container.offsetWidth || 0;
434
+ const containerHeight = this.container.offsetHeight || 0;
435
+ const node = this.node;
436
+ if (!node) return;
437
+ const hasOverlayApi = typeof node.isToolbarOverlay === 'function' && typeof node.positionToolbarOverlay === 'function';
438
+ if (hasOverlayApi && node.isToolbarOverlay()) {
439
+ const padding = (typeof node.getOverlayPadding === 'function') ? node.getOverlayPadding() : 16;
440
+ node.positionToolbarOverlay(containerWidth, containerHeight, padding);
441
+ }
442
+ }
443
+ }