@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,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
|
+
}
|