|
|
@@ -103,7 +103,10 @@ function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, palett
|
|
|
const role = pageRole(page, index)
|
|
|
const intent = pageRenderIntent(page, role)
|
|
|
const master = getAiCoursewareTemplateMaster(options.templateId!)
|
|
|
- const baseMasterPage = getTemplateMasterPage(master, role, isScorePage(page) ? "diagram_focus" : videoBinding(page) || page.requiredVisual ? "split_content" : page.interaction?.question ? "text_focus" : preferredLayout(page, intent))
|
|
|
+ const requested = preferredLayout(page, intent)
|
|
|
+ const teachingImage = hasImage(page) && page.visualPlan?.imagePurpose !== "decoration" && page.visualPlan?.imagePurpose !== "none"
|
|
|
+ const needsImageSlot = role !== "cover" && teachingImage && (page.interaction?.question || ["text_focus", "compare", "activity", "summary", "objectives"].includes(requested || ""))
|
|
|
+ const baseMasterPage = getTemplateMasterPage(master, role, isScorePage(page) ? "diagram_focus" : videoBinding(page) || page.requiredVisual || needsImageSlot ? "split_content" : page.interaction?.question ? "text_focus" : requested)
|
|
|
const occurrence = layoutOccurrences.get(baseMasterPage.id) || 0
|
|
|
layoutOccurrences.set(baseMasterPage.id, occurrence + 1)
|
|
|
const masterPage = adaptMasterPage(selectTemplateMasterPageVariant(baseMasterPage, occurrence), intent)
|
|
|
@@ -124,6 +127,21 @@ function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, palett
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/** 传给图片生成器的真实模板槽位比例;媒体行存在时使用独立尺寸。 */
|
|
|
+export function getAiPptImageAspectRatios(templateId: AiCoursewareTemplateId): Record<string, number> {
|
|
|
+ const master = getAiCoursewareTemplateMaster(templateId)
|
|
|
+ const layouts: Record<string, string> = { cover: "cover_scene", hero: "hero_focus", media: "media_focus", split: "split_content", text: "text_focus", diagram: "diagram_focus", compare: "compare", activity: "activity", summary: "summary", objectives: "objectives" }
|
|
|
+ const ratios: Record<string, number> = {}
|
|
|
+ for (const page of master.pages) {
|
|
|
+ for (const showMedia of [true, false]) {
|
|
|
+ const adapted = adaptMasterPage(page, { role: page.roles[0], density: "medium", visualKind: "image", showMedia })
|
|
|
+ const slot = adapted.slots.find(slot => slot.kind === "image" && slot.enabled !== false)
|
|
|
+ if (slot) ratios[layouts[page.layout] + (showMedia ? ":media" : "")] = slot.width / slot.height
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return ratios
|
|
|
+}
|
|
|
+
|
|
|
function fillTemplatePage(master: TemplateMaster, masterPage: TemplateMasterPage, page: AiCoursewarePage, outline: AiCoursewareOutline, palette: Palette, role: PageRole = masterPage.roles[0], intent = pageRenderIntent(page, role)): PPTElement[] {
|
|
|
master = {
|
|
|
...master,
|
|
|
@@ -783,7 +801,8 @@ function slideRemark(page: AiCoursewarePage) {
|
|
|
const answer = page.interaction?.answer ? `参考答案:${page.interaction.answer}` : ""
|
|
|
const explanation = page.interaction?.explanation ? `解析:${page.interaction.explanation}` : ""
|
|
|
const resources = (page.resourceBindings || []).map(item => `${item.name || item.resourceType}:${item.purpose || "课堂资源"}`).join("\n")
|
|
|
- return [lessonMeta, page.teachingPurpose, activity, fallbackInstruction, page.speakerNotes, page.teacherTips, evidence, answer, explanation, resources]
|
|
|
+ const mediaReminder = /listen|sing/.test(page.type) && audioBindings(page).length === 0 ? "资源准备:请为本页听唱任务补充对应音频。" : ""
|
|
|
+ return [lessonMeta, page.teachingPurpose, activity, fallbackInstruction, page.speakerNotes, page.teacherTips, evidence, answer, explanation, resources, mediaReminder, page.imageReview]
|
|
|
.map(cleanText)
|
|
|
.filter(Boolean)
|
|
|
.filter((item, index, all) => all.indexOf(item) === index)
|
|
|
@@ -792,7 +811,7 @@ function slideRemark(page: AiCoursewarePage) {
|
|
|
|
|
|
function buildPalette(outline: AiCoursewareOutline, variant: number, templateId?: AiCoursewareTemplateId): Palette {
|
|
|
const resolvedTemplateId = templateId || recommendAiCoursewareTemplate(outline).id
|
|
|
- return { ...getAiCoursewarePalette(resolvedTemplateId, variant) }
|
|
|
+ return { ...getAiCoursewarePalette(resolvedTemplateId, variant), ...(resolvedTemplateId === "dark-lute" ? { soft: "#16485C" } : {}) }
|
|
|
}
|
|
|
|
|
|
function backgroundConfig(masterPage: TemplateMasterPage, options: AiPptOptions): SlideBackground {
|
|
|
@@ -851,19 +870,23 @@ function textElement(params: {
|
|
|
function imageElement(page: AiCoursewarePage, rect: ImageRect): PPTImageElement {
|
|
|
const width = page.image?.width || 0
|
|
|
const height = page.image?.height || 0
|
|
|
+ const contain = page.requiredVisual || page.visualPlan?.imagePurpose !== "decoration" || page.visualPlan?.imageFit !== "cover"
|
|
|
+ const scale = width > 0 && height > 0 ? Math.min(rect.width / width, rect.height / height) : 0
|
|
|
+ const fittedWidth = contain && scale ? width * scale : rect.width
|
|
|
+ const fittedHeight = contain && scale ? height * scale : rect.height
|
|
|
return {
|
|
|
id: nanoid(10),
|
|
|
type: "image",
|
|
|
- left: rect.left,
|
|
|
- top: rect.top,
|
|
|
- width: rect.width,
|
|
|
- height: rect.height,
|
|
|
+ left: rect.left + (rect.width - fittedWidth) / 2,
|
|
|
+ top: rect.top + (rect.height - fittedHeight) / 2,
|
|
|
+ width: fittedWidth,
|
|
|
+ height: fittedHeight,
|
|
|
rotate: 0,
|
|
|
src: page.image?.url || "",
|
|
|
- fixedRatio: false,
|
|
|
+ fixedRatio: Boolean(contain),
|
|
|
radius: rect.radius || 0,
|
|
|
shadow: rect.radius ? softShadow() : undefined,
|
|
|
- clip: width > 0 && height > 0 ? { shape: "rect", range: coverRange(width, height, rect.width, rect.height) } : undefined
|
|
|
+ clip: !contain && width > 0 && height > 0 ? { shape: "rect", range: coverRange(width, height, rect.width, rect.height) } : undefined
|
|
|
}
|
|
|
}
|
|
|
|