fix: render editable score at readable scale

This commit is contained in:
2026-09-22 16:51:44 +08:00
parent bd7585776d
commit 18963168bb
2 changed files with 20 additions and 12 deletions
+16 -12
View File
@@ -10,19 +10,23 @@ function u(value: number): string {
return (value * MM_SCALE).toFixed(2).replace(/\.00$/, "");
}
function fontSizePx(value: number): string {
return u(value * 25.4 / 96);
}
function renderTitle(page: PageLayout, snapshot: LayoutSnapshot): string {
if (page.pageNumber !== 1 || !snapshot.pages[0]) return "";
const title = snapshot.pages[0].titleRectMm;
const text = snapshot.documentTitle || snapshot.sourceDocumentId;
return `<g data-layer="title"><text data-role="document-id" x="${u(title.x)}" y="${u(title.y + title.height * 0.72)}" font-family="${esc(snapshot.config.fontRoles.title.family)}" font-size="${snapshot.config.fontRoles.title.sizePx}" >${esc(text)}</text></g>`;
return `<g data-layer="title"><text data-role="document-id" x="${u(title.x)}" y="${u(title.y + title.height * 0.72)}" font-family="${esc(snapshot.config.fontRoles.title.family)}" font-size="${fontSizePx(snapshot.config.fontRoles.title.sizePx)}" >${esc(text)}</text></g>`;
}
function renderSymbol(symbol: SymbolLayout): string {
const attrs = `data-id="${esc(symbol.id)}" data-kind="${esc(symbol.kind)}" data-measure-id="${esc(symbol.measureId)}"${symbol.voiceId ? ` data-voice-id="${esc(symbol.voiceId)}"` : ""}`;
const hidden = symbol.hidden ? " visibility=\"hidden\"" : "";
const note = `<text x="${u(symbol.x)}" y="${u(symbol.baseline)}" font-family="${esc(symbol.font?.family ?? "sans-serif")}" font-size="${symbol.font ? symbol.font.heightMm * 96 / 25.4 : 12}">${esc(symbol.text)}</text>`;
const decorations = (symbol.decorationText ?? []).map((text, index) => `<text data-layer="decoration" x="${u(symbol.x + index * 2)}" y="${u(symbol.y - 1 - index * 2)}" font-size="7">${esc(text)}</text>`).join("");
const chord = (symbol.chordText ?? []).map((text, index) => `<text data-layer="chord" x="${u(symbol.x + index * 2)}" y="${u(symbol.y + symbol.height + 3)}" font-size="8">${esc(text)}</text>`).join("");
const note = `<text x="${u(symbol.x)}" y="${u(symbol.baseline)}" font-family="${esc(symbol.font?.family ?? "sans-serif")}" font-size="${symbol.font ? u(symbol.font.heightMm) : fontSizePx(12)}">${esc(symbol.text)}</text>`;
const decorations = (symbol.decorationText ?? []).map((text, index) => `<text data-layer="decoration" x="${u(symbol.x + index * 2)}" y="${u(symbol.y - 1 - index * 2)}" font-size="${fontSizePx(7)}">${esc(text)}</text>`).join("");
const chord = (symbol.chordText ?? []).map((text, index) => `<text data-layer="chord" x="${u(symbol.x + index * 2)}" y="${u(symbol.y + symbol.height + 3)}" font-size="${fontSizePx(8)}">${esc(text)}</text>`).join("");
return `<g ${attrs}${hidden}>${note}${decorations}${chord}</g>`;
}
@@ -30,8 +34,8 @@ function renderBarline(x: number, y: number, height: number, style: string, id:
if (style === "unknown") return "";
const dashed = style === "dashed" ? ` stroke-dasharray="2 1"` : "";
const double = style === "||" || style === "::" || style === ":|" || style === "|:";
const lines = `<line x1="${u(x)}" y1="${u(y)}" x2="${u(x)}" y2="${u(y + height)}" stroke="#222" stroke-width="0.35"${dashed} />${double ? `<line x1="${u(x + 1)}" y1="${u(y)}" x2="${u(x + 1)}" y2="${u(y + height)}" stroke="#222" stroke-width="0.35"${dashed} />` : ""}`;
const repeat = style === ":|" || style === "|:" || style === "::" ? `<circle cx="${u(x - 2)}" cy="${u(y + height * 0.38)}" r="0.55" fill="#222" /><circle cx="${u(x - 2)}" cy="${u(y + height * 0.62)}" r="0.55" fill="#222" />` : "";
const lines = `<line x1="${u(x)}" y1="${u(y)}" x2="${u(x)}" y2="${u(y + height)}" stroke="#222" stroke-width="${u(0.35)}"${dashed} />${double ? `<line x1="${u(x + 1)}" y1="${u(y)}" x2="${u(x + 1)}" y2="${u(y + height)}" stroke="#222" stroke-width="${u(0.35)}"${dashed} />` : ""}`;
const repeat = style === ":|" || style === "|:" || style === "::" ? `<circle cx="${u(x - 2)}" cy="${u(y + height * 0.38)}" r="${u(0.55)}" fill="#222" /><circle cx="${u(x - 2)}" cy="${u(y + height * 0.62)}" r="${u(0.55)}" fill="#222" />` : "";
return `<g data-layer="barline" data-id="${esc(id)}" data-style="${esc(style)}">${lines}${repeat}</g>`;
}
@@ -48,19 +52,19 @@ function renderAttachment(attachment: AttachmentLayout): string {
const lines = attachment.lines?.length ? attachment.lines : [attachment.raw];
const kind = attachment.kind ?? "attachment";
const curve = /^(?:Arc|Extend|Branch|Slur|Tie)$/i.test(kind)
? `<path data-layer="attachment-curve" d="M ${u(attachment.x)} ${u(attachment.y + attachment.height / 2)} Q ${u(attachment.x + attachment.width / 2)} ${u(attachment.y - 3)} ${u(attachment.x + attachment.width)} ${u(attachment.y + attachment.height / 2)}" fill="none" stroke="#333" stroke-width="0.45" />`
: "";
? `<path data-layer="attachment-curve" d="M ${u(attachment.x)} ${u(attachment.y + attachment.height / 2)} Q ${u(attachment.x + attachment.width / 2)} ${u(attachment.y - 3)} ${u(attachment.x + attachment.width)} ${u(attachment.y + attachment.height / 2)}" fill="none" stroke="#333" stroke-width="${u(0.45)}" />`
: "";
const text = /^(?:Arc|Extend|Branch|Slur|Tie)$/i.test(kind)
? ""
: attachment.syllableMapping
? (attachment.syllables ?? []).map((syllable) => `<text data-layer="lyric-syllable" data-event-id="${esc(syllable.eventId ?? "")}" x="${u(syllable.x)}" y="${u(syllable.y)}" font-size="10">${esc(syllable.text)}</text>`).join("")
: lines.map((line, index) => `<text x="${u(attachment.x)}" y="${u(attachment.y + 4 + index * 4)}" font-size="10">${esc(line)}</text>`).join("");
? (attachment.syllables ?? []).map((syllable) => `<text data-layer="lyric-syllable" data-event-id="${esc(syllable.eventId ?? "")}" x="${u(syllable.x)}" y="${u(syllable.y)}" font-size="${fontSizePx(10)}">${esc(syllable.text)}</text>`).join("")
: lines.map((line, index) => `<text x="${u(attachment.x)}" y="${u(attachment.y + 4 + index * 4)}" font-size="${fontSizePx(10)}">${esc(line)}</text>`).join("");
return `<g data-layer="attachments" data-id="${esc(attachment.id)}" data-kind="attachment" data-attachment-kind="${esc(kind)}" data-raw="${esc(attachment.raw)}"${continuation}>${curve}${text}</g>`;
}
function renderPageObject(object: PageObjectLayout): string {
const hidden = object.visible === false ? " visibility=\"hidden\"" : "";
return `<g data-layer="page-objects" data-id="${esc(object.id)}" data-kind="page-object"${hidden}><rect x="${u(object.x)}" y="${u(object.y)}" width="${u(object.width)}" height="${u(object.height)}" fill="none" stroke="#777" stroke-dasharray="2 1" /><text x="${u(object.x)}" y="${u(object.y + 3)}" font-size="8">${esc(object.raw)}</text></g>`;
return `<g data-layer="page-objects" data-id="${esc(object.id)}" data-kind="page-object"${hidden}><rect x="${u(object.x)}" y="${u(object.y)}" width="${u(object.width)}" height="${u(object.height)}" fill="none" stroke="#777" stroke-dasharray="${u(2)} ${u(1)}" /><text x="${u(object.x)}" y="${u(object.y + 3)}" font-size="${fontSizePx(8)}">${esc(object.raw)}</text></g>`;
}
export function renderPageSvg(snapshot: LayoutSnapshot, page: PageLayout, includeAssistiveLayer = false): string {
@@ -68,7 +72,7 @@ export function renderPageSvg(snapshot: LayoutSnapshot, page: PageLayout, includ
const measures = page.measures.map(renderMeasure).join("");
const attachments = page.attachments.map(renderAttachment).join("");
const pageObjects = page.pageObjects.map(renderPageObject).join("");
const assistive = includeAssistiveLayer ? `<g data-layer="assistive" aria-label="layout diagnostics"><text x="${u(page.contentRectMm.x)}" y="${u(page.contentRectMm.y)}" font-size="8">辅助层:${snapshot.warnings.length} 条布局诊断</text></g>` : "";
const assistive = includeAssistiveLayer ? `<g data-layer="assistive" aria-label="layout diagnostics"><text x="${u(page.contentRectMm.x)}" y="${u(page.contentRectMm.y)}" font-size="${fontSizePx(8)}">辅助层:${snapshot.warnings.length} 条布局诊断</text></g>` : "";
return `<svg xmlns="http://www.w3.org/2000/svg" width="${page.widthMm}mm" height="${page.heightMm}mm" viewBox="0 0 ${u(page.widthMm)} ${u(page.heightMm)}" data-layout-version="${snapshot.version}" data-page="${page.pageNumber}"><title>JPW7 page ${page.pageNumber}</title><rect data-layer="paper" x="0" y="0" width="${u(page.widthMm)}" height="${u(page.heightMm)}" fill="white" />${renderTitle(page, snapshot)}<g data-layer="score">${measures}${symbols}</g>${attachments}${pageObjects}${assistive}</svg>`;
}
+4
View File
@@ -59,6 +59,10 @@ test("M3 SVG renderer preserves A4 physical units, semantic IDs and layer separa
assert.equal(probe.hasScoreLayer, true);
assert.equal(probe.hasAssistiveLayer, false);
assert.ok(probe.dataIds.length >= Object.keys(snapshot.symbols).length);
const titleFontSize = Number(svg.match(/data-role="document-id"[^>]*font-size="([0-9.]+)"/)?.[1] ?? 0);
const noteFontSize = Number(svg.match(/data-kind="note"[^>]*>[\s\S]*?font-size="([0-9.]+)"/)?.[1] ?? 0);
assert.ok(titleFontSize > 100);
assert.ok(noteFontSize > 100);
const accessibleProbe = probeSvg(renderSnapshotSvg(snapshot, true)[0]);
assert.equal(accessibleProbe.hasAssistiveLayer, true);
});