@teachinglab/omd 0.2.6 → 0.2.8
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/canvas/ui/toolbar.js +0 -15
- package/omd/config/omdConfigManager.js +2 -2
- package/omd/core/omdEquationStack.js +11 -2
- package/omd/display/omdDisplay.js +494 -80
- package/omd/nodes/omdEquationNode.js +46 -6
- package/omd/step-visualizer/omdStepVisualizer.js +387 -42
- package/omd/step-visualizer/omdStepVisualizerLayout.js +654 -11
- package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +46 -8
- package/omd/utils/omdStepVisualizerInteractiveSteps.js +318 -121
- package/package.json +1 -1
- package/src/omdBalanceHanger.js +31 -1
- package/src/omdColor.js +1 -0
- package/src/omdCoordinatePlane.js +53 -3
- package/src/omdMetaExpression.js +8 -4
- package/src/omdTable.js +182 -52
|
@@ -20,6 +20,7 @@ export class omdDisplay {
|
|
|
20
20
|
autoScale: true, // Automatically scale content to fit container
|
|
21
21
|
maxScale: 1, // Do not upscale beyond 1 by default
|
|
22
22
|
edgePadding: 16, // Horizontal padding from edges when scaling
|
|
23
|
+
autoCloseStepVisualizer: true, // Close active step visualizer text boxes before autoscale to avoid shrink
|
|
23
24
|
...options
|
|
24
25
|
};
|
|
25
26
|
|
|
@@ -41,6 +42,19 @@ export class omdDisplay {
|
|
|
41
42
|
this.svg.svgObject.style.overflow = 'hidden';
|
|
42
43
|
this.container.appendChild(this.svg.svgObject);
|
|
43
44
|
|
|
45
|
+
// Create a dedicated content group we can translate to compensate for
|
|
46
|
+
// viewBox origin changes (so expanding the origin doesn't visually move content).
|
|
47
|
+
try {
|
|
48
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
49
|
+
this._contentGroup = document.createElementNS(ns, 'g');
|
|
50
|
+
this._contentGroup.setAttribute('id', 'omd-content-root');
|
|
51
|
+
this.svg.svgObject.appendChild(this._contentGroup);
|
|
52
|
+
this._contentOffsetX = 0;
|
|
53
|
+
this._contentOffsetY = 0;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
this._contentGroup = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
// Handle resize
|
|
45
59
|
if (window.ResizeObserver) {
|
|
46
60
|
this.resizeObserver = new ResizeObserver(() => {
|
|
@@ -61,6 +75,324 @@ export class omdDisplay {
|
|
|
61
75
|
|
|
62
76
|
// Reposition overlay toolbar (if any) on resize
|
|
63
77
|
this._repositionOverlayToolbar();
|
|
78
|
+
if (this.options.debugExtents) this._drawDebugOverlays();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Ensure the internal SVG viewBox is at least as large as the provided content dimensions.
|
|
83
|
+
* This prevents clipping when content is larger than the current viewBox.
|
|
84
|
+
* @param {number} contentWidth
|
|
85
|
+
* @param {number} contentHeight
|
|
86
|
+
*/
|
|
87
|
+
_ensureViewboxFits(contentWidth, contentHeight) {
|
|
88
|
+
// If caller provided just width/height, but we prefer extents, bail early
|
|
89
|
+
if (!this.node) return;
|
|
90
|
+
const pad = 10;
|
|
91
|
+
|
|
92
|
+
// Prefer DOM measured extents (accounts for strokes, transforms, children SVG geometry)
|
|
93
|
+
let ext = null;
|
|
94
|
+
try {
|
|
95
|
+
const collected = this._collectNodeExtents(this.node);
|
|
96
|
+
if (collected) {
|
|
97
|
+
ext = { minX: collected.minX, minY: collected.minY, maxX: collected.maxX, maxY: collected.maxY };
|
|
98
|
+
}
|
|
99
|
+
} catch (e) {
|
|
100
|
+
ext = null;
|
|
101
|
+
}
|
|
102
|
+
if (!ext) {
|
|
103
|
+
ext = this._computeNodeExtents(this.node);
|
|
104
|
+
}
|
|
105
|
+
if (!ext) return;
|
|
106
|
+
|
|
107
|
+
const minX = Math.floor(ext.minX - pad);
|
|
108
|
+
const minY = Math.floor(ext.minY - pad);
|
|
109
|
+
const maxX = Math.ceil(ext.maxX + pad);
|
|
110
|
+
const maxY = Math.ceil(ext.maxY + pad);
|
|
111
|
+
|
|
112
|
+
const curView = this.svg.svgObject.getAttribute('viewBox') || '';
|
|
113
|
+
let curX = 0, curY = 0, curW = 0, curH = 0;
|
|
114
|
+
if (curView) {
|
|
115
|
+
const parts = curView.split(/\s+/).map(Number).filter(n => !isNaN(n));
|
|
116
|
+
if (parts.length === 4) {
|
|
117
|
+
curX = parts[0];
|
|
118
|
+
curY = parts[1];
|
|
119
|
+
curW = parts[2];
|
|
120
|
+
curH = parts[3];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// To avoid shifting visible content, keep the current viewBox origin (curX,curY)
|
|
125
|
+
// and only expand width/height as needed. Changing the origin would change
|
|
126
|
+
// the mapping from SVG coordinates to screen coordinates and appear to move
|
|
127
|
+
// existing content.
|
|
128
|
+
const desiredX = curX;
|
|
129
|
+
const desiredY = curY;
|
|
130
|
+
const desiredRight = Math.max(curX + curW, maxX);
|
|
131
|
+
const desiredBottom = Math.max(curY + curH, maxY);
|
|
132
|
+
const desiredW = Math.max(curW, desiredRight - desiredX);
|
|
133
|
+
const desiredH = Math.max(curH, desiredBottom - desiredY);
|
|
134
|
+
|
|
135
|
+
if (desiredW !== curW || desiredH !== curH) {
|
|
136
|
+
this.svg.svgObject.setAttribute('viewBox', `${desiredX} ${desiredY} ${desiredW} ${desiredH}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Walk the node tree and compute absolute extents in SVG coordinates.
|
|
142
|
+
* Uses `xpos`/`ypos` and `width`/`height` properties; falls back to 0 when missing.
|
|
143
|
+
* @param {omdNode} root
|
|
144
|
+
* @returns {{minX:number,minY:number,maxX:number,maxY:number}}
|
|
145
|
+
*/
|
|
146
|
+
_computeNodeExtents(root) {
|
|
147
|
+
if (!root) return null;
|
|
148
|
+
const visited = new Set();
|
|
149
|
+
const stack = [{ node: root, absX: root.xpos || 0, absY: root.ypos || 0 }];
|
|
150
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
151
|
+
|
|
152
|
+
while (stack.length) {
|
|
153
|
+
const { node, absX, absY } = stack.pop();
|
|
154
|
+
if (!node || visited.has(node)) continue;
|
|
155
|
+
visited.add(node);
|
|
156
|
+
|
|
157
|
+
const w = node.width || 0;
|
|
158
|
+
const h = node.height || 0;
|
|
159
|
+
const nx = absX;
|
|
160
|
+
const ny = absY;
|
|
161
|
+
minX = Math.min(minX, nx);
|
|
162
|
+
minY = Math.min(minY, ny);
|
|
163
|
+
maxX = Math.max(maxX, nx + w);
|
|
164
|
+
maxY = Math.max(maxY, ny + h);
|
|
165
|
+
|
|
166
|
+
// push children
|
|
167
|
+
if (Array.isArray(node.childList)) {
|
|
168
|
+
for (const c of node.childList) {
|
|
169
|
+
if (!c) continue;
|
|
170
|
+
const cx = (c.xpos || 0) + nx;
|
|
171
|
+
const cy = (c.ypos || 0) + ny;
|
|
172
|
+
stack.push({ node: c, absX: cx, absY: cy });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (node.argumentNodeList) {
|
|
176
|
+
for (const val of Object.values(node.argumentNodeList)) {
|
|
177
|
+
if (Array.isArray(val)) {
|
|
178
|
+
for (const v of val) {
|
|
179
|
+
if (!v) continue;
|
|
180
|
+
const vx = (v.xpos || 0) + nx;
|
|
181
|
+
const vy = (v.ypos || 0) + ny;
|
|
182
|
+
stack.push({ node: v, absX: vx, absY: vy });
|
|
183
|
+
}
|
|
184
|
+
} else if (val) {
|
|
185
|
+
const vx = (val.xpos || 0) + nx;
|
|
186
|
+
const vy = (val.ypos || 0) + ny;
|
|
187
|
+
stack.push({ node: val, absX: vx, absY: vy });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (minX === Infinity) return null;
|
|
194
|
+
return { minX, minY, maxX, maxY };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Collect extents for each node and return per-node list plus overall extents.
|
|
199
|
+
* Useful for debugging elements that extend outside parent coordinates.
|
|
200
|
+
* @param {omdNode} root
|
|
201
|
+
* @returns {{nodes:Array, minX:number, minY:number, maxX:number, maxY:number}}
|
|
202
|
+
*/
|
|
203
|
+
_collectNodeExtents(root) {
|
|
204
|
+
if (!root) return null;
|
|
205
|
+
const visited = new Set();
|
|
206
|
+
const stack = [{ node: root, absX: root.xpos || 0, absY: root.ypos || 0, parent: null }];
|
|
207
|
+
const nodes = [];
|
|
208
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
while (stack.length) {
|
|
212
|
+
const { node, absX, absY, parent } = stack.pop();
|
|
213
|
+
if (!node || visited.has(node)) continue;
|
|
214
|
+
visited.add(node);
|
|
215
|
+
|
|
216
|
+
// Prefer DOM measurement if available for accuracy
|
|
217
|
+
let nx = absX;
|
|
218
|
+
let ny = absY;
|
|
219
|
+
let nminX = nx;
|
|
220
|
+
let nminY = ny;
|
|
221
|
+
let nmaxX = nx;
|
|
222
|
+
let nmaxY = ny;
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
if (node.svgObject && typeof node.svgObject.getBBox === 'function' && typeof node.svgObject.getCTM === 'function') {
|
|
226
|
+
const bbox = node.svgObject.getBBox();
|
|
227
|
+
const ctm = node.svgObject.getCTM();
|
|
228
|
+
// Transform all four bbox corners into root SVG coordinates
|
|
229
|
+
const corners = [
|
|
230
|
+
{ x: bbox.x, y: bbox.y },
|
|
231
|
+
{ x: bbox.x + bbox.width, y: bbox.y },
|
|
232
|
+
{ x: bbox.x, y: bbox.y + bbox.height },
|
|
233
|
+
{ x: bbox.x + bbox.width, y: bbox.y + bbox.height }
|
|
234
|
+
];
|
|
235
|
+
const tx = corners.map(p => ({
|
|
236
|
+
x: ctm.a * p.x + ctm.c * p.y + ctm.e,
|
|
237
|
+
y: ctm.b * p.x + ctm.d * p.y + ctm.f
|
|
238
|
+
}));
|
|
239
|
+
nminX = Math.min(...tx.map(t => t.x));
|
|
240
|
+
nminY = Math.min(...tx.map(t => t.y));
|
|
241
|
+
nmaxX = Math.max(...tx.map(t => t.x));
|
|
242
|
+
nmaxY = Math.max(...tx.map(t => t.y));
|
|
243
|
+
nx = nminX;
|
|
244
|
+
ny = nminY;
|
|
245
|
+
} else {
|
|
246
|
+
const w = node.width || 0;
|
|
247
|
+
const h = node.height || 0;
|
|
248
|
+
nx = absX;
|
|
249
|
+
ny = absY;
|
|
250
|
+
nminX = nx;
|
|
251
|
+
nminY = ny;
|
|
252
|
+
nmaxX = nx + w;
|
|
253
|
+
nmaxY = ny + h;
|
|
254
|
+
}
|
|
255
|
+
} catch (e) {
|
|
256
|
+
const w = node.width || 0;
|
|
257
|
+
const h = node.height || 0;
|
|
258
|
+
nx = absX;
|
|
259
|
+
ny = absY;
|
|
260
|
+
nminX = nx;
|
|
261
|
+
nminY = ny;
|
|
262
|
+
nmaxX = nx + w;
|
|
263
|
+
nmaxY = ny + h;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
nodes.push({ node, minX: nminX, minY: nminY, maxX: nmaxX, maxY: nmaxY, parent });
|
|
267
|
+
|
|
268
|
+
minX = Math.min(minX, nminX);
|
|
269
|
+
minY = Math.min(minY, nminY);
|
|
270
|
+
maxX = Math.max(maxX, nmaxX);
|
|
271
|
+
maxY = Math.max(maxY, nmaxY);
|
|
272
|
+
|
|
273
|
+
// push children
|
|
274
|
+
if (Array.isArray(node.childList)) {
|
|
275
|
+
for (const c of node.childList) {
|
|
276
|
+
if (!c) continue;
|
|
277
|
+
const cx = (c.xpos || 0) + nx;
|
|
278
|
+
const cy = (c.ypos || 0) + ny;
|
|
279
|
+
stack.push({ node: c, absX: cx, absY: cy, parent: node });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (node.argumentNodeList) {
|
|
283
|
+
for (const val of Object.values(node.argumentNodeList)) {
|
|
284
|
+
if (Array.isArray(val)) {
|
|
285
|
+
for (const v of val) {
|
|
286
|
+
if (!v) continue;
|
|
287
|
+
const vx = (v.xpos || 0) + nx;
|
|
288
|
+
const vy = (v.ypos || 0) + ny;
|
|
289
|
+
stack.push({ node: v, absX: vx, absY: vy, parent: node });
|
|
290
|
+
}
|
|
291
|
+
} else if (val) {
|
|
292
|
+
const vx = (val.xpos || 0) + nx;
|
|
293
|
+
const vy = (val.ypos || 0) + ny;
|
|
294
|
+
stack.push({ node: val, absX: vx, absY: vy, parent: node });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (minX === Infinity) return null;
|
|
301
|
+
return { nodes, minX, minY, maxX, maxY };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
_clearDebugOverlays() {
|
|
305
|
+
if (!this.svg || !this.svg.svgObject) return;
|
|
306
|
+
const existing = this.svg.svgObject.querySelector('#omd-debug-overlays');
|
|
307
|
+
if (existing) existing.remove();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
_drawDebugOverlays() {
|
|
311
|
+
if (!this.options.debugExtents) return;
|
|
312
|
+
if (!this.svg || !this.svg.svgObject || !this.node) return;
|
|
313
|
+
|
|
314
|
+
this._clearDebugOverlays();
|
|
315
|
+
|
|
316
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
317
|
+
const group = document.createElementNS(ns, 'g');
|
|
318
|
+
group.setAttribute('id', 'omd-debug-overlays');
|
|
319
|
+
group.setAttribute('pointer-events', 'none');
|
|
320
|
+
|
|
321
|
+
// overall node extents
|
|
322
|
+
const collected = this._collectNodeExtents(this.node);
|
|
323
|
+
if (!collected) return;
|
|
324
|
+
|
|
325
|
+
const { nodes, minX, minY, maxX, maxY } = collected;
|
|
326
|
+
|
|
327
|
+
// Draw content extents (blue dashed)
|
|
328
|
+
const contentRect = document.createElementNS(ns, 'rect');
|
|
329
|
+
contentRect.setAttribute('x', String(minX));
|
|
330
|
+
contentRect.setAttribute('y', String(minY));
|
|
331
|
+
contentRect.setAttribute('width', String(maxX - minX));
|
|
332
|
+
contentRect.setAttribute('height', String(maxY - minY));
|
|
333
|
+
contentRect.setAttribute('fill', 'none');
|
|
334
|
+
contentRect.setAttribute('stroke', 'blue');
|
|
335
|
+
contentRect.setAttribute('stroke-dasharray', '6 4');
|
|
336
|
+
contentRect.setAttribute('stroke-width', '0.8');
|
|
337
|
+
group.appendChild(contentRect);
|
|
338
|
+
|
|
339
|
+
// Draw viewBox rect (orange)
|
|
340
|
+
const curView = this.svg.svgObject.getAttribute('viewBox') || '';
|
|
341
|
+
if (curView) {
|
|
342
|
+
const parts = curView.split(/\s+/).map(Number).filter(n => !isNaN(n));
|
|
343
|
+
if (parts.length === 4) {
|
|
344
|
+
const [vx, vy, vw, vh] = parts;
|
|
345
|
+
const vbRect = document.createElementNS(ns, 'rect');
|
|
346
|
+
vbRect.setAttribute('x', String(vx));
|
|
347
|
+
vbRect.setAttribute('y', String(vy));
|
|
348
|
+
vbRect.setAttribute('width', String(vw));
|
|
349
|
+
vbRect.setAttribute('height', String(vh));
|
|
350
|
+
vbRect.setAttribute('fill', 'none');
|
|
351
|
+
vbRect.setAttribute('stroke', 'orange');
|
|
352
|
+
vbRect.setAttribute('stroke-width', '1');
|
|
353
|
+
vbRect.setAttribute('opacity', '0.9');
|
|
354
|
+
group.appendChild(vbRect);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Per-node boxes: green if inside parent, red if overflowing parent bounds
|
|
359
|
+
const overflowing = [];
|
|
360
|
+
for (const item of nodes) {
|
|
361
|
+
const r = document.createElementNS(ns, 'rect');
|
|
362
|
+
r.setAttribute('x', String(item.minX));
|
|
363
|
+
r.setAttribute('y', String(item.minY));
|
|
364
|
+
r.setAttribute('width', String(Math.max(0, item.maxX - item.minX)));
|
|
365
|
+
r.setAttribute('height', String(Math.max(0, item.maxY - item.minY)));
|
|
366
|
+
r.setAttribute('fill', 'none');
|
|
367
|
+
r.setAttribute('stroke-width', '0.6');
|
|
368
|
+
|
|
369
|
+
let stroke = 'green';
|
|
370
|
+
if (item.parent) {
|
|
371
|
+
const pMinX = (item.parent.xpos || 0) + (item.parent._absX || 0);
|
|
372
|
+
const pMinY = (item.parent.ypos || 0) + (item.parent._absY || 0);
|
|
373
|
+
// fallback compute parent's absX/Y from nodes list if available
|
|
374
|
+
const parentEntry = nodes.find(n => n.node === item.parent);
|
|
375
|
+
const pminX = parentEntry ? parentEntry.minX : pMinX;
|
|
376
|
+
const pminY = parentEntry ? parentEntry.minY : pMinY;
|
|
377
|
+
const pmaxX = parentEntry ? parentEntry.maxX : pminX + (item.parent.width || 0);
|
|
378
|
+
const pmaxY = parentEntry ? parentEntry.maxY : pminY + (item.parent.height || 0);
|
|
379
|
+
|
|
380
|
+
if (item.minX < pminX || item.minY < pminY || item.maxX > pmaxX || item.maxY > pmaxY) {
|
|
381
|
+
stroke = 'red';
|
|
382
|
+
overflowing.push({ node: item.node, bounds: item });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
r.setAttribute('stroke', stroke);
|
|
387
|
+
r.setAttribute('opacity', stroke === 'red' ? '0.9' : '0.6');
|
|
388
|
+
group.appendChild(r);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (overflowing.length) {
|
|
392
|
+
console.warn('omdDisplay: debugExtents found overflowing nodes:', overflowing.map(o => ({ type: o.node?.type, bounds: o.bounds })));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
this.svg.svgObject.appendChild(group);
|
|
64
396
|
}
|
|
65
397
|
|
|
66
398
|
centerNode() {
|
|
@@ -68,6 +400,19 @@ export class omdDisplay {
|
|
|
68
400
|
const containerWidth = this.container.offsetWidth || 0;
|
|
69
401
|
const containerHeight = this.container.offsetHeight || 0;
|
|
70
402
|
|
|
403
|
+
// Early auto-close of step visualizer UI before measuring dimensions to avoid transient height inflation
|
|
404
|
+
if (this.options.autoCloseStepVisualizer && this.node) {
|
|
405
|
+
try {
|
|
406
|
+
if (typeof this.node.forceCloseAll === 'function') {
|
|
407
|
+
this.node.forceCloseAll();
|
|
408
|
+
} else if (typeof this.node.closeAllTextBoxes === 'function') {
|
|
409
|
+
this.node.closeAllTextBoxes();
|
|
410
|
+
} else if (typeof this.node.closeActiveDot === 'function') {
|
|
411
|
+
this.node.closeActiveDot();
|
|
412
|
+
}
|
|
413
|
+
} catch (e) { /* no-op */ }
|
|
414
|
+
}
|
|
415
|
+
|
|
71
416
|
// Determine actual content size (prefer sequence/current step when available)
|
|
72
417
|
let contentWidth = this.node.width || 0;
|
|
73
418
|
let contentHeight = this.node.height || 0;
|
|
@@ -75,38 +420,71 @@ export class omdDisplay {
|
|
|
75
420
|
const seq = this.node.getSequence();
|
|
76
421
|
if (seq) {
|
|
77
422
|
if (seq.width && seq.height) {
|
|
78
|
-
|
|
79
|
-
|
|
423
|
+
// For step visualizers, use sequenceWidth/Height instead of total dimensions to exclude visualizer elements from autoscale
|
|
424
|
+
contentWidth = seq.sequenceWidth || seq.width;
|
|
425
|
+
contentHeight = seq.sequenceHeight || seq.height;
|
|
426
|
+
console.log('omdDisplay: Using sequence dimensions for autoscale - sequenceWidth:', seq.sequenceWidth, 'sequenceHeight:', seq.sequenceHeight, 'width:', seq.width, 'height:', seq.height, 'contentWidth:', contentWidth, 'contentHeight:', contentHeight);
|
|
80
427
|
}
|
|
81
428
|
if (seq.getCurrentStep) {
|
|
82
429
|
const step = seq.getCurrentStep();
|
|
83
430
|
if (step && step.width && step.height) {
|
|
84
|
-
|
|
85
|
-
|
|
431
|
+
// For step visualizers, prioritize sequenceWidth/Height for dimension calculations
|
|
432
|
+
const stepWidth = seq.sequenceWidth || step.width;
|
|
433
|
+
const stepHeight = seq.sequenceHeight || step.height;
|
|
434
|
+
contentWidth = Math.max(contentWidth, stepWidth);
|
|
435
|
+
contentHeight = Math.max(contentHeight, stepHeight);
|
|
436
|
+
console.log('omdDisplay: Considering step dimensions - stepWidth:', stepWidth, 'stepHeight:', stepHeight, 'finalContentWidth:', contentWidth, 'finalContentHeight:', contentHeight);
|
|
86
437
|
}
|
|
87
438
|
}
|
|
88
439
|
}
|
|
89
440
|
}
|
|
441
|
+
console.log('omdDisplay: Final content dimensions for autoscale - width:', contentWidth, 'height:', contentHeight);
|
|
90
442
|
|
|
91
443
|
// Compute scale to keep within bounds
|
|
92
444
|
let scale = 1;
|
|
93
445
|
if (this.options.autoScale && contentWidth > 0 && contentHeight > 0) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
446
|
+
// Optionally close any open step visualizer textbox to prevent transient height expansion
|
|
447
|
+
if (this.options.autoCloseStepVisualizer && this.node) {
|
|
448
|
+
try {
|
|
449
|
+
if (typeof this.node.closeActiveDot === 'function') {
|
|
450
|
+
this.node.closeActiveDot();
|
|
451
|
+
} else if (typeof this.node.closeAllTextBoxes === 'function') {
|
|
452
|
+
this.node.closeAllTextBoxes();
|
|
453
|
+
}
|
|
454
|
+
} catch (e) { /* no-op */ }
|
|
455
|
+
}
|
|
456
|
+
// Detect step visualizer directly on node (getSequence returns underlying sequence only)
|
|
457
|
+
let hasStepVisualizer = false;
|
|
458
|
+
if (this.node) {
|
|
459
|
+
const ctorName = this.node.constructor?.name;
|
|
460
|
+
hasStepVisualizer = (ctorName === 'omdStepVisualizer') || this.node.type === 'omdStepVisualizer' || (typeof omdStepVisualizer !== 'undefined' && this.node instanceof omdStepVisualizer);
|
|
461
|
+
console.log('omdDisplay: Step visualizer detection (node) ctor:', ctorName, 'type:', this.node.type, 'detected:', hasStepVisualizer);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (hasStepVisualizer) {
|
|
465
|
+
// Preserve existing scale if already set on node; otherwise lock to 1.
|
|
466
|
+
const existingScale = (this.node && typeof this.node.scale === 'number') ? this.node.scale : undefined;
|
|
467
|
+
scale = (existingScale && existingScale > 0) ? existingScale : 1;
|
|
468
|
+
console.log('omdDisplay: Step visualizer detected - locking scale at', scale);
|
|
469
|
+
} else {
|
|
470
|
+
const hPad = this.options.edgePadding || 0;
|
|
471
|
+
const vPadTop = this.options.topMargin || 0;
|
|
472
|
+
const vPadBottom = this.options.bottomMargin || 0;
|
|
473
|
+
// Reserve extra space for overlay toolbar if needed
|
|
474
|
+
let reserveBottom = vPadBottom;
|
|
475
|
+
if (this.node && typeof this.node.isToolbarOverlay === 'function' && this.node.isToolbarOverlay()) {
|
|
476
|
+
const tH = (typeof this.node.getToolbarVisualHeight === 'function') ? this.node.getToolbarVisualHeight() : 0;
|
|
477
|
+
reserveBottom += (tH + (this.node.getOverlayPadding ? this.node.getOverlayPadding() : 16));
|
|
478
|
+
}
|
|
479
|
+
const availW = Math.max(0, containerWidth - hPad * 2);
|
|
480
|
+
const availH = Math.max(0, containerHeight - (vPadTop + reserveBottom));
|
|
481
|
+
const sx = availW > 0 ? (availW / contentWidth) : 1;
|
|
482
|
+
const sy = availH > 0 ? (availH / contentHeight) : 1;
|
|
483
|
+
const maxScale = (typeof this.options.maxScale === 'number') ? this.options.maxScale : 1;
|
|
484
|
+
scale = Math.min(sx, sy, maxScale);
|
|
485
|
+
if (!isFinite(scale) || scale <= 0) scale = 1;
|
|
486
|
+
console.log('omdDisplay: Autoscale calculation - availW:', availW, 'availH:', availH, 'contentW:', contentWidth, 'contentH:', contentHeight, 'scale:', scale);
|
|
102
487
|
}
|
|
103
|
-
const availW = Math.max(0, containerWidth - hPad * 2);
|
|
104
|
-
const availH = Math.max(0, containerHeight - (vPadTop + reserveBottom));
|
|
105
|
-
const sx = availW > 0 ? (availW / contentWidth) : 1;
|
|
106
|
-
const sy = availH > 0 ? (availH / contentHeight) : 1;
|
|
107
|
-
const maxScale = (typeof this.options.maxScale === 'number') ? this.options.maxScale : 1;
|
|
108
|
-
scale = Math.min(sx, sy, maxScale);
|
|
109
|
-
if (!isFinite(scale) || scale <= 0) scale = 1;
|
|
110
488
|
}
|
|
111
489
|
|
|
112
490
|
// Apply scale
|
|
@@ -120,24 +498,53 @@ export class omdDisplay {
|
|
|
120
498
|
const screenCenterX = containerWidth / 2;
|
|
121
499
|
x = screenCenterX - (this.node.alignPointX * scale);
|
|
122
500
|
} else {
|
|
123
|
-
const scaledWidth =
|
|
501
|
+
const scaledWidth = contentWidth * scale;
|
|
124
502
|
x = (containerWidth - scaledWidth) / 2;
|
|
125
503
|
}
|
|
126
504
|
|
|
127
|
-
//
|
|
128
|
-
|
|
505
|
+
// Decide whether positioning would move content outside container. If so,
|
|
506
|
+
// prefer expanding the SVG viewBox instead of moving nodes.
|
|
507
|
+
const scaledWidthFinal = contentWidth * scale;
|
|
508
|
+
const scaledHeightFinal = contentHeight * scale;
|
|
509
|
+
const totalNeededH = scaledHeightFinal + (this.options.topMargin || 0) + (this.options.bottomMargin || 0);
|
|
510
|
+
|
|
511
|
+
const willOverflowHoriz = scaledWidthFinal > containerWidth;
|
|
512
|
+
const willOverflowVert = totalNeededH > containerHeight;
|
|
513
|
+
|
|
514
|
+
if (willOverflowHoriz || willOverflowVert) {
|
|
515
|
+
// Set scale but do NOT reposition node (preserve its absolute positions).
|
|
516
|
+
if (this.node.setScale) this.node.setScale(scale);
|
|
517
|
+
// Expand viewBox to contain entire unscaled content so nothing is clipped.
|
|
518
|
+
this._ensureViewboxFits(contentWidth, contentHeight);
|
|
519
|
+
// Reposition overlay toolbar in case viewBox/container changed
|
|
520
|
+
this._repositionOverlayToolbar();
|
|
521
|
+
|
|
522
|
+
// If content still exceeds available height in the host, allow vertical scrolling
|
|
523
|
+
if (willOverflowVert) {
|
|
524
|
+
this.container.style.overflowY = 'auto';
|
|
525
|
+
this.container.style.overflowX = 'hidden';
|
|
526
|
+
} else {
|
|
527
|
+
this.container.style.overflow = 'hidden';
|
|
528
|
+
}
|
|
529
|
+
if (this.options.debugExtents) this._drawDebugOverlays();
|
|
530
|
+
} else {
|
|
531
|
+
// Y is top margin; scaled content will grow downward
|
|
532
|
+
this.node.setPosition(x, this.options.topMargin);
|
|
129
533
|
|
|
130
|
-
|
|
131
|
-
|
|
534
|
+
// Reposition overlay toolbar (if any)
|
|
535
|
+
this._repositionOverlayToolbar();
|
|
132
536
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
537
|
+
// Ensure viewBox can contain the (unscaled) content to avoid clipping in some hosts
|
|
538
|
+
this._ensureViewboxFits(contentWidth, contentHeight);
|
|
539
|
+
|
|
540
|
+
if (totalNeededH > containerHeight) {
|
|
541
|
+
// Let the host scroll vertically; keep horizontal overflow hidden to avoid layout shift
|
|
542
|
+
this.container.style.overflowY = 'auto';
|
|
543
|
+
this.container.style.overflowX = 'hidden';
|
|
544
|
+
} else {
|
|
545
|
+
this.container.style.overflow = 'hidden';
|
|
546
|
+
}
|
|
547
|
+
if (this.options.debugExtents) this._drawDebugOverlays();
|
|
141
548
|
}
|
|
142
549
|
}
|
|
143
550
|
|
|
@@ -157,15 +564,17 @@ export class omdDisplay {
|
|
|
157
564
|
if (this.node.getSequence) {
|
|
158
565
|
const sequence = this.node.getSequence();
|
|
159
566
|
if (sequence && sequence.width && sequence.height) {
|
|
160
|
-
sequenceWidth
|
|
161
|
-
|
|
567
|
+
// For step visualizers, use sequenceWidth/Height instead of total dimensions to exclude visualizer elements from autoscale
|
|
568
|
+
sequenceWidth = sequence.sequenceWidth || sequence.width;
|
|
569
|
+
sequenceHeight = sequence.sequenceHeight || sequence.height;
|
|
162
570
|
|
|
163
571
|
// Check current step dimensions too
|
|
164
572
|
if (sequence.getCurrentStep) {
|
|
165
573
|
const currentStep = sequence.getCurrentStep();
|
|
166
574
|
if (currentStep && currentStep.width && currentStep.height) {
|
|
167
|
-
|
|
168
|
-
|
|
575
|
+
// For step visualizers, prioritize sequenceWidth/Height for dimension calculations
|
|
576
|
+
stepWidth = sequence.sequenceWidth || currentStep.width;
|
|
577
|
+
stepHeight = sequence.sequenceHeight || currentStep.height;
|
|
169
578
|
}
|
|
170
579
|
}
|
|
171
580
|
|
|
@@ -204,6 +613,8 @@ export class omdDisplay {
|
|
|
204
613
|
// Update container
|
|
205
614
|
this.container.style.width = `${newWidth}px`;
|
|
206
615
|
this.container.style.height = `${newHeight}px`;
|
|
616
|
+
if (this.options.debugExtents) this._drawDebugOverlays();
|
|
617
|
+
else this._clearDebugOverlays();
|
|
207
618
|
}
|
|
208
619
|
|
|
209
620
|
/**
|
|
@@ -214,7 +625,11 @@ export class omdDisplay {
|
|
|
214
625
|
render(expression) {
|
|
215
626
|
// Clear previous node
|
|
216
627
|
if (this.node) {
|
|
217
|
-
this.
|
|
628
|
+
if (this._contentGroup && this.node && this.node.svgObject) {
|
|
629
|
+
this._contentGroup.removeChild(this.node.svgObject);
|
|
630
|
+
} else {
|
|
631
|
+
this.svg.removeChild(this.node);
|
|
632
|
+
}
|
|
218
633
|
}
|
|
219
634
|
|
|
220
635
|
// Create node from expression
|
|
@@ -223,18 +638,18 @@ export class omdDisplay {
|
|
|
223
638
|
// Multiple equations
|
|
224
639
|
const equationStrings = expression.split(';').filter(s => s.trim() !== '');
|
|
225
640
|
const steps = equationStrings.map(str => omdEquationNode.fromString(str));
|
|
226
|
-
this.node = new omdStepVisualizer(steps);
|
|
641
|
+
this.node = new omdStepVisualizer(steps, this.options.styling || {});
|
|
227
642
|
} else {
|
|
228
643
|
// Single expression or equation
|
|
229
644
|
if (expression.includes('=')) {
|
|
230
645
|
const firstStep = omdEquationNode.fromString(expression);
|
|
231
|
-
this.node = new omdStepVisualizer([firstStep]);
|
|
646
|
+
this.node = new omdStepVisualizer([firstStep], this.options.styling || {});
|
|
232
647
|
} else {
|
|
233
648
|
// Create node directly from expression
|
|
234
649
|
const parsedAST = math.parse(expression);
|
|
235
650
|
const NodeClass = getNodeForAST(parsedAST);
|
|
236
651
|
const firstStep = new NodeClass(parsedAST);
|
|
237
|
-
this.node = new omdStepVisualizer([firstStep]);
|
|
652
|
+
this.node = new omdStepVisualizer([firstStep], this.options.styling || {});
|
|
238
653
|
}
|
|
239
654
|
}
|
|
240
655
|
} else {
|
|
@@ -249,14 +664,16 @@ export class omdDisplay {
|
|
|
249
664
|
// Apply filtering based on filterLevel
|
|
250
665
|
sequence.updateStepsVisibility(step => (step.stepMark ?? 0) === sequence.getFilterLevel());
|
|
251
666
|
}
|
|
252
|
-
|
|
667
|
+
// Prefer appending the node's svgObject into our content group so DOM measurements are consistent
|
|
668
|
+
if (this._contentGroup && this.node && this.node.svgObject) {
|
|
669
|
+
try { this._contentGroup.appendChild(this.node.svgObject); } catch (e) { this.svg.addChild(this.node); }
|
|
670
|
+
} else {
|
|
671
|
+
this.svg.addChild(this.node);
|
|
672
|
+
}
|
|
253
673
|
|
|
254
674
|
// Apply any stored font settings
|
|
255
675
|
if (this.options.fontFamily) {
|
|
256
|
-
// Small delay to ensure SVG elements are fully rendered
|
|
257
|
-
setTimeout(() => {
|
|
258
676
|
this.setFont(this.options.fontFamily, this.options.fontWeight || '400');
|
|
259
|
-
}, 10);
|
|
260
677
|
}
|
|
261
678
|
|
|
262
679
|
// Only use fitToContent for tight sizing when explicitly requested
|
|
@@ -268,6 +685,14 @@ export class omdDisplay {
|
|
|
268
685
|
// Ensure overlay toolbar is positioned initially
|
|
269
686
|
this._repositionOverlayToolbar();
|
|
270
687
|
|
|
688
|
+
// Also ensure the viewBox is large enough to contain the node (avoid clipping)
|
|
689
|
+
const cw = (this.node && this.node.width) ? this.node.width : 0;
|
|
690
|
+
const ch = (this.node && this.node.height) ? this.node.height : 0;
|
|
691
|
+
this._ensureViewboxFits(cw, ch);
|
|
692
|
+
|
|
693
|
+
if (this.options.debugExtents) this._drawDebugOverlays();
|
|
694
|
+
else this._clearDebugOverlays();
|
|
695
|
+
|
|
271
696
|
// Provide a default global refresh function if not present
|
|
272
697
|
if (typeof window !== 'undefined' && !window.refreshDisplayAndFilters) {
|
|
273
698
|
window.refreshDisplayAndFilters = () => {
|
|
@@ -282,7 +707,7 @@ export class omdDisplay {
|
|
|
282
707
|
sequence.updateStepsVisibility(step => (step.stepMark ?? 0) === 0);
|
|
283
708
|
}
|
|
284
709
|
if (typeof node.updateLayout === 'function') {
|
|
285
|
-
|
|
710
|
+
node.updateLayout();
|
|
286
711
|
}
|
|
287
712
|
}
|
|
288
713
|
if (this.options.centerContent) {
|
|
@@ -303,7 +728,11 @@ export class omdDisplay {
|
|
|
303
728
|
* @param {object} child - A jsvg node to add
|
|
304
729
|
*/
|
|
305
730
|
addChild(child) {
|
|
306
|
-
this.
|
|
731
|
+
if (this._contentGroup && child && child.svgObject) {
|
|
732
|
+
try { this._contentGroup.appendChild(child.svgObject); } catch (e) { this.svg.addChild(child); }
|
|
733
|
+
} else {
|
|
734
|
+
this.svg.addChild(child);
|
|
735
|
+
}
|
|
307
736
|
|
|
308
737
|
if (this.options.centerContent) this.centerNode();
|
|
309
738
|
|
|
@@ -317,10 +746,16 @@ export class omdDisplay {
|
|
|
317
746
|
removeChild(child) {
|
|
318
747
|
if (!this.svg) return;
|
|
319
748
|
try {
|
|
320
|
-
if (
|
|
749
|
+
if (child && child.svgObject) {
|
|
750
|
+
if (this._contentGroup && this._contentGroup.contains(child.svgObject)) {
|
|
751
|
+
this._contentGroup.removeChild(child.svgObject);
|
|
752
|
+
} else if (this.svg.svgObject && this.svg.svgObject.contains(child.svgObject)) {
|
|
753
|
+
this.svg.svgObject.removeChild(child.svgObject);
|
|
754
|
+
} else if (typeof this.svg.removeChild === 'function') {
|
|
755
|
+
this.svg.removeChild(child);
|
|
756
|
+
}
|
|
757
|
+
} else if (typeof this.svg.removeChild === 'function') {
|
|
321
758
|
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
759
|
}
|
|
325
760
|
} catch (e) {
|
|
326
761
|
// no-op
|
|
@@ -335,13 +770,21 @@ export class omdDisplay {
|
|
|
335
770
|
*/
|
|
336
771
|
update(newNode) {
|
|
337
772
|
if (this.node) {
|
|
338
|
-
this.
|
|
773
|
+
if (this._contentGroup && this.node && this.node.svgObject && this._contentGroup.contains(this.node.svgObject)) {
|
|
774
|
+
this._contentGroup.removeChild(this.node.svgObject);
|
|
775
|
+
} else if (typeof this.svg.removeChild === 'function') {
|
|
776
|
+
this.svg.removeChild(this.node);
|
|
777
|
+
}
|
|
339
778
|
}
|
|
340
779
|
|
|
341
780
|
this.node = newNode;
|
|
342
781
|
this.node.setFontSize(this.options.fontSize);
|
|
343
782
|
this.node.initialize();
|
|
344
|
-
this.
|
|
783
|
+
if (this._contentGroup && this.node && this.node.svgObject) {
|
|
784
|
+
try { this._contentGroup.appendChild(this.node.svgObject); } catch (e) { this.svg.addChild(this.node); }
|
|
785
|
+
} else {
|
|
786
|
+
this.svg.addChild(this.node);
|
|
787
|
+
}
|
|
345
788
|
|
|
346
789
|
if (this.options.centerContent) {
|
|
347
790
|
this.centerNode();
|
|
@@ -350,36 +793,7 @@ export class omdDisplay {
|
|
|
350
793
|
this._repositionOverlayToolbar();
|
|
351
794
|
}
|
|
352
795
|
|
|
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
796
|
|
|
382
|
-
|
|
383
797
|
/**
|
|
384
798
|
* Sets the font size
|
|
385
799
|
* @param {number} size - The font size
|