|
@@ -16,7 +16,7 @@ import type {
|
|
|
SlideTheme
|
|
SlideTheme
|
|
|
} from "@/types/slides"
|
|
} from "@/types/slides"
|
|
|
import type { AiCoursewareOutline, AiCoursewarePage } from "@/api/aiCourseware"
|
|
import type { AiCoursewareOutline, AiCoursewarePage } from "@/api/aiCourseware"
|
|
|
-import { audioBindings, scoreBinding, isScorePage, usableResourceUrl, isTeacherFacingText, teacherFacingTexts } from "@/utils/aiPptContent"
|
|
|
|
|
|
|
+import { audioBindings, scoreBinding, isScorePage, usableResourceUrl, isTeacherFacingText, teacherFacingTexts, modelPageContent, rhythmRows } from "@/utils/aiPptContent"
|
|
|
import { compilePageDesign, hasModelPageDesign, externalVisual } from "@/utils/aiPptPageDesign"
|
|
import { compilePageDesign, hasModelPageDesign, externalVisual } from "@/utils/aiPptPageDesign"
|
|
|
import {
|
|
import {
|
|
|
AI_COURSEWARE_TEMPLATES,
|
|
AI_COURSEWARE_TEMPLATES,
|
|
@@ -111,7 +111,7 @@ function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, palett
|
|
|
const adapted = adaptMasterPage(selectTemplateMasterPageVariant(baseMasterPage, occurrence), intent)
|
|
const adapted = adaptMasterPage(selectTemplateMasterPageVariant(baseMasterPage, occurrence), intent)
|
|
|
const nativeVisual = isScorePage(page) || (role === "rhythm" && adapted.layout === "diagram")
|
|
const nativeVisual = isScorePage(page) || (role === "rhythm" && adapted.layout === "diagram")
|
|
|
const designed = hasModelPageDesign(page) && !isScorePage(page)
|
|
const designed = hasModelPageDesign(page) && !isScorePage(page)
|
|
|
- const headline = cleanText(page.interaction?.question) || activityTask(page) || studentHeadline(page)
|
|
|
|
|
|
|
+ const headline = designed ? modelPageContent(page).heading : cleanText(page.interaction?.question) || activityTask(page) || studentHeadline(page)
|
|
|
const masterPage = designed
|
|
const masterPage = designed
|
|
|
? compilePageDesign(page, index, master, intent.showMedia, (externalVisual(page) && hasImage(page)) || Boolean(videoBinding(page)), Boolean(headline))
|
|
? compilePageDesign(page, index, master, intent.showMedia, (externalVisual(page) && hasImage(page)) || Boolean(videoBinding(page)), Boolean(headline))
|
|
|
: fitVisualSlots(adapted, hasImage(page) || Boolean(videoBinding(page)), nativeVisual)
|
|
: fitVisualSlots(adapted, hasImage(page) || Boolean(videoBinding(page)), nativeVisual)
|
|
@@ -147,40 +147,75 @@ export function getAiPptImageAspectRatios(templateId: AiCoursewareTemplateId): R
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
ratios.model_design_v1 = 1 // 服务端依据本版编译器的安全区域和模型比例计算图片尺寸。
|
|
ratios.model_design_v1 = 1 // 服务端依据本版编译器的安全区域和模型比例计算图片尺寸。
|
|
|
|
|
+ ratios.model_design_v2 = 1
|
|
|
return ratios
|
|
return ratios
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function fillDesignedPage(master: TemplateMaster, layout: TemplateMasterPage, page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, index: number, headline: string, intent: PageRenderIntent): PPTElement[] {
|
|
function fillDesignedPage(master: TemplateMaster, layout: TemplateMasterPage, page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, index: number, headline: string, intent: PageRenderIntent): PPTElement[] {
|
|
|
const elements: PPTElement[] = []
|
|
const elements: PPTElement[] = []
|
|
|
- const points = uniqueVisibleTexts([activityTask(page), ...studentPoints(page), ...(page.interaction?.options || [])]).filter(point => !sameVisibleText(point, headline))
|
|
|
|
|
|
|
+ const content = modelPageContent(page)
|
|
|
|
|
+ const points = content.points
|
|
|
addSlotText(elements, slotOf(layout, 'title'), page.title, master.titleColor, true)
|
|
addSlotText(elements, slotOf(layout, 'title'), page.title, master.titleColor, true)
|
|
|
if (index === 0) addSlotText(elements, slotOf(layout, 'meta'), [outline.gradeName, conciseTextbook(outline.textbookName), outline.unitName].filter(Boolean).join(' · '), master.mutedColor)
|
|
if (index === 0) addSlotText(elements, slotOf(layout, 'meta'), [outline.gradeName, conciseTextbook(outline.textbookName), outline.unitName].filter(Boolean).join(' · '), master.mutedColor)
|
|
|
addSlotText(elements, slotOf(layout, 'headline'), headline, master.textColor, true)
|
|
addSlotText(elements, slotOf(layout, 'headline'), headline, master.textColor, true)
|
|
|
|
|
+ addSlotText(elements, layout.slots.find(slot => slot.id === 'instruction'), content.task, master.mutedColor)
|
|
|
addVisualOrResource(elements, slotOf(layout, 'image'), page, master, palette)
|
|
addVisualOrResource(elements, slotOf(layout, 'image'), page, master, palette)
|
|
|
const composition = page.visualPlan!.composition
|
|
const composition = page.visualPlan!.composition
|
|
|
const presentation = page.visualPlan!.textPresentation || 'plain'
|
|
const presentation = page.visualPlan!.textPresentation || 'plain'
|
|
|
const paintText = (slot: TemplateSlot | undefined, values: string[]) => {
|
|
const paintText = (slot: TemplateSlot | undefined, values: string[]) => {
|
|
|
|
|
+ if (slot) slot = compactTextSlot(slot, values, presentation)
|
|
|
if (presentation === 'steps') addStepGrid(elements, slot, values, master, palette)
|
|
if (presentation === 'steps') addStepGrid(elements, slot, values, master, palette)
|
|
|
else if (presentation === 'grid') addCardGrid(elements, slot, values, master, palette)
|
|
else if (presentation === 'grid') addCardGrid(elements, slot, values, master, palette)
|
|
|
else addSlotList(elements, slot, values, master, palette, presentation === 'cards')
|
|
else addSlotList(elements, slot, values, master, palette, presentation === 'cards')
|
|
|
}
|
|
}
|
|
|
- if (composition === 'comparison') {
|
|
|
|
|
- const midpoint = Math.ceil(points.length / 2)
|
|
|
|
|
- paintText(slotOf(layout, 'left'), points.slice(0, midpoint))
|
|
|
|
|
- paintText(slotOf(layout, 'right'), points.slice(midpoint))
|
|
|
|
|
|
|
+ if (composition === 'comparison' && content.groups.length === 2) {
|
|
|
|
|
+ content.groups.forEach((group, i) => {
|
|
|
|
|
+ const slot = slotOf(layout, i === 0 ? 'left' : 'right')!
|
|
|
|
|
+ if (group.label) addSlotText(elements, { ...slot, height: 52 }, group.label, master.titleColor, true)
|
|
|
|
|
+ paintText(group.label ? { ...slot, top: slot.top + 66, height: slot.height - 66 } : slot, group.points)
|
|
|
|
|
+ })
|
|
|
|
|
+ paintText(slotOf(layout, 'body'), points)
|
|
|
} else {
|
|
} else {
|
|
|
const body = slotOf(layout, 'body')!
|
|
const body = slotOf(layout, 'body')!
|
|
|
|
|
+ const rows = page.visualPlan?.visualMode === 'native_diagram' ? rhythmRows(points) : []
|
|
|
const pattern = page.visualPlan?.visualMode === 'native_diagram' && pageRole(page, index) === 'rhythm'
|
|
const pattern = page.visualPlan?.visualMode === 'native_diagram' && pageRole(page, index) === 'rhythm'
|
|
|
? [headline, ...points].join(' ').match(/[短长](?:\s*[-—·、]\s*[短长]){1,7}/)?.[0].match(/[短长]/g) : undefined
|
|
? [headline, ...points].join(' ').match(/[短长](?:\s*[-—·、]\s*[短长]){1,7}/)?.[0].match(/[短长]/g) : undefined
|
|
|
- if (pattern) {
|
|
|
|
|
|
|
+ if (!content.steps.length && rows.length && rows.length === points.length && body.height >= rows.length * 160) {
|
|
|
|
|
+ addRhythmRows(elements, body, rows, master, palette)
|
|
|
|
|
+ } else if (pattern && !content.steps.length) {
|
|
|
addDurationDiagram(elements, { ...body, height: body.height * .55 }, pattern, master, palette)
|
|
addDurationDiagram(elements, { ...body, height: body.height * .55 }, pattern, master, palette)
|
|
|
paintText({ ...body, top: body.top + body.height * .60, height: body.height * .40 }, points)
|
|
paintText({ ...body, top: body.top + body.height * .60, height: body.height * .40 }, points)
|
|
|
- } else paintText(body, points)
|
|
|
|
|
|
|
+ } else paintText(body, [...content.steps, ...points])
|
|
|
}
|
|
}
|
|
|
addInlineMedia(elements, slotOf(layout, 'media'), page, master, palette, intent.showMedia)
|
|
addInlineMedia(elements, slotOf(layout, 'media'), page, master, palette, intent.showMedia)
|
|
|
return elements
|
|
return elements
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function compactTextSlot(slot: TemplateSlot, values: string[], presentation: string): TemplateSlot {
|
|
|
|
|
+ const grid = presentation === 'grid' || presentation === 'steps'
|
|
|
|
|
+ const columns = grid ? Math.max(1, Math.min(presentation === 'steps' ? 4 : 3, values.length, Math.floor(slot.width / 300))) : 1
|
|
|
|
|
+ const rows = Math.max(1, Math.ceil(values.length / columns))
|
|
|
|
|
+ const width = (slot.width - 22 * (columns - 1)) / columns
|
|
|
|
|
+ const lines = Math.max(1, ...values.map(text => Math.ceil(text.length / Math.max(4, (width - 68) / 34))))
|
|
|
|
|
+ const rowHeight = Math.max(grid ? 190 : 80, lines * 44 + (presentation === 'steps' ? 84 : presentation === 'plain' ? 16 : 48))
|
|
|
|
|
+ const height = Math.min(slot.height, rowHeight * rows + 22 * (rows - 1))
|
|
|
|
|
+ return { ...slot, top: slot.top + (grid ? Math.min(100, (slot.height - height) / 2) : 0), height, preferredFontSize: Math.max(slot.preferredFontSize || 34, 38) }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function addRhythmRows(elements: PPTElement[], slot: TemplateSlot, rows: ReturnType<typeof rhythmRows>, master: TemplateMaster, palette: Palette) {
|
|
|
|
|
+ const rowHeight = Math.min(220, (slot.height - 24 * (rows.length - 1)) / rows.length)
|
|
|
|
|
+ rows.forEach((row, i) => {
|
|
|
|
|
+ const top = slot.top + i * (rowHeight + 24)
|
|
|
|
|
+ addSlotText(elements, { ...slot, top, height: 58, preferredFontSize: 38 }, row.source, master.textColor, true)
|
|
|
|
|
+ const width = Math.min(160, (slot.width - 12 * (row.tokens.length - 1)) / row.tokens.length)
|
|
|
|
|
+ row.tokens.forEach((token, j) => {
|
|
|
|
|
+ const cell = { left: slot.left + j * (width + 12), top: top + 76, width, height: Math.max(48, rowHeight - 82) }
|
|
|
|
|
+ if (!/[||]/.test(token)) elements.push(shapeElement({ ...cell, fill: palette.soft, radius: 10 }))
|
|
|
|
|
+ addSlotText(elements, { ...slot, ...cell, preferredFontSize: 44 }, token, master.textColor, true)
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function fillTemplatePage(master: TemplateMaster, masterPage: TemplateMasterPage, page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, role: PageRole = masterPage.roles[0], intent = pageRenderIntent(page, role)): PPTElement[] {
|
|
function fillTemplatePage(master: TemplateMaster, masterPage: TemplateMasterPage, page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, role: PageRole = masterPage.roles[0], intent = pageRenderIntent(page, role)): PPTElement[] {
|
|
|
master = {
|
|
master = {
|
|
|
...master,
|
|
...master,
|
|
@@ -506,7 +541,7 @@ function addCardGrid(elements: PPTElement[], slot: TemplateSlot | undefined, sou
|
|
|
if (!slot) return
|
|
if (!slot) return
|
|
|
const items = uniqueVisibleTexts(sourceItems.map(cleanText).filter(Boolean))
|
|
const items = uniqueVisibleTexts(sourceItems.map(cleanText).filter(Boolean))
|
|
|
if (!items.length) return
|
|
if (!items.length) return
|
|
|
- const columns = items.length === 1 ? 1 : Math.min(3, items.length)
|
|
|
|
|
|
|
+ const columns = items.length === 1 ? 1 : Math.max(1, Math.min(3, items.length, (slot.minimumFontSize || 0) >= 28 ? Math.floor(slot.width / 300) : 3))
|
|
|
const rows = Math.ceil(items.length / columns)
|
|
const rows = Math.ceil(items.length / columns)
|
|
|
const gap = collectionGap(slot.height, rows, 22)
|
|
const gap = collectionGap(slot.height, rows, 22)
|
|
|
const width = (slot.width - gap * (columns - 1)) / columns
|
|
const width = (slot.width - gap * (columns - 1)) / columns
|
|
@@ -595,7 +630,7 @@ function addStepGrid(elements: PPTElement[], slot: TemplateSlot | undefined, sou
|
|
|
if (!slot) return
|
|
if (!slot) return
|
|
|
const items = uniqueVisibleTexts(sourceItems.map(cleanText).filter(Boolean))
|
|
const items = uniqueVisibleTexts(sourceItems.map(cleanText).filter(Boolean))
|
|
|
if (!items.length) return
|
|
if (!items.length) return
|
|
|
- const columns = Math.min(4, items.length)
|
|
|
|
|
|
|
+ const columns = Math.max(1, Math.min(4, items.length, (slot.minimumFontSize || 0) >= 28 ? Math.floor(slot.width / 300) : 4))
|
|
|
const rows = Math.ceil(items.length / columns)
|
|
const rows = Math.ceil(items.length / columns)
|
|
|
const gap = collectionGap(slot.height, rows, 22)
|
|
const gap = collectionGap(slot.height, rows, 22)
|
|
|
const width = (slot.width - gap * (columns - 1)) / columns
|
|
const width = (slot.width - gap * (columns - 1)) / columns
|
|
@@ -641,7 +676,7 @@ function addStepGrid(elements: PPTElement[], slot: TemplateSlot | undefined, sou
|
|
|
width: Math.max(36, width - textInset * 2),
|
|
width: Math.max(36, width - textInset * 2),
|
|
|
height: textHeight,
|
|
height: textHeight,
|
|
|
content: item,
|
|
content: item,
|
|
|
- fontSize: fitFont(item, width - textInset * 2, textHeight, 29, minimumFontSize),
|
|
|
|
|
|
|
+ fontSize: fitFont(item, width - textInset * 2, textHeight, (slot.minimumFontSize || 0) >= 28 ? 40 : 29, minimumFontSize),
|
|
|
minimumFontSize,
|
|
minimumFontSize,
|
|
|
color: master.textColor,
|
|
color: master.textColor,
|
|
|
bold: true
|
|
bold: true
|