feat(编辑能力): 修复字体选择与裁剪边界问题
build.yaml / build (push) Successful in 35s

This commit is contained in:
xiaoqidun committed 2026-09-16 18:32:20 +08:00
1 parent 71e67be9c1
commit 1914b5329a
25 files changed
+747 -253

No files matched your search

+2 -1
View File
@@ -93,7 +93,8 @@
</div>
<div class="editor-group editor-erase">
<button id="eraseObjectButton" class="button" type="button" title="整对象擦除" aria-label="整对象擦除" aria-pressed="false">对象</button>
<button id="eraseRegionButton" class="button" type="button" title="区域擦除(视觉裁剪)" aria-label="区域擦除" aria-pressed="false">区域</button>
<button id="eraseRegionButton" class="button" type="button" title="矩形擦除(视觉裁剪)" aria-label="矩形擦除" aria-pressed="false">矩形</button>
<button id="erasePathButton" class="button" type="button" title="套索擦除(视觉裁剪)" aria-label="套索擦除" aria-pressed="false">套索</button>
<span>擦除</span>
</div>
<div class="editor-group">
+7 -2
View File
@@ -112,6 +112,7 @@ a:focus-visible {
}
.editor-file,
.editor-erase,
.editor-paragraph,
.editor-image,
.editor-group:has(#textFont),
@@ -119,11 +120,15 @@ a:focus-visible {
grid-template-columns: repeat(2, max-content);
}
.editor-transform,
.editor-erase {
.editor-transform {
grid-template-columns: max-content;
}
#eraseObjectButton {
grid-row: span 2;
height: 100%;
}
.editor-image > select {
grid-column: 1 / -1;
}
+24 -13
View File
@@ -120,6 +120,7 @@ const el = {
drawEllipseButton: document.querySelector("#drawEllipseButton"),
eraseObjectButton: document.querySelector("#eraseObjectButton"),
eraseRegionButton: document.querySelector("#eraseRegionButton"),
erasePathButton: document.querySelector("#erasePathButton"),
shapeFill: document.querySelector("#shapeFill"),
shapeFillColor: document.querySelector("#shapeFillColor"),
shapeStroke: document.querySelector("#shapeStroke"),
@@ -277,10 +278,11 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
if (state.fontRenderPending) window.setTimeout(refreshPendingFonts, 0);
},
onDelete: (item) => changeDocument(item.items ? "ofdgoDeleteObjects" : "ofdgoDeleteObject", item),
onErase: (items, box) => {
onErase: (items, box, points) => {
const blocked = items.find(item => !canEditObject(item, box ? "arrange" : "delete"));
if (blocked) { setStatus(objectEditReason(blocked) || "对象暂不可擦除"); return false; }
const item = { index: items[0].index, id: items.map(item => item.id) };
if (points) return changeDocument("ofdgoEraseObjectsPath", item, points);
return box ? changeDocument("ofdgoEraseObjects", item, box.x, box.y, box.width, box.height)
: changeDocument("ofdgoDeleteObjects", item);
},
@@ -437,7 +439,7 @@ el.resetCropButton.addEventListener("click", () => {
editorClick(el.undoButton, () => changeDocument("ofdgoUndo"));
editorClick(el.redoButton, () => changeDocument("ofdgoRedo"));
for (const [button, tool] of [[el.drawLineButton, "line"], [el.drawRectangleButton, "rectangle"], [el.drawEllipseButton, "ellipse"],
[el.eraseObjectButton, "erase-object"], [el.eraseRegionButton, "erase-region"]]) {
[el.eraseObjectButton, "erase-object"], [el.eraseRegionButton, "erase-region"], [el.erasePathButton, "erase-path"]]) {
editorClick(button, () => {
if (!state.editing || document.body.hasAttribute("aria-busy")) {
return;
@@ -976,7 +978,7 @@ async function beginCanvasText(index, box, surface, page) {
const openSeq = state.openSeq;
setBusy(true);
try {
const data = font.embedded ? (await callWASM("ofdgoEditorFont", font.id.slice(9))).bytes : await fontManager.read(font);
const data = await readTextFont(font);
const face = new FontFace("ofdgo-draft", data);
await face.load();
if (openSeq !== state.openSeq || !surface.isConnected) return;
@@ -991,7 +993,7 @@ async function beginCanvasText(index, box, surface, page) {
}
function editorFonts(item) {
const fonts = [...fontManager.userFonts.filter((font) => font.enabled), ...fontManager.catalog];
const fonts = fontManager.editorFonts();
if (item) {
fonts.unshift({ id: `embedded:${item.font}`, name: item.fontName || item.font, embedded: true, disabled: !canEditObject(item, "update") });
}
@@ -999,22 +1001,31 @@ function editorFonts(item) {
}
async function loadEditorFonts() {
if (!fontManager.canReadLocal() || fontManager.catalogLoaded || fontManager.permission === "denied" || state.fontCatalogLoading) return;
const local = fontManager.canReadLocal() && !fontManager.catalogLoaded && fontManager.permission !== "denied";
if (state.fontCatalogLoading || !local && !fontManager.userFonts.some(font => font.enabled && !font.faces)) return;
state.fontCatalogLoading = true;
try {
await fontManager.queryLocal();
if (local) {
try { await fontManager.queryLocal(); }
catch (err) { setStatus(err?.name === "NotAllowedError" ? "字体尚未授权" : String(err.message || err)); }
}
await fontManager.loadFaces(data => callWASM("ofdgoFontFaces", data));
const open = fontPicker.open;
const selected = state.textFonts[Number(fontPicker.value)];
const query = el.textFont.value === (selected?.fullName || selected?.name || "") ? "" : el.textFont.value;
updateTextFonts(canvasEditor.selected, true);
if (open && !el.textFont.disabled) { if (query) el.textFont.value = query; fontPicker.show(query); }
} catch (err) {
setStatus(err?.name === "NotAllowedError" ? "字体尚未授权" : String(err.message || err));
} finally {
state.fontCatalogLoading = false;
}
}
async function readTextFont(font) {
if (font.embedded) return (await callWASM("ofdgoEditorFont", font.id.slice(9))).bytes;
if (font.file) return (await callWASM("ofdgoFontFace", await fontManager.read(font.file), font.index)).bytes;
return fontManager.read(font);
}
function updateTextFonts(item, refresh = false) {
const id = item?.type === "TextObject" && !item.draft ? item.font : null;
if (refresh || state.textFontID !== id) {
@@ -1039,7 +1050,7 @@ async function changeTextFont() {
const openSeq = state.openSeq;
setBusy(true);
try {
const data = font.embedded ? (await callWASM("ofdgoEditorFont", font.id.slice(9))).bytes : await fontManager.read(font);
const data = await readTextFont(font);
const face = new FontFace("ofdgo-edit-font", data);
await face.load();
if (openSeq !== state.openSeq || canvasEditor.input !== editing) return;
@@ -1056,7 +1067,7 @@ async function changeTextFont() {
}
} else if (item?.type === "TextObject") {
if (!font.embedded && canEditObject(item, "replaceFont")) {
await changeDocument("ofdgoUpdateText", item, canEditObject(item, "layoutKnown") ? item.text : null, fontManager.read(font), item.size, null);
await changeDocument("ofdgoUpdateText", item, canEditObject(item, "layoutKnown") ? item.text : null, readTextFont(font), item.size, null);
updateTextFonts(canvasEditor.selected, true);
}
} else {
@@ -1221,7 +1232,7 @@ function updateDrawingControls() {
button.disabled = disabled || !pageCan("insert");
button.setAttribute("aria-pressed", String(tool === name));
}
for (const [button, name] of [[el.eraseObjectButton, "erase-object"], [el.eraseRegionButton, "erase-region"]]) {
for (const [button, name] of [[el.eraseObjectButton, "erase-object"], [el.eraseRegionButton, "erase-region"], [el.erasePathButton, "erase-path"]]) {
button.disabled = disabled;
button.setAttribute("aria-pressed", String(tool === name));
}
@@ -1256,7 +1267,7 @@ async function changeDocument(name, item, ...args) {
return true;
}
setEditorInfo(doc);
if (!item || name === "ofdgoDeleteObject" || name === "ofdgoDeleteObjects" || name === "ofdgoEraseObjects") {
if (!item || name === "ofdgoDeleteObject" || name === "ofdgoDeleteObjects" || name === "ofdgoEraseObjects" || name === "ofdgoEraseObjectsPath") {
canvasEditor.clear();
}
if (name === "ofdgoCopyObjects" || name === "ofdgoPasteObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
@@ -3021,7 +3032,7 @@ async function pasteEditorContent(event) {
return;
}
const style = currentTextStyle();
const data = font.embedded ? callWASM("ofdgoEditorFont", font.id.slice(9)).then(result => result.bytes) : fontManager.read(font);
const data = readTextFont(font);
await changeDocument("ofdgoInsertText", null, index, value, data, x, y, style.size, style.color,
page.width - x * 2, style.wrap, style.align || "left", style.paragraphHeight || 0, style.letterSpacing || 0);
}
+18 -1
View File
@@ -505,7 +505,7 @@ export class CanvasEditor {
? drag.items.filter(item => item.x < drag.box.x + drag.box.width && item.x + item.width > drag.box.x
&& item.y < drag.box.y + drag.box.height && item.y + item.height > drag.box.y) : [];
this.cancel();
if (items.length) this.options.onErase(items, drag.erase === "erase-region" ? drag.box : null);
if (items.length && (!drag.points || drag.points.length >= 3)) this.options.onErase(items, drag.box, drag.points);
return;
}
if (drag.crop) {
@@ -591,6 +591,11 @@ export class CanvasEditor {
rect: surface.getBoundingClientRect(), rotation: this.options.rotation() };
if (this.tool === "erase-region") Object.assign(this.drag, this.createPreview(surface, page, "rectangle",
{ fill: true, fillColor: "var(--accent-soft)", stroke: true, strokeColor: "var(--accent)", lineWidth: 0.2 }));
if (this.tool === "erase-path") {
Object.assign(this.drag, this.createPreview(surface, page, "path",
{ fill: true, fillColor: "var(--accent-soft)", stroke: true, strokeColor: "var(--accent)", lineWidth: 0.2 }), { points: [] });
this.drag.node.setAttribute("fill-rule", "evenodd");
}
this.viewer.setPointerCapture(event.pointerId);
this.moveErase(event, this.drag);
}
@@ -598,6 +603,18 @@ export class CanvasEditor {
moveErase(event, drag) {
if (drag.erase === "erase-region") { this.moveShape(event, drag); return; }
const point = pagePoint(event.clientX, event.clientY, drag.rect, drag.page, drag.rotation);
if (drag.points) {
const last = drag.lastPointer;
if (last && Math.hypot(event.clientX - last.x, event.clientY - last.y) < 2) return;
drag.lastPointer = { x: event.clientX, y: event.clientY };
drag.points.push(point);
const box = drag.box || { x: point.x, y: point.y, width: 0, height: 0 };
const right = Math.max(box.x + box.width, point.x), bottom = Math.max(box.y + box.height, point.y);
drag.box = { x: Math.min(box.x, point.x), y: Math.min(box.y, point.y) };
Object.assign(drag.box, { width: right - drag.box.x, height: bottom - drag.box.y });
drag.node.setAttribute("d", drag.points.map((point, index) => `${index ? "L" : "M"}${point.x} ${point.y}`).join(" ") + " Z");
return;
}
const item = [...drag.items].reverse().find(item => point.x >= item.x && point.x <= item.x + item.width
&& point.y >= item.y && point.y <= item.y + item.height);
if (item) { drag.erased.add(item); item.node.classList.add("erasing"); }
+22 -3
View File
@@ -48,7 +48,7 @@ export class FontPicker {
const normalize = value => value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/g, " ").trim();
const terms = normalize(query).split(" ").filter(Boolean);
this.matches = this.fonts.map((font, index) => ({ font, index })).filter(({ font }) => {
const name = normalize([font.fullName, font.name, font.family, font.postscriptName].filter(Boolean).join(" "));
const name = normalize([font.fullName, font.name, font.family, font.style, font.postscriptName, ...(font.names || [])].filter(Boolean).join(" "));
return terms.every(term => name.includes(term));
});
this.list.replaceChildren();
@@ -105,8 +105,10 @@ export class FontPicker {
commitExact() {
const value = this.input.value.trim().toLocaleLowerCase();
const index = this.fonts.findIndex(font => !font.disabled
&& [font.fullName, font.name, font.postscriptName].some(name => name?.toLocaleLowerCase() === value));
const matches = font => font && !font.disabled
&& [font.fullName, font.name, font.postscriptName, ...(font.names || [])].some(name => name?.toLocaleLowerCase() === value);
const selected = Number(this.value);
const index = matches(this.fonts[selected]) ? selected : this.fonts.findIndex(matches);
if (index >= 0) this.choose(index);
}
@@ -283,6 +285,22 @@ export class FontManager {
return font.data;
}
editorFonts() {
return [...this.userFonts.filter(font => font.enabled).flatMap(file => {
if (!file.faces) return [file];
if (!file.faces.length) return [{ ...file, disabled: true }];
return file.faces.map(face => ({ ...face, id: face.index ? `${file.id}:${face.index}` : file.id, name: file.name, file }));
}), ...this.catalog];
}
async loadFaces(inspect) {
for (const file of this.userFonts) {
if (!file.enabled || file.faces) continue;
try { file.faces = await inspect(await this.read(file)); }
catch { file.faces = []; }
}
}
async add(fonts) {
let saved = true;
try {
@@ -320,6 +338,7 @@ export class FontManager {
const fonts = stored.map((font) => ({
...font,
data: previous.get(font.id)?.checksum === font.checksum ? previous.get(font.id).data : font.data,
faces: previous.get(font.id)?.checksum === font.checksum ? previous.get(font.id).faces : undefined,
source: "stored",
}));
fonts.push(...this.userFonts.filter((font) => font.source === "upload"));