@teachinglab/omd 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +257 -251
- package/README.old.md +137 -137
- package/canvas/core/canvasConfig.js +202 -202
- package/canvas/drawing/segment.js +167 -167
- package/canvas/drawing/stroke.js +385 -385
- package/canvas/events/eventManager.js +444 -444
- package/canvas/events/pointerEventHandler.js +262 -262
- package/canvas/index.js +48 -48
- package/canvas/tools/PointerTool.js +71 -71
- package/canvas/tools/tool.js +222 -222
- package/canvas/utils/boundingBox.js +377 -377
- package/canvas/utils/mathUtils.js +258 -258
- package/docs/api/configuration-options.md +198 -198
- package/docs/api/eventManager.md +82 -82
- package/docs/api/focusFrameManager.md +144 -144
- package/docs/api/index.md +105 -105
- package/docs/api/main.md +62 -62
- package/docs/api/omdBinaryExpressionNode.md +86 -86
- package/docs/api/omdCanvas.md +83 -83
- package/docs/api/omdConfigManager.md +112 -112
- package/docs/api/omdConstantNode.md +52 -52
- package/docs/api/omdDisplay.md +87 -87
- package/docs/api/omdEquationNode.md +174 -174
- package/docs/api/omdEquationSequenceNode.md +258 -258
- package/docs/api/omdEquationStack.md +192 -192
- package/docs/api/omdFunctionNode.md +82 -82
- package/docs/api/omdGroupNode.md +78 -78
- package/docs/api/omdHelpers.md +87 -87
- package/docs/api/omdLeafNode.md +85 -85
- package/docs/api/omdNode.md +201 -201
- package/docs/api/omdOperationDisplayNode.md +117 -117
- package/docs/api/omdOperatorNode.md +91 -91
- package/docs/api/omdParenthesisNode.md +133 -133
- package/docs/api/omdPopup.md +191 -191
- package/docs/api/omdPowerNode.md +131 -131
- package/docs/api/omdRationalNode.md +144 -144
- package/docs/api/omdSequenceNode.md +128 -128
- package/docs/api/omdSimplification.md +78 -78
- package/docs/api/omdSqrtNode.md +144 -144
- package/docs/api/omdStepVisualizer.md +146 -146
- package/docs/api/omdStepVisualizerHighlighting.md +65 -65
- package/docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
- package/docs/api/omdStepVisualizerLayout.md +70 -70
- package/docs/api/omdStepVisualizerNodeUtils.md +140 -140
- package/docs/api/omdStepVisualizerTextBoxes.md +76 -76
- package/docs/api/omdToolbar.md +130 -130
- package/docs/api/omdTranscriptionService.md +95 -95
- package/docs/api/omdTreeDiff.md +169 -169
- package/docs/api/omdUnaryExpressionNode.md +137 -137
- package/docs/api/omdUtilities.md +82 -82
- package/docs/api/omdVariableNode.md +123 -123
- package/docs/api/selectTool.md +74 -74
- package/docs/api/simplificationEngine.md +97 -97
- package/docs/api/simplificationRules.md +76 -76
- package/docs/api/simplificationUtils.md +64 -64
- package/docs/api/transcribe.md +43 -43
- package/docs/api-reference.md +85 -85
- package/docs/index.html +453 -453
- package/docs/index.md +38 -38
- package/docs/omd-objects.md +258 -258
- package/index.js +79 -79
- package/jsvg/index.js +3 -0
- package/jsvg/jsvg.js +898 -898
- package/jsvg/jsvgComponents.js +357 -358
- package/npm-docs/DOCUMENTATION_SUMMARY.md +220 -220
- package/npm-docs/README.md +251 -251
- package/npm-docs/api/api-reference.md +85 -85
- package/npm-docs/api/configuration-options.md +198 -198
- package/npm-docs/api/eventManager.md +82 -82
- package/npm-docs/api/expression-nodes.md +561 -561
- package/npm-docs/api/focusFrameManager.md +144 -144
- package/npm-docs/api/index.md +105 -105
- package/npm-docs/api/main.md +62 -62
- package/npm-docs/api/omdBinaryExpressionNode.md +86 -86
- package/npm-docs/api/omdCanvas.md +83 -83
- package/npm-docs/api/omdConfigManager.md +112 -112
- package/npm-docs/api/omdConstantNode.md +52 -52
- package/npm-docs/api/omdDisplay.md +87 -87
- package/npm-docs/api/omdEquationNode.md +174 -174
- package/npm-docs/api/omdEquationSequenceNode.md +258 -258
- package/npm-docs/api/omdEquationStack.md +192 -192
- package/npm-docs/api/omdFunctionNode.md +82 -82
- package/npm-docs/api/omdGroupNode.md +78 -78
- package/npm-docs/api/omdHelpers.md +87 -87
- package/npm-docs/api/omdLeafNode.md +85 -85
- package/npm-docs/api/omdNode.md +201 -201
- package/npm-docs/api/omdOperationDisplayNode.md +117 -117
- package/npm-docs/api/omdOperatorNode.md +91 -91
- package/npm-docs/api/omdParenthesisNode.md +133 -133
- package/npm-docs/api/omdPopup.md +191 -191
- package/npm-docs/api/omdPowerNode.md +131 -131
- package/npm-docs/api/omdRationalNode.md +144 -144
- package/npm-docs/api/omdSequenceNode.md +128 -128
- package/npm-docs/api/omdSimplification.md +78 -78
- package/npm-docs/api/omdSqrtNode.md +144 -144
- package/npm-docs/api/omdStepVisualizer.md +146 -146
- package/npm-docs/api/omdStepVisualizerHighlighting.md +65 -65
- package/npm-docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
- package/npm-docs/api/omdStepVisualizerLayout.md +70 -70
- package/npm-docs/api/omdStepVisualizerNodeUtils.md +140 -140
- package/npm-docs/api/omdStepVisualizerTextBoxes.md +76 -76
- package/npm-docs/api/omdToolbar.md +130 -130
- package/npm-docs/api/omdTranscriptionService.md +95 -95
- package/npm-docs/api/omdTreeDiff.md +169 -169
- package/npm-docs/api/omdUnaryExpressionNode.md +137 -137
- package/npm-docs/api/omdUtilities.md +82 -82
- package/npm-docs/api/omdVariableNode.md +123 -123
- package/npm-docs/api/selectTool.md +74 -74
- package/npm-docs/api/simplificationEngine.md +97 -97
- package/npm-docs/api/simplificationRules.md +76 -76
- package/npm-docs/api/simplificationUtils.md +64 -64
- package/npm-docs/api/transcribe.md +43 -43
- package/npm-docs/guides/equations.md +854 -854
- package/npm-docs/guides/factory-functions.md +354 -354
- package/npm-docs/guides/getting-started.md +318 -318
- package/npm-docs/guides/quick-examples.md +525 -525
- package/npm-docs/guides/visualizations.md +682 -682
- package/npm-docs/index.html +12 -0
- package/npm-docs/json-schemas.md +826 -826
- package/omd/config/omdConfigManager.js +279 -267
- package/omd/core/index.js +158 -158
- package/omd/core/omdEquationStack.js +606 -547
- package/omd/core/omdUtilities.js +113 -113
- package/omd/display/omdDisplay.js +1045 -963
- package/omd/display/omdToolbar.js +501 -501
- package/omd/nodes/omdBinaryExpressionNode.js +459 -459
- package/omd/nodes/omdConstantNode.js +141 -141
- package/omd/nodes/omdEquationNode.js +1327 -1327
- package/omd/nodes/omdFunctionNode.js +351 -351
- package/omd/nodes/omdGroupNode.js +67 -67
- package/omd/nodes/omdLeafNode.js +76 -76
- package/omd/nodes/omdNode.js +556 -556
- package/omd/nodes/omdOperationDisplayNode.js +321 -321
- package/omd/nodes/omdOperatorNode.js +108 -108
- package/omd/nodes/omdParenthesisNode.js +292 -292
- package/omd/nodes/omdPowerNode.js +235 -235
- package/omd/nodes/omdRationalNode.js +295 -295
- package/omd/nodes/omdSqrtNode.js +307 -307
- package/omd/nodes/omdUnaryExpressionNode.js +227 -227
- package/omd/nodes/omdVariableNode.js +122 -122
- package/omd/simplification/omdSimplification.js +140 -140
- package/omd/simplification/omdSimplificationEngine.js +887 -887
- package/omd/simplification/package.json +5 -5
- package/omd/simplification/rules/binaryRules.js +1037 -1037
- package/omd/simplification/rules/functionRules.js +111 -111
- package/omd/simplification/rules/index.js +48 -48
- package/omd/simplification/rules/parenthesisRules.js +19 -19
- package/omd/simplification/rules/powerRules.js +143 -143
- package/omd/simplification/rules/rationalRules.js +725 -725
- package/omd/simplification/rules/sqrtRules.js +48 -48
- package/omd/simplification/rules/unaryRules.js +37 -37
- package/omd/simplification/simplificationRules.js +31 -31
- package/omd/simplification/simplificationUtils.js +1055 -1055
- package/omd/step-visualizer/omdStepVisualizer.js +947 -947
- package/omd/step-visualizer/omdStepVisualizerHighlighting.js +246 -246
- package/omd/step-visualizer/omdStepVisualizerLayout.js +892 -892
- package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +200 -200
- package/omd/utils/aiNextEquationStep.js +106 -106
- package/omd/utils/omdNodeOverlay.js +638 -638
- package/omd/utils/omdPopup.js +1203 -1203
- package/omd/utils/omdStepVisualizerInteractiveSteps.js +684 -684
- package/omd/utils/omdStepVisualizerNodeUtils.js +267 -267
- package/omd/utils/omdTranscriptionService.js +123 -123
- package/omd/utils/omdTreeDiff.js +733 -733
- package/package.json +59 -57
- package/readme.html +184 -120
- package/src/index.js +74 -74
- package/src/json-schemas.md +576 -576
- package/src/omd-json-samples.js +147 -147
- package/src/omdApp.js +391 -391
- package/src/omdAppCanvas.js +335 -335
- package/src/omdBalanceHanger.js +199 -199
- package/src/omdColor.js +13 -13
- package/src/omdCoordinatePlane.js +541 -541
- package/src/omdExpression.js +115 -115
- package/src/omdFactory.js +150 -150
- package/src/omdFunction.js +114 -114
- package/src/omdMetaExpression.js +290 -290
- package/src/omdNaturalExpression.js +563 -563
- package/src/omdNode.js +383 -383
- package/src/omdNumber.js +52 -52
- package/src/omdNumberLine.js +114 -112
- package/src/omdNumberTile.js +118 -118
- package/src/omdOperator.js +72 -72
- package/src/omdPowerExpression.js +91 -91
- package/src/omdProblem.js +259 -259
- package/src/omdRatioChart.js +251 -251
- package/src/omdRationalExpression.js +114 -114
- package/src/omdSampleData.js +215 -215
- package/src/omdShapes.js +512 -512
- package/src/omdSpinner.js +151 -151
- package/src/omdString.js +49 -49
- package/src/omdTable.js +498 -498
- package/src/omdTapeDiagram.js +244 -244
- package/src/omdTerm.js +91 -91
- package/src/omdTileEquation.js +349 -349
- package/src/omdUtils.js +84 -84
- package/src/omdVariable.js +51 -51
|
@@ -1,963 +1,1045 @@
|
|
|
1
|
-
import { omdEquationNode } from '../nodes/omdEquationNode.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
this.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
this.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this.
|
|
47
|
-
this.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
this.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
this.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (this.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
this.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
nminX =
|
|
305
|
-
nminY =
|
|
306
|
-
nmaxX =
|
|
307
|
-
nmaxY =
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (!
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (
|
|
457
|
-
console.warn('omdDisplay:
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (this.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (this.node) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
//
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
if (
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
this.
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
//
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
} else {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
this.
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
if (this.
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
this.
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
}
|
|
1
|
+
import { omdEquationNode } from '../nodes/omdEquationNode.js';
|
|
2
|
+
import { omdEquationStack } from '../core/omdEquationStack.js';
|
|
3
|
+
import { omdStepVisualizer } from '../step-visualizer/omdStepVisualizer.js';
|
|
4
|
+
import { getNodeForAST } from '../core/omdUtilities.js';
|
|
5
|
+
import { jsvgContainer } from '@teachinglab/jsvg';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* OMD Renderer - Handles rendering of mathematical expressions
|
|
9
|
+
* This class provides a cleaner API for rendering expressions without
|
|
10
|
+
* being tied to specific DOM elements or UI concerns.
|
|
11
|
+
*/
|
|
12
|
+
export class omdDisplay {
|
|
13
|
+
constructor(container, options = {}) {
|
|
14
|
+
this.container = container;
|
|
15
|
+
const {
|
|
16
|
+
stepVisualizer = false,
|
|
17
|
+
stackOptions = null,
|
|
18
|
+
math: mathInstance = (typeof window !== 'undefined' && window.math) ? window.math : null,
|
|
19
|
+
...otherOptions
|
|
20
|
+
} = options || {};
|
|
21
|
+
|
|
22
|
+
this.options = {
|
|
23
|
+
fontSize: 32,
|
|
24
|
+
centerContent: true,
|
|
25
|
+
topMargin: 40,
|
|
26
|
+
bottomMargin: 16,
|
|
27
|
+
fitToContent: true, // Fit to content size by default
|
|
28
|
+
autoScale: false, // Don't auto-scale by default
|
|
29
|
+
maxScale: 1, // Do not upscale beyond 1 by default
|
|
30
|
+
edgePadding: 16, // Horizontal padding from edges when scaling
|
|
31
|
+
autoCloseStepVisualizer: true, // Close active step visualizer text boxes before autoscale to avoid shrink
|
|
32
|
+
stepVisualizer,
|
|
33
|
+
stackOptions,
|
|
34
|
+
math: mathInstance,
|
|
35
|
+
...otherOptions
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Create SVG container
|
|
39
|
+
this.svg = new jsvgContainer();
|
|
40
|
+
this.node = null;
|
|
41
|
+
|
|
42
|
+
// Internal guards to prevent recursive resize induced growth
|
|
43
|
+
this._suppressResizeObserver = false; // When true, _handleResize is a no-op
|
|
44
|
+
this._lastViewbox = null; // Cache last applied viewBox string
|
|
45
|
+
this._lastContentExtents = null; // Cache last measured content extents to detect real growth
|
|
46
|
+
this._viewboxLocked = false; // When true, suppress micro growth adjustments
|
|
47
|
+
this._viewboxLockThreshold = 8; // Require at least 8px growth once locked
|
|
48
|
+
|
|
49
|
+
// Set up the SVG
|
|
50
|
+
this._setupSVG();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_setupSVG() {
|
|
54
|
+
const width = this.container.offsetWidth || 800;
|
|
55
|
+
const height = this.container.offsetHeight || 600;
|
|
56
|
+
|
|
57
|
+
this.svg.setViewbox(width, height);
|
|
58
|
+
this.svg.svgObject.style.verticalAlign = "middle";
|
|
59
|
+
// Enable internal scrolling via native SVG scrolling if content overflows
|
|
60
|
+
this.svg.svgObject.style.overflow = 'hidden';
|
|
61
|
+
this.container.appendChild(this.svg.svgObject);
|
|
62
|
+
|
|
63
|
+
// Create a dedicated content group we can translate to compensate for
|
|
64
|
+
// viewBox origin changes (so expanding the origin doesn't visually move content).
|
|
65
|
+
try {
|
|
66
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
67
|
+
this._contentGroup = document.createElementNS(ns, 'g');
|
|
68
|
+
this._contentGroup.setAttribute('id', 'omd-content-root');
|
|
69
|
+
this.svg.svgObject.appendChild(this._contentGroup);
|
|
70
|
+
this._contentOffsetX = 0;
|
|
71
|
+
this._contentOffsetY = 0;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
this._contentGroup = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Handle resize
|
|
77
|
+
if (window.ResizeObserver) {
|
|
78
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
79
|
+
this._handleResize();
|
|
80
|
+
});
|
|
81
|
+
this.resizeObserver.observe(this.container);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_handleResize() {
|
|
86
|
+
if (this._suppressResizeObserver) return; // Prevent re-entrant resize loops
|
|
87
|
+
const width = this.container.offsetWidth;
|
|
88
|
+
const height = this.container.offsetHeight;
|
|
89
|
+
// Skip if size unchanged; avoids loops where internal changes trigger observer without real container delta
|
|
90
|
+
if (this._lastContainerWidth === width && this._lastContainerHeight === height) return;
|
|
91
|
+
this._lastContainerWidth = width;
|
|
92
|
+
this._lastContainerHeight = height;
|
|
93
|
+
this.svg.setViewbox(width, height);
|
|
94
|
+
|
|
95
|
+
if (this.options.centerContent && this.node) {
|
|
96
|
+
this.centerNode();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Reposition overlay toolbar (if any) on resize
|
|
100
|
+
this._repositionOverlayToolbar();
|
|
101
|
+
if (this.options.debugExtents) this._drawDebugOverlays();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Ensure the internal SVG viewBox is at least as large as the provided content dimensions.
|
|
106
|
+
* This prevents clipping when content is larger than the current viewBox.
|
|
107
|
+
* @param {number} contentWidth
|
|
108
|
+
* @param {number} contentHeight
|
|
109
|
+
*/
|
|
110
|
+
_ensureViewboxFits(contentWidth, contentHeight) {
|
|
111
|
+
// If caller provided just width/height, but we prefer extents, bail early
|
|
112
|
+
if (!this.node) return;
|
|
113
|
+
const pad = 10;
|
|
114
|
+
|
|
115
|
+
// Prefer DOM measured extents (accounts for strokes, transforms, children SVG geometry)
|
|
116
|
+
let ext = null;
|
|
117
|
+
try {
|
|
118
|
+
const collected = this._collectNodeExtents(this.node);
|
|
119
|
+
if (collected) {
|
|
120
|
+
ext = { minX: collected.minX, minY: collected.minY, maxX: collected.maxX, maxY: collected.maxY };
|
|
121
|
+
}
|
|
122
|
+
} catch (e) {
|
|
123
|
+
ext = null;
|
|
124
|
+
}
|
|
125
|
+
if (!ext) {
|
|
126
|
+
ext = this._computeNodeExtents(this.node);
|
|
127
|
+
}
|
|
128
|
+
if (!ext) return;
|
|
129
|
+
|
|
130
|
+
const minX = Math.floor(ext.minX - pad);
|
|
131
|
+
const minY = Math.floor(ext.minY - pad);
|
|
132
|
+
const maxX = Math.ceil(ext.maxX + pad);
|
|
133
|
+
const maxY = Math.ceil(ext.maxY + pad);
|
|
134
|
+
|
|
135
|
+
const curView = this.svg.svgObject.getAttribute('viewBox') || '';
|
|
136
|
+
let curX = 0, curY = 0, curW = 0, curH = 0;
|
|
137
|
+
if (curView) {
|
|
138
|
+
const parts = curView.split(/\s+/).map(Number).filter(n => !isNaN(n));
|
|
139
|
+
if (parts.length === 4) {
|
|
140
|
+
curX = parts[0];
|
|
141
|
+
curY = parts[1];
|
|
142
|
+
curW = parts[2];
|
|
143
|
+
curH = parts[3];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// To avoid shifting visible content, keep the current viewBox origin (curX,curY)
|
|
148
|
+
// and only expand width/height as needed. Changing the origin would change
|
|
149
|
+
// the mapping from SVG coordinates to screen coordinates and appear to move
|
|
150
|
+
// existing content.
|
|
151
|
+
const desiredX = curX;
|
|
152
|
+
const desiredY = curY;
|
|
153
|
+
const desiredRight = Math.max(curX + curW, maxX);
|
|
154
|
+
const desiredBottom = Math.max(curY + curH, maxY);
|
|
155
|
+
const desiredW = Math.max(curW, desiredRight - desiredX);
|
|
156
|
+
const desiredH = Math.max(curH, desiredBottom - desiredY);
|
|
157
|
+
|
|
158
|
+
// Guard: If the desired size change is negligible (< 0.5px), skip.
|
|
159
|
+
const widthDelta = Math.abs(desiredW - curW);
|
|
160
|
+
const heightDelta = Math.abs(desiredH - curH);
|
|
161
|
+
|
|
162
|
+
// Safety cap to avoid runaway expansion due to logic errors.
|
|
163
|
+
const MAX_DIM = 10000; // arbitrary large but finite limit
|
|
164
|
+
if (desiredW > MAX_DIM || desiredH > MAX_DIM) {
|
|
165
|
+
console.warn('omdDisplay: viewBox growth capped to prevent runaway expansion', desiredW, desiredH);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (widthDelta < 0.5 && heightDelta < 0.5) return;
|
|
170
|
+
|
|
171
|
+
// Detect repeated growth with identical content extents (suggests feedback loop)
|
|
172
|
+
const curExtSignature = `${minX},${minY},${maxX},${maxY}`;
|
|
173
|
+
if (this._lastContentExtents === curExtSignature && heightDelta > 0 && desiredH > curH) {
|
|
174
|
+
return; // content unchanged, skip
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// If locked, only allow substantial growth
|
|
178
|
+
if (this._viewboxLocked) {
|
|
179
|
+
const growW = desiredW - curW;
|
|
180
|
+
const growH = desiredH - curH;
|
|
181
|
+
if (growW < this._viewboxLockThreshold && growH < this._viewboxLockThreshold) {
|
|
182
|
+
return; // ignore micro growth attempts
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const newViewBox = `${desiredX} ${desiredY} ${desiredW} ${desiredH}`;
|
|
187
|
+
if (this._lastViewbox === newViewBox) return;
|
|
188
|
+
|
|
189
|
+
this._suppressResizeObserver = true;
|
|
190
|
+
try {
|
|
191
|
+
this.svg.svgObject.setAttribute('viewBox', newViewBox);
|
|
192
|
+
} finally {
|
|
193
|
+
// Allow ResizeObserver events after microtask; use timeout to defer
|
|
194
|
+
setTimeout(() => { this._suppressResizeObserver = false; }, 0);
|
|
195
|
+
}
|
|
196
|
+
this._lastViewbox = newViewBox;
|
|
197
|
+
this._lastContentExtents = curExtSignature;
|
|
198
|
+
|
|
199
|
+
// Lock if the growth applied was small; prevents future tiny increments
|
|
200
|
+
if (heightDelta < 2 && widthDelta < 2 && !this._viewboxLocked) {
|
|
201
|
+
this._viewboxLocked = true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Walk the node tree and compute absolute extents in SVG coordinates.
|
|
207
|
+
* Uses `xpos`/`ypos` and `width`/`height` properties; falls back to 0 when missing.
|
|
208
|
+
* @param {omdNode} root
|
|
209
|
+
* @returns {{minX:number,minY:number,maxX:number,maxY:number}}
|
|
210
|
+
*/
|
|
211
|
+
_computeNodeExtents(root) {
|
|
212
|
+
if (!root) return null;
|
|
213
|
+
const visited = new Set();
|
|
214
|
+
const stack = [{ node: root, absX: root.xpos || 0, absY: root.ypos || 0 }];
|
|
215
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
216
|
+
|
|
217
|
+
while (stack.length) {
|
|
218
|
+
const { node, absX, absY } = stack.pop();
|
|
219
|
+
if (!node || visited.has(node)) continue;
|
|
220
|
+
visited.add(node);
|
|
221
|
+
|
|
222
|
+
const w = node.width || 0;
|
|
223
|
+
const h = node.height || 0;
|
|
224
|
+
const nx = absX;
|
|
225
|
+
const ny = absY;
|
|
226
|
+
minX = Math.min(minX, nx);
|
|
227
|
+
minY = Math.min(minY, ny);
|
|
228
|
+
maxX = Math.max(maxX, nx + w);
|
|
229
|
+
maxY = Math.max(maxY, ny + h);
|
|
230
|
+
|
|
231
|
+
// push children
|
|
232
|
+
if (Array.isArray(node.childList)) {
|
|
233
|
+
for (const c of node.childList) {
|
|
234
|
+
if (!c) continue;
|
|
235
|
+
const cx = (c.xpos || 0) + nx;
|
|
236
|
+
const cy = (c.ypos || 0) + ny;
|
|
237
|
+
stack.push({ node: c, absX: cx, absY: cy });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (node.argumentNodeList) {
|
|
241
|
+
for (const val of Object.values(node.argumentNodeList)) {
|
|
242
|
+
if (Array.isArray(val)) {
|
|
243
|
+
for (const v of val) {
|
|
244
|
+
if (!v) continue;
|
|
245
|
+
const vx = (v.xpos || 0) + nx;
|
|
246
|
+
const vy = (v.ypos || 0) + ny;
|
|
247
|
+
stack.push({ node: v, absX: vx, absY: vy });
|
|
248
|
+
}
|
|
249
|
+
} else if (val) {
|
|
250
|
+
const vx = (val.xpos || 0) + nx;
|
|
251
|
+
const vy = (val.ypos || 0) + ny;
|
|
252
|
+
stack.push({ node: val, absX: vx, absY: vy });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (minX === Infinity) return null;
|
|
259
|
+
return { minX, minY, maxX, maxY };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Collect extents for each node and return per-node list plus overall extents.
|
|
264
|
+
* Useful for debugging elements that extend outside parent coordinates.
|
|
265
|
+
* @param {omdNode} root
|
|
266
|
+
* @returns {{nodes:Array, minX:number, minY:number, maxX:number, maxY:number}}
|
|
267
|
+
*/
|
|
268
|
+
_collectNodeExtents(root) {
|
|
269
|
+
if (!root) return null;
|
|
270
|
+
const visited = new Set();
|
|
271
|
+
const stack = [{ node: root, absX: root.xpos || 0, absY: root.ypos || 0, parent: null }];
|
|
272
|
+
const nodes = [];
|
|
273
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
while (stack.length) {
|
|
277
|
+
const { node, absX, absY, parent } = stack.pop();
|
|
278
|
+
if (!node || visited.has(node)) continue;
|
|
279
|
+
visited.add(node);
|
|
280
|
+
|
|
281
|
+
// Prefer DOM measurement if available for accuracy
|
|
282
|
+
let nx = absX;
|
|
283
|
+
let ny = absY;
|
|
284
|
+
let nminX = nx;
|
|
285
|
+
let nminY = ny;
|
|
286
|
+
let nmaxX = nx;
|
|
287
|
+
let nmaxY = ny;
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
if (node.svgObject && typeof node.svgObject.getBBox === 'function' && typeof node.svgObject.getCTM === 'function') {
|
|
291
|
+
const bbox = node.svgObject.getBBox();
|
|
292
|
+
const ctm = node.svgObject.getCTM();
|
|
293
|
+
// Transform all four bbox corners into root SVG coordinates
|
|
294
|
+
const corners = [
|
|
295
|
+
{ x: bbox.x, y: bbox.y },
|
|
296
|
+
{ x: bbox.x + bbox.width, y: bbox.y },
|
|
297
|
+
{ x: bbox.x, y: bbox.y + bbox.height },
|
|
298
|
+
{ x: bbox.x + bbox.width, y: bbox.y + bbox.height }
|
|
299
|
+
];
|
|
300
|
+
const tx = corners.map(p => ({
|
|
301
|
+
x: ctm.a * p.x + ctm.c * p.y + ctm.e,
|
|
302
|
+
y: ctm.b * p.x + ctm.d * p.y + ctm.f
|
|
303
|
+
}));
|
|
304
|
+
nminX = Math.min(...tx.map(t => t.x));
|
|
305
|
+
nminY = Math.min(...tx.map(t => t.y));
|
|
306
|
+
nmaxX = Math.max(...tx.map(t => t.x));
|
|
307
|
+
nmaxY = Math.max(...tx.map(t => t.y));
|
|
308
|
+
nx = nminX;
|
|
309
|
+
ny = nminY;
|
|
310
|
+
} else {
|
|
311
|
+
const w = node.width || 0;
|
|
312
|
+
const h = node.height || 0;
|
|
313
|
+
nx = absX;
|
|
314
|
+
ny = absY;
|
|
315
|
+
nminX = nx;
|
|
316
|
+
nminY = ny;
|
|
317
|
+
nmaxX = nx + w;
|
|
318
|
+
nmaxY = ny + h;
|
|
319
|
+
}
|
|
320
|
+
} catch (e) {
|
|
321
|
+
const w = node.width || 0;
|
|
322
|
+
const h = node.height || 0;
|
|
323
|
+
nx = absX;
|
|
324
|
+
ny = absY;
|
|
325
|
+
nminX = nx;
|
|
326
|
+
nminY = ny;
|
|
327
|
+
nmaxX = nx + w;
|
|
328
|
+
nmaxY = ny + h;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
nodes.push({ node, minX: nminX, minY: nminY, maxX: nmaxX, maxY: nmaxY, parent });
|
|
332
|
+
|
|
333
|
+
minX = Math.min(minX, nminX);
|
|
334
|
+
minY = Math.min(minY, nminY);
|
|
335
|
+
maxX = Math.max(maxX, nmaxX);
|
|
336
|
+
maxY = Math.max(maxY, nmaxY);
|
|
337
|
+
|
|
338
|
+
// push children
|
|
339
|
+
if (Array.isArray(node.childList)) {
|
|
340
|
+
for (const c of node.childList) {
|
|
341
|
+
if (!c) continue;
|
|
342
|
+
const cx = (c.xpos || 0) + nx;
|
|
343
|
+
const cy = (c.ypos || 0) + ny;
|
|
344
|
+
stack.push({ node: c, absX: cx, absY: cy, parent: node });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (node.argumentNodeList) {
|
|
348
|
+
for (const val of Object.values(node.argumentNodeList)) {
|
|
349
|
+
if (Array.isArray(val)) {
|
|
350
|
+
for (const v of val) {
|
|
351
|
+
if (!v) continue;
|
|
352
|
+
const vx = (v.xpos || 0) + nx;
|
|
353
|
+
const vy = (v.ypos || 0) + ny;
|
|
354
|
+
stack.push({ node: v, absX: vx, absY: vy, parent: node });
|
|
355
|
+
}
|
|
356
|
+
} else if (val) {
|
|
357
|
+
const vx = (val.xpos || 0) + nx;
|
|
358
|
+
const vy = (val.ypos || 0) + ny;
|
|
359
|
+
stack.push({ node: val, absX: vx, absY: vy, parent: node });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (minX === Infinity) return null;
|
|
366
|
+
return { nodes, minX, minY, maxX, maxY };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
_clearDebugOverlays() {
|
|
370
|
+
if (!this.svg || !this.svg.svgObject) return;
|
|
371
|
+
const existing = this.svg.svgObject.querySelector('#omd-debug-overlays');
|
|
372
|
+
if (existing) existing.remove();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
_drawDebugOverlays() {
|
|
376
|
+
if (!this.options.debugExtents) return;
|
|
377
|
+
if (!this.svg || !this.svg.svgObject || !this.node) return;
|
|
378
|
+
|
|
379
|
+
this._clearDebugOverlays();
|
|
380
|
+
|
|
381
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
382
|
+
const group = document.createElementNS(ns, 'g');
|
|
383
|
+
group.setAttribute('id', 'omd-debug-overlays');
|
|
384
|
+
group.setAttribute('pointer-events', 'none');
|
|
385
|
+
|
|
386
|
+
// overall node extents
|
|
387
|
+
const collected = this._collectNodeExtents(this.node);
|
|
388
|
+
if (!collected) return;
|
|
389
|
+
|
|
390
|
+
const { nodes, minX, minY, maxX, maxY } = collected;
|
|
391
|
+
|
|
392
|
+
// Draw content extents (blue dashed)
|
|
393
|
+
const contentRect = document.createElementNS(ns, 'rect');
|
|
394
|
+
contentRect.setAttribute('x', String(minX));
|
|
395
|
+
contentRect.setAttribute('y', String(minY));
|
|
396
|
+
contentRect.setAttribute('width', String(maxX - minX));
|
|
397
|
+
contentRect.setAttribute('height', String(maxY - minY));
|
|
398
|
+
contentRect.setAttribute('fill', 'none');
|
|
399
|
+
contentRect.setAttribute('stroke', 'blue');
|
|
400
|
+
contentRect.setAttribute('stroke-dasharray', '6 4');
|
|
401
|
+
contentRect.setAttribute('stroke-width', '0.8');
|
|
402
|
+
group.appendChild(contentRect);
|
|
403
|
+
|
|
404
|
+
// Draw viewBox rect (orange)
|
|
405
|
+
const curView = this.svg.svgObject.getAttribute('viewBox') || '';
|
|
406
|
+
if (curView) {
|
|
407
|
+
const parts = curView.split(/\s+/).map(Number).filter(n => !isNaN(n));
|
|
408
|
+
if (parts.length === 4) {
|
|
409
|
+
const [vx, vy, vw, vh] = parts;
|
|
410
|
+
const vbRect = document.createElementNS(ns, 'rect');
|
|
411
|
+
vbRect.setAttribute('x', String(vx));
|
|
412
|
+
vbRect.setAttribute('y', String(vy));
|
|
413
|
+
vbRect.setAttribute('width', String(vw));
|
|
414
|
+
vbRect.setAttribute('height', String(vh));
|
|
415
|
+
vbRect.setAttribute('fill', 'none');
|
|
416
|
+
vbRect.setAttribute('stroke', 'orange');
|
|
417
|
+
vbRect.setAttribute('stroke-width', '1');
|
|
418
|
+
vbRect.setAttribute('opacity', '0.9');
|
|
419
|
+
group.appendChild(vbRect);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Per-node boxes: green if inside parent, red if overflowing parent bounds
|
|
424
|
+
const overflowing = [];
|
|
425
|
+
for (const item of nodes) {
|
|
426
|
+
const r = document.createElementNS(ns, 'rect');
|
|
427
|
+
r.setAttribute('x', String(item.minX));
|
|
428
|
+
r.setAttribute('y', String(item.minY));
|
|
429
|
+
r.setAttribute('width', String(Math.max(0, item.maxX - item.minX)));
|
|
430
|
+
r.setAttribute('height', String(Math.max(0, item.maxY - item.minY)));
|
|
431
|
+
r.setAttribute('fill', 'none');
|
|
432
|
+
r.setAttribute('stroke-width', '0.6');
|
|
433
|
+
|
|
434
|
+
let stroke = 'green';
|
|
435
|
+
if (item.parent) {
|
|
436
|
+
const pMinX = (item.parent.xpos || 0) + (item.parent._absX || 0);
|
|
437
|
+
const pMinY = (item.parent.ypos || 0) + (item.parent._absY || 0);
|
|
438
|
+
// fallback compute parent's absX/Y from nodes list if available
|
|
439
|
+
const parentEntry = nodes.find(n => n.node === item.parent);
|
|
440
|
+
const pminX = parentEntry ? parentEntry.minX : pMinX;
|
|
441
|
+
const pminY = parentEntry ? parentEntry.minY : pMinY;
|
|
442
|
+
const pmaxX = parentEntry ? parentEntry.maxX : pminX + (item.parent.width || 0);
|
|
443
|
+
const pmaxY = parentEntry ? parentEntry.maxY : pminY + (item.parent.height || 0);
|
|
444
|
+
|
|
445
|
+
if (item.minX < pminX || item.minY < pminY || item.maxX > pmaxX || item.maxY > pmaxY) {
|
|
446
|
+
stroke = 'red';
|
|
447
|
+
overflowing.push({ node: item.node, bounds: item });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
r.setAttribute('stroke', stroke);
|
|
452
|
+
r.setAttribute('opacity', stroke === 'red' ? '0.9' : '0.6');
|
|
453
|
+
group.appendChild(r);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (overflowing.length) {
|
|
457
|
+
console.warn('omdDisplay: debugExtents found overflowing nodes:', overflowing.map(o => ({ type: o.node?.type, bounds: o.bounds })));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
this.svg.svgObject.appendChild(group);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
centerNode() {
|
|
464
|
+
if (!this.node) return;
|
|
465
|
+
if (!this._centerCallCount) this._centerCallCount = 0;
|
|
466
|
+
this._centerCallCount++;
|
|
467
|
+
if (this._centerCallCount > 500) {
|
|
468
|
+
console.warn('omdDisplay: excessive centerNode calls detected; halting further centering to prevent loop');
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const containerWidth = this.container.offsetWidth || 0;
|
|
472
|
+
const containerHeight = this.container.offsetHeight || 0;
|
|
473
|
+
|
|
474
|
+
// Early auto-close of step visualizer UI before measuring dimensions to avoid transient height inflation
|
|
475
|
+
if (this.options.autoCloseStepVisualizer && this.node) {
|
|
476
|
+
try {
|
|
477
|
+
if (typeof this.node.forceCloseAll === 'function') {
|
|
478
|
+
this.node.forceCloseAll();
|
|
479
|
+
} else if (typeof this.node.closeAllTextBoxes === 'function') {
|
|
480
|
+
this.node.closeAllTextBoxes();
|
|
481
|
+
} else if (typeof this.node.closeActiveDot === 'function') {
|
|
482
|
+
this.node.closeActiveDot();
|
|
483
|
+
}
|
|
484
|
+
} catch (e) { /* no-op */ }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Determine actual content size (prefer sequence/current step when available)
|
|
488
|
+
let contentWidth = this.node.width || 0;
|
|
489
|
+
let contentHeight = this.node.height || 0;
|
|
490
|
+
if (this.node.getSequence) {
|
|
491
|
+
const seq = this.node.getSequence();
|
|
492
|
+
if (seq) {
|
|
493
|
+
if (seq.width && seq.height) {
|
|
494
|
+
// For step visualizers, use sequenceWidth/Height instead of total dimensions to exclude visualizer elements from autoscale
|
|
495
|
+
contentWidth = seq.sequenceWidth || seq.width;
|
|
496
|
+
contentHeight = seq.sequenceHeight || seq.height;
|
|
497
|
+
}
|
|
498
|
+
if (seq.getCurrentStep) {
|
|
499
|
+
const step = seq.getCurrentStep();
|
|
500
|
+
if (step && step.width && step.height) {
|
|
501
|
+
// For step visualizers, prioritize sequenceWidth/Height for dimension calculations
|
|
502
|
+
const stepWidth = seq.sequenceWidth || step.width;
|
|
503
|
+
const stepHeight = seq.sequenceHeight || step.height;
|
|
504
|
+
contentWidth = Math.max(contentWidth, stepWidth);
|
|
505
|
+
contentHeight = Math.max(contentHeight, stepHeight);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Compute scale to keep within bounds
|
|
512
|
+
let scale = 1;
|
|
513
|
+
if (this.options.autoScale && contentWidth > 0 && contentHeight > 0) {
|
|
514
|
+
// Optionally close any open step visualizer textbox to prevent transient height expansion
|
|
515
|
+
if (this.options.autoCloseStepVisualizer && this.node) {
|
|
516
|
+
try {
|
|
517
|
+
if (typeof this.node.closeActiveDot === 'function') {
|
|
518
|
+
this.node.closeActiveDot();
|
|
519
|
+
} else if (typeof this.node.closeAllTextBoxes === 'function') {
|
|
520
|
+
this.node.closeAllTextBoxes();
|
|
521
|
+
}
|
|
522
|
+
} catch (e) { /* no-op */ }
|
|
523
|
+
}
|
|
524
|
+
// Detect step visualizer directly on node (getSequence returns underlying sequence only)
|
|
525
|
+
let hasStepVisualizer = false;
|
|
526
|
+
if (this.node) {
|
|
527
|
+
const ctorName = this.node.constructor?.name;
|
|
528
|
+
hasStepVisualizer = (ctorName === 'omdStepVisualizer') || this.node.type === 'omdStepVisualizer' || (typeof omdStepVisualizer !== 'undefined' && this.node instanceof omdStepVisualizer);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (hasStepVisualizer) {
|
|
532
|
+
// Preserve existing scale if already set on node; otherwise lock to 1.
|
|
533
|
+
const existingScale = (this.node && typeof this.node.scale === 'number') ? this.node.scale : undefined;
|
|
534
|
+
scale = (existingScale && existingScale > 0) ? existingScale : 1;
|
|
535
|
+
} else {
|
|
536
|
+
const hPad = this.options.edgePadding || 0;
|
|
537
|
+
const vPadTop = this.options.topMargin || 0;
|
|
538
|
+
const vPadBottom = this.options.bottomMargin || 0;
|
|
539
|
+
// Reserve extra space for overlay toolbar if needed
|
|
540
|
+
let reserveBottom = vPadBottom;
|
|
541
|
+
if (this.node && typeof this.node.isToolbarOverlay === 'function' && this.node.isToolbarOverlay()) {
|
|
542
|
+
const tH = (typeof this.node.getToolbarVisualHeight === 'function') ? this.node.getToolbarVisualHeight() : 0;
|
|
543
|
+
reserveBottom += (tH + (this.node.getOverlayPadding ? this.node.getOverlayPadding() : 16));
|
|
544
|
+
}
|
|
545
|
+
const availW = Math.max(0, containerWidth - hPad * 2);
|
|
546
|
+
const availH = Math.max(0, containerHeight - (vPadTop + reserveBottom));
|
|
547
|
+
const sx = availW > 0 ? (availW / contentWidth) : 1;
|
|
548
|
+
const sy = availH > 0 ? (availH / contentHeight) : 1;
|
|
549
|
+
const maxScale = (typeof this.options.maxScale === 'number') ? this.options.maxScale : 1;
|
|
550
|
+
scale = Math.min(sx, sy, maxScale);
|
|
551
|
+
if (!isFinite(scale) || scale <= 0) scale = 1;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Apply scale
|
|
556
|
+
if (typeof this.node.setScale === 'function') {
|
|
557
|
+
this.node.setScale(scale);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Compute X so that equals anchor (if present) is centered after scaling
|
|
561
|
+
let x;
|
|
562
|
+
if (this.node.type === 'omdEquationSequenceNode' && this.node.alignPointX !== undefined) {
|
|
563
|
+
const screenCenterX = containerWidth / 2;
|
|
564
|
+
x = screenCenterX - (this.node.alignPointX * scale);
|
|
565
|
+
} else {
|
|
566
|
+
const scaledWidth = contentWidth * scale;
|
|
567
|
+
x = (containerWidth - scaledWidth) / 2;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Decide whether positioning would move content outside container. If so,
|
|
571
|
+
// prefer expanding the SVG viewBox instead of moving nodes.
|
|
572
|
+
const scaledWidthFinal = contentWidth * scale;
|
|
573
|
+
const scaledHeightFinal = contentHeight * scale;
|
|
574
|
+
const totalNeededH = scaledHeightFinal + (this.options.topMargin || 0) + (this.options.bottomMargin || 0);
|
|
575
|
+
|
|
576
|
+
const willOverflowHoriz = scaledWidthFinal > containerWidth;
|
|
577
|
+
const willOverflowVert = totalNeededH > containerHeight;
|
|
578
|
+
|
|
579
|
+
// Avoid looping if content dimension signature hasn't changed
|
|
580
|
+
const contentSig = `${contentWidth}x${contentHeight}x${scale}`;
|
|
581
|
+
if (this._lastCenterSignature === contentSig && !willOverflowHoriz && !willOverflowVert) {
|
|
582
|
+
// Only update position; skip expensive ensureViewboxFits
|
|
583
|
+
if (this.node.setPosition) this.node.setPosition(x, this.options.topMargin);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (willOverflowHoriz || willOverflowVert) {
|
|
588
|
+
// Set scale but do NOT reposition node (preserve its absolute positions).
|
|
589
|
+
if (this.node.setScale) this.node.setScale(scale);
|
|
590
|
+
// Expand viewBox to contain entire unscaled content so nothing is clipped.
|
|
591
|
+
this._ensureViewboxFits(contentWidth, contentHeight);
|
|
592
|
+
// Reposition overlay toolbar in case viewBox/container changed
|
|
593
|
+
this._repositionOverlayToolbar();
|
|
594
|
+
|
|
595
|
+
// If content still exceeds available height in the host, allow vertical scrolling
|
|
596
|
+
if (willOverflowVert) {
|
|
597
|
+
this.container.style.overflowY = 'auto';
|
|
598
|
+
this.container.style.overflowX = 'hidden';
|
|
599
|
+
} else {
|
|
600
|
+
this.container.style.overflow = 'hidden';
|
|
601
|
+
}
|
|
602
|
+
if (this.options.debugExtents) this._drawDebugOverlays();
|
|
603
|
+
} else {
|
|
604
|
+
// Y is top margin; scaled content will grow downward
|
|
605
|
+
this.node.setPosition(x, this.options.topMargin);
|
|
606
|
+
|
|
607
|
+
// Reposition overlay toolbar (if any)
|
|
608
|
+
this._repositionOverlayToolbar();
|
|
609
|
+
|
|
610
|
+
// Ensure viewBox can contain the (unscaled) content to avoid clipping in some hosts
|
|
611
|
+
this._ensureViewboxFits(contentWidth, contentHeight);
|
|
612
|
+
|
|
613
|
+
if (totalNeededH > containerHeight) {
|
|
614
|
+
// Let the host scroll vertically; keep horizontal overflow hidden to avoid layout shift
|
|
615
|
+
this.container.style.overflowY = 'auto';
|
|
616
|
+
this.container.style.overflowX = 'hidden';
|
|
617
|
+
} else {
|
|
618
|
+
this.container.style.overflow = 'hidden';
|
|
619
|
+
}
|
|
620
|
+
if (this.options.debugExtents) this._drawDebugOverlays();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
this._lastCenterSignature = contentSig;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
_getMathInstance() {
|
|
627
|
+
if (this.options.math) {
|
|
628
|
+
return this.options.math;
|
|
629
|
+
}
|
|
630
|
+
if (typeof window !== 'undefined' && window.math) {
|
|
631
|
+
return window.math;
|
|
632
|
+
}
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
_createNodeFromSegment(segment, mathLib) {
|
|
637
|
+
try {
|
|
638
|
+
if (segment.includes('=')) {
|
|
639
|
+
return omdEquationNode.fromString(segment);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (!mathLib || typeof mathLib.parse !== 'function') {
|
|
643
|
+
throw new Error('math.js parser is unavailable');
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const ast = mathLib.parse(segment);
|
|
647
|
+
const NodeClass = getNodeForAST(ast);
|
|
648
|
+
return new NodeClass(ast);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
const reason = error?.message || String(error);
|
|
651
|
+
throw new Error(`Failed to parse expression "${segment}": ${reason}`, { cause: error });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
_createNodesFromString(expressionString) {
|
|
656
|
+
const segments = (expressionString || '')
|
|
657
|
+
.split(';')
|
|
658
|
+
.map(segment => segment.trim())
|
|
659
|
+
.filter(Boolean);
|
|
660
|
+
|
|
661
|
+
if (!segments.length) {
|
|
662
|
+
throw new Error('omdDisplay.render() received an empty expression string.');
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const mathLib = this._getMathInstance();
|
|
666
|
+
return segments.map(segment => this._createNodeFromSegment(segment, mathLib));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
_buildStackOptions() {
|
|
670
|
+
const baseOptions = {};
|
|
671
|
+
if (typeof this.options.stepVisualizer === 'boolean') {
|
|
672
|
+
baseOptions.stepVisualizer = this.options.stepVisualizer;
|
|
673
|
+
}
|
|
674
|
+
if (this.options.styling) {
|
|
675
|
+
baseOptions.styling = this.options.styling;
|
|
676
|
+
}
|
|
677
|
+
if (Object.prototype.hasOwnProperty.call(this.options, 'toolbar')) {
|
|
678
|
+
baseOptions.toolbar = this.options.toolbar;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (this.options.stackOptions && typeof this.options.stackOptions === 'object') {
|
|
682
|
+
return { ...baseOptions, ...this.options.stackOptions };
|
|
683
|
+
}
|
|
684
|
+
return baseOptions;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
_createStackFromSteps(steps) {
|
|
688
|
+
if (!steps || !steps.length) {
|
|
689
|
+
throw new Error('omdDisplay.render() received no steps to render.');
|
|
690
|
+
}
|
|
691
|
+
return new omdEquationStack(steps, this._buildStackOptions());
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
fitToContent() {
|
|
695
|
+
if (!this.node) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Try to get actual rendered dimensions
|
|
700
|
+
let actualWidth = 0;
|
|
701
|
+
let actualHeight = 0;
|
|
702
|
+
|
|
703
|
+
// Get both sequence and current step dimensions
|
|
704
|
+
let sequenceWidth = 0, sequenceHeight = 0;
|
|
705
|
+
let stepWidth = 0, stepHeight = 0;
|
|
706
|
+
|
|
707
|
+
if (this.node.getSequence) {
|
|
708
|
+
const sequence = this.node.getSequence();
|
|
709
|
+
if (sequence && sequence.width && sequence.height) {
|
|
710
|
+
// For step visualizers, use sequenceWidth/Height instead of total dimensions to exclude visualizer elements from autoscale
|
|
711
|
+
sequenceWidth = sequence.sequenceWidth || sequence.width;
|
|
712
|
+
sequenceHeight = sequence.sequenceHeight || sequence.height;
|
|
713
|
+
|
|
714
|
+
// Check current step dimensions too
|
|
715
|
+
if (sequence.getCurrentStep) {
|
|
716
|
+
const currentStep = sequence.getCurrentStep();
|
|
717
|
+
if (currentStep && currentStep.width && currentStep.height) {
|
|
718
|
+
// For step visualizers, prioritize sequenceWidth/Height for dimension calculations
|
|
719
|
+
stepWidth = sequence.sequenceWidth || currentStep.width;
|
|
720
|
+
stepHeight = sequence.sequenceHeight || currentStep.height;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Use the larger of sequence or step dimensions
|
|
725
|
+
actualWidth = Math.max(sequenceWidth, stepWidth);
|
|
726
|
+
actualHeight = Math.max(sequenceHeight, stepHeight);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Fallback to node dimensions only if sequence/step dimensions aren't available
|
|
731
|
+
if ((actualWidth === 0 || actualHeight === 0) && this.node.width && this.node.height) {
|
|
732
|
+
actualWidth = this.node.width;
|
|
733
|
+
actualHeight = this.node.height;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Fallback dimensions
|
|
737
|
+
if (actualWidth === 0 || actualHeight === 0) {
|
|
738
|
+
actualWidth = 200;
|
|
739
|
+
actualHeight = 60;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const padding = 10; // More comfortable padding to match user expectation
|
|
743
|
+
const newWidth = actualWidth + (padding * 2);
|
|
744
|
+
const newHeight = actualHeight + (padding * 2);
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
// Position the content at the minimal padding offset FIRST
|
|
748
|
+
if (this.node && this.node.setPosition) {
|
|
749
|
+
this.node.setPosition(padding, padding);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Update SVG dimensions with viewBox starting from 0,0 since we repositioned content
|
|
753
|
+
this.svg.setViewbox(newWidth, newHeight);
|
|
754
|
+
this.svg.setWidthAndHeight(newWidth, newHeight);
|
|
755
|
+
|
|
756
|
+
// Update container
|
|
757
|
+
this.container.style.width = `${newWidth}px`;
|
|
758
|
+
this.container.style.height = `${newHeight}px`;
|
|
759
|
+
if (this.options.debugExtents) this._drawDebugOverlays();
|
|
760
|
+
else this._clearDebugOverlays();
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* s a mathematical expression or equation
|
|
765
|
+
* @param {string|omdNode} expression - Expression string or node
|
|
766
|
+
* @returns {omdNode} The rendered node
|
|
767
|
+
*/
|
|
768
|
+
render(expression) {
|
|
769
|
+
// Clear previous node
|
|
770
|
+
if (this.node) {
|
|
771
|
+
if (this._contentGroup && this.node && this.node.svgObject) {
|
|
772
|
+
try {
|
|
773
|
+
if (this.node.svgObject.parentNode === this._contentGroup) {
|
|
774
|
+
this._contentGroup.removeChild(this.node.svgObject);
|
|
775
|
+
}
|
|
776
|
+
} catch (e) {
|
|
777
|
+
// Fallback to svg remove
|
|
778
|
+
this.svg.removeChild(this.node);
|
|
779
|
+
}
|
|
780
|
+
} else {
|
|
781
|
+
this.svg.removeChild(this.node);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Create node from expression
|
|
786
|
+
if (typeof expression === 'string') {
|
|
787
|
+
const steps = this._createNodesFromString(expression);
|
|
788
|
+
this.node = this._createStackFromSteps(steps);
|
|
789
|
+
} else if (Array.isArray(expression)) {
|
|
790
|
+
const steps = expression.flatMap(item => {
|
|
791
|
+
if (typeof item === 'string') {
|
|
792
|
+
return this._createNodesFromString(item);
|
|
793
|
+
}
|
|
794
|
+
return item;
|
|
795
|
+
}).filter(Boolean);
|
|
796
|
+
this.node = this._createStackFromSteps(steps);
|
|
797
|
+
} else {
|
|
798
|
+
// Assume it's already a node
|
|
799
|
+
this.node = expression;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (!this.node) {
|
|
803
|
+
throw new Error('omdDisplay.render() was unable to create a node from the provided expression.');
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Initialize and render
|
|
807
|
+
const sequence = this.node.getSequence ? this.node.getSequence() : null;
|
|
808
|
+
if (sequence) {
|
|
809
|
+
sequence.setFontSize(this.options.fontSize);
|
|
810
|
+
// Apply filtering based on filterLevel
|
|
811
|
+
sequence.updateStepsVisibility(step => (step.stepMark ?? 0) === sequence.getFilterLevel());
|
|
812
|
+
}
|
|
813
|
+
// Prefer appending the node's svgObject into our content group so DOM measurements are consistent
|
|
814
|
+
if (this._contentGroup && this.node && this.node.svgObject) {
|
|
815
|
+
try { this._contentGroup.appendChild(this.node.svgObject); } catch (e) { this.svg.addChild(this.node); }
|
|
816
|
+
} else {
|
|
817
|
+
this.svg.addChild(this.node);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Apply any stored font settings
|
|
821
|
+
if (this.options.fontFamily) {
|
|
822
|
+
this.setFont(this.options.fontFamily, this.options.fontWeight || '400');
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Only use fitToContent for tight sizing when explicitly requested
|
|
826
|
+
if (this.options.fitToContent) {
|
|
827
|
+
this.fitToContent();
|
|
828
|
+
} else if (this.options.centerContent) {
|
|
829
|
+
this.centerNode();
|
|
830
|
+
}
|
|
831
|
+
// Ensure overlay toolbar is positioned initially
|
|
832
|
+
this._repositionOverlayToolbar();
|
|
833
|
+
|
|
834
|
+
// Also ensure the viewBox is large enough to contain the node (avoid clipping)
|
|
835
|
+
const cw = (this.node && this.node.width) ? this.node.width : 0;
|
|
836
|
+
const ch = (this.node && this.node.height) ? this.node.height : 0;
|
|
837
|
+
this._ensureViewboxFits(cw, ch);
|
|
838
|
+
|
|
839
|
+
if (this.options.debugExtents) this._drawDebugOverlays();
|
|
840
|
+
else this._clearDebugOverlays();
|
|
841
|
+
|
|
842
|
+
// Provide a default global refresh function if not present
|
|
843
|
+
if (typeof window !== 'undefined' && !window.refreshDisplayAndFilters) {
|
|
844
|
+
window.refreshDisplayAndFilters = () => {
|
|
845
|
+
try {
|
|
846
|
+
const node = this.getCurrentNode();
|
|
847
|
+
const sequence = node?.getSequence ? node.getSequence() : null;
|
|
848
|
+
if (sequence) {
|
|
849
|
+
if (typeof sequence.simplifyAll === 'function') {
|
|
850
|
+
sequence.simplifyAll();
|
|
851
|
+
}
|
|
852
|
+
if (typeof sequence.updateStepsVisibility === 'function') {
|
|
853
|
+
sequence.updateStepsVisibility(step => (step.stepMark ?? 0) === 0);
|
|
854
|
+
}
|
|
855
|
+
if (typeof node.updateLayout === 'function') {
|
|
856
|
+
node.updateLayout();
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (this.options.centerContent) {
|
|
860
|
+
this.centerNode();
|
|
861
|
+
}
|
|
862
|
+
} catch (e) {
|
|
863
|
+
// no-op
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return this.node;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Add a jsvg child to the internal SVG container and optionally
|
|
873
|
+
* trigger layout/centering.
|
|
874
|
+
* @param {object} child - A jsvg node to add
|
|
875
|
+
*/
|
|
876
|
+
addChild(child) {
|
|
877
|
+
if (this._contentGroup && child && child.svgObject) {
|
|
878
|
+
try { this._contentGroup.appendChild(child.svgObject); } catch (e) { this.svg.addChild(child); }
|
|
879
|
+
} else {
|
|
880
|
+
this.svg.addChild(child);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (this.options.centerContent) this.centerNode();
|
|
884
|
+
|
|
885
|
+
return child;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Remove a child previously added to the internal SVG container.
|
|
890
|
+
* @param {object} child
|
|
891
|
+
*/
|
|
892
|
+
removeChild(child) {
|
|
893
|
+
if (!this.svg) return;
|
|
894
|
+
try {
|
|
895
|
+
if (child && child.svgObject) {
|
|
896
|
+
if (this._contentGroup && this._contentGroup.contains(child.svgObject)) {
|
|
897
|
+
this._contentGroup.removeChild(child.svgObject);
|
|
898
|
+
} else if (this.svg.svgObject && this.svg.svgObject.contains(child.svgObject)) {
|
|
899
|
+
this.svg.svgObject.removeChild(child.svgObject);
|
|
900
|
+
} else if (typeof this.svg.removeChild === 'function') {
|
|
901
|
+
this.svg.removeChild(child);
|
|
902
|
+
}
|
|
903
|
+
} else if (typeof this.svg.removeChild === 'function') {
|
|
904
|
+
this.svg.removeChild(child);
|
|
905
|
+
}
|
|
906
|
+
} catch (e) {
|
|
907
|
+
// no-op
|
|
908
|
+
}
|
|
909
|
+
// If the removed child was the main node, clear reference
|
|
910
|
+
if (this.node === child) this.node = null;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Updates the display with a new node
|
|
915
|
+
* @param {omdNode} newNode - The new node to display
|
|
916
|
+
*/
|
|
917
|
+
update(newNode) {
|
|
918
|
+
if (this.node) {
|
|
919
|
+
if (this._contentGroup && this.node && this.node.svgObject && this._contentGroup.contains(this.node.svgObject)) {
|
|
920
|
+
this._contentGroup.removeChild(this.node.svgObject);
|
|
921
|
+
} else if (typeof this.svg.removeChild === 'function') {
|
|
922
|
+
this.svg.removeChild(this.node);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
this.node = newNode;
|
|
927
|
+
this.node.setFontSize(this.options.fontSize);
|
|
928
|
+
this.node.initialize();
|
|
929
|
+
if (this._contentGroup && this.node && this.node.svgObject) {
|
|
930
|
+
try { this._contentGroup.appendChild(this.node.svgObject); } catch (e) { this.svg.addChild(this.node); }
|
|
931
|
+
} else {
|
|
932
|
+
this.svg.addChild(this.node);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (this.options.centerContent) {
|
|
936
|
+
this.centerNode();
|
|
937
|
+
}
|
|
938
|
+
// Ensure overlay toolbar is positioned on updates
|
|
939
|
+
this._repositionOverlayToolbar();
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Sets the font size
|
|
945
|
+
* @param {number} size - The font size
|
|
946
|
+
*/
|
|
947
|
+
setFontSize(size) {
|
|
948
|
+
this.options.fontSize = size;
|
|
949
|
+
if (this.node) {
|
|
950
|
+
// Apply font size - handle different node types
|
|
951
|
+
if (this.node.getSequence && typeof this.node.getSequence === 'function') {
|
|
952
|
+
// For omdEquationStack, set font size on the sequence
|
|
953
|
+
this.node.getSequence().setFontSize(size);
|
|
954
|
+
} else if (this.node.setFontSize && typeof this.node.setFontSize === 'function') {
|
|
955
|
+
// For regular nodes with setFontSize method
|
|
956
|
+
this.node.setFontSize(size);
|
|
957
|
+
}
|
|
958
|
+
this.node.initialize();
|
|
959
|
+
if (this.options.centerContent) {
|
|
960
|
+
this.centerNode();
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Sets the font family for all elements in the display
|
|
967
|
+
* @param {string} fontFamily - CSS font-family string (e.g., '"Shantell Sans", cursive')
|
|
968
|
+
* @param {string} fontWeight - CSS font-weight (default: '400')
|
|
969
|
+
*/
|
|
970
|
+
setFont(fontFamily, fontWeight = '400') {
|
|
971
|
+
if (this.svg?.svgObject) {
|
|
972
|
+
const applyFont = (element) => {
|
|
973
|
+
if (element.style) {
|
|
974
|
+
element.style.fontFamily = fontFamily;
|
|
975
|
+
element.style.fontWeight = fontWeight;
|
|
976
|
+
}
|
|
977
|
+
// Recursively apply to all children
|
|
978
|
+
Array.from(element.children || []).forEach(applyFont);
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
// Apply font to the entire SVG
|
|
982
|
+
applyFont(this.svg.svgObject);
|
|
983
|
+
|
|
984
|
+
// Store font settings for future use
|
|
985
|
+
this.options.fontFamily = fontFamily;
|
|
986
|
+
this.options.fontWeight = fontWeight;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Clears the display
|
|
992
|
+
*/
|
|
993
|
+
clear() {
|
|
994
|
+
if (this.node) {
|
|
995
|
+
this.svg.removeChild(this.node);
|
|
996
|
+
this.node = null;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Destroys the renderer and cleans up resources
|
|
1002
|
+
*/
|
|
1003
|
+
destroy() {
|
|
1004
|
+
this.clear();
|
|
1005
|
+
if (this.resizeObserver) {
|
|
1006
|
+
this.resizeObserver.disconnect();
|
|
1007
|
+
}
|
|
1008
|
+
if (this.container.contains(this.svg.svgObject)) {
|
|
1009
|
+
this.container.removeChild(this.svg.svgObject);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Repositions overlay toolbar if current node supports it
|
|
1015
|
+
* @private
|
|
1016
|
+
*/
|
|
1017
|
+
_repositionOverlayToolbar() {
|
|
1018
|
+
// Use same width calculation as centering to ensure consistency
|
|
1019
|
+
const containerWidth = this.container.offsetWidth || 0;
|
|
1020
|
+
const containerHeight = this.container.offsetHeight || 0;
|
|
1021
|
+
const node = this.node;
|
|
1022
|
+
if (!node) return;
|
|
1023
|
+
const hasOverlayApi = typeof node.isToolbarOverlay === 'function' && typeof node.positionToolbarOverlay === 'function';
|
|
1024
|
+
if (hasOverlayApi && node.isToolbarOverlay()) {
|
|
1025
|
+
const padding = (typeof node.getOverlayPadding === 'function') ? node.getOverlayPadding() : 16;
|
|
1026
|
+
node.positionToolbarOverlay(containerWidth, containerHeight, padding);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Public API: returns the currently rendered root node (could be a step visualizer, sequence, or plain node)
|
|
1032
|
+
* @returns {object|null}
|
|
1033
|
+
*/
|
|
1034
|
+
getCurrentNode() {
|
|
1035
|
+
return this.node;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Returns the SVG element for the entire display.
|
|
1040
|
+
* @returns {SVGElement} The SVG element representing the display.
|
|
1041
|
+
*/
|
|
1042
|
+
getSVG() {
|
|
1043
|
+
return this.svg.svgObject;
|
|
1044
|
+
}
|
|
1045
|
+
}
|