From 6992ecdf80579adb521c213ddf5ce9775ab59ba4 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 13 Sep 2026 14:54:17 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20a=20flowchart=20slide=20=E2=80=94=20ste?= =?UTF-8?q?ps=20in,=20boxes=20and=20arrows=20out?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pathway or an algorithm had no slide of its own and came out as bullets. "flow" is a slide of two to eight steps, each a short phrase with an optional note; the exporter lays it out — across for up to four, down for more — as rounded boxes joined by arrows in the shape vocabulary the renderer already draws, so the model is never asked for coordinates and Python gains one small builder. Word gets the steps as a numbered list. The theme sample shows one, so every template demonstrates it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU --- .../__pycache__/render_pptx.cpython-313.pyc | Bin 0 -> 44145 bytes scripts/render_pptx.py | 13 ++++ src/utils/deckSample.js | 8 +++ src/utils/deckSchema.js | 68 +++++++++++++++++- src/utils/docSpec.js | 4 ++ src/utils/documentExport.js | 9 +++ test/deck-theme-sample.test.js | 32 ++++++++- 7 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 scripts/__pycache__/render_pptx.cpython-313.pyc diff --git a/scripts/__pycache__/render_pptx.cpython-313.pyc b/scripts/__pycache__/render_pptx.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..06a570e296edc46e2f214bbc76640e1494cf4617 GIT binary patch literal 44145 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ℜy4Q4sIl%#UF69TCrTl5@SqZ4XK87 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 UnfGkxeze`X*J}8&MF;r50bM$HmjD0& literal 0 HcmV?d00001 diff --git a/scripts/render_pptx.py b/scripts/render_pptx.py index ba12fd7a..aed3f814 100644 --- a/scripts/render_pptx.py +++ b/scripts/render_pptx.py @@ -674,6 +674,18 @@ def draw_image(slide, shape): Emu(int(top) + (int(height) - draw_h) // 2), Emu(draw_w), Emu(draw_h)) +def slide_flow(prs, spec): + """A flowchart. Laid out by the exporter (deckSchema.flowShapes) into + shapes before it gets here, so it is drawn as a custom slide; a bare flow + that arrives with steps only is drawn as the numbered list it is.""" + if spec.get("shapes"): + return slide_custom(prs, spec) + steps = spec.get("steps") or [] + return slide_bullets(prs, {"heading": spec.get("heading"), "notes": spec.get("notes"), + "bullets": [{"text": f"{i + 1}. {s.get('text', '')}", "level": 0} + for i, s in enumerate(steps) if isinstance(s, dict)]}) + + def slide_custom(prs, spec): slide = _blank(prs) if spec.get("heading"): @@ -709,6 +721,7 @@ BUILDERS = { "compare": slide_compare, "callout": slide_callout, "figure": slide_figure, + "flow": slide_flow, "custom": slide_custom, } diff --git a/src/utils/deckSample.js b/src/utils/deckSample.js index 4566d082..69678654 100644 --- a/src/utils/deckSample.js +++ b/src/utils/deckSample.js @@ -86,6 +86,14 @@ function build(theme) { 'The grey panel stands in for an illustration' ]) }, + { type: 'flow', heading: 'A flowchart: a pathway laid out for you', + steps: [ + { text: 'Assess', note: 'a note under the step' }, + { text: 'Decide' }, + { text: 'Act', note: 'a second line' }, + { text: 'Reassess' } + ] }, + { type: 'section', heading: 'A section divider' }, { type: 'image', heading: 'A full-slide figure', diff --git a/src/utils/deckSchema.js b/src/utils/deckSchema.js index 52664d69..733cbca9 100644 --- a/src/utils/deckSchema.js +++ b/src/utils/deckSchema.js @@ -16,7 +16,7 @@ var slideShapes = require('./slideShapes'); -var VALID = ['title', 'section', 'bullets', 'two', 'compare', 'table', 'callout', 'figure', 'image', 'custom']; +var VALID = ['title', 'section', 'bullets', 'two', 'compare', 'table', 'callout', 'figure', 'image', 'flow', 'custom']; // Given to the model verbatim. Written as prose rather than a JSON Schema dump // because the failure to avoid is a model that produces valid JSON describing a @@ -54,6 +54,12 @@ function instructions(slideCount, figureCount) { '{"type":"section","heading":"..."}', ' A divider between parts of a long deck.', '', + '{"type":"flow","heading":"...","steps":[{"text":"Assess airway","note":"stridor at rest?"},{"text":"Dexamethasone 0.15 mg/kg"},{"text":"Reassess at 30 min"}]}', + ' A flowchart: boxes joined by arrows, laid out for you. Use it for a pathway,', + ' an algorithm, a sequence of decisions — two to eight steps, each a short', + ' phrase; "note" is an optional second line. Far better than bullets for', + ' anything that is really a sequence.', + '', slideShapes.instructions(), '', figureCount @@ -134,6 +140,18 @@ function normalise(raw, gaps) { return (Array.isArray(row) ? row : []).slice(0, 6).map(function (c) { return text(c, 200); }); }).filter(function (row) { return row.some(Boolean); }); if (!out.rows.length) { out.type = 'bullets'; out.bullets = bullets(slide.bullets); } + } else if (type === 'flow') { + out.steps = (Array.isArray(slide.steps) ? slide.steps : []).slice(0, 8).map(function (step) { + if (typeof step === 'string') step = { text: step }; + return { text: text(step && step.text, 90), note: text(step && step.note, 90) }; + }).filter(function (step) { return step.text; }); + // One box is not a flow; it is a bullet. + if (out.steps.length < 2) { + out.type = 'bullets'; + out.bullets = bullets(out.steps.map(function (s) { return s.text + (s.note ? ' — ' + s.note : ''); })); + delete out.steps; + if (!out.bullets.length && !out.heading) return; + } } else if (type === 'callout') { out.text = text(slide.text, 400); if (!out.text) return; @@ -267,6 +285,54 @@ function figureRequests(deck) { return wanted; } +// ── The flowchart, laid out ─────────────────────────────────── +// A flow slide is stored as steps and drawn as a custom slide: rounded boxes +// joined by arrows, in a row when there are up to four, in a column above +// that. The author names the steps; the geometry is ours, so the model is +// never asked for coordinates. Percent units, like every custom shape. +var FLOW_FILL = 'EFF6FF', FLOW_LINE = '2563EB', FLOW_ARROW = '94A3B8'; +function flowShapes(slide) { + var steps = (slide.steps || []).slice(0, 8); + var n = steps.length; + var shapes = []; + if (n < 2) return shapes; + function box(x, y, w, h, step, size) { + var runs = [{ text: step.text, bold: true, align: 'center', size: size }]; + if (step.note) runs.push({ text: step.note, align: 'center', size: Math.max(9, size - 3), color: '475569' }); + shapes.push({ kind: 'roundRect', x: x, y: y, w: w, h: h, fill: FLOW_FILL, line: FLOW_LINE, runs: runs }); + } + if (n <= 4) { + // Across: boxes share 90% of the width, a 6%-wide arrow between each pair. + var arrowW = 6, gap = 1.5; + var boxW = (90 - (n - 1) * (arrowW + 2 * gap)) / n; + var y = 36, h = 24, x = 5; + steps.forEach(function (step, i) { + box(x, y, boxW, h, step, n <= 3 ? 16 : 13); + x += boxW; + if (i < n - 1) { + shapes.push({ kind: 'arrow', x: x + gap, y: y + h / 2 - 4, w: arrowW, h: 8, fill: FLOW_ARROW }); + x += arrowW + 2 * gap; + } + }); + } else { + // Down: a column in the middle, an arrow between each pair. + var arrowH = 4, gapV = 1; + var top = 26, bottom = 94; + var boxH = (bottom - top - (n - 1) * (arrowH + 2 * gapV)) / n; + var cy = top, w = 62, cx = 19; + steps.forEach(function (step, i) { + box(cx, cy, w, boxH, step, n <= 6 ? 12 : 10); + cy += boxH; + if (i < n - 1) { + shapes.push({ kind: 'arrowDown', x: cx + w / 2 - 3, y: cy + gapV, w: 6, h: arrowH, fill: FLOW_ARROW }); + cy += arrowH + 2 * gapV; + } + }); + } + return shapes; +} + module.exports = { + flowShapes: flowShapes, themes: themes, themeId: themeId, instructions, normalise, toMarkdown, figureRequests, VALID }; diff --git a/src/utils/docSpec.js b/src/utils/docSpec.js index e80baa9f..78972e05 100644 --- a/src/utils/docSpec.js +++ b/src/utils/docSpec.js @@ -42,6 +42,10 @@ function fromDeck(deck, images) { rows.push([text((left.bullets[i] || {}).text, 600), text((right.bullets[i] || {}).text, 600)]); } blocks.push({ type: 'table', header: [text(left.label, 120), text(right.label, 120)], rows: rows }); + } else if (slide.type === 'flow' && (slide.steps || []).length) { + blocks.push({ type: 'bullets', items: slide.steps.map(function (step, i) { + return { text: (i + 1) + '. ' + text(step.text, 200) + (step.note ? ' — ' + text(step.note, 200) : ''), level: 0 }; + }) }); } else if (slide.type === 'custom') { // A diagram has no Word equivalent, so it becomes what it says: the words // in reading order, its tables as tables, its figures as figures. Lossy, diff --git a/src/utils/documentExport.js b/src/utils/documentExport.js index bcdfc1bb..ed2b9467 100644 --- a/src/utils/documentExport.js +++ b/src/utils/documentExport.js @@ -18,6 +18,7 @@ var pathMod = require('path'); var { execFile, spawn } = require('child_process'); var JSZip = require('jszip'); var slideSpec = require('./slideSpec'); +var deckSchema = require('./deckSchema'); var docSpec = require('./docSpec'); var REFERENCE_DECK = pathMod.join(__dirname, '..', '..', 'assets', 'deck', 'slides-reference.pptx'); @@ -215,6 +216,14 @@ function attachFigures(deck, files, figureIds) { var byJob = figuresByJob(files, figureIds); var slides = (deck.slides || []).map(function (slide) { var copy = Object.assign({}, slide); + // A flowchart is stored as its steps and drawn as shapes: the layout is + // computed here, at render, so a re-skin or a wider box never has to be + // asked of the model. + if (copy.type === 'flow') { + copy.shapes = deckSchema.flowShapes(copy); + copy.type = 'custom'; + delete copy.steps; + } // A custom slide carries its figures on image shapes, under the same rule. if (Array.isArray(copy.shapes)) { copy.shapes = copy.shapes.map(function (shape) { diff --git a/test/deck-theme-sample.test.js b/test/deck-theme-sample.test.js index aa7b20e4..40716c04 100644 --- a/test/deck-theme-sample.test.js +++ b/test/deck-theme-sample.test.js @@ -15,7 +15,7 @@ const deckSchema = require('../src/utils/deckSchema'); // The vocabulary the renderer can draw, from the schema itself, so a layout // added later fails this until the sample shows it too. const SCHEMA_TYPES = ['title', 'section', 'bullets', 'two', 'compare', 'table', - 'callout', 'figure', 'image', 'custom']; + 'callout', 'figure', 'image', 'custom', 'flow']; test('the sample shows every layout the renderer can draw', () => { const deck = deckSample.build({ id: 'clinical-blue', name: 'Clinical Blue' }); @@ -117,3 +117,33 @@ test('the link names the theme it will download', () => { const fn = ui.slice(ui.indexOf('function showThemeSample'), ui.indexOf('function describeTheme')); assert.match(fn, /'Download a sample deck in ' \+ name/); }); + +test('a flowchart is steps in the schema, boxes and arrows on the slide, a numbered list in Word', () => { + const deckSchema = require('../src/utils/deckSchema'); + const docSpec = require('../src/utils/docSpec'); + const deck = deckSchema.normalise({ title: 't', slides: [ + { type: 'flow', heading: 'Croup', steps: [{ text: 'Assess', note: 'stridor at rest?' }, { text: 'Dexamethasone' }, { text: 'Reassess' }, { text: 'Admit or home' }] }, + { type: 'flow', heading: 'Long', steps: [1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => 'Step ' + i) }, + { type: 'flow', heading: 'One', steps: ['only one'] } + ] }, []); + assert.deepEqual(deck.slides.map(s => [s.type, (s.steps || s.bullets).length]), [['flow', 4], ['flow', 8], ['bullets', 1]], + 'eight steps at most; a single step is a bullet, not a flow'); + // Across for four: four boxes, three arrows, all inside the slide. + const across = deckSchema.flowShapes(deck.slides[0]); + assert.deepEqual(across.map(s => s.kind), ['roundRect', 'arrow', 'roundRect', 'arrow', 'roundRect', 'arrow', 'roundRect']); + assert.ok(across.every(s => s.x >= 0 && s.y >= 0 && s.x + s.w <= 100 && s.y + s.h <= 100)); + assert.equal(across[0].runs[1].text, 'stridor at rest?', 'the note is the second line of its box'); + // Down for eight: arrows point down, nothing runs off the bottom. + const down = deckSchema.flowShapes(deck.slides[1]); + assert.equal(down.filter(s => s.kind === 'arrowDown').length, 7); + assert.ok(Math.max(...down.map(s => s.y + s.h)) <= 100); + // Word gets the steps as a numbered list, in order. + const doc = docSpec.build({ deck: { title: 't', slides: [deck.slides[0]] }, markdown: '' }); + const list = doc.blocks.find(b => b.type === 'bullets'); + assert.equal(list.items[0].text, '1. Assess — stridor at rest?'); + assert.equal(list.items[3].text, '4. Admit or home'); + // The renderer never sees "flow": it is handed the shapes. + const src = fs.readFileSync(path.join(__dirname, '..', 'src/utils/documentExport.js'), 'utf8'); + assert.match(src, /if \(copy\.type === 'flow'\) \{\s*copy\.shapes = deckSchema\.flowShapes\(copy\);\s*copy\.type = 'custom';/); + assert.match(deckSchema.instructions(6, 0), /"type":"flow"/, 'the model is told it exists'); +});