yonge 1 hari lalu
induk
melakukan
1cbe3d955e

+ 1 - 1
dist/assets/index-ykApa_GF.js → dist/assets/index-BAKc3dgA.js

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-D_52pD1d.js";import{a5 as n,ae as i,af as r,ak as o,aq as _,am as d,P as l,aS as p,bO as m,bP as f,bH as u}from"./index-DFtuSmYr.js";const s=a=>(m("data-v-340bbdaa"),a=a(),f(),a),h={class:"login"},b={class:"error"},g=s(()=>o("img",{src:t,class:"img",alt:""},null,-1)),k=s(()=>o("div",{class:"tit"},"登录已过期或服务器错误!",-1)),v=n({__name:"login",setup(a){function e(){window.close()}return(x,B)=>(i(),r("div",h,[o("div",b,[g,k,_(l(c),{class:"backBtn",type:"primary",plain:"",onClick:e},{default:d(()=>[p("关闭页面")]),_:1})])]))}}),C=u(v,[["__scopeId","data-v-340bbdaa"]]);export{C as default};
+import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-CHshgQoq.js";import{a5 as n,ae as i,af as r,ak as o,aq as _,am as d,P as l,aS as p,bO as m,bP as f,bH as u}from"./index-V3qgdgGd.js";const s=a=>(m("data-v-340bbdaa"),a=a(),f(),a),h={class:"login"},b={class:"error"},g=s(()=>o("img",{src:t,class:"img",alt:""},null,-1)),k=s(()=>o("div",{class:"tit"},"登录已过期或服务器错误!",-1)),v=n({__name:"login",setup(a){function e(){window.close()}return(x,B)=>(i(),r("div",h,[o("div",b,[g,k,_(l(c),{class:"backBtn",type:"primary",plain:"",onClick:e},{default:d(()=>[p("关闭页面")]),_:1})])]))}}),C=u(v,[["__scopeId","data-v-340bbdaa"]]);export{C as default};

+ 1 - 1
dist/assets/index-DSieeq3F.js → dist/assets/index-BXNEIpoI.js

@@ -1,4 +1,4 @@
-import{cH as pw,cI as gw,aG as mw,cJ as yw,cK as Sw,cL as bw,cM as xw,ah as Wt,cN as Cw,cO as ww,cP as Tw,cQ as Iw,aM as _w,aF as Aw,cR as Ew,aB as d1,bg as Pw,cS as Mw,cT as Rw,cU as Dw,cV as Lw,cW as kw,cX as Ow,cY as Bw,aE as Nw,cZ as Fw,s as Te,bl as $w,al as je,ag as Fe,af as he,ak as Z,c_ as zw,c$ as Uw,d0 as Hw,d1 as Vw,bk as Gw,cy as cl,aS as wv,aq as Ye,d2 as Ww,d3 as jw,a5 as ze,d4 as Kw,d5 as Xw,d6 as Yw,d7 as Zw,d8 as Qw,d9 as Jw,da as qw,db as eT,dc as tT,dd as rT,de as nT,v as iT,df as aT,dg as oT,dh as sT,aW as lT,di as uT,dj as cT,dk as fT,dl as dT,dm as hT,ay as ur,dn as vT,dp as pT,dq as gT,dr as mT,A as yT,ds as ST,dt as bT,bq as xT,du as CT,dv as wT,dw as TT,ap as IT,ac as Tv,ai as ct,dx as _T,au as xe,dy as AT,X as h1,y as ET,dz as PT,aP as MT,dA as RT,w as Er,dB as DT,dC as LT,H as kT,dD as OT,bA as hn,aD as BT,ae as ne,bP as ea,$ as Tc,dE as NT,bO as ta,dF as FT,aC as $T,aO as zT,R as fe,dG as UT,br as ih,b3 as _r,aj as Hs,b1 as lt,b2 as Ua,an as Iv,dH as HT,dI as VT,dJ as GT,dK as WT,dL as jT,dM as KT,dN as XT,Q as YT,dO as ZT,dP as QT,dQ as JT,at as Mt,dR as qT,aR as eI,aY as tI,ad as fl,aZ as rI,dS as nI,dT as iI,dU as aI,P as le,a6 as oI,dV as sI,dW as lI,dX as uI,dY as cI,a7 as fI,dZ as dI,bM as hI,d_ as vI,d$ as pI,bL as gI,b4 as v1,aA as _v,e0 as mI,e1 as yI,E as Jt,x as SI,e2 as bI,e3 as xI,e4 as CI,am as _n,e5 as wI,az as sr,aU as Vs,e6 as TI,as as pr,e7 as II,e8 as Av,cG as yr,by as ah,cw as oh,bI as _I,bH as Ve,bu as sh,bv as Ev,bx as jt,bB as Ic,bQ as di,e9 as Tu,bE as hi,ea as AI,bJ as ra,bC as EI,bw as Lt,eb as PI,ec as ui,bN as MI,ed as RI,ee as DI,ef as LI,eg as kI,eh as Jp,ei as OI,bF as qp,cD as _c,cC as BI,ej as Vu,ek as _l}from"./index-DFtuSmYr.js";/**
+import{cH as pw,cI as gw,aG as mw,cJ as yw,cK as Sw,cL as bw,cM as xw,ah as Wt,cN as Cw,cO as ww,cP as Tw,cQ as Iw,aM as _w,aF as Aw,cR as Ew,aB as d1,bg as Pw,cS as Mw,cT as Rw,cU as Dw,cV as Lw,cW as kw,cX as Ow,cY as Bw,aE as Nw,cZ as Fw,s as Te,bl as $w,al as je,ag as Fe,af as he,ak as Z,c_ as zw,c$ as Uw,d0 as Hw,d1 as Vw,bk as Gw,cy as cl,aS as wv,aq as Ye,d2 as Ww,d3 as jw,a5 as ze,d4 as Kw,d5 as Xw,d6 as Yw,d7 as Zw,d8 as Qw,d9 as Jw,da as qw,db as eT,dc as tT,dd as rT,de as nT,v as iT,df as aT,dg as oT,dh as sT,aW as lT,di as uT,dj as cT,dk as fT,dl as dT,dm as hT,ay as ur,dn as vT,dp as pT,dq as gT,dr as mT,A as yT,ds as ST,dt as bT,bq as xT,du as CT,dv as wT,dw as TT,ap as IT,ac as Tv,ai as ct,dx as _T,au as xe,dy as AT,X as h1,y as ET,dz as PT,aP as MT,dA as RT,w as Er,dB as DT,dC as LT,H as kT,dD as OT,bA as hn,aD as BT,ae as ne,bP as ea,$ as Tc,dE as NT,bO as ta,dF as FT,aC as $T,aO as zT,R as fe,dG as UT,br as ih,b3 as _r,aj as Hs,b1 as lt,b2 as Ua,an as Iv,dH as HT,dI as VT,dJ as GT,dK as WT,dL as jT,dM as KT,dN as XT,Q as YT,dO as ZT,dP as QT,dQ as JT,at as Mt,dR as qT,aR as eI,aY as tI,ad as fl,aZ as rI,dS as nI,dT as iI,dU as aI,P as le,a6 as oI,dV as sI,dW as lI,dX as uI,dY as cI,a7 as fI,dZ as dI,bM as hI,d_ as vI,d$ as pI,bL as gI,b4 as v1,aA as _v,e0 as mI,e1 as yI,E as Jt,x as SI,e2 as bI,e3 as xI,e4 as CI,am as _n,e5 as wI,az as sr,aU as Vs,e6 as TI,as as pr,e7 as II,e8 as Av,cG as yr,by as ah,cw as oh,bI as _I,bH as Ve,bu as sh,bv as Ev,bx as jt,bB as Ic,bQ as di,e9 as Tu,bE as hi,ea as AI,bJ as ra,bC as EI,bw as Lt,eb as PI,ec as ui,bN as MI,ed as RI,ee as DI,ef as LI,eg as kI,eh as Jp,ei as OI,bF as qp,cD as _c,cC as BI,ej as Vu,ek as _l}from"./index-V3qgdgGd.js";/**
 * vue v3.4.34
 * (c) 2018-present Yuxi (Evan) You and Vue contributors
 * @license MIT

+ 1 - 1
dist/assets/index-tMsysvx2.js → dist/assets/index-C0Fh1NG_.js

@@ -1 +1 @@
-import{u as s,ag as r}from"./index-DSieeq3F.js";import{a5 as c,cD as n,R as p,ae as t,af as _,al as u,ag as i,bH as l}from"./index-DFtuSmYr.js";const m={class:"pptScreen"},d=c({__name:"pptScreen",setup(f){const a=n(),e=p(!0),o=s();return a.setScreenMode("pptScreen"),o.initPPTData().then(()=>{e.value=!1}),(S,k)=>(t(),_("div",m,[e.value?i("",!0):(t(),u(r,{key:0}))]))}}),g=l(d,[["__scopeId","data-v-678905e9"]]);export{g as default};
+import{u as s,ag as r}from"./index-BXNEIpoI.js";import{a5 as c,cD as n,R as p,ae as t,af as _,al as u,ag as i,bH as l}from"./index-V3qgdgGd.js";const m={class:"pptScreen"},d=c({__name:"pptScreen",setup(f){const a=n(),e=p(!0),o=s();return a.setScreenMode("pptScreen"),o.initPPTData().then(()=>{e.value=!1}),(S,k)=>(t(),_("div",m,[e.value?i("",!0):(t(),u(r,{key:0}))]))}}),g=l(d,[["__scopeId","data-v-678905e9"]]);export{g as default};

File diff ditekan karena terlalu besar
+ 0 - 0
dist/assets/index-CHshgQoq.js


File diff ditekan karena terlalu besar
+ 0 - 0
dist/assets/index-CL-4I1LD.js


+ 1 - 1
dist/assets/index-BuoWoiic.js → dist/assets/index-DUkyeNq7.js

@@ -1 +1 @@
-import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,eo as t,ae as r,af as c,bO as _,bP as d,ak as s,bH as p}from"./index-DFtuSmYr.js";const i=e=>(_("data-v-dda25179"),e=e(),d(),e),n={class:"errorPage"},m=i(()=>s("div",{class:"error"},[s("img",{src:a,class:"img",alt:""}),s("div",{class:"tit"},"页面找不到了~")],-1)),l=[m],f=o({__name:"errorPage",setup(e){return t(),(u,h)=>(r(),c("div",n,l))}}),I=p(f,[["__scopeId","data-v-dda25179"]]);export{I as default};
+import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,eo as t,ae as r,af as c,bO as _,bP as d,ak as s,bH as p}from"./index-V3qgdgGd.js";const i=e=>(_("data-v-dda25179"),e=e(),d(),e),n={class:"errorPage"},m=i(()=>s("div",{class:"error"},[s("img",{src:a,class:"img",alt:""}),s("div",{class:"tit"},"页面找不到了~")],-1)),l=[m],f=o({__name:"errorPage",setup(e){return t(),(u,h)=>(r(),c("div",n,l))}}),I=p(f,[["__scopeId","data-v-dda25179"]]);export{I as default};

+ 1 - 1
dist/assets/index-DC_-oGFJ.js → dist/assets/index-Dfi5vntL.js

@@ -1 +1 @@
-import{u as c,ag as l}from"./index-DSieeq3F.js";import{w as p,bA as d,a5 as g,cD as m,R as t,ae as r,af as u,al as f,ag as w,bH as k}from"./index-DFtuSmYr.js";import"./shapes-DQLOV3Qn.js";const S=()=>{function o(a){if(window.parent===window)return;const{api:e,playState:n}=a.data||{};e==="headerTogge"?window.parent.postMessage({api:"headerTogge",playState:n},"*"):e==="changeTogge"?window.parent.postMessage({api:"changeTogge"},"*"):["clickTempo","clickViewFigner"].includes(e)&&window.parent.postMessage({api:e},"*")}p(()=>{window.addEventListener("message",o)}),d(()=>{window.removeEventListener("message",o)})},_=g({__name:"mobileScreen",setup(o){S();const a=m(),e=t(!0),n=t(!1),i=c();return a.setScreenMode("mobileScreen"),i.initPPTData().then(()=>{e.value=!1}),(v,s)=>(r(),u("div",{class:"mobileScreen",onClick:s[0]||(s[0]=b=>n.value=!1)},[e.value?w("",!0):(r(),f(l,{key:0}))]))}}),P=k(_,[["__scopeId","data-v-90b2692a"]]);export{P as default};
+import{u as c,ag as l}from"./index-BXNEIpoI.js";import{w as p,bA as d,a5 as g,cD as m,R as t,ae as r,af as u,al as f,ag as w,bH as k}from"./index-V3qgdgGd.js";import"./shapes-BQ6QuFfS.js";const S=()=>{function o(a){if(window.parent===window)return;const{api:e,playState:n}=a.data||{};e==="headerTogge"?window.parent.postMessage({api:"headerTogge",playState:n},"*"):e==="changeTogge"?window.parent.postMessage({api:"changeTogge"},"*"):["clickTempo","clickViewFigner"].includes(e)&&window.parent.postMessage({api:e},"*")}p(()=>{window.addEventListener("message",o)}),d(()=>{window.removeEventListener("message",o)})},_=g({__name:"mobileScreen",setup(o){S();const a=m(),e=t(!0),n=t(!1),i=c();return a.setScreenMode("mobileScreen"),i.initPPTData().then(()=>{e.value=!1}),(v,s)=>(r(),u("div",{class:"mobileScreen",onClick:s[0]||(s[0]=b=>n.value=!1)},[e.value?w("",!0):(r(),f(l,{key:0}))]))}}),P=k(_,[["__scopeId","data-v-90b2692a"]]);export{P as default};

File diff ditekan karena terlalu besar
+ 1 - 1
dist/assets/index-V3qgdgGd.js


+ 1 - 1
dist/assets/shapes-DQLOV3Qn.js → dist/assets/shapes-BQ6QuFfS.js

@@ -1,4 +1,4 @@
-import{cG as k,bB as $1}from"./index-DFtuSmYr.js";import{ah as Z,ai as H1,aj as U,ak as n0,al as c0,am as i0,an as j1,ao as K1,ap as Y1,aq as R}from"./index-DSieeq3F.js";var z1={exports:{}};/*!
+import{cG as k,bB as $1}from"./index-V3qgdgGd.js";import{ah as Z,ai as H1,aj as U,ak as n0,al as c0,am as i0,an as j1,ao as K1,ap as Y1,aq as R}from"./index-BXNEIpoI.js";var z1={exports:{}};/*!
  * clipboard.js v2.0.11
  * https://clipboardjs.com/
  *

+ 1 - 1
dist/index.html

@@ -10,7 +10,7 @@
     <meta name="description" content="ppt编辑器" />
     <meta name="keywords" content="ppt,powerpoint,office powerpoint,在线ppt,幻灯片,演示文稿,ppt在线制作,Vue3,TypeScript" />
     <title>PPT</title>
-    <script type="module" crossorigin src="./assets/index-DFtuSmYr.js"></script>
+    <script type="module" crossorigin src="./assets/index-V3qgdgGd.js"></script>
     <link rel="stylesheet" crossorigin href="./assets/index-AavUEaYy.css">
   </head>
   <body>

+ 17 - 2
scripts/aiPptSelfTest.mjs

@@ -3,7 +3,7 @@ import { existsSync } from "node:fs"
 import { resolve } from "node:path"
 import assert from "node:assert/strict"
 import { classroomFixture } from "./fixtures/aiPptClassroom.mjs"
-import { semanticDesignFixture, densityRegressionFixture } from "./fixtures/aiPptSemanticDesign.mjs"
+import { semanticDesignFixture, densityRegressionFixture, heroImageRegressionFixture } from "./fixtures/aiPptSemanticDesign.mjs"
 import { modelDesignFixture } from "./fixtures/aiPptModelDesign.mjs"
 
 const vite = await createServer({ server: { middlewareMode: true }, appType: "custom", logLevel: "error" })
@@ -400,12 +400,27 @@ try {
   const brokenDensity = structuredClone(densitySlides)
   brokenDensity[4].elements.filter(el => el.type === 'text' && density.pages[4].studentContent.points.some(point => elementText(el).includes(point))).forEach((el, i) => { el.top = 783.915 + i * 55.6875; el.height = 41.1075 })
   assert(inspectAiPptQuality(density, brokenDensity).errors.some(issue => issue.pageNo === 5 && issue.code === 'rendered_text_geometry'), '必须拦截之前坐标检查漏掉的真实高度溢出')
+  const heroImages = heroImageRegressionFixture(scoreImage)
+  const heroSlides = render(heroImages)
+  for (const index of [0, 1, 11]) {
+    const image = heroSlides[index].elements.find(el => el.type === 'image' && el.src === scoreImage)
+    assert(image.width * image.height / (1920 * 1080) >= .24, `第 ${index + 1} 页主图不能退化成小横幅`)
+    assert(Math.abs(image.width / image.height - 1584 / 560) < .001 && !image.clip, '主图保留完整比例,不裁切角色')
+    const captions = heroImages.pages[index].studentContent.points.map(text => heroSlides[index].elements.find(el => elementText(el) === text))
+    assert(captions.every(el => el && el.top === captions[0].top && el.top >= image.top + image.height), '短提示完整横排在主图下方')
+  }
+  assert.deepEqual(inspectAiPptQuality(heroImages, heroSlides).errors.filter(issue => ['out_of_bounds', 'text_overlap', 'text_overflow', 'rendered_text_geometry', 'missing_content', 'small_font'].includes(issue.code)), [])
+  const longCaption = structuredClone(heroImages)
+  longCaption.pages[1].studentContent.points[0] = '请仔细观察山谷里的溪水、树木与远山,再和身边的小伙伴一起讨论不同声音的变化以及自己最喜欢的声音,并用动作向大家展示你的发现。'
+  const longCaptionSlides = render(longCaption)
+  const longText = longCaptionSlides[1].elements.find(el => elementText(el) === longCaption.pages[1].studentContent.points[0])
+  assert(longText.width > 1000, '长内容保留整行,不硬塞进三列短提示')
   const { aiPptEntryChanged } = await vite.ssrLoadModule('/src/utils/aiPptClientVersion.ts')
   const html = '<script type="module" crossorigin src="./assets/index-new.js"></script>'
   assert(aiPptEntryChanged(html, 'https://example.com/classroom-ppt/', './assets/index-old.js'))
   assert(!aiPptEntryChanged(html, 'https://example.com/classroom-ppt/', 'https://example.com/classroom-ppt/assets/index-new.js'))
   assert(!aiPptEntryChanged('<html>login</html>', 'https://example.com/', './assets/index-old.js'))
-  console.log(JSON.stringify({ passed: failures.length === 0, scenarioCount: results.length, contractChecks: 13, imageTemplateChecks: AI_COURSEWARE_TEMPLATES.length, modelDesignChecks, semanticDesignChecks: 10, regressionChecks: 6,
+  console.log(JSON.stringify({ passed: failures.length === 0, scenarioCount: results.length, contractChecks: 13, imageTemplateChecks: AI_COURSEWARE_TEMPLATES.length, modelDesignChecks, semanticDesignChecks: 10, regressionChecks: 17,
     scenarios: courseVariants.map(item => item.id), ...(process.argv.includes("--verbose") ? { results } : {}), failures }, null, 2))
   if (failures.length) process.exitCode = 1
 } finally {

+ 2 - 2
scripts/aiPptVisualTest.html

@@ -23,7 +23,7 @@
       import "/src/assets/styles/prosemirror.scss"
       import { hydrateSlideBackgroundImages } from "/src/libs/jsonTool.ts"
       import { classroomFixture } from "./fixtures/aiPptClassroom.mjs"
-      import { semanticDesignFixture, densityRegressionFixture } from "./fixtures/aiPptSemanticDesign.mjs"
+      import { semanticDesignFixture, densityRegressionFixture, heroImageRegressionFixture } from "./fixtures/aiPptSemanticDesign.mjs"
       import { modelDesignFixture } from "./fixtures/aiPptModelDesign.mjs"
 
       const id = new URLSearchParams(location.search).get("template") || "ethnic-flower"
@@ -45,7 +45,7 @@
       }
       const scenario = new URLSearchParams(location.search).get("scenario")
       const fixtureImage = '/ai-courseware-templates/extracted/bird-garden/background-01.jpg'
-      const fixture = scenario === 'density' ? densityRegressionFixture(fixtureImage) : scenario === 'semantic' ? semanticDesignFixture(fixtureImage) : scenario === 'model-design' ? modelDesignFixture(fixtureImage)
+      const fixture = scenario === 'hero-images' ? heroImageRegressionFixture(fixtureImage) : scenario === 'density' ? densityRegressionFixture(fixtureImage) : scenario === 'semantic' ? semanticDesignFixture(fixtureImage) : scenario === 'model-design' ? modelDesignFixture(fixtureImage)
         : scenario === 'classroom' ? classroomFixture(fixtureImage) : outline
       const result = aiCoursewareToSlides(fixture, { templateId: id })
       const group = ref(0)

+ 18 - 0
scripts/fixtures/aiPptSemanticDesign.mjs

@@ -43,3 +43,21 @@ export function densityRegressionFixture(imageUrl) {
   })
   return outline
 }
+
+// 最新线上主图过小:三条短提示被纵排,挤压横向场景图。
+export function heroImageRegressionFixture(imageUrl) {
+  const outline = semanticDesignFixture(imageUrl)
+  outline.textbookName = '人教版(2024版)一年级上册(新)'
+  const scenes = [
+    [0, 'cover', '凤鸣山谷的故事', '观看图片,记住角色名字。', ['凤凰小九', '伶伦', '麒麟麒麒']],
+    [1, 'scene_intro', '走进凤鸣山谷', '仔细观察画面,说出看到的事物和想到的声音。', ['你看到了什么?', '听到了什么?', '谁住在山谷里?']],
+    [11, 'summary', '回忆山谷之旅', '回忆并分享自己的收获。', ['今天我们学会了什么?', '声音有强、弱、长、短、高、低', '音乐是沟通的语言']]
+  ]
+  for (const [index, type, title, headline, points] of scenes) Object.assign(outline.pages[index], {
+    type, title, bullets: points, subtitle: '', studentContent: { headline, points }, classroomAction: {}, interaction: null,
+    image: { url: imageUrl, width: 1584, height: 560 }, resourceBindings: [], requiredVisual: true,
+    visualPlan: { composition: index === 0 ? 'full_scene' : 'image_top', visualMode: 'generated_image', textPresentation: 'plain',
+      visualShare: .55, imageNecessity: 'essential', imagePurpose: 'scene' }
+  })
+  return outline
+}

+ 2 - 0
src/config/aiCoursewareTemplateMasters.ts

@@ -34,6 +34,8 @@ export interface TemplateSlot {
   maxItems?: number
   preferredFontSize?: number
   minimumFontSize?: number
+  /** 主图下的短提示横向排列,避免逐行占用主图高度。 */
+  textFlow?: "columns"
   /** 背景已承担该功能时保留槽位语义,但禁止生成器写入。 */
   enabled?: boolean
 }

+ 11 - 2
src/utils/aiPptPageDesign.ts

@@ -54,11 +54,20 @@ export function compilePageDesign(page: AiCoursewarePage, index: number, master:
       slots.push(slot('image', imageLeft, top, imageWidth, height))
       body = slot('body', composition === 'image_right' ? 80 : 80 + imageWidth + 36, top, bodyWidth, height)
     } else {
+      // 场景主图下的少量短提示横排,正文不能逐行挤掉主图。
+      const captionWidth = (1440 - 28 * (content.points.length - 1)) / Math.max(1, content.points.length)
+      const captionHeight = Math.max(80, ...content.points.map(text => measureAiPptText(text, captionWidth, 38)))
+      const captionRow = ['full_scene', 'image_top'].includes(composition) && presentation === 'plain'
+        && !content.steps.length && !content.groups.length && content.points.length >= 2 && content.points.length <= 3
+        && captionHeight <= 140 && height - captionHeight - 28 >= 240
       // 短句必须保留可读空间,不能靠缩小字号挤到图片下面。
-      const imageHeight = Math.max(80, Math.min(height * share, height - Math.max(180, bodyHeight(1440)) - 28))
-      const imageWidth = Math.min(1440, imageHeight * 2.8)
+      const imageHeight = captionRow ? height - captionHeight - 28
+        : Math.max(80, Math.min(height * share, height - Math.max(180, bodyHeight(1440)) - 28))
+      const imageRatio = page.image?.width && page.image?.height ? page.image.width / page.image.height : 2.8
+      const imageWidth = Math.min(1440, imageHeight * imageRatio)
       slots.push(slot('image', 80 + (1440 - imageWidth) / 2, top, imageWidth, imageHeight))
       body = slot('body', 80, top + imageHeight + 28, 1440, height - imageHeight - 28)
+      if (captionRow) body.textFlow = 'columns'
     }
   }
   if (composition === 'comparison' && content.groups.length === 2) {

+ 6 - 0
src/utils/aiPptTemplateEngine.ts

@@ -175,6 +175,12 @@ function fillDesignedPage(master: TemplateMaster, layout: TemplateMasterPage, pa
   const composition = page.visualPlan!.composition
   const presentation = page.visualPlan!.textPresentation || 'plain'
   const paintText = (slot: TemplateSlot | undefined, values: string[]) => {
+    if (slot?.textFlow === 'columns' && values.length) {
+      const width = (slot.width - 28 * (values.length - 1)) / values.length
+      for (let index = 0; index < values.length; index++) addSlotText(elements, { ...slot, left: slot.left + index * (width + 28), width,
+        preferredFontSize: 38 }, values[index], master.textColor)
+      return
+    }
     if (slot) slot = compactTextSlot(slot, values, presentation)
     if (presentation === 'steps') addStepGrid(elements, slot, values, master, palette)
     else if (presentation === 'grid') addCardGrid(elements, slot, values, master, palette)

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini