@teachinglab/omd
Version:
omd
260 lines (227 loc) • 13.3 kB
JavaScript
import { omdColor } from "./omdColor.js";
import { jsvgGroup, jsvgTextBox } from "@teachinglab/jsvg";
export class omdProblem extends jsvgGroup
{
constructor()
{
// initialization
super();
this.type = "omdProblem";
this.theText = "";
this.problemText = new jsvgTextBox();
this.problemText.setWidthAndHeight( 250,30 );
this.problemText.setText ( "this it the problem text" );
this.problemText.setFontFamily( "Albert Sans" );
this.problemText.setFontColor( "black" );
this.problemText.setFontSize( 18 );
this.addChild( this.problemText );
}
loadFromJSON( data, handleAIResponse = null )
{
console.log(data)
if ( typeof data.problemText != "undefined" )
this.theText = data.problemText;
if ( typeof data.visualization != "undefined" )
{
console.log(data)
this.visualJSON = data.visualiation;
console.log( this.visualJSON );
console.log('testing 1 ')
// // Fast-path: if the caller supplied an already-rendered SVG DOM element
// // (for example, captured from the canvas), use it directly instead of
// // trying to regenerate the graphic. This avoids constructing a temp
// // omd container and the related initialization issues.
if (data.svgElement) {
try {
// Ensure the problem text is set so measurement is accurate
this.problemText.setText(this.theText);
const padding = 10;
const SVG_NS = 'http://www.w3.org/2000/svg';
// utility: hidden measuring svg for getBBox when needed
function getMeasuringSVG() {
let m = document.getElementById('_omd_measuring_svg');
if (!m) {
m = document.createElementNS(SVG_NS, 'svg');
m.setAttribute('id', '_omd_measuring_svg');
m.style.position = 'absolute';
m.style.left = '-9999px';
m.style.top = '-9999px';
m.style.width = '1px';
m.style.height = '1px';
m.style.visibility = 'hidden';
document.body.appendChild(m);
}
return m;
}
const incoming = data.svgElement;
const cloneRoot = incoming.cloneNode(true);
// Attempt 1: find a clip rect inside (common pattern)
let crop = null; // {x,y,width,height}
try {
const rect = cloneRoot.querySelector && cloneRoot.querySelector('clipPath rect');
if (rect) {
const x = parseFloat(rect.getAttribute('x') || '0');
const y = parseFloat(rect.getAttribute('y') || '0');
const w = parseFloat(rect.getAttribute('width') || '0');
const h = parseFloat(rect.getAttribute('height') || '0');
if (w > 0 && h > 0) crop = { x, y, width: w, height: h };
}
} catch (e) { /* ignore */ }
// Attempt 2: viewBox on root or nested svg
if (!crop) {
try {
let vb = null;
if (cloneRoot.getAttribute) vb = cloneRoot.getAttribute('viewBox');
if (!vb) {
const nested = cloneRoot.querySelector && cloneRoot.querySelector('svg');
if (nested && nested.getAttribute) vb = nested.getAttribute('viewBox');
}
if (vb) {
const parts = vb.trim().split(/\s+/).map(Number);
if (parts.length === 4) crop = { x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
}
} catch (e) { /* ignore */ }
}
// Attempt 3: measure bounding box by attaching to hidden SVG
if (!crop) {
const meas = getMeasuringSVG();
const wrapper = document.createElementNS(SVG_NS, 'g');
wrapper.appendChild(cloneRoot);
meas.appendChild(wrapper);
try {
const bb = wrapper.getBBox();
console.debug('omdProblem: measured bbox from wrapper', bb);
if (bb && bb.width > 0 && bb.height > 0) crop = { x: bb.x, y: bb.y, width: bb.width, height: bb.height };
} catch (e) {
// ignore
}
try { meas.removeChild(wrapper); } catch (_) {}
}
if (!crop) crop = { x: 0, y: 0, width: 250, height: 250 };
// Allow caller to request a small margin around the crop to avoid clipping
const cropMargin = (data && typeof data.cropMargin === 'number') ? data.cropMargin : 6;
// expand crop safely
const expandedCrop = {
x: Math.max(0, crop.x - cropMargin),
y: Math.max(0, crop.y - cropMargin),
width: crop.width + cropMargin * 2,
height: crop.height + cropMargin * 2
};
crop = expandedCrop;
console.debug('omdProblem: final crop chosen (expanded)', crop);
// Build compact svg sized to crop area
const compact = document.createElementNS(SVG_NS, 'svg');
compact.setAttribute('width', String(crop.width));
compact.setAttribute('height', String(crop.height));
compact.setAttribute('viewBox', `0 0 ${crop.width} ${crop.height}`);
const contentWrapper = document.createElementNS(SVG_NS, 'g');
// don't apply arbitrary offsets; translate content so crop.x/y maps to 0,0
contentWrapper.setAttribute('transform', `translate(-55, -80)`);
contentWrapper.appendChild(cloneRoot);
compact.appendChild(contentWrapper);
// Position compact svg under the problem text
let textBoxHeight = 0;
try {
if (this.problemText.height) textBoxHeight = this.problemText.height;
else if (this.problemText.svgObject && this.problemText.svgObject.getBBox) textBoxHeight = this.problemText.svgObject.getBBox().height;
else textBoxHeight = 30;
} catch (e) { textBoxHeight = 30; }
// Center horizontally inside the problem box. Determine available width
// Caller can provide containerInfo to indicate the rounded-rect container dimensions
let containerWidth = 300;
let containerOffsetY = null;
let containerInnerPadding = 8;
try {
if (data && data.containerInfo) {
if (typeof data.containerInfo.width === 'number') containerWidth = data.containerInfo.width;
if (typeof data.containerInfo.offsetY === 'number') containerOffsetY = data.containerInfo.offsetY;
if (typeof data.containerInfo.innerPadding === 'number') containerInnerPadding = data.containerInfo.innerPadding;
} else if (this.width) containerWidth = this.width;
else if (this.problemText && this.problemText.width) containerWidth = Math.max(300, this.problemText.width);
} catch (e) { containerWidth = 300; }
const desiredY = Math.round((containerOffsetY !== null) ? (containerOffsetY + padding) : (textBoxHeight + padding));
// Ensure the compact SVG never extends outside the container: clamp and scale if needed
const innerPadding = containerInnerPadding; // keep some breathing room inside the rounded rectangle
const maxAllowedWidth = Math.max(20, containerWidth - innerPadding * 2);
let scale = 1;
if (crop.width > maxAllowedWidth) scale = maxAllowedWidth / crop.width;
const scaledWidth = Math.round(crop.width * scale);
const scaledHeight = Math.round(crop.height * scale);
// Apply scaled pixel dimensions to compact so it renders at the clamped size
compact.setAttribute('width', String(scaledWidth));
compact.setAttribute('height', String(scaledHeight));
// center: (containerWidth - scaledWidth) / 2, but don't go negative
let desiredX = Math.max(innerPadding, Math.round((containerWidth - scaledWidth) / 2));
console.debug('omdProblem: containerWidth, desiredX, desiredY', { containerWidth, desiredX, desiredY, cropWidth: crop.width, cropHeight: crop.height, scaledWidth, scaledHeight, scale });
const outerWrapper = document.createElementNS(SVG_NS, 'g');
outerWrapper.appendChild(compact);
outerWrapper.setAttribute('transform', `translate(${desiredX}, ${desiredY})`);
this.svgObject.appendChild(outerWrapper);
// Optional visual debug overlay: outlines the compact area if requested
if (data && data.debugPlacement) {
try {
const debugRect = document.createElementNS(SVG_NS, 'rect');
debugRect.setAttribute('x', '0');
debugRect.setAttribute('y', '0');
// debug rect shows the scaled layout area
debugRect.setAttribute('width', String(scaledWidth));
debugRect.setAttribute('height', String(scaledHeight));
debugRect.setAttribute('fill', 'none');
debugRect.setAttribute('stroke', 'rgba(255,0,0,0.9)');
debugRect.setAttribute('stroke-width', '2');
debugRect.setAttribute('pointer-events', 'none');
outerWrapper.appendChild(debugRect);
} catch (e) { console.warn('omdProblem: failed to add debugPlacement rect', e); }
}
this.updateLayout();
return;
} catch (e) {
console.warn('omdProblem: svgElement fast-path failed, falling back to regenerate:', e);
// fall through to regeneration attempt
}
}
}
this.updateLayout();
}
setName( newName )
{
this.name = newName;
this.updateLayout();
}
updateLayout()
{
// Update text content and size the problem container to fit the text and any child visualization
this.problemText.setText( this.theText );
// Measure the text box (use properties or fall back to getBBox)
let textBoxWidth = 400;
let textBoxHeight = 130;
try {
if (this.problemText.width) textBoxWidth = this.problemText.width;
if (this.problemText.height) textBoxHeight = this.problemText.height;
else if (this.problemText.svgObject && this.problemText.svgObject.getBBox) {
const bb = this.problemText.svgObject.getBBox();
if (bb.width) textBoxWidth = bb.width;
if (bb.height) textBoxHeight = bb.height;
}
} catch (e) {
// keep defaults
}
// Compute extra height from any visualization child we added
let extraHeight = 0;
try {
// Find a child that isn't the problemText (likely the visualization)
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (child !== this.problemText && child && child.svgObject && child.svgObject.getBBox) {
const bb = child.svgObject.getBBox();
extraHeight = Math.max(extraHeight, bb.height + 20); // include padding
}
}
} catch (e) {
extraHeight = 0;
}
const totalWidth = Math.max(300, textBoxWidth);
const totalHeight = Math.max(200, textBoxHeight + extraHeight + 20);
this.setWidthAndHeight( totalWidth, totalHeight );
}
}