@teachinglab/omd 0.2.6 → 0.2.7
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.
|
@@ -34,7 +34,7 @@ export class omdEquationStack extends jsvgGroup {
|
|
|
34
34
|
|
|
35
35
|
// The sequence is the core. If a visualizer is needed, that's our sequence.
|
|
36
36
|
if (options.stepVisualizer) {
|
|
37
|
-
this.sequence = new omdStepVisualizer(steps);
|
|
37
|
+
this.sequence = new omdStepVisualizer(steps, this.stylingOptions || {});
|
|
38
38
|
} else {
|
|
39
39
|
this.sequence = new omdEquationSequenceNode(steps);
|
|
40
40
|
}
|
|
@@ -41,6 +41,19 @@ export class omdDisplay {
|
|
|
41
41
|
this.svg.svgObject.style.overflow = 'hidden';
|
|
42
42
|
this.container.appendChild(this.svg.svgObject);
|
|
43
43
|
|
|
44
|
+
// Create a dedicated content group we can translate to compensate for
|
|
45
|
+
// viewBox origin changes (so expanding the origin doesn't visually move content).
|
|
46
|
+
try {
|
|
47
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
48
|
+
this._contentGroup = document.createElementNS(ns, 'g');
|
|
49
|
+
this._contentGroup.setAttribute('id', 'omd-content-root');
|
|
50
|
+
this.svg.svgObject.appendChild(this._contentGroup);
|
|
51
|
+
this._contentOffsetX = 0;
|
|
52
|
+
this._contentOffsetY = 0;
|
|
53
|
+
} catch (e) {
|
|
54
|
+
this._contentGroup = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
44
57
|
// Handle resize
|
|
45
58
|
if (window.ResizeObserver) {
|
|
46
59
|
this.resizeObserver = new ResizeObserver(() => {
|
|
@@ -61,6 +74,324 @@ export class omdDisplay {
|
|
|
61
74
|
|
|
62
75
|
// Reposition overlay toolbar (if any) on resize
|
|
63
76
|
this._repositionOverlayToolbar();
|
|
77
|
+
if (this.options.debugExtents) this._drawDebugOverlays();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Ensure the internal SVG viewBox is at least as large as the provided content dimensions.
|
|
82
|
+
* This prevents clipping when content is larger than the current viewBox.
|
|
83
|
+
* @param {number} contentWidth
|
|
84
|
+
* @param {number} contentHeight
|
|
85
|
+
*/
|
|
86
|
+
_ensureViewboxFits(contentWidth, contentHeight) {
|
|
87
|
+
// If caller provided just width/height, but we prefer extents, bail early
|
|
88
|
+
if (!this.node) return;
|
|
89
|
+
const pad = 10;
|
|
90
|
+
|
|
91
|
+
// Prefer DOM measured extents (accounts for strokes, transforms, children SVG geometry)
|
|
92
|
+
let ext = null;
|
|
93
|
+
try {
|
|
94
|
+
const collected = this._collectNodeExtents(this.node);
|
|
95
|
+
if (collected) {
|
|
96
|
+
ext = { minX: collected.minX, minY: collected.minY, maxX: collected.maxX, maxY: collected.maxY };
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {
|
|
99
|
+
ext = null;
|
|
100
|
+
}
|
|
101
|
+
if (!ext) {
|
|
102
|
+
ext = this._computeNodeExtents(this.node);
|
|
103
|
+
}
|
|
104
|
+
if (!ext) return;
|
|
105
|
+
|
|
106
|
+
const minX = Math.floor(ext.minX - pad);
|
|
107
|
+
const minY = Math.floor(ext.minY - pad);
|
|
108
|
+
const maxX = Math.ceil(ext.maxX + pad);
|
|
109
|
+
const maxY = Math.ceil(ext.maxY + pad);
|
|
110
|
+
|
|
111
|
+
const curView = this.svg.svgObject.getAttribute('viewBox') || '';
|
|
112
|
+
let curX = 0, curY = 0, curW = 0, curH = 0;
|
|
113
|
+
if (curView) {
|
|
114
|
+
const parts = curView.split(/\s+/).map(Number).filter(n => !isNaN(n));
|
|
115
|
+
if (parts.length === 4) {
|
|
116
|
+
curX = parts[0];
|
|
117
|
+
curY = parts[1];
|
|
118
|
+
curW = parts[2];
|
|
119
|
+
curH = parts[3];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// To avoid shifting visible content, keep the current viewBox origin (curX,curY)
|
|
124
|
+
// and only expand width/height as needed. Changing the origin would change
|
|
125
|
+
// the mapping from SVG coordinates to screen coordinates and appear to move
|
|
126
|
+
// existing content.
|
|
127
|
+
const desiredX = curX;
|
|
128
|
+
const desiredY = curY;
|
|
129
|
+
const desiredRight = Math.max(curX + curW, maxX);
|
|
130
|
+
const desiredBottom = Math.max(curY + curH, maxY);
|
|
131
|
+
const desiredW = Math.max(curW, desiredRight - desiredX);
|
|
132
|
+
const desiredH = Math.max(curH, desiredBottom - desiredY);
|
|
133
|
+
|
|
134
|
+
if (desiredW !== curW || desiredH !== curH) {
|
|
135
|
+
this.svg.svgObject.setAttribute('viewBox', `${desiredX} ${desiredY} ${desiredW} ${desiredH}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Walk the node tree and compute absolute extents in SVG coordinates.
|
|
141
|
+
* Uses `xpos`/`ypos` and `width`/`height` properties; falls back to 0 when missing.
|
|
142
|
+
* @param {omdNode} root
|
|
143
|
+
* @returns {{minX:number,minY:number,maxX:number,maxY:number}}
|
|
144
|
+
*/
|
|
145
|
+
_computeNodeExtents(root) {
|
|
146
|
+
if (!root) return null;
|
|
147
|
+
const visited = new Set();
|
|
148
|
+
const stack = [{ node: root, absX: root.xpos || 0, absY: root.ypos || 0 }];
|
|
149
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
150
|
+
|
|
151
|
+
while (stack.length) {
|
|
152
|
+
const { node, absX, absY } = stack.pop();
|
|
153
|
+
if (!node || visited.has(node)) continue;
|
|
154
|
+
visited.add(node);
|
|
155
|
+
|
|
156
|
+
const w = node.width || 0;
|
|
157
|
+
const h = node.height || 0;
|
|
158
|
+
const nx = absX;
|
|
159
|
+
const ny = absY;
|
|
160
|
+
minX = Math.min(minX, nx);
|
|
161
|
+
minY = Math.min(minY, ny);
|
|
162
|
+
maxX = Math.max(maxX, nx + w);
|
|
163
|
+
maxY = Math.max(maxY, ny + h);
|
|
164
|
+
|
|
165
|
+
// push children
|
|
166
|
+
if (Array.isArray(node.childList)) {
|
|
167
|
+
for (const c of node.childList) {
|
|
168
|
+
if (!c) continue;
|
|
169
|
+
const cx = (c.xpos || 0) + nx;
|
|
170
|
+
const cy = (c.ypos || 0) + ny;
|
|
171
|
+
stack.push({ node: c, absX: cx, absY: cy });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (node.argumentNodeList) {
|
|
175
|
+
for (const val of Object.values(node.argumentNodeList)) {
|
|
176
|
+
if (Array.isArray(val)) {
|
|
177
|
+
for (const v of val) {
|
|
178
|
+
if (!v) continue;
|
|
179
|
+
const vx = (v.xpos || 0) + nx;
|
|
180
|
+
const vy = (v.ypos || 0) + ny;
|
|
181
|
+
stack.push({ node: v, absX: vx, absY: vy });
|
|
182
|
+
}
|
|
183
|
+
} else if (val) {
|
|
184
|
+
const vx = (val.xpos || 0) + nx;
|
|
185
|
+
const vy = (val.ypos || 0) + ny;
|
|
186
|
+
stack.push({ node: val, absX: vx, absY: vy });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (minX === Infinity) return null;
|
|
193
|
+
return { minX, minY, maxX, maxY };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Collect extents for each node and return per-node list plus overall extents.
|
|
198
|
+
* Useful for debugging elements that extend outside parent coordinates.
|
|
199
|
+
* @param {omdNode} root
|
|
200
|
+
* @returns {{nodes:Array, minX:number, minY:number, maxX:number, maxY:number}}
|
|
201
|
+
*/
|
|
202
|
+
_collectNodeExtents(root) {
|
|
203
|
+
if (!root) return null;
|
|
204
|
+
const visited = new Set();
|
|
205
|
+
const stack = [{ node: root, absX: root.xpos || 0, absY: root.ypos || 0, parent: null }];
|
|
206
|
+
const nodes = [];
|
|
207
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
while (stack.length) {
|
|
211
|
+
const { node, absX, absY, parent } = stack.pop();
|
|
212
|
+
if (!node || visited.has(node)) continue;
|
|
213
|
+
visited.add(node);
|
|
214
|
+
|
|
215
|
+
// Prefer DOM measurement if available for accuracy
|
|
216
|
+
let nx = absX;
|
|
217
|
+
let ny = absY;
|
|
218
|
+
let nminX = nx;
|
|
219
|
+
let nminY = ny;
|
|
220
|
+
let nmaxX = nx;
|
|
221
|
+
let nmaxY = ny;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
if (node.svgObject && typeof node.svgObject.getBBox === 'function' && typeof node.svgObject.getCTM === 'function') {
|
|
225
|
+
const bbox = node.svgObject.getBBox();
|
|
226
|
+
const ctm = node.svgObject.getCTM();
|
|
227
|
+
// Transform all four bbox corners into root SVG coordinates
|
|
228
|
+
const corners = [
|
|
229
|
+
{ x: bbox.x, y: bbox.y },
|
|
230
|
+
{ x: bbox.x + bbox.width, y: bbox.y },
|
|
231
|
+
{ x: bbox.x, y: bbox.y + bbox.height },
|
|
232
|
+
{ x: bbox.x + bbox.width, y: bbox.y + bbox.height }
|
|
233
|
+
];
|
|
234
|
+
const tx = corners.map(p => ({
|
|
235
|
+
x: ctm.a * p.x + ctm.c * p.y + ctm.e,
|
|
236
|
+
y: ctm.b * p.x + ctm.d * p.y + ctm.f
|
|
237
|
+
}));
|
|
238
|
+
nminX = Math.min(...tx.map(t => t.x));
|
|
239
|
+
nminY = Math.min(...tx.map(t => t.y));
|
|
240
|
+
nmaxX = Math.max(...tx.map(t => t.x));
|
|
241
|
+
nmaxY = Math.max(...tx.map(t => t.y));
|
|
242
|
+
nx = nminX;
|
|
243
|
+
ny = nminY;
|
|
244
|
+
} else {
|
|
245
|
+
const w = node.width || 0;
|
|
246
|
+
const h = node.height || 0;
|
|
247
|
+
nx = absX;
|
|
248
|
+
ny = absY;
|
|
249
|
+
nminX = nx;
|
|
250
|
+
nminY = ny;
|
|
251
|
+
nmaxX = nx + w;
|
|
252
|
+
nmaxY = ny + h;
|
|
253
|
+
}
|
|
254
|
+
} catch (e) {
|
|
255
|
+
const w = node.width || 0;
|
|
256
|
+
const h = node.height || 0;
|
|
257
|
+
nx = absX;
|
|
258
|
+
ny = absY;
|
|
259
|
+
nminX = nx;
|
|
260
|
+
nminY = ny;
|
|
261
|
+
nmaxX = nx + w;
|
|
262
|
+
nmaxY = ny + h;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
nodes.push({ node, minX: nminX, minY: nminY, maxX: nmaxX, maxY: nmaxY, parent });
|
|
266
|
+
|
|
267
|
+
minX = Math.min(minX, nminX);
|
|
268
|
+
minY = Math.min(minY, nminY);
|
|
269
|
+
maxX = Math.max(maxX, nmaxX);
|
|
270
|
+
maxY = Math.max(maxY, nmaxY);
|
|
271
|
+
|
|
272
|
+
// push children
|
|
273
|
+
if (Array.isArray(node.childList)) {
|
|
274
|
+
for (const c of node.childList) {
|
|
275
|
+
if (!c) continue;
|
|
276
|
+
const cx = (c.xpos || 0) + nx;
|
|
277
|
+
const cy = (c.ypos || 0) + ny;
|
|
278
|
+
stack.push({ node: c, absX: cx, absY: cy, parent: node });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (node.argumentNodeList) {
|
|
282
|
+
for (const val of Object.values(node.argumentNodeList)) {
|
|
283
|
+
if (Array.isArray(val)) {
|
|
284
|
+
for (const v of val) {
|
|
285
|
+
if (!v) continue;
|
|
286
|
+
const vx = (v.xpos || 0) + nx;
|
|
287
|
+
const vy = (v.ypos || 0) + ny;
|
|
288
|
+
stack.push({ node: v, absX: vx, absY: vy, parent: node });
|
|
289
|
+
}
|
|
290
|
+
} else if (val) {
|
|
291
|
+
const vx = (val.xpos || 0) + nx;
|
|
292
|
+
const vy = (val.ypos || 0) + ny;
|
|
293
|
+
stack.push({ node: val, absX: vx, absY: vy, parent: node });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (minX === Infinity) return null;
|
|
300
|
+
return { nodes, minX, minY, maxX, maxY };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
_clearDebugOverlays() {
|
|
304
|
+
if (!this.svg || !this.svg.svgObject) return;
|
|
305
|
+
const existing = this.svg.svgObject.querySelector('#omd-debug-overlays');
|
|
306
|
+
if (existing) existing.remove();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_drawDebugOverlays() {
|
|
310
|
+
if (!this.options.debugExtents) return;
|
|
311
|
+
if (!this.svg || !this.svg.svgObject || !this.node) return;
|
|
312
|
+
|
|
313
|
+
this._clearDebugOverlays();
|
|
314
|
+
|
|
315
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
316
|
+
const group = document.createElementNS(ns, 'g');
|
|
317
|
+
group.setAttribute('id', 'omd-debug-overlays');
|
|
318
|
+
group.setAttribute('pointer-events', 'none');
|
|
319
|
+
|
|
320
|
+
// overall node extents
|
|
321
|
+
const collected = this._collectNodeExtents(this.node);
|
|
322
|
+
if (!collected) return;
|
|
323
|
+
|
|
324
|
+
const { nodes, minX, minY, maxX, maxY } = collected;
|
|
325
|
+
|
|
326
|
+
// Draw content extents (blue dashed)
|
|
327
|
+
const contentRect = document.createElementNS(ns, 'rect');
|
|
328
|
+
contentRect.setAttribute('x', String(minX));
|
|
329
|
+
contentRect.setAttribute('y', String(minY));
|
|
330
|
+
contentRect.setAttribute('width', String(maxX - minX));
|
|
331
|
+
contentRect.setAttribute('height', String(maxY - minY));
|
|
332
|
+
contentRect.setAttribute('fill', 'none');
|
|
333
|
+
contentRect.setAttribute('stroke', 'blue');
|
|
334
|
+
contentRect.setAttribute('stroke-dasharray', '6 4');
|
|
335
|
+
contentRect.setAttribute('stroke-width', '0.8');
|
|
336
|
+
group.appendChild(contentRect);
|
|
337
|
+
|
|
338
|
+
// Draw viewBox rect (orange)
|
|
339
|
+
const curView = this.svg.svgObject.getAttribute('viewBox') || '';
|
|
340
|
+
if (curView) {
|
|
341
|
+
const parts = curView.split(/\s+/).map(Number).filter(n => !isNaN(n));
|
|
342
|
+
if (parts.length === 4) {
|
|
343
|
+
const [vx, vy, vw, vh] = parts;
|
|
344
|
+
const vbRect = document.createElementNS(ns, 'rect');
|
|
345
|
+
vbRect.setAttribute('x', String(vx));
|
|
346
|
+
vbRect.setAttribute('y', String(vy));
|
|
347
|
+
vbRect.setAttribute('width', String(vw));
|
|
348
|
+
vbRect.setAttribute('height', String(vh));
|
|
349
|
+
vbRect.setAttribute('fill', 'none');
|
|
350
|
+
vbRect.setAttribute('stroke', 'orange');
|
|
351
|
+
vbRect.setAttribute('stroke-width', '1');
|
|
352
|
+
vbRect.setAttribute('opacity', '0.9');
|
|
353
|
+
group.appendChild(vbRect);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Per-node boxes: green if inside parent, red if overflowing parent bounds
|
|
358
|
+
const overflowing = [];
|
|
359
|
+
for (const item of nodes) {
|
|
360
|
+
const r = document.createElementNS(ns, 'rect');
|
|
361
|
+
r.setAttribute('x', String(item.minX));
|
|
362
|
+
r.setAttribute('y', String(item.minY));
|
|
363
|
+
r.setAttribute('width', String(Math.max(0, item.maxX - item.minX)));
|
|
364
|
+
r.setAttribute('height', String(Math.max(0, item.maxY - item.minY)));
|
|
365
|
+
r.setAttribute('fill', 'none');
|
|
366
|
+
r.setAttribute('stroke-width', '0.6');
|
|
367
|
+
|
|
368
|
+
let stroke = 'green';
|
|
369
|
+
if (item.parent) {
|
|
370
|
+
const pMinX = (item.parent.xpos || 0) + (item.parent._absX || 0);
|
|
371
|
+
const pMinY = (item.parent.ypos || 0) + (item.parent._absY || 0);
|
|
372
|
+
// fallback compute parent's absX/Y from nodes list if available
|
|
373
|
+
const parentEntry = nodes.find(n => n.node === item.parent);
|
|
374
|
+
const pminX = parentEntry ? parentEntry.minX : pMinX;
|
|
375
|
+
const pminY = parentEntry ? parentEntry.minY : pMinY;
|
|
376
|
+
const pmaxX = parentEntry ? parentEntry.maxX : pminX + (item.parent.width || 0);
|
|
377
|
+
const pmaxY = parentEntry ? parentEntry.maxY : pminY + (item.parent.height || 0);
|
|
378
|
+
|
|
379
|
+
if (item.minX < pminX || item.minY < pminY || item.maxX > pmaxX || item.maxY > pmaxY) {
|
|
380
|
+
stroke = 'red';
|
|
381
|
+
overflowing.push({ node: item.node, bounds: item });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
r.setAttribute('stroke', stroke);
|
|
386
|
+
r.setAttribute('opacity', stroke === 'red' ? '0.9' : '0.6');
|
|
387
|
+
group.appendChild(r);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (overflowing.length) {
|
|
391
|
+
console.warn('omdDisplay: debugExtents found overflowing nodes:', overflowing.map(o => ({ type: o.node?.type, bounds: o.bounds })));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
this.svg.svgObject.appendChild(group);
|
|
64
395
|
}
|
|
65
396
|
|
|
66
397
|
centerNode() {
|
|
@@ -124,20 +455,49 @@ export class omdDisplay {
|
|
|
124
455
|
x = (containerWidth - scaledWidth) / 2;
|
|
125
456
|
}
|
|
126
457
|
|
|
127
|
-
//
|
|
128
|
-
|
|
458
|
+
// Decide whether positioning would move content outside container. If so,
|
|
459
|
+
// prefer expanding the SVG viewBox instead of moving nodes.
|
|
460
|
+
const scaledWidthFinal = (this.node.width || contentWidth) * scale;
|
|
461
|
+
const scaledHeightFinal = (this.node.height || contentHeight) * scale;
|
|
462
|
+
const totalNeededH = scaledHeightFinal + (this.options.topMargin || 0) + (this.options.bottomMargin || 0);
|
|
463
|
+
|
|
464
|
+
const willOverflowHoriz = scaledWidthFinal > containerWidth;
|
|
465
|
+
const willOverflowVert = totalNeededH > containerHeight;
|
|
466
|
+
|
|
467
|
+
if (willOverflowHoriz || willOverflowVert) {
|
|
468
|
+
// Set scale but do NOT reposition node (preserve its absolute positions).
|
|
469
|
+
if (this.node.setScale) this.node.setScale(scale);
|
|
470
|
+
// Expand viewBox to contain entire unscaled content so nothing is clipped.
|
|
471
|
+
this._ensureViewboxFits(contentWidth, contentHeight);
|
|
472
|
+
// Reposition overlay toolbar in case viewBox/container changed
|
|
473
|
+
this._repositionOverlayToolbar();
|
|
474
|
+
|
|
475
|
+
// If content still exceeds available height in the host, allow vertical scrolling
|
|
476
|
+
if (willOverflowVert) {
|
|
477
|
+
this.container.style.overflowY = 'auto';
|
|
478
|
+
this.container.style.overflowX = 'hidden';
|
|
479
|
+
} else {
|
|
480
|
+
this.container.style.overflow = 'hidden';
|
|
481
|
+
}
|
|
482
|
+
if (this.options.debugExtents) this._drawDebugOverlays();
|
|
483
|
+
} else {
|
|
484
|
+
// Y is top margin; scaled content will grow downward
|
|
485
|
+
this.node.setPosition(x, this.options.topMargin);
|
|
486
|
+
|
|
487
|
+
// Reposition overlay toolbar (if any)
|
|
488
|
+
this._repositionOverlayToolbar();
|
|
129
489
|
|
|
130
|
-
|
|
131
|
-
|
|
490
|
+
// Ensure viewBox can contain the (unscaled) content to avoid clipping in some hosts
|
|
491
|
+
this._ensureViewboxFits(contentWidth, contentHeight);
|
|
132
492
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
this.
|
|
493
|
+
if (totalNeededH > containerHeight) {
|
|
494
|
+
// Let the host scroll vertically; keep horizontal overflow hidden to avoid layout shift
|
|
495
|
+
this.container.style.overflowY = 'auto';
|
|
496
|
+
this.container.style.overflowX = 'hidden';
|
|
497
|
+
} else {
|
|
498
|
+
this.container.style.overflow = 'hidden';
|
|
499
|
+
}
|
|
500
|
+
if (this.options.debugExtents) this._drawDebugOverlays();
|
|
141
501
|
}
|
|
142
502
|
}
|
|
143
503
|
|
|
@@ -204,6 +564,8 @@ export class omdDisplay {
|
|
|
204
564
|
// Update container
|
|
205
565
|
this.container.style.width = `${newWidth}px`;
|
|
206
566
|
this.container.style.height = `${newHeight}px`;
|
|
567
|
+
if (this.options.debugExtents) this._drawDebugOverlays();
|
|
568
|
+
else this._clearDebugOverlays();
|
|
207
569
|
}
|
|
208
570
|
|
|
209
571
|
/**
|
|
@@ -214,7 +576,11 @@ export class omdDisplay {
|
|
|
214
576
|
render(expression) {
|
|
215
577
|
// Clear previous node
|
|
216
578
|
if (this.node) {
|
|
217
|
-
this.
|
|
579
|
+
if (this._contentGroup && this.node && this.node.svgObject) {
|
|
580
|
+
this._contentGroup.removeChild(this.node.svgObject);
|
|
581
|
+
} else {
|
|
582
|
+
this.svg.removeChild(this.node);
|
|
583
|
+
}
|
|
218
584
|
}
|
|
219
585
|
|
|
220
586
|
// Create node from expression
|
|
@@ -223,18 +589,18 @@ export class omdDisplay {
|
|
|
223
589
|
// Multiple equations
|
|
224
590
|
const equationStrings = expression.split(';').filter(s => s.trim() !== '');
|
|
225
591
|
const steps = equationStrings.map(str => omdEquationNode.fromString(str));
|
|
226
|
-
this.node = new omdStepVisualizer(steps);
|
|
592
|
+
this.node = new omdStepVisualizer(steps, this.options.styling || {});
|
|
227
593
|
} else {
|
|
228
594
|
// Single expression or equation
|
|
229
595
|
if (expression.includes('=')) {
|
|
230
596
|
const firstStep = omdEquationNode.fromString(expression);
|
|
231
|
-
this.node = new omdStepVisualizer([firstStep]);
|
|
597
|
+
this.node = new omdStepVisualizer([firstStep], this.options.styling || {});
|
|
232
598
|
} else {
|
|
233
599
|
// Create node directly from expression
|
|
234
600
|
const parsedAST = math.parse(expression);
|
|
235
601
|
const NodeClass = getNodeForAST(parsedAST);
|
|
236
602
|
const firstStep = new NodeClass(parsedAST);
|
|
237
|
-
this.node = new omdStepVisualizer([firstStep]);
|
|
603
|
+
this.node = new omdStepVisualizer([firstStep], this.options.styling || {});
|
|
238
604
|
}
|
|
239
605
|
}
|
|
240
606
|
} else {
|
|
@@ -249,7 +615,12 @@ export class omdDisplay {
|
|
|
249
615
|
// Apply filtering based on filterLevel
|
|
250
616
|
sequence.updateStepsVisibility(step => (step.stepMark ?? 0) === sequence.getFilterLevel());
|
|
251
617
|
}
|
|
252
|
-
|
|
618
|
+
// Prefer appending the node's svgObject into our content group so DOM measurements are consistent
|
|
619
|
+
if (this._contentGroup && this.node && this.node.svgObject) {
|
|
620
|
+
try { this._contentGroup.appendChild(this.node.svgObject); } catch (e) { this.svg.addChild(this.node); }
|
|
621
|
+
} else {
|
|
622
|
+
this.svg.addChild(this.node);
|
|
623
|
+
}
|
|
253
624
|
|
|
254
625
|
// Apply any stored font settings
|
|
255
626
|
if (this.options.fontFamily) {
|
|
@@ -268,6 +639,14 @@ export class omdDisplay {
|
|
|
268
639
|
// Ensure overlay toolbar is positioned initially
|
|
269
640
|
this._repositionOverlayToolbar();
|
|
270
641
|
|
|
642
|
+
// Also ensure the viewBox is large enough to contain the node (avoid clipping)
|
|
643
|
+
const cw = (this.node && this.node.width) ? this.node.width : 0;
|
|
644
|
+
const ch = (this.node && this.node.height) ? this.node.height : 0;
|
|
645
|
+
this._ensureViewboxFits(cw, ch);
|
|
646
|
+
|
|
647
|
+
if (this.options.debugExtents) this._drawDebugOverlays();
|
|
648
|
+
else this._clearDebugOverlays();
|
|
649
|
+
|
|
271
650
|
// Provide a default global refresh function if not present
|
|
272
651
|
if (typeof window !== 'undefined' && !window.refreshDisplayAndFilters) {
|
|
273
652
|
window.refreshDisplayAndFilters = () => {
|
|
@@ -282,7 +661,7 @@ export class omdDisplay {
|
|
|
282
661
|
sequence.updateStepsVisibility(step => (step.stepMark ?? 0) === 0);
|
|
283
662
|
}
|
|
284
663
|
if (typeof node.updateLayout === 'function') {
|
|
285
|
-
|
|
664
|
+
node.updateLayout();
|
|
286
665
|
}
|
|
287
666
|
}
|
|
288
667
|
if (this.options.centerContent) {
|
|
@@ -303,7 +682,11 @@ export class omdDisplay {
|
|
|
303
682
|
* @param {object} child - A jsvg node to add
|
|
304
683
|
*/
|
|
305
684
|
addChild(child) {
|
|
306
|
-
this.
|
|
685
|
+
if (this._contentGroup && child && child.svgObject) {
|
|
686
|
+
try { this._contentGroup.appendChild(child.svgObject); } catch (e) { this.svg.addChild(child); }
|
|
687
|
+
} else {
|
|
688
|
+
this.svg.addChild(child);
|
|
689
|
+
}
|
|
307
690
|
|
|
308
691
|
if (this.options.centerContent) this.centerNode();
|
|
309
692
|
|
|
@@ -317,10 +700,16 @@ export class omdDisplay {
|
|
|
317
700
|
removeChild(child) {
|
|
318
701
|
if (!this.svg) return;
|
|
319
702
|
try {
|
|
320
|
-
if (
|
|
703
|
+
if (child && child.svgObject) {
|
|
704
|
+
if (this._contentGroup && this._contentGroup.contains(child.svgObject)) {
|
|
705
|
+
this._contentGroup.removeChild(child.svgObject);
|
|
706
|
+
} else if (this.svg.svgObject && this.svg.svgObject.contains(child.svgObject)) {
|
|
707
|
+
this.svg.svgObject.removeChild(child.svgObject);
|
|
708
|
+
} else if (typeof this.svg.removeChild === 'function') {
|
|
709
|
+
this.svg.removeChild(child);
|
|
710
|
+
}
|
|
711
|
+
} else if (typeof this.svg.removeChild === 'function') {
|
|
321
712
|
this.svg.removeChild(child);
|
|
322
|
-
} else if (child && child.svgObject && this.svg.svgObject && this.svg.svgObject.contains(child.svgObject)) {
|
|
323
|
-
this.svg.svgObject.removeChild(child.svgObject);
|
|
324
713
|
}
|
|
325
714
|
} catch (e) {
|
|
326
715
|
// no-op
|
|
@@ -335,13 +724,21 @@ export class omdDisplay {
|
|
|
335
724
|
*/
|
|
336
725
|
update(newNode) {
|
|
337
726
|
if (this.node) {
|
|
338
|
-
this.
|
|
727
|
+
if (this._contentGroup && this.node && this.node.svgObject && this._contentGroup.contains(this.node.svgObject)) {
|
|
728
|
+
this._contentGroup.removeChild(this.node.svgObject);
|
|
729
|
+
} else if (typeof this.svg.removeChild === 'function') {
|
|
730
|
+
this.svg.removeChild(this.node);
|
|
731
|
+
}
|
|
339
732
|
}
|
|
340
733
|
|
|
341
734
|
this.node = newNode;
|
|
342
735
|
this.node.setFontSize(this.options.fontSize);
|
|
343
736
|
this.node.initialize();
|
|
344
|
-
this.
|
|
737
|
+
if (this._contentGroup && this.node && this.node.svgObject) {
|
|
738
|
+
try { this._contentGroup.appendChild(this.node.svgObject); } catch (e) { this.svg.addChild(this.node); }
|
|
739
|
+
} else {
|
|
740
|
+
this.svg.addChild(this.node);
|
|
741
|
+
}
|
|
345
742
|
|
|
346
743
|
if (this.options.centerContent) {
|
|
347
744
|
this.centerNode();
|
|
@@ -350,36 +747,7 @@ export class omdDisplay {
|
|
|
350
747
|
this._repositionOverlayToolbar();
|
|
351
748
|
}
|
|
352
749
|
|
|
353
|
-
/**
|
|
354
|
-
* Gets the current node
|
|
355
|
-
* @returns {omdNode|null} The current node
|
|
356
|
-
*/
|
|
357
|
-
getCurrentNode() {
|
|
358
|
-
return this.node;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Repositions overlay toolbar if current node supports it
|
|
363
|
-
* @private
|
|
364
|
-
*/
|
|
365
|
-
_repositionOverlayToolbar() {
|
|
366
|
-
const rect = this.container.getBoundingClientRect();
|
|
367
|
-
const paddingTop = parseFloat(getComputedStyle(this.container).paddingTop || '0');
|
|
368
|
-
const paddingBottom = parseFloat(getComputedStyle(this.container).paddingBottom || '0');
|
|
369
|
-
const paddingLeft = parseFloat(getComputedStyle(this.container).paddingLeft || '0');
|
|
370
|
-
const paddingRight = parseFloat(getComputedStyle(this.container).paddingRight || '0');
|
|
371
|
-
const containerWidth = (rect.width - paddingLeft - paddingRight) || this.container.clientWidth || 0;
|
|
372
|
-
const containerHeight = (rect.height - paddingTop - paddingBottom) || this.container.clientHeight || 0;
|
|
373
|
-
const node = this.node;
|
|
374
|
-
if (!node) return;
|
|
375
|
-
const hasOverlayApi = typeof node.isToolbarOverlay === 'function' && typeof node.positionToolbarOverlay === 'function';
|
|
376
|
-
if (hasOverlayApi && node.isToolbarOverlay()) {
|
|
377
|
-
node.positionToolbarOverlay(containerWidth, containerHeight, 16);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
750
|
|
|
382
|
-
|
|
383
751
|
/**
|
|
384
752
|
* Sets the font size
|
|
385
753
|
* @param {number} size - The font size
|