|
|
@@ -172,6 +172,7 @@ export function getAiPptImageAspectRatios(templateId: AiCoursewareTemplateId): R
|
|
|
ratios.model_design_v2 = 1
|
|
|
ratios.model_design_v3 = 1 // 完整示范、判断标准及结构化教学图示。
|
|
|
ratios.model_design_v4 = 1 // 整课一致性复核与内容驱动的图片尺寸。
|
|
|
+ ratios.model_design_v5 = 1 // 按拍分组、学生范例和内容复核完成状态。
|
|
|
return ratios
|
|
|
}
|
|
|
|
|
|
@@ -186,18 +187,27 @@ function fillDesignedPage(master: TemplateMaster, layout: TemplateMasterPage, pa
|
|
|
addVisualOrResource(elements, slotOf(layout, 'image'), page, master, palette)
|
|
|
const composition = page.visualPlan!.composition
|
|
|
const presentation = page.visualPlan!.textPresentation || 'plain'
|
|
|
- const paintText = (slot: TemplateSlot | undefined, values: string[]) => {
|
|
|
+ const paintText = (slot: TemplateSlot | undefined, values: string[], format = presentation) => {
|
|
|
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)
|
|
|
- else if (presentation === 'cards' && values.length === 4 && slot && slot.width >= 1000) addCardGrid(elements, slot, values, master, palette, 2)
|
|
|
- else addSlotList(elements, slot, values, master, palette, presentation === 'cards')
|
|
|
+ if (slot) slot = compactTextSlot(slot, values, format)
|
|
|
+ if (format === 'steps') addStepGrid(elements, slot, values, master, palette)
|
|
|
+ else if (format === 'grid') addCardGrid(elements, slot, values, master, palette)
|
|
|
+ else if (format === 'cards' && values.length === 4 && slot && slot.width >= 1000) addCardGrid(elements, slot, values, master, palette, 2)
|
|
|
+ else addSlotList(elements, slot, values, master, palette, format === 'cards')
|
|
|
+ }
|
|
|
+ const bodyTextHeight = (width: number) => aiPptBlockHeight(content.steps, width, presentation)
|
|
|
+ + aiPptBlockHeight(points, width, content.steps.length ? 'plain' : presentation) + (content.steps.length && points.length ? 24 : 0)
|
|
|
+ const paintBody = (slot: TemplateSlot) => {
|
|
|
+ if (!content.steps.length || !points.length) { paintText(slot, [...content.steps, ...points]); return }
|
|
|
+ const pointsHeight = aiPptBlockHeight(points, slot.width, 'plain')
|
|
|
+ const stepsHeight = Math.min(aiPptBlockHeight(content.steps, slot.width, presentation), slot.height - pointsHeight - 24)
|
|
|
+ paintText({ ...slot, height: stepsHeight }, content.steps)
|
|
|
+ paintText({ ...slot, top: slot.top + stepsHeight + 24, height: slot.height - stepsHeight - 24 }, points, 'plain')
|
|
|
}
|
|
|
const focusBody = slotOf(layout, 'body')
|
|
|
if (composition === 'focus_question' && page.interaction?.options?.length && !hasImage(page)
|
|
|
@@ -213,15 +223,19 @@ function fillDesignedPage(master: TemplateMaster, layout: TemplateMasterPage, pa
|
|
|
addInlineMedia(elements, slotOf(layout, 'media'), page, master, palette, intent.showMedia)
|
|
|
return elements
|
|
|
}
|
|
|
- if (composition === 'comparison' && content.groups.length === 2) {
|
|
|
+ if (composition === 'comparison' && content.groups.length >= 2) {
|
|
|
content.groups.forEach((group, i) => {
|
|
|
- const slot = slotOf(layout, i === 0 ? 'left' : 'right')!
|
|
|
+ const slot = slotOf(layout, i === 0 ? 'left' : i === 1 ? 'right' : `comparison-${i}`)!
|
|
|
const spacious = !hasImage(page) && slot.height >= 300 && group.label.length <= 12 && group.points.length <= 3 && group.points.every(point => point.length <= 32)
|
|
|
if (spacious) {
|
|
|
elements.push(shapeElement({ left: slot.left, top: slot.top, width: slot.width, height: slot.height, fill: i === 0 ? palette.soft : palette.surface, radius: 12 }))
|
|
|
elements.push(lineElement(slot.left + 34, slot.top + 100, slot.left + slot.width - 34, slot.top + 100, palette.secondary, 4))
|
|
|
if (group.label) addSlotText(elements, { ...slot, left: slot.left + 34, top: slot.top + 20, width: slot.width - 68, height: 72, preferredFontSize: 48 }, group.label, master.titleColor, true)
|
|
|
paintText({ ...slot, left: slot.left + 34, width: slot.width - 68, top: slot.top + 134, height: slot.height - 160 }, group.points)
|
|
|
+ } else if (content.groups.length > 2 && !hasImage(page) && slot.height >= 170) {
|
|
|
+ elements.push(shapeElement({ left: slot.left, top: slot.top, width: slot.width, height: slot.height, fill: i % 2 ? palette.surface : palette.soft, radius: 10 }))
|
|
|
+ addSlotText(elements, { ...slot, left: slot.left + 28, top: slot.top + 18, width: slot.width - 56, height: 64, preferredFontSize: 46 }, group.label, master.titleColor, true)
|
|
|
+ paintText({ ...slot, left: slot.left + 28, top: slot.top + 100, width: slot.width - 56, height: slot.height - 118 }, group.points, 'plain')
|
|
|
} else {
|
|
|
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)
|
|
|
@@ -235,11 +249,11 @@ function fillDesignedPage(master: TemplateMaster, layout: TemplateMasterPage, pa
|
|
|
? [headline, ...points].join(' ').match(/[短长](?:\s*[-—·、]\s*[短长]){1,7}/)?.[0].match(/[短长]/g) : undefined
|
|
|
if (page.diagram && page.visualPlan?.visualMode === 'native_diagram') {
|
|
|
const values = [...content.steps, ...points]
|
|
|
- const textHeight = values.length ? aiPptBlockHeight(values, body.width, presentation) : 0
|
|
|
+ const textHeight = values.length ? bodyTextHeight(body.width) : 0
|
|
|
const diagramHeight = body.height - textHeight - (values.length ? 24 : 0)
|
|
|
if (diagramHeight >= 210 && validTeachingDiagram(page.diagram)) {
|
|
|
addTeachingDiagram(elements, { ...body, height: diagramHeight }, page.diagram, master, palette)
|
|
|
- if (values.length) paintText({ ...body, top: body.top + diagramHeight + 24, height: textHeight }, values)
|
|
|
+ if (values.length) paintBody({ ...body, top: body.top + diagramHeight + 24, height: textHeight })
|
|
|
} else {
|
|
|
// 异常或过密数据保留完整文本,由质量检查提示,不编造或截掉图示内容。
|
|
|
paintText(body, [page.diagram.caption, ...(page.diagram.items || []).map(item => item?.label || ''), ...values].filter(Boolean))
|
|
|
@@ -249,16 +263,17 @@ function fillDesignedPage(master: TemplateMaster, layout: TemplateMasterPage, pa
|
|
|
} else if (pattern && !content.steps.length) {
|
|
|
addDurationDiagram(elements, { ...body, height: body.height * .55 }, pattern, master, palette)
|
|
|
paintText({ ...body, top: body.top + body.height * .60, height: body.height * .40 }, points)
|
|
|
- } else paintText(body, [...content.steps, ...points])
|
|
|
+ } else paintBody(body)
|
|
|
}
|
|
|
addInlineMedia(elements, slotOf(layout, 'media'), page, master, palette, intent.showMedia)
|
|
|
return elements
|
|
|
}
|
|
|
|
|
|
export function validTeachingDiagram(diagram: NonNullable<AiCoursewarePage['diagram']>) {
|
|
|
- return ['beats', 'duration', 'pitch', 'sequence'].includes(diagram.kind) && Array.isArray(diagram.items) && diagram.items.length >= 2 && diagram.items.length <= 6
|
|
|
+ return ['beats', 'rhythm', 'duration', 'pitch', 'sequence'].includes(diagram.kind) && Array.isArray(diagram.items) && diagram.items.length >= 2 && diagram.items.length <= 6
|
|
|
&& diagram.items.every(item => item?.label?.trim() && item.label.length <= 12)
|
|
|
&& (diagram.kind === 'sequence' || diagram.items.every(item => Number.isFinite(item.value) && item.value! > 0 && item.value! <= 8))
|
|
|
+ && (diagram.kind !== 'rhythm' || diagram.items.every(item => Number.isInteger(item.value) && item.value! <= 4))
|
|
|
}
|
|
|
|
|
|
function addTeachingDiagram(elements: PPTElement[], slot: TemplateSlot, diagram: NonNullable<AiCoursewarePage['diagram']>, master: TemplateMaster, palette: Palette) {
|
|
|
@@ -276,7 +291,17 @@ function addTeachingDiagram(elements: PPTElement[], slot: TemplateSlot, diagram:
|
|
|
const size = diagram.kind === 'beats' ? Math.min(width - 24, shapeHeight) * Math.sqrt(item.value! / max) : shapeHeight
|
|
|
const top = area.top + Math.max(0, (area.height - shapeHeight - 68) / 2)
|
|
|
const color = index % 2 ? palette.secondary : palette.primary
|
|
|
- if (diagram.kind === 'pitch') {
|
|
|
+ if (diagram.kind === 'rhythm') {
|
|
|
+ // 每个等宽格是一拍;圆点数只表示该拍内的发声次数,不用大小或颜色暗示强弱。
|
|
|
+ const sounds = item.value!, dotSize = Math.min(54, (width - 32) / (sounds * 1.5), shapeHeight - 18)
|
|
|
+ elements.push(shapeElement({ left, top, width, height: shapeHeight, fill: palette.soft, radius: 10 }))
|
|
|
+ for (let sound = 0; sound < sounds; sound++) {
|
|
|
+ const circle = shapeElement({ left: left + width * (sound + .5) / sounds - dotSize / 2,
|
|
|
+ top: top + (shapeHeight - dotSize) / 2, width: dotSize, height: dotSize, fill: palette.primary })
|
|
|
+ circle.path = 'M100,0 A100,100 0 1,1 99.99,0 Z'
|
|
|
+ elements.push(circle)
|
|
|
+ }
|
|
|
+ } else if (diagram.kind === 'pitch') {
|
|
|
const y = area.top + 28 + (max - item.value!) / Math.max(1, max - min) * Math.max(0, area.height - 146)
|
|
|
elements.push(shapeElement({ left, top: y, width, height: 10, fill: color }))
|
|
|
addSlotText(elements, { ...slot, left, top: y + 16, width, height: 64, preferredFontSize: 40 }, item.label, master.textColor, true, 'center')
|
|
|
@@ -292,7 +317,7 @@ function addTeachingDiagram(elements: PPTElement[], slot: TemplateSlot, diagram:
|
|
|
elements.push(lineElement(left, top + shapeHeight / 2, left + width, top + shapeHeight / 2, palette.secondary, 4))
|
|
|
addSlotText(elements, { ...slot, left, top, width, height: 65, preferredFontSize: 44 }, String(index + 1).padStart(2, '0'), palette.primary, true)
|
|
|
}
|
|
|
- addSlotText(elements, { ...slot, left, top: top + shapeHeight + 12, width, height: 64, preferredFontSize: 32 }, item.label, master.textColor, true, diagram.kind === 'beats' ? 'center' : 'left')
|
|
|
+ addSlotText(elements, { ...slot, left, top: top + shapeHeight + 12, width, height: 64, preferredFontSize: 32 }, item.label, master.textColor, true, ['beats', 'rhythm'].includes(diagram.kind) ? 'center' : 'left')
|
|
|
left += width + gap
|
|
|
})
|
|
|
if (diagram.caption) addSlotText(elements, { ...slot, top: slot.top + slot.height - captionHeight, height: captionHeight, preferredFontSize: 32 }, diagram.caption, master.mutedColor)
|