mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-21 05:32:34 -06:00
Merge pull request #60 from screentinker/fix/ai-deoverlap
fix(ai): de-overlap text + layer shapes behind text (#41)
This commit is contained in:
commit
df4110d9ca
|
|
@ -95,9 +95,45 @@ function normalizeDesign(raw) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// De-overlap text lines (models stack them at the same y) and order shapes
|
||||||
|
// behind text so accent bands never hide the words.
|
||||||
|
const shapes = out.elements.filter((e) => e.type === 'shape');
|
||||||
|
const texts = out.elements.filter((e) => e.type === 'text');
|
||||||
|
deoverlapTexts(texts);
|
||||||
|
out.elements = [...shapes, ...texts];
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Push text lines apart so they don't sit on top of each other. Only nudges a
|
||||||
|
// line down when it also overlaps horizontally (leaves side-by-side text alone),
|
||||||
|
// then shifts the whole stack up if it ran past the bottom margin. CW/CH match
|
||||||
|
// fitText's width/height estimates.
|
||||||
|
function deoverlapTexts(texts) {
|
||||||
|
const M = 4, GAP = 2.5, CW = 0.075, CH = 0.26;
|
||||||
|
const widthOf = (el) => Math.max(1, el.text.length) * el.fontSize * CW;
|
||||||
|
const heightOf = (el) => el.fontSize * CH;
|
||||||
|
const ordered = texts.map((el, i) => ({ el, i })).sort((a, b) => a.el.y - b.el.y || a.i - b.i);
|
||||||
|
const placed = [];
|
||||||
|
for (const cur of ordered) {
|
||||||
|
const cw = widthOf(cur.el);
|
||||||
|
let minY = M;
|
||||||
|
for (const p of placed) {
|
||||||
|
const hOverlap = cur.el.x < p.el.x + widthOf(p.el) && p.el.x < cur.el.x + cw;
|
||||||
|
if (hOverlap) minY = Math.max(minY, p.el.y + heightOf(p.el) + GAP);
|
||||||
|
}
|
||||||
|
if (cur.el.y < minY) cur.el.y = Math.round(minY * 10) / 10;
|
||||||
|
placed.push(cur);
|
||||||
|
}
|
||||||
|
let maxBottom = 0;
|
||||||
|
for (const p of placed) maxBottom = Math.max(maxBottom, p.el.y + heightOf(p.el));
|
||||||
|
const overflow = maxBottom - (100 - M);
|
||||||
|
if (overflow > 0 && placed.length) {
|
||||||
|
const shift = Math.min(overflow, Math.min(...placed.map((p) => p.el.y)) - M);
|
||||||
|
if (shift > 0) for (const p of placed) p.el.y = Math.round((p.el.y - shift) * 10) / 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GET /api/ai/settings — workspace members (never returns the key)
|
// GET /api/ai/settings — workspace members (never returns the key)
|
||||||
router.get('/settings', (req, res) => {
|
router.get('/settings', (req, res) => {
|
||||||
const row = db.prepare('SELECT base_url, model, image_base_url, image_model, api_key_enc FROM ai_settings WHERE workspace_id = ?').get(req.workspaceId);
|
const row = db.prepare('SELECT base_url, model, image_base_url, image_model, api_key_enc FROM ai_settings WHERE workspace_id = ?').get(req.workspaceId);
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,9 @@ test('normalizeDesign: keeps valid text+shape, sets background', () => {
|
||||||
]});
|
]});
|
||||||
assert.equal(d.background, '#102030');
|
assert.equal(d.background, '#102030');
|
||||||
assert.equal(d.elements.length, 2);
|
assert.equal(d.elements.length, 2);
|
||||||
assert.equal(d.elements[0].text, 'HELLO');
|
const txt = d.elements.find((e) => e.type === 'text');
|
||||||
assert.equal(d.elements[0].fontFamily, 'Arial');
|
assert.equal(txt.text, 'HELLO');
|
||||||
|
assert.equal(txt.fontFamily, 'Arial');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('normalizeDesign: converts pixel shape dims to %, clamps ranges', () => {
|
test('normalizeDesign: converts pixel shape dims to %, clamps ranges', () => {
|
||||||
|
|
@ -86,3 +87,16 @@ test('normalizeDesign: long/large text is shrunk + repositioned to fit canvas',
|
||||||
assert.ok(e.fontSize < 160, 'fontSize was shrunk');
|
assert.ok(e.fontSize < 160, 'fontSize was shrunk');
|
||||||
assert.ok(e.x >= 4 && e.y >= 4, 'within margins');
|
assert.ok(e.x >= 4 && e.y >= 4, 'within margins');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('normalizeDesign: separates overlapping text + orders shapes behind text', () => {
|
||||||
|
const d = normalizeDesign({ elements: [
|
||||||
|
{ type: 'text', x: 5, y: 40, text: 'HEADLINE TEXT HERE', fontSize: 60, color: '#fff' },
|
||||||
|
{ type: 'text', x: 5, y: 41, text: 'SUBTEXT OVERLAPPING IT', fontSize: 40, color: '#fff' },
|
||||||
|
{ type: 'shape', x: 0, y: 0, width: 100, height: 100, color: '#000', opacity: 0.5 },
|
||||||
|
]});
|
||||||
|
assert.equal(d.elements[0].type, 'shape', 'shape rendered behind (first in array)');
|
||||||
|
const texts = d.elements.filter((e) => e.type === 'text');
|
||||||
|
const hi = texts[0].y <= texts[1].y ? texts[0] : texts[1];
|
||||||
|
const lo = texts[0].y <= texts[1].y ? texts[1] : texts[0];
|
||||||
|
assert.ok(lo.y >= hi.y + hi.fontSize * 0.22, 'text lines no longer overlap vertically');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue