mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
Compare commits
2
Commits
9ee47731c9
...
20374f52f1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20374f52f1 | ||
|
|
6dae7bdc20 |
No files matched your search
@@ -82,7 +82,6 @@
|
|||||||
<span>操作</span>
|
<span>操作</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-group editor-objects">
|
<div class="editor-group editor-objects">
|
||||||
<button id="compositeBackButton" class="button" type="button" hidden>返回</button>
|
|
||||||
<button id="selectObjectButton" class="button" type="button" data-pressed="true">选择</button>
|
<button id="selectObjectButton" class="button" type="button" data-pressed="true">选择</button>
|
||||||
<button id="multiSelectButton" class="button" type="button" data-pressed="false">多选</button>
|
<button id="multiSelectButton" class="button" type="button" data-pressed="false">多选</button>
|
||||||
<button id="editObjectButton" class="button" type="button" disabled>修改</button>
|
<button id="editObjectButton" class="button" type="button" disabled>修改</button>
|
||||||
|
|||||||
@@ -3230,6 +3230,10 @@ button.outline-link:hover {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-object.edit-parent {
|
||||||
|
outline: 1px dashed var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
.edit-text-source {
|
.edit-text-source {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
+110
-112
@@ -59,7 +59,6 @@ function createInvoicePanel() {
|
|||||||
function createDocumentState() {
|
function createDocumentState() {
|
||||||
return {
|
return {
|
||||||
conversionWarnings: [],
|
conversionWarnings: [],
|
||||||
composite: null,
|
|
||||||
annotationEdit: null,
|
annotationEdit: null,
|
||||||
annotationCreate: null,
|
annotationCreate: null,
|
||||||
ready: false,
|
ready: false,
|
||||||
@@ -285,7 +284,6 @@ const el = {
|
|||||||
selectObjectButton: document.querySelector("#selectObjectButton"),
|
selectObjectButton: document.querySelector("#selectObjectButton"),
|
||||||
deleteObjectButton: document.querySelector("#deleteObjectButton"),
|
deleteObjectButton: document.querySelector("#deleteObjectButton"),
|
||||||
editObjectButton: document.querySelector("#editObjectButton"),
|
editObjectButton: document.querySelector("#editObjectButton"),
|
||||||
compositeBackButton: document.querySelector("#compositeBackButton"),
|
|
||||||
copyObjectButton: document.querySelector("#copyObjectButton"),
|
copyObjectButton: document.querySelector("#copyObjectButton"),
|
||||||
objectOrder: document.querySelector("#objectOrder"),
|
objectOrder: document.querySelector("#objectOrder"),
|
||||||
objectAlign: document.querySelector("#objectAlign"),
|
objectAlign: document.querySelector("#objectAlign"),
|
||||||
@@ -743,18 +741,13 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
|||||||
onErase: (items, box, points) => {
|
onErase: (items, box, points) => {
|
||||||
const blocked = items.find(item => !canEditObject(item, box ? "arrange" : "delete"));
|
const blocked = items.find(item => !canEditObject(item, box ? "arrange" : "delete"));
|
||||||
if (blocked) { setStatus(objectEditReason(blocked) || "对象暂不可擦除"); return false; }
|
if (blocked) { setStatus(objectEditReason(blocked) || "对象暂不可擦除"); return false; }
|
||||||
const item = { index: items[0].index, id: items.map(item => item.id), scoped: items.every(item => item.scoped) };
|
const item = { index: items[0].index, id: items.map(item => item.id), items };
|
||||||
if (points) return changeDocument("ofdgoEraseObjectsPath", item, points);
|
if (points) return changeDocument("ofdgoEraseObjectsPath", item, points);
|
||||||
return box ? changeDocument("ofdgoEraseObjects", item, box.x, box.y, box.width, box.height)
|
return box ? changeDocument("ofdgoEraseObjects", item, box.x, box.y, box.width, box.height)
|
||||||
: changeDocument("ofdgoDeleteObjects", item);
|
: changeDocument("ofdgoDeleteObjects", item);
|
||||||
},
|
},
|
||||||
onEdit: editCanvasObject,
|
onEdit: editCanvasObject,
|
||||||
onPick: openObjectPicker,
|
onPick: openObjectPicker,
|
||||||
onExitScope: () => {
|
|
||||||
if (!state.composite) return false;
|
|
||||||
exitCompositeScope();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
drawStyle: shapeStyle,
|
drawStyle: shapeStyle,
|
||||||
onTool: () => {
|
onTool: () => {
|
||||||
updateDrawingControls();
|
updateDrawingControls();
|
||||||
@@ -813,7 +806,7 @@ const pageView = createPageView({
|
|||||||
callWASM, canvasEditor, clearRegionHighlights, documentTab, el,
|
callWASM, canvasEditor, clearRegionHighlights, documentTab, el,
|
||||||
enablePageDrag, formatSize, isDocumentSelection, isForegroundDocument, mountAnnotationNotes,
|
enablePageDrag, formatSize, isDocumentSelection, isForegroundDocument, mountAnnotationNotes,
|
||||||
mountEditorObjects, mountPageLinks, nextFrame, pageCan, pageStatus,
|
mountEditorObjects, mountPageLinks, nextFrame, pageCan, pageStatus,
|
||||||
releaseMediaPlayers, renderSearchHighlights, resetCompositeScope, scheduleOpenActions, selectThumbnailPage,
|
releaseMediaPlayers, renderSearchHighlights, loadEditorMembers, scheduleOpenActions, selectThumbnailPage,
|
||||||
setBusy, setProgress, setStatus, showError, syncPageSelection,
|
setBusy, setProgress, setStatus, showError, syncPageSelection,
|
||||||
updateControls, updateEditorTools, wasmRequests,
|
updateControls, updateEditorTools, wasmRequests,
|
||||||
});
|
});
|
||||||
@@ -858,7 +851,6 @@ editorClick(el.multiSelectButton, () => {
|
|||||||
canvasEditor.multiple = !canvasEditor.multiple;
|
canvasEditor.multiple = !canvasEditor.multiple;
|
||||||
el.multiSelectButton.setAttribute("data-pressed", String(canvasEditor.multiple));
|
el.multiSelectButton.setAttribute("data-pressed", String(canvasEditor.multiple));
|
||||||
});
|
});
|
||||||
editorClick(el.compositeBackButton, exitCompositeScope);
|
|
||||||
editorClick(el.editObjectButton, () => {
|
editorClick(el.editObjectButton, () => {
|
||||||
if (canvasEditor.selected) {
|
if (canvasEditor.selected) {
|
||||||
editCanvasObject(canvasEditor.selected);
|
editCanvasObject(canvasEditor.selected);
|
||||||
@@ -1092,9 +1084,9 @@ el.annotationTool.addEventListener("change", async () => {
|
|||||||
el.annotationTool.value = "";
|
el.annotationTool.value = "";
|
||||||
if (kind === "objects") { openObjectPicker(); return; }
|
if (kind === "objects") { openObjectPicker(); return; }
|
||||||
let selected = canvasEditor.selected;
|
let selected = canvasEditor.selected;
|
||||||
if (state.composite?.key.startsWith("annotation:")) {
|
if (selected?.parent?.startsWith("annotation:")) {
|
||||||
const id = state.composite.key.split("/")[0];
|
const id = selected.parent.split("/")[0];
|
||||||
selected = { ...state.pageCache.get(state.composite.index)?.objects.find(item => item.id === id), index: state.composite.index, id, type: "Annotation" };
|
selected = { ...state.pageCache.get(selected.index)?.objects.find(item => item.id === id), index: selected.index, id, type: "Annotation" };
|
||||||
}
|
}
|
||||||
if (kind === "details" || kind === "replace") {
|
if (kind === "details" || kind === "replace") {
|
||||||
if (selected?.type !== "Annotation") { setStatus("请先选择注解"); return; }
|
if (selected?.type !== "Annotation") { setStatus("请先选择注解"); return; }
|
||||||
@@ -1111,7 +1103,6 @@ el.annotationTool.addEventListener("change", async () => {
|
|||||||
openAnnotationCreate(kind, selected.index, selected, selected, info);
|
openAnnotationCreate(kind, selected.index, selected, selected, info);
|
||||||
} catch (error) { showError(error, false); }
|
} catch (error) { showError(error, false); }
|
||||||
} else {
|
} else {
|
||||||
resetCompositeScope();
|
|
||||||
updateControls();
|
updateControls();
|
||||||
if (kind === "watermark") openAnnotationCreate(kind, state.pageIndex);
|
if (kind === "watermark") openAnnotationCreate(kind, state.pageIndex);
|
||||||
else {
|
else {
|
||||||
@@ -1411,7 +1402,7 @@ el.copyStyleButton.addEventListener("click", async () => {
|
|||||||
if (!canCopyStyle(item)) return;
|
if (!canCopyStyle(item)) return;
|
||||||
setBusy(true, "", 0, "", tab);
|
setBusy(true, "", 0, "", tab);
|
||||||
try {
|
try {
|
||||||
await callTabWASM(tab, "ofdgoCaptureStyle", item.index, item.id, state.composite?.key || "");
|
await callTabWASM(tab, "ofdgoCaptureStyle", item.index, item.id, objectParent(item));
|
||||||
if (!tab.closed) state.styleClipboard = item.type;
|
if (!tab.closed) state.styleClipboard = item.type;
|
||||||
if (isForegroundDocument(tab) && seq === state.openSeq) el.objectStylePanel.close();
|
if (isForegroundDocument(tab) && seq === state.openSeq) el.objectStylePanel.close();
|
||||||
} catch (err) { if (isForegroundDocument(tab) && seq === state.openSeq) el.objectStyleStatus.textContent = err.message; }
|
} catch (err) { if (isForegroundDocument(tab) && seq === state.openSeq) el.objectStyleStatus.textContent = err.message; }
|
||||||
@@ -3257,7 +3248,6 @@ async function switchReadingVersion(id) {
|
|||||||
if (tab.closed || tab.state !== state || state.doc !== originalDocument || openSeq !== state.openSeq && view === tab.view) return;
|
if (tab.closed || tab.state !== state || state.doc !== originalDocument || openSeq !== state.openSeq && view === tab.view) return;
|
||||||
openSeq = state.openSeq = workspace.sequence = Math.max(workspace.sequence, state.openSeq) + 1;
|
openSeq = state.openSeq = workspace.sequence = Math.max(workspace.sequence, state.openSeq) + 1;
|
||||||
state.objectClipboard = state.styleClipboard = null;
|
state.objectClipboard = state.styleClipboard = null;
|
||||||
state.composite = null;
|
|
||||||
delete state.textDefaults.fontChoice;
|
delete state.textDefaults.fontChoice;
|
||||||
if (state.editorInfo) {
|
if (state.editorInfo) {
|
||||||
if (!dirty) state.savedRevision = doc.revision;
|
if (!dirty) state.savedRevision = doc.revision;
|
||||||
@@ -3275,7 +3265,6 @@ async function switchReadingVersion(id) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
canvasEditor.richClipboard = null;
|
canvasEditor.richClipboard = null;
|
||||||
resetCompositeScope();
|
|
||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
canvasEditor.setTool("");
|
canvasEditor.setTool("");
|
||||||
updateTextFonts(null, true);
|
updateTextFonts(null, true);
|
||||||
@@ -3492,7 +3481,6 @@ async function switchDocument(index) {
|
|||||||
state.dirty = state.packageDirty || state.encryptionDirty || doc.revision !== state.savedRevision;
|
state.dirty = state.packageDirty || state.encryptionDirty || doc.revision !== state.savedRevision;
|
||||||
state.doc = doc;
|
state.doc = doc;
|
||||||
if (!isForegroundDocument(tab)) {
|
if (!isForegroundDocument(tab)) {
|
||||||
state.composite = null;
|
|
||||||
state.pageCache.clear();
|
state.pageCache.clear();
|
||||||
state.pageIndex = 0;
|
state.pageIndex = 0;
|
||||||
tab.view = null;
|
tab.view = null;
|
||||||
@@ -3502,7 +3490,6 @@ async function switchDocument(index) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
canvasEditor.richClipboard = null;
|
canvasEditor.richClipboard = null;
|
||||||
resetCompositeScope();
|
|
||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
canvasEditor.setTool("");
|
canvasEditor.setTool("");
|
||||||
updateTextFonts(null, true);
|
updateTextFonts(null, true);
|
||||||
@@ -3527,14 +3514,14 @@ async function switchDocument(index) {
|
|||||||
function editorLocation() {
|
function editorLocation() {
|
||||||
const pending = canvasEditor.pendingSelection;
|
const pending = canvasEditor.pendingSelection;
|
||||||
const index = pending?.index ?? canvasEditor.selected?.index ?? state.pageIndex;
|
const index = pending?.index ?? canvasEditor.selected?.index ?? state.pageIndex;
|
||||||
return { page: state.doc.pages[index].id, scope: state.composite?.key || "", ids: pending?.ids || canvasEditor.items().map(item => item.id) };
|
return { page: state.doc.pages[index].id, ids: pending?.ids || canvasEditor.items().map(item => item.id) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function rememberEditorView(before, revision, reindex = false) {
|
function rememberEditorView(before, revision) {
|
||||||
for (const key of state.editorViews.keys()) {
|
for (const key of state.editorViews.keys()) {
|
||||||
if (key > revision) state.editorViews.delete(key);
|
if (key > revision) state.editorViews.delete(key);
|
||||||
}
|
}
|
||||||
state.editorViews.set(state.editorInfo.revision, { before, after: editorLocation(), reindex });
|
state.editorViews.set(state.editorInfo.revision, { before, after: editorLocation() });
|
||||||
if (state.editorViews.size > 100) state.editorViews.delete(state.editorViews.keys().next().value);
|
if (state.editorViews.size > 100) state.editorViews.delete(state.editorViews.keys().next().value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3555,7 +3542,6 @@ async function toggleEditor() {
|
|||||||
const anchor = pageView.scaleAnchor(0);
|
const anchor = pageView.scaleAnchor(0);
|
||||||
state.editing = !state.editing;
|
state.editing = !state.editing;
|
||||||
renderDocumentFonts();
|
renderDocumentFonts();
|
||||||
resetCompositeScope();
|
|
||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
canvasEditor.setTool("");
|
canvasEditor.setTool("");
|
||||||
if (state.editing) {
|
if (state.editing) {
|
||||||
@@ -3602,7 +3588,6 @@ async function toggleEditor() {
|
|||||||
pageCapabilities: doc.pageCapabilities, editWarnings: doc.editWarnings || []};
|
pageCapabilities: doc.pageCapabilities, editWarnings: doc.editWarnings || []};
|
||||||
state.editing = true;
|
state.editing = true;
|
||||||
state.ofdBytes = null;
|
state.ofdBytes = null;
|
||||||
state.composite = null;
|
|
||||||
state.selectObjects = true;
|
state.selectObjects = true;
|
||||||
state.conversionWarnings = [];
|
state.conversionWarnings = [];
|
||||||
state.objectClipboard = state.styleClipboard = null;
|
state.objectClipboard = state.styleClipboard = null;
|
||||||
@@ -3614,7 +3599,6 @@ async function toggleEditor() {
|
|||||||
setEditorInfo(doc);
|
setEditorInfo(doc);
|
||||||
state.editing = true;
|
state.editing = true;
|
||||||
releaseMediaPlayers();
|
releaseMediaPlayers();
|
||||||
state.composite = null;
|
|
||||||
state.selectObjects = true;
|
state.selectObjects = true;
|
||||||
state.ofdBytes = null;
|
state.ofdBytes = null;
|
||||||
state.conversionWarnings = [];
|
state.conversionWarnings = [];
|
||||||
@@ -3897,7 +3881,7 @@ function updateAnnotationFields() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function editCanvasObject(item) {
|
async function editCanvasObject(item, point = null) {
|
||||||
const tab = documentTab(), state = tab.state;
|
const tab = documentTab(), state = tab.state;
|
||||||
const callWASM = (name, ...args) => callTabWASM(tab, name, ...args);
|
const callWASM = (name, ...args) => callTabWASM(tab, name, ...args);
|
||||||
if (!state.editing || document.body.hasAttribute("data-busy") || canvasEditor.input) return;
|
if (!state.editing || document.body.hasAttribute("data-busy") || canvasEditor.input) return;
|
||||||
@@ -3927,7 +3911,7 @@ async function editCanvasObject(item) {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!item.richText && canEditObject(item, "enter")) return enterCompositeScope(item.index, item.id);
|
if (!item.richText && canEditObject(item, "children")) return selectCompositeContent(item, point);
|
||||||
if (item.type === "Annotation") {
|
if (item.type === "Annotation") {
|
||||||
editAnnotationDetails(item);
|
editAnnotationDetails(item);
|
||||||
return;
|
return;
|
||||||
@@ -3972,56 +3956,89 @@ async function editCanvasObject(item) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function objectParent(item) {
|
||||||
|
const ids = item?.items?.map(member => member.id) || (Array.isArray(item?.id) ? item.id : item ? [item.id] : []);
|
||||||
|
const parents = ids.map(id => id.includes("/") ? id.slice(0, id.lastIndexOf("/")) : "");
|
||||||
|
return parents.every(parent => parent === parents[0]) ? parents[0] || "" : null;
|
||||||
|
}
|
||||||
|
|
||||||
function mountEditorObjects(index, page, surface) {
|
function mountEditorObjects(index, page, surface) {
|
||||||
const scope = state.composite?.index === index ? state.composite : null;
|
const objects = [];
|
||||||
canvasEditor.mount(index, { ...page, objects: scope ? scope.objects : page.objects.filter(item => state.renderAnnotations || item.type !== "Annotation") }, surface);
|
const append = members => {
|
||||||
|
for (const item of members) {
|
||||||
|
if (!state.renderAnnotations && item.type === "Annotation") continue;
|
||||||
|
objects.push(item);
|
||||||
|
const children = page.members?.get(item.id);
|
||||||
|
if (children) append(children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
append(page.objects);
|
||||||
|
canvasEditor.mount(index, { ...page, objects }, surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeAnnotations(item, action, ...args) {
|
function changeAnnotations(item, action, ...args) {
|
||||||
return changeDocument("ofdgoChangeAnnotations", { ...item, id: item.items ? item.id : [item.id] }, action, ...args);
|
return changeDocument("ofdgoChangeAnnotations", { ...item, id: item.items ? item.id : [item.id] }, action, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetCompositeScope() {
|
async function loadEditorMembers(tab, page, index) {
|
||||||
const scope = state.composite;
|
const state = tab.state, openSeq = state.openSeq;
|
||||||
if (!scope) return;
|
const pending = canvasEditor.pendingSelection;
|
||||||
state.composite = null;
|
const ids = pending?.index === index ? pending.ids || [] : canvasEditor.items().filter(item => item.index === index).map(item => item.id);
|
||||||
canvasEditor.clear();
|
page.members ||= new Map();
|
||||||
const page = state.pageCache.get(scope.index), surface = pageView.pageShell(scope.index)?.querySelector(".page-surface");
|
const paths = new Set();
|
||||||
if (page && surface) mountEditorObjects(scope.index, page, surface);
|
for (const id of ids) {
|
||||||
|
const parts = id.split("/");
|
||||||
|
for (let i = 1; i < parts.length; i++) paths.add(parts.slice(0, i).join("/"));
|
||||||
|
}
|
||||||
|
for (const key of [...paths].sort((a, b) => a.split("/").length - b.split("/").length)) {
|
||||||
|
if (page.members.has(key)) continue;
|
||||||
|
const parent = key.includes("/") ? page.members.get(key.slice(0, key.lastIndexOf("/"))) : page.objects;
|
||||||
|
if (!parent?.some(item => item.id === key && canEditObject(item, "children"))) continue;
|
||||||
|
const members = await callTabWASM(tab, "ofdgoCompositeObjects", index, key);
|
||||||
|
if (!isForegroundDocument(tab) || openSeq !== state.openSeq) return;
|
||||||
|
page.members.set(key, members);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enterCompositeScope(index, key) {
|
async function selectCompositeContent(item, point) {
|
||||||
const tab = documentTab(), state = tab.state;
|
const tab = documentTab(), state = tab.state, openSeq = state.openSeq;
|
||||||
const openSeq = state.openSeq;
|
const page = state.pageCache.get(item.index), surface = item.surface;
|
||||||
|
if (!page) return;
|
||||||
|
const original = canvasEditor.selected;
|
||||||
tab.editTask = true;
|
tab.editTask = true;
|
||||||
setBusy(true, "", 0, "", tab);
|
setBusy(true, "", 0, "", tab);
|
||||||
|
let target = item;
|
||||||
try {
|
try {
|
||||||
const objects = await callTabWASM(tab, "ofdgoCompositeObjects", index, key);
|
page.members ||= new Map();
|
||||||
if (!isForegroundDocument(tab) || openSeq !== state.openSeq) return;
|
const rect = point && surface.getBoundingClientRect();
|
||||||
canvasEditor.clear();
|
const tolerance = point ? 3 * page.width / (state.rotation % 180 ? rect.height : rect.width) : 0;
|
||||||
canvasEditor.setTool("");
|
while (canEditObject(target, "children") && !target.richText) {
|
||||||
state.composite = { index, key, objects };
|
const key = target.id;
|
||||||
const page = state.pageCache.get(index), surface = pageView.pageShell(index)?.querySelector(".page-surface");
|
let members = page.members.get(key);
|
||||||
if (page && surface) mountEditorObjects(index, page, surface);
|
if (!members) {
|
||||||
updateControls();
|
members = await callTabWASM(tab, "ofdgoCompositeObjects", item.index, key);
|
||||||
setStatus(pageStatus(index, state.doc.pageCount));
|
if (!isForegroundDocument(tab) || openSeq !== state.openSeq || canvasEditor.selected !== original || !surface.isConnected) return;
|
||||||
|
page.members.set(key, members);
|
||||||
|
}
|
||||||
|
const next = point ? [...members].reverse().find(member => member.type === "TextObject" || canEditObject(member, "children")
|
||||||
|
? point.x >= member.x && point.x <= member.x + member.width && point.y >= member.y && point.y <= member.y + member.height
|
||||||
|
: canvasEditor.contains(member, point, tolerance)) : members[0];
|
||||||
|
if (!next) break;
|
||||||
|
target = next;
|
||||||
|
}
|
||||||
|
if (target.id === item.id) return;
|
||||||
|
mountEditorObjects(item.index, page, surface);
|
||||||
|
target = canvasEditor.pageItems(item.index).find(member => member.id === target.id);
|
||||||
|
canvasEditor.select(target);
|
||||||
canvasEditor.focus();
|
canvasEditor.focus();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isForegroundDocument(tab) && openSeq === state.openSeq) showError(err, false);
|
if (isForegroundDocument(tab) && openSeq === state.openSeq) showError(err, false);
|
||||||
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
tab.editTask = false;
|
tab.editTask = false;
|
||||||
setBusy(false, "", 0, "", tab);
|
setBusy(false, "", 0, "", tab);
|
||||||
}
|
}
|
||||||
}
|
if (target && target.id !== item.id && !canEditObject(target, "children") && isForegroundDocument(tab) && openSeq === state.openSeq) return editCanvasObject(target);
|
||||||
|
|
||||||
async function exitCompositeScope() {
|
|
||||||
const scope = state.composite;
|
|
||||||
if (!scope) return;
|
|
||||||
const split = scope.key.lastIndexOf("/");
|
|
||||||
if (split >= 0) return enterCompositeScope(scope.index, scope.key.slice(0, split));
|
|
||||||
resetCompositeScope();
|
|
||||||
updateControls();
|
|
||||||
canvasEditor.focus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startImageCrop(item) {
|
async function startImageCrop(item) {
|
||||||
@@ -4429,7 +4446,7 @@ async function insertObject(event) {
|
|||||||
const item = state.insertObject;
|
const item = state.insertObject;
|
||||||
const before = editorLocation(), revision = state.editorInfo.revision;
|
const before = editorLocation(), revision = state.editorInfo.revision;
|
||||||
const originalDocument = state.doc;
|
const originalDocument = state.doc;
|
||||||
const file = el.insertImage.files[0], fit = el.insertFit.value, scope = state.composite?.key || "";
|
const file = el.insertImage.files[0], fit = el.insertFit.value;
|
||||||
const index = item?.index ?? state.pageIndex;
|
const index = item?.index ?? state.pageIndex;
|
||||||
const page = state.doc.pages[index];
|
const page = state.doc.pages[index];
|
||||||
const x = Math.min(20, page.width / 10);
|
const x = Math.min(20, page.width / 10);
|
||||||
@@ -4443,9 +4460,9 @@ async function insertObject(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const doc = item?.scoped
|
const doc = item?.scoped
|
||||||
? await callWASM("ofdgoChangeCompositeObjects", item.index, scope, [item.id], "image", data)
|
? await callWASM("ofdgoChangeCompositeObjects", item.index, objectParent(item), [item.id], "image", data)
|
||||||
: item ? await callWASM("ofdgoReplaceImage", item.index, item.id, data, fit)
|
: item ? await callWASM("ofdgoReplaceImage", item.index, item.id, data, fit)
|
||||||
: await callWASM("ofdgoInsertImage", index, data, x, y, page.width - x * 2, scope);
|
: await callWASM("ofdgoInsertImage", index, data, x, y, page.width - x * 2);
|
||||||
if (tab.closed || state.doc !== originalDocument || state.editorInfo?.revision !== revision) return;
|
if (tab.closed || state.doc !== originalDocument || state.editorInfo?.revision !== revision) return;
|
||||||
if (!isForegroundDocument(tab) || openSeq !== state.openSeq) {
|
if (!isForegroundDocument(tab) || openSeq !== state.openSeq) {
|
||||||
state.doc = doc;
|
state.doc = doc;
|
||||||
@@ -4470,7 +4487,7 @@ async function insertObject(event) {
|
|||||||
openSeq = advanceDocumentSequence();
|
openSeq = advanceDocumentSequence();
|
||||||
await refreshEditorPage(doc, index, openSeq);
|
await refreshEditorPage(doc, index, openSeq);
|
||||||
if (isForegroundDocument(tab) && openSeq === state.openSeq) {
|
if (isForegroundDocument(tab) && openSeq === state.openSeq) {
|
||||||
rememberEditorView(before, revision, !item && !!state.composite);
|
rememberEditorView(before, revision);
|
||||||
canvasEditor.focus();
|
canvasEditor.focus();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -4548,7 +4565,7 @@ async function changeTextStyle(color) {
|
|||||||
updateObjectControls(canvasEditor.selected, true);
|
updateObjectControls(canvasEditor.selected, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.composite && color) {
|
if (item?.scoped && color) {
|
||||||
if (color && fill !== null && canEditObject(item, "paint")) {
|
if (color && fill !== null && canEditObject(item, "paint")) {
|
||||||
await changeDocument("ofdgoCompositeTextColor", item, fill);
|
await changeDocument("ofdgoCompositeTextColor", item, fill);
|
||||||
}
|
}
|
||||||
@@ -4693,26 +4710,30 @@ async function changeDocument(name, item, ...args) {
|
|||||||
if (tab.closed) {
|
if (tab.closed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const scope = state.composite;
|
const scope = objectParent(item);
|
||||||
const operation = { ofdgoTransformObject: "transform", ofdgoTransformObjects: "transform", ofdgoRotateObjects: "rotate", ofdgoFlipObjects: "flip", ofdgoResizeObjects: "resize",
|
const operation = { ofdgoTransformObject: "transform", ofdgoTransformObjects: "transform", ofdgoRotateObjects: "rotate", ofdgoFlipObjects: "flip", ofdgoResizeObjects: "resize",
|
||||||
ofdgoAlignObject: "align", ofdgoAlignObjects: "align", ofdgoDistributeObjects: "distribute", ofdgoStyleObjects: "style", ofdgoUpdatePathStyle: "paint", ofdgoCompositeTextColor: "textColor",
|
ofdgoAlignObject: "align", ofdgoAlignObjects: "align", ofdgoDistributeObjects: "distribute", ofdgoStyleObjects: "style", ofdgoUpdatePathStyle: "paint", ofdgoCompositeTextColor: "textColor",
|
||||||
ofdgoUpdateText: "text", ofdgoStyleText: "textStyle", ofdgoCropImage: "crop", ofdgoLayoutText: "layout", ofdgoFitImage: "fit", ofdgoResetCompositeCrop: "resetCrop",
|
ofdgoUpdateText: "text", ofdgoStyleText: "textStyle", ofdgoCropImage: "crop", ofdgoLayoutText: "layout", ofdgoFitImage: "fit", ofdgoResetCompositeCrop: "resetCrop",
|
||||||
ofdgoReshapeObject: "reshape", ofdgoReshapeLine: "line", ofdgoEraseObjects: "erase", ofdgoEraseObjectsPath: "erasePath", ofdgoPasteStyle: "pasteStyle",
|
ofdgoReshapeObject: "reshape", ofdgoReshapeLine: "line", ofdgoEraseObjects: "erase", ofdgoEraseObjectsPath: "erasePath", ofdgoPasteStyle: "pasteStyle",
|
||||||
ofdgoDeleteObject: "delete", ofdgoDeleteObjects: "delete", ofdgoCopyObjects: "copy", ofdgoOrderObjects: "order", ofdgoGroupObjects: "group" }[name];
|
ofdgoDeleteObject: "delete", ofdgoDeleteObjects: "delete", ofdgoCopyObjects: "copy", ofdgoOrderObjects: "order", ofdgoGroupObjects: "group" }[name];
|
||||||
const members = item?.items || (item ? [item] : []);
|
const members = item?.items ? [...item.items].sort((a, b) => a.order - b.order) : item ? [item] : [];
|
||||||
const scoped = scope && members.length && members.every(member => member.scoped);
|
const scoped = members.some(member => member.scoped);
|
||||||
const ids = item ? Array.isArray(item.id) ? item.id : members.map(member => member.id) : [];
|
const ids = item ? Array.isArray(item.id) && !item.items ? item.id : members.map(member => member.id) : [];
|
||||||
const annotated = !scoped && ids.some(id => id.startsWith("annotation:"));
|
const annotated = !scoped && ids.some(id => id.startsWith("annotation:"));
|
||||||
const inserting = ["ofdgoInsertShape", "ofdgoInsertText", "ofdgoInsertImage", "ofdgoInsertInk"].includes(name) || name === "ofdgoSaveRichText" && !item;
|
const inserting = ["ofdgoInsertShape", "ofdgoInsertText", "ofdgoInsertImage", "ofdgoInsertInk"].includes(name) || name === "ofdgoSaveRichText" && !item;
|
||||||
const editingAnnotation = name === "ofdgoWriteAnnotation" && Boolean(state.annotationCreate?.item);
|
const editingAnnotation = name === "ofdgoWriteAnnotation" && Boolean(state.annotationCreate?.item);
|
||||||
const scopedRichText = scoped && name === "ofdgoSaveRichText";
|
const scopedRichText = scoped && name === "ofdgoSaveRichText";
|
||||||
if (scope && (inserting || scopedRichText)) args.push(scope.key);
|
if (scopedRichText) args.push(scope);
|
||||||
const textSelection = name === "ofdgoUpdateTextSelection";
|
const textSelection = name === "ofdgoUpdateTextSelection";
|
||||||
if (scoped && !operation && !textSelection && !scopedRichText) throw new Error("内部对象暂不支持此操作");
|
if (scoped && !operation && !textSelection && !scopedRichText) throw new Error("内部对象暂不支持此操作");
|
||||||
const doc = textSelection
|
const doc = name === "ofdgoStyleText"
|
||||||
|
? await callWASM(name, item.index, ids, ...args)
|
||||||
|
: textSelection
|
||||||
? await callWASM(name, item.index, item.selectionIDs, ...args)
|
? await callWASM(name, item.index, item.selectionIDs, ...args)
|
||||||
|
: scoped && operation && (scope === null || operation === "copy" && members.some(member => member.container !== members[0].container))
|
||||||
|
? await callWASM("ofdgoChangeSelection", item.index, ids, operation, ...args)
|
||||||
: scoped && !scopedRichText
|
: scoped && !scopedRichText
|
||||||
? await callWASM("ofdgoChangeCompositeObjects", item.index, scope.key, Array.isArray(item.id) ? item.id : members.map(member => member.id), operation, ...args)
|
? await callWASM("ofdgoChangeCompositeObjects", item.index, scope, ids, operation, ...args)
|
||||||
: annotated && operation ? await callWASM("ofdgoChangeSelection", item.index, ids, operation, ...args)
|
: annotated && operation ? await callWASM("ofdgoChangeSelection", item.index, ids, operation, ...args)
|
||||||
: await callWASM(name, ...(item ? [item.index, item.id] : []), ...args);
|
: await callWASM(name, ...(item ? [item.index, item.id] : []), ...args);
|
||||||
if (tab.closed || state.doc !== originalDocument || state.editorInfo?.revision !== revision) return;
|
if (tab.closed || state.doc !== originalDocument || state.editorInfo?.revision !== revision) return;
|
||||||
@@ -4756,7 +4777,6 @@ async function changeDocument(name, item, ...args) {
|
|||||||
if (state.documentManagement && (name === "ofdgoImportDocuments" || name === "ofdgoChangeDocument" && args[0] === "add")) {
|
if (state.documentManagement && (name === "ofdgoImportDocuments" || name === "ofdgoChangeDocument" && args[0] === "add")) {
|
||||||
state.documentManagement.key = documentEntryKey(nextDocument);
|
state.documentManagement.key = documentEntryKey(nextDocument);
|
||||||
}
|
}
|
||||||
resetCompositeScope();
|
|
||||||
state.objectClipboard = state.styleClipboard = null;
|
state.objectClipboard = state.styleClipboard = null;
|
||||||
canvasEditor.richClipboard = null;
|
canvasEditor.richClipboard = null;
|
||||||
state.pageSelection.clear();
|
state.pageSelection.clear();
|
||||||
@@ -4782,18 +4802,8 @@ async function changeDocument(name, item, ...args) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const view = restoredView;
|
const view = restoredView;
|
||||||
const reindex = !!scope && (scoped && ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoCopyObjects", "ofdgoOrderObjects", "ofdgoEraseObjects", "ofdgoEraseObjectsPath", "ofdgoGroupObjects"].includes(name)
|
|
||||||
|| textSelection || inserting || name === "ofdgoPasteObjects" && !!args[4]);
|
|
||||||
const reindexed = restoring ? view?.reindex && view.before : reindex && before;
|
|
||||||
const clipboard = state.objectClipboard;
|
|
||||||
if (reindexed && clipboard?.scope?.startsWith(reindexed.scope + "/") && clipboard.page === reindexed.page) state.objectClipboard = null;
|
|
||||||
const location = name === "ofdgoUndo" ? view?.before : view?.after;
|
const location = name === "ofdgoUndo" ? view?.before : view?.after;
|
||||||
const restoredIndex = location ? pageIndexByID(doc.pages, location.page) : -1;
|
const restoredIndex = location ? pageIndexByID(doc.pages, location.page) : -1;
|
||||||
if (restoredIndex >= 0) {
|
|
||||||
state.composite = location.scope ? { index: restoredIndex, key: location.scope, objects: [] } : null;
|
|
||||||
} else if (!item && !inserting && !editingAnnotation && !(name === "ofdgoPasteObjects" && args[4])) {
|
|
||||||
resetCompositeScope();
|
|
||||||
}
|
|
||||||
if (restoring && JSON.stringify(doc.outlines) !== JSON.stringify(state.doc.outlines)) {
|
if (restoring && JSON.stringify(doc.outlines) !== JSON.stringify(state.doc.outlines)) {
|
||||||
state.outlineSelection = null;
|
state.outlineSelection = null;
|
||||||
state.outlineExpanded.clear();
|
state.outlineExpanded.clear();
|
||||||
@@ -4845,7 +4855,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
previewScroll: keepScroll ? { scrollLeft, scrollTop } : null });
|
previewScroll: keepScroll ? { scrollLeft, scrollTop } : null });
|
||||||
}
|
}
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
if (!restoring) rememberEditorView(before, revision, reindex);
|
if (!restoring) rememberEditorView(before, revision);
|
||||||
if (el.annotationsTab.getAttribute("data-selected") === "true") await loadAnnotationSummary();
|
if (el.annotationsTab.getAttribute("data-selected") === "true") await loadAnnotationSummary();
|
||||||
if (!toolbarFocus && !changingFont) canvasEditor.focus();
|
if (!toolbarFocus && !changingFont) canvasEditor.focus();
|
||||||
return true;
|
return true;
|
||||||
@@ -8218,7 +8228,7 @@ function renderLayerSettings() {
|
|||||||
objects.multiple = true;
|
objects.multiple = true;
|
||||||
objects.size = Math.min(4, Math.max(2, objects.options.length));
|
objects.size = Math.min(4, Math.max(2, objects.options.length));
|
||||||
for (const option of objects.options) option.selected = canvasEditor.items().some(item => item.index === settings.page && item.id === option.value && !item.scoped);
|
for (const option of objects.options) option.selected = canvasEditor.items().some(item => item.index === settings.page && item.id === option.value && !item.scoped);
|
||||||
settingsButton("选取", () => beginSettingsPick("layer"), Boolean(state.composite), "PickLayer", binding.actions);
|
settingsButton("选取", () => beginSettingsPick("layer"), false, "PickLayer", binding.actions);
|
||||||
const move = settingsButton("移入", () => write("objects", 0, [...objects.selectedOptions].map(option => option.value)), !objects.selectedOptions.length, "MoveObjects", binding.actions);
|
const move = settingsButton("移入", () => write("objects", 0, [...objects.selectedOptions].map(option => option.value)), !objects.selectedOptions.length, "MoveObjects", binding.actions);
|
||||||
objects.addEventListener("change", () => { move.disabled = !objects.selectedOptions.length; });
|
objects.addEventListener("change", () => { move.disabled = !objects.selectedOptions.length; });
|
||||||
binding.section.append(binding.actions);
|
binding.section.append(binding.actions);
|
||||||
@@ -8337,12 +8347,12 @@ function settingsPickControl(field, kind) {
|
|||||||
wrapper.className = "form-unit settings-pick";
|
wrapper.className = "form-unit settings-pick";
|
||||||
field.replaceWith(wrapper);
|
field.replaceWith(wrapper);
|
||||||
wrapper.append(field);
|
wrapper.append(field);
|
||||||
settingsButton("选取", () => beginSettingsPick(kind), Boolean(state.composite), "PickObject", wrapper);
|
settingsButton("选取", () => beginSettingsPick(kind), false, "PickObject", wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function beginSettingsPick(kind) {
|
async function beginSettingsPick(kind) {
|
||||||
const tab = documentTab(), state = tab.state, settings = tab.settings;
|
const tab = documentTab(), state = tab.state, settings = tab.settings;
|
||||||
if (!state.editing || settings.busy || state.composite) return;
|
if (!state.editing || settings.busy) return;
|
||||||
rememberSettingsDraft();
|
rememberSettingsDraft();
|
||||||
const page = kind === "style" ? Number(settingsValue("TargetPage")) : settings.page, sequence = state.openSeq;
|
const page = kind === "style" ? Number(settingsValue("TargetPage")) : settings.page, sequence = state.openSeq;
|
||||||
if (!await pageView.renderPage(page, {fit: false}) || !isForegroundDocument(tab) || sequence !== state.openSeq || !settingsElements.Panel.open) return;
|
if (!await pageView.renderPage(page, {fit: false}) || !isForegroundDocument(tab) || sequence !== state.openSeq || !settingsElements.Panel.open) return;
|
||||||
@@ -8688,7 +8698,7 @@ function updateResourceControls() {
|
|||||||
const actions = resourceElements.Binding.hidden ? resourceElements.Actions : resourceElements.BindingActions;
|
const actions = resourceElements.Binding.hidden ? resourceElements.Actions : resourceElements.BindingActions;
|
||||||
if (resourceElements.Close.parentElement !== actions) actions.append(resourceElements.Close);
|
if (resourceElements.Close.parentElement !== actions) actions.append(resourceElements.Close);
|
||||||
const target = resourceManager.targets.find(target => target.id === resourceElements.Object.value);
|
const target = resourceManager.targets.find(target => target.id === resourceElements.Object.value);
|
||||||
resourceElements.Pick.disabled = disabled || !editing || !actionable || Boolean(state.composite);
|
resourceElements.Pick.disabled = disabled || !editing || !actionable;
|
||||||
resourceElements.Bind.disabled = disabled || !actionable || !target || target.bound;
|
resourceElements.Bind.disabled = disabled || !actionable || !target || target.bound;
|
||||||
resourceElements.Unbind.disabled = disabled || !actionable || !target?.bound;
|
resourceElements.Unbind.disabled = disabled || !actionable || !target?.bound;
|
||||||
}
|
}
|
||||||
@@ -8758,7 +8768,7 @@ function resourceChoices(items, label, choose, title = "选择对象") {
|
|||||||
|
|
||||||
async function beginResourcePick() {
|
async function beginResourcePick() {
|
||||||
const tab = documentTab(), state = tab.state, resourceManager = tab.resources;
|
const tab = documentTab(), state = tab.state, resourceManager = tab.resources;
|
||||||
if (!state.editing || state.composite || !["Audio", "Video", "Attachment"].includes(resourceManager.selected?.kind)) return;
|
if (!state.editing || !["Audio", "Video", "Attachment"].includes(resourceManager.selected?.kind)) return;
|
||||||
const sequence = state.openSeq;
|
const sequence = state.openSeq;
|
||||||
if (!await pageView.renderPage(Number(resourceElements.Page.value), {fit: false})) return;
|
if (!await pageView.renderPage(Number(resourceElements.Page.value), {fit: false})) return;
|
||||||
if (!isForegroundDocument(tab) || sequence !== state.openSeq || !resourceElements.Panel.open) return;
|
if (!isForegroundDocument(tab) || sequence !== state.openSeq || !resourceElements.Panel.open) return;
|
||||||
@@ -10464,23 +10474,18 @@ async function selectDocumentText() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function copyEditorSelection(event) {
|
function copyEditorSelection(event) {
|
||||||
|
const tab = documentTab(), state = tab.state;
|
||||||
if (!state.editing || !canvasEditor.enabled || !canvasEditor.selected || !el.viewerPanel.contains(event.target)
|
if (!state.editing || !canvasEditor.enabled || !canvasEditor.selected || !el.viewerPanel.contains(event.target)
|
||||||
|| canvasEditor.input || canvasEditor.crop || canvasEditor.drag || document.body.hasAttribute("data-busy")) return false;
|
|| canvasEditor.input || canvasEditor.crop || canvasEditor.drag || document.body.hasAttribute("data-busy")) return false;
|
||||||
const items = canvasEditor.items().slice().sort((a, b) => a.order - b.order);
|
const items = canvasEditor.items().slice().sort((a, b) => a.order - b.order);
|
||||||
const cut = event.type === "cut";
|
const cut = event.type === "cut";
|
||||||
if (state.composite && items.some(item => item.container !== items[0].container)) {
|
|
||||||
event.preventDefault();
|
|
||||||
setStatus("请选择同一组内的对象");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!canEditObject(canvasEditor.selected, "copy") || cut && !canEditObject(canvasEditor.selected, "delete")) {
|
if (!canEditObject(canvasEditor.selected, "copy") || cut && !canEditObject(canvasEditor.selected, "delete")) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setStatus(cut ? "对象不可剪切" : "对象不可复制");
|
setStatus(cut ? "对象不可剪切" : "对象不可复制");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const index = items[0].index, ids = items.map(item => item.id);
|
const index = items[0].index, ids = items.map(item => item.id);
|
||||||
const clipboard = { token: crypto.randomUUID(), page: state.doc.pages[index].id, scope: state.composite?.key || "", bounds: selectionBounds(items), x: 0, y: 0, cut,
|
const clipboard = { token: crypto.randomUUID(), page: state.doc.pages[index].id, bounds: selectionBounds(items), x: 0, y: 0, cut };
|
||||||
compound: items.some(item => item.type === "CompositeObject" || item.type === "CompositeGraphicUnit") };
|
|
||||||
if (canvasEditor.nudge) {
|
if (canvasEditor.nudge) {
|
||||||
clipboard.bounds.x += canvasEditor.nudge.x;
|
clipboard.bounds.x += canvasEditor.nudge.x;
|
||||||
clipboard.bounds.y += canvasEditor.nudge.y;
|
clipboard.bounds.y += canvasEditor.nudge.y;
|
||||||
@@ -10491,14 +10496,14 @@ function copyEditorSelection(event) {
|
|||||||
state.objectClipboard = clipboard;
|
state.objectClipboard = clipboard;
|
||||||
const capture = () => {
|
const capture = () => {
|
||||||
const openSeq = state.openSeq, revision = state.editorInfo.revision;
|
const openSeq = state.openSeq, revision = state.editorInfo.revision;
|
||||||
return callWASM("ofdgoCaptureObjects", index, ids, clipboard.token, clipboard.scope).then(async () => {
|
return callTabWASM(tab, "ofdgoCaptureObjects", index, ids, clipboard.token).then(async () => {
|
||||||
clipboard.cut = false;
|
clipboard.cut = false;
|
||||||
if (cut && openSeq === state.openSeq && revision === state.editorInfo?.revision && index === state.pageIndex && state.objectClipboard === clipboard && clipboard.scope === (state.composite?.key || "")) {
|
if (cut && isForegroundDocument(tab) && openSeq === state.openSeq && revision === state.editorInfo?.revision && index === state.pageIndex && state.objectClipboard === clipboard) {
|
||||||
clipboard.cut = Boolean(await changeDocument("ofdgoDeleteObjects", { index, id: ids, items }));
|
clipboard.cut = Boolean(await changeDocument("ofdgoDeleteObjects", { index, id: ids, items }));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}, err => {
|
}, err => {
|
||||||
if (state.objectClipboard === clipboard) showError(err, false);
|
if (isForegroundDocument(tab) && state.objectClipboard === clipboard) showError(err, false);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -10519,13 +10524,14 @@ function visiblePageBounds(index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function pasteEditorContent(event) {
|
async function pasteEditorContent(event) {
|
||||||
|
const tab = documentTab(), state = tab.state;
|
||||||
if (!state.editing || event.target.closest?.("input, textarea, select, [contenteditable]") || formDialogOpen()) return;
|
if (!state.editing || event.target.closest?.("input, textarea, select, [contenteditable]") || formDialogOpen()) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const token = event.clipboardData.getData(OBJECT_CLIPBOARD_TYPE);
|
const token = event.clipboardData.getData(OBJECT_CLIPBOARD_TYPE);
|
||||||
const files = Array.from(event.clipboardData.files);
|
const files = Array.from(event.clipboardData.files);
|
||||||
const value = event.clipboardData.getData("text/plain").replace(/\t/g, " ");
|
const value = event.clipboardData.getData("text/plain").replace(/\t/g, " ");
|
||||||
if ((canvasEditor.nudge || canvasEditor.nudgeCommit) && !await canvasEditor.commitNudge()) return;
|
if ((canvasEditor.nudge || canvasEditor.nudgeCommit) && !await canvasEditor.commitNudge()) return;
|
||||||
if (document.body.hasAttribute("data-busy") || canvasEditor.input || canvasEditor.crop || canvasEditor.drag) return;
|
if (!isForegroundDocument(tab) || document.body.hasAttribute("data-busy") || canvasEditor.input || canvasEditor.crop || canvasEditor.drag) return;
|
||||||
const index = state.pageIndex, page = state.doc.pages[index];
|
const index = state.pageIndex, page = state.doc.pages[index];
|
||||||
if (token) {
|
if (token) {
|
||||||
const clipboard = state.objectClipboard;
|
const clipboard = state.objectClipboard;
|
||||||
@@ -10534,12 +10540,7 @@ async function pasteEditorContent(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
if (!await clipboard.ready || openSeq !== state.openSeq || index !== state.pageIndex || state.objectClipboard !== clipboard) return;
|
if (!await clipboard.ready || !isForegroundDocument(tab) || openSeq !== state.openSeq || index !== state.pageIndex || state.objectClipboard !== clipboard) return;
|
||||||
const scope = state.composite?.key || "";
|
|
||||||
if (clipboard.compound && (scope || clipboard.scope) && (scope !== clipboard.scope || page.id !== clipboard.page)) {
|
|
||||||
setStatus("复合对象请在原范围粘贴");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!pageCan("insert")) return;
|
if (!pageCan("insert")) return;
|
||||||
const visible = visiblePageBounds(index), bounds = clipboard.bounds;
|
const visible = visiblePageBounds(index), bounds = clipboard.bounds;
|
||||||
const offset = (clipboard.targetPage || clipboard.page) === page.id ? { x: clipboard.x + (clipboard.cut ? 0 : 3), y: clipboard.y + (clipboard.cut ? 0 : 3) } : { x: 0, y: 0 };
|
const offset = (clipboard.targetPage || clipboard.page) === page.id ? { x: clipboard.x + (clipboard.cut ? 0 : 3), y: clipboard.y + (clipboard.cut ? 0 : 3) } : { x: 0, y: 0 };
|
||||||
@@ -10550,7 +10551,7 @@ async function pasteEditorContent(event) {
|
|||||||
offset[axis] = Math.max(visible[axis] + margin, Math.min(position, visible[axis] + visible[size] - margin - bounds[size])) - bounds[axis];
|
offset[axis] = Math.max(visible[axis] + margin, Math.min(position, visible[axis] + visible[size] - margin - bounds[size])) - bounds[axis];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (await changeDocument("ofdgoPasteObjects", null, index, token, offset.x, offset.y, state.composite?.key || "")) {
|
if (await changeDocument("ofdgoPasteObjects", null, index, token, offset.x, offset.y)) {
|
||||||
clipboard.targetPage = page.id;
|
clipboard.targetPage = page.id;
|
||||||
clipboard.x = offset.x;
|
clipboard.x = offset.x;
|
||||||
clipboard.y = offset.y;
|
clipboard.y = offset.y;
|
||||||
@@ -12302,18 +12303,15 @@ function updateEditorTools() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateObjectControls(item, reset = false) {
|
function updateObjectControls(item, reset = false) {
|
||||||
el.compositeBackButton.hidden = !state.composite;
|
|
||||||
el.compositeBackButton.disabled = !state.ready || state.exporting;
|
|
||||||
el.selectObjectButton.hidden = Boolean(state.composite);
|
|
||||||
const members = item?.items || (item ? [item] : []);
|
const members = item?.items || (item ? [item] : []);
|
||||||
const disabled = !item || Boolean(item.draft) || !state.ready || state.exporting;
|
const disabled = !item || Boolean(item.draft) || !state.ready || state.exporting;
|
||||||
el.deleteObjectButton.disabled = disabled || !canEditObject(item, "delete");
|
el.deleteObjectButton.disabled = disabled || !canEditObject(item, "delete");
|
||||||
el.copyObjectButton.disabled = disabled || !canEditObject(item, "copy") || Boolean(state.composite && members.some(member => member.container !== members[0].container));
|
el.copyObjectButton.disabled = disabled || !canEditObject(item, "copy");
|
||||||
el.objectStyleButton.disabled = disabled || !canEditObject(item, "transform");
|
el.objectStyleButton.disabled = disabled || !canEditObject(item, "transform");
|
||||||
const cropping = Boolean(canvasEditor.crop);
|
const cropping = Boolean(canvasEditor.crop);
|
||||||
el.objectBoundsButton.disabled = disabled || cropping || !canEditObject(item, "transform");
|
el.objectBoundsButton.disabled = disabled || cropping || !canEditObject(item, "transform");
|
||||||
el.objectAlign.disabled = disabled || cropping || !canEditObject(item, "arrange");
|
el.objectAlign.disabled = disabled || cropping || !canEditObject(item, "arrange");
|
||||||
el.objectRotate.disabled = el.objectFlip.disabled = disabled || cropping || !canEditObject(item, state.composite ? "transform" : "arrange");
|
el.objectRotate.disabled = el.objectFlip.disabled = disabled || cropping || !canEditObject(item, members.some(member => member.scoped) ? "transform" : "arrange");
|
||||||
el.cropImageButton.disabled = disabled || item.type !== "ImageObject" || !canEditObject(item, "cropImage");
|
el.cropImageButton.disabled = disabled || item.type !== "ImageObject" || !canEditObject(item, "cropImage");
|
||||||
el.saveImageButton.disabled = disabled || !item.imageResource;
|
el.saveImageButton.disabled = disabled || !item.imageResource;
|
||||||
el.imageFit.disabled = el.cropImageButton.disabled || cropping || Boolean(item.scoped && !canEditObject(item, "fitImage"));
|
el.imageFit.disabled = el.cropImageButton.disabled || cropping || Boolean(item.scoped && !canEditObject(item, "fitImage"));
|
||||||
@@ -12323,9 +12321,9 @@ function updateObjectControls(item, reset = false) {
|
|||||||
el.resetCropButton.disabled = canEditObject(item, "resetCrop") ? disabled : el.cropImageButton.disabled || !cropping && (item.scoped ? !item.cropped : !item.imageBounds || ["x", "y", "width", "height"].every(key => Math.abs(item[key] - item.imageBounds[key]) < 1e-9));
|
el.resetCropButton.disabled = canEditObject(item, "resetCrop") ? disabled : el.cropImageButton.disabled || !cropping && (item.scoped ? !item.cropped : !item.imageBounds || ["x", "y", "width", "height"].every(key => Math.abs(item[key] - item.imageBounds[key]) < 1e-9));
|
||||||
el.objectDistribute.disabled = el.objectAlign.disabled || !item.items || item.items.length < 3;
|
el.objectDistribute.disabled = el.objectAlign.disabled || !item.items || item.items.length < 3;
|
||||||
el.editObjectButton.disabled = disabled || (item.items ? !item.items.every(member => member.type === "TextObject" && !member.richText)
|
el.editObjectButton.disabled = disabled || (item.items ? !item.items.every(member => member.type === "TextObject" && !member.richText)
|
||||||
: !canEditObject(item, "enter") && (item.type === "PathObject"
|
: !canEditObject(item, "children") && (item.type === "PathObject"
|
||||||
|| !(item.type === "TextObject" && canEditObject(item, "rewriteText")) && !canEditObject(item, item.type === "TextObject" ? "textContent" : item.type === "ImageObject" ? "replaceImage" : "update")));
|
|| !(item.type === "TextObject" && canEditObject(item, "rewriteText")) && !canEditObject(item, item.type === "TextObject" ? "textContent" : item.type === "ImageObject" ? "replaceImage" : "update")));
|
||||||
el.editObjectButton.textContent = canEditObject(item, "enter") ? "进入" : item?.type === "ImageObject" ? "替换" : "修改";
|
el.editObjectButton.textContent = item?.type === "ImageObject" ? "替换" : "修改";
|
||||||
el.multiSelectButton.disabled = !state.editing || !canvasEditor.enabled || !state.ready || state.exporting;
|
el.multiSelectButton.disabled = !state.editing || !canvasEditor.enabled || !state.ready || state.exporting;
|
||||||
const selection = selectedText(item);
|
const selection = selectedText(item);
|
||||||
const text = selection ? currentTextStyle() : state.textDefaults;
|
const text = selection ? currentTextStyle() : state.textDefaults;
|
||||||
|
|||||||
+26
-10
@@ -563,7 +563,7 @@ export class CanvasEditor {
|
|||||||
if (!event.altKey && this.enabled && item && !item.items && !this.input && !this.options.busy()
|
if (!event.altKey && this.enabled && item && !item.items && !this.input && !this.options.busy()
|
||||||
&& target && (!target.dataset.corner || item.type === "TextObject")
|
&& target && (!target.dataset.corner || item.type === "TextObject")
|
||||||
&& this.targetAt({ target, clientX: event.clientX, clientY: event.clientY }) === item) {
|
&& this.targetAt({ target, clientX: event.clientX, clientY: event.clientY }) === item) {
|
||||||
this.options.onEdit(item);
|
this.options.onEdit(item, canEditObject(item, "children") ? pagePoint(event.clientX, event.clientY, item.surface.getBoundingClientRect(), item.page, this.options.rotation()) : null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
viewer.addEventListener("scroll", () => this.scroll());
|
viewer.addEventListener("scroll", () => this.scroll());
|
||||||
@@ -607,14 +607,15 @@ export class CanvasEditor {
|
|||||||
const layer = document.createElement("div");
|
const layer = document.createElement("div");
|
||||||
layer.className = "edit-layer";
|
layer.className = "edit-layer";
|
||||||
const artwork = new Map([...surface.querySelectorAll("[data-ofd-object], [data-ofd-child]")].map((node, order) => [node.getAttribute("data-ofd-object") || node.getAttribute("data-ofd-child"), { node, order }]));
|
const artwork = new Map([...surface.querySelectorAll("[data-ofd-object], [data-ofd-child]")].map((node, order) => [node.getAttribute("data-ofd-object") || node.getAttribute("data-ofd-child"), { node, order }]));
|
||||||
const objects = [...page.objects].sort((a, b) => (artwork.get(a.id)?.order ?? 0) - (artwork.get(b.id)?.order ?? 0));
|
const paintOrder = object => artwork.get(object.id)?.order ?? artwork.get(object.id.split("/")[0])?.order ?? 0;
|
||||||
for (const object of objects) {
|
const objects = [...page.objects].sort((a, b) => paintOrder(a) - paintOrder(b));
|
||||||
|
for (const [order, object] of objects.entries()) {
|
||||||
const node = document.createElement("div");
|
const node = document.createElement("div");
|
||||||
node.className = "edit-object";
|
node.className = "edit-object";
|
||||||
node.classList.toggle("read-only", !canEditObject(object, "move"));
|
node.classList.toggle("read-only", !canEditObject(object, "move"));
|
||||||
node.title = objectEditReason(object);
|
node.title = objectEditReason(object);
|
||||||
node.tabIndex = 0;
|
node.tabIndex = 0;
|
||||||
const item = { ...object, index, page: { width: page.width, height: page.height }, node, surface };
|
const item = { ...object, order, index, page: { width: page.width, height: page.height }, node, surface };
|
||||||
item.artwork = artwork.get(object.id)?.node;
|
item.artwork = artwork.get(object.id)?.node;
|
||||||
if (object.note) {
|
if (object.note) {
|
||||||
const icon = document.createElement("span");
|
const icon = document.createElement("span");
|
||||||
@@ -760,6 +761,7 @@ export class CanvasEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSelection(items) {
|
setSelection(items) {
|
||||||
|
items = items.filter(item => !items.some(parent => parent !== item && item.id.startsWith(parent.id + "/")));
|
||||||
this.cancelNudge();
|
this.cancelNudge();
|
||||||
const previous = this.selected;
|
const previous = this.selected;
|
||||||
if (this.crop && (items.length !== 1 || items[0] !== this.crop.item)) this.closeCrop();
|
if (this.crop && (items.length !== 1 || items[0] !== this.crop.item)) this.closeCrop();
|
||||||
@@ -805,6 +807,10 @@ export class CanvasEditor {
|
|||||||
item.node.classList.toggle("multi-selected", items.length > 1);
|
item.node.classList.toggle("multi-selected", items.length > 1);
|
||||||
item.node.setAttribute("data-pressed", "true");
|
item.node.setAttribute("data-pressed", "true");
|
||||||
}
|
}
|
||||||
|
for (const node of this.viewer.querySelectorAll(".edit-parent")) node.classList.remove("edit-parent");
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.parent) this.pageItems(item.index).find(parent => parent.id === item.parent)?.node.classList.add("edit-parent");
|
||||||
|
}
|
||||||
this.options.onSelect(this.selected, previous);
|
this.options.onSelect(this.selected, previous);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -817,6 +823,16 @@ export class CanvasEditor {
|
|||||||
this.setSelection(items.includes(item) ? items.filter(member => member !== item) : [...items, item]);
|
this.setSelection(items.includes(item) ? items.filter(member => member !== item) : [...items, item]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectParent() {
|
||||||
|
const item = this.selected;
|
||||||
|
if (!item?.parent) return false;
|
||||||
|
const parent = this.pageItems(item.index).find(parent => parent.id === item.parent);
|
||||||
|
if (!parent) return false;
|
||||||
|
this.select(parent);
|
||||||
|
this.focus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.closeText();
|
this.closeText();
|
||||||
this.closeCrop();
|
this.closeCrop();
|
||||||
@@ -902,7 +918,7 @@ export class CanvasEditor {
|
|||||||
if (!this.drag.corner) {
|
if (!this.drag.corner) {
|
||||||
const selected = this.items();
|
const selected = this.items();
|
||||||
this.drag.targets = [...item.surface.querySelectorAll(".edit-object")]
|
this.drag.targets = [...item.surface.querySelectorAll(".edit-object")]
|
||||||
.map(node => this.nodes.get(node)).filter(member => !selected.includes(member));
|
.map(node => this.nodes.get(node)).filter(member => !selected.some(item => item === member || item.id.startsWith(member.id + "/") || member.id.startsWith(item.id + "/")));
|
||||||
this.drag.targets.push({ x: 0, y: 0, ...item.page });
|
this.drag.targets.push({ x: 0, y: 0, ...item.page });
|
||||||
}
|
}
|
||||||
if (lineShape(item.shape) && this.drag.corner) {
|
if (lineShape(item.shape) && this.drag.corner) {
|
||||||
@@ -930,7 +946,7 @@ export class CanvasEditor {
|
|||||||
event = { target, clientX: event.clientX, clientY: event.clientY };
|
event = { target, clientX: event.clientX, clientY: event.clientY };
|
||||||
}
|
}
|
||||||
const direct = this.nodes.get(event.target.closest(".edit-object, .edit-selection"));
|
const direct = this.nodes.get(event.target.closest(".edit-object, .edit-selection"));
|
||||||
return event.target.dataset.corner || direct && !direct.contours ? direct : this.itemsAt(event)[0];
|
return event.target.dataset.corner || direct && !direct.contours && (!direct.parent || this.items().some(selected => selected.id.startsWith(direct.parent + "/"))) ? direct : this.itemsAt(event)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
itemsAt(event) {
|
itemsAt(event) {
|
||||||
@@ -940,7 +956,7 @@ export class CanvasEditor {
|
|||||||
const rect = surface.getBoundingClientRect(), rotation = this.options.rotation();
|
const rect = surface.getBoundingClientRect(), rotation = this.options.rotation();
|
||||||
const point = pagePoint(event.clientX, event.clientY, rect, page, rotation);
|
const point = pagePoint(event.clientX, event.clientY, rect, page, rotation);
|
||||||
const tolerance = 3 * page.width / (rotation % 180 ? rect.height : rect.width);
|
const tolerance = 3 * page.width / (rotation % 180 ? rect.height : rect.width);
|
||||||
return [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node)).reverse().filter(item => item.type === "TextObject"
|
return [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node)).reverse().filter(item => !item.parent || this.items().some(selected => selected.id.startsWith(item.parent + "/"))).filter(item => item.type === "TextObject"
|
||||||
? point.x >= item.x - tolerance && point.x <= item.x + item.width + tolerance
|
? point.x >= item.x - tolerance && point.x <= item.x + item.width + tolerance
|
||||||
&& point.y >= item.y - tolerance && point.y <= item.y + item.height + tolerance
|
&& point.y >= item.y - tolerance && point.y <= item.y + item.height + tolerance
|
||||||
: this.contains(item, point, tolerance));
|
: this.contains(item, point, tolerance));
|
||||||
@@ -1256,7 +1272,7 @@ export class CanvasEditor {
|
|||||||
if (!page) return;
|
if (!page) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.focus();
|
this.focus();
|
||||||
this.drag = { erase: this.tool, page, surface, items: [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node)),
|
this.drag = { erase: this.tool, page, surface, items: [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node)).filter(item => !item.parent),
|
||||||
erased: new Set(), shape: "rectangle", pointerID: event.pointerId, clientX: event.clientX, clientY: event.clientY,
|
erased: new Set(), shape: "rectangle", pointerID: event.pointerId, clientX: event.clientX, clientY: event.clientY,
|
||||||
rect: surface.getBoundingClientRect(), rotation: this.options.rotation() };
|
rect: surface.getBoundingClientRect(), rotation: this.options.rotation() };
|
||||||
if (this.tool === "erase-region") Object.assign(this.drag, this.createPreview(surface, page, "rectangle",
|
if (this.tool === "erase-region") Object.assign(this.drag, this.createPreview(surface, page, "rectangle",
|
||||||
@@ -1315,7 +1331,7 @@ export class CanvasEditor {
|
|||||||
const items = [...drag.base];
|
const items = [...drag.base];
|
||||||
for (const node of drag.surface.querySelectorAll(".edit-object")) {
|
for (const node of drag.surface.querySelectorAll(".edit-object")) {
|
||||||
const item = this.nodes.get(node);
|
const item = this.nodes.get(node);
|
||||||
if (!items.includes(item) && item.x >= box.x && item.x+item.width <= box.x+box.width && item.y >= box.y && item.y+item.height <= box.y+box.height) {
|
if (!item.parent && !items.includes(item) && item.x >= box.x && item.x+item.width <= box.x+box.width && item.y >= box.y && item.y+item.height <= box.y+box.height) {
|
||||||
items.push(item);
|
items.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1458,7 +1474,7 @@ export class CanvasEditor {
|
|||||||
this.setTool("");
|
this.setTool("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === "Escape" && !this.input && this.enabled && !this.options.busy() && !this.nudge && this.options.onExitScope?.()) {
|
if (event.key === "Escape" && !this.input && this.enabled && !this.options.busy() && !this.nudge && this.selectParent()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -417,10 +417,10 @@ export function createPageView(context) {
|
|||||||
task.resolve(null);
|
task.resolve(null);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const scope = state.composite;
|
if (state.editing) await context.loadEditorMembers(tab, page, task.index);
|
||||||
if (scope?.index === task.index) {
|
if (context.activeTab !== tab || state.pageInFlight.get(task.key) !== task || task.openSeq !== state.openSeq) {
|
||||||
const objects = await context.callWASM("ofdgoCompositeObjects", task.index, scope.key);
|
task.resolve(null);
|
||||||
if (scope === state.composite && task.openSeq === state.openSeq) scope.objects = objects;
|
continue;
|
||||||
}
|
}
|
||||||
await pageView.loadSVGFonts(page.fonts, task.openSeq);
|
await pageView.loadSVGFonts(page.fonts, task.openSeq);
|
||||||
for (const font of page.fonts) delete font.data;
|
for (const font of page.fonts) delete font.data;
|
||||||
@@ -817,7 +817,6 @@ export function createPageView(context) {
|
|||||||
|
|
||||||
setCurrentPage(index) {
|
setCurrentPage(index) {
|
||||||
if (index !== context.state.pageIndex && !context.state.openActionTask?.navigating) context.state.openActionTask = null;
|
if (index !== context.state.pageIndex && !context.state.openActionTask?.navigating) context.state.openActionTask = null;
|
||||||
if (context.state.composite && context.state.composite.index !== index) context.resetCompositeScope();
|
|
||||||
context.state.pageIndex = index;
|
context.state.pageIndex = index;
|
||||||
pageView.prunePageRenderQueue();
|
pageView.prunePageRenderQueue();
|
||||||
if (context.state.editing) context.state.openActionPage = context.state.doc?.pages[index]?.id;
|
if (context.state.editing) context.state.openActionPage = context.state.doc?.pages[index]?.id;
|
||||||
|
|||||||
+107
-116
@@ -359,7 +359,6 @@ type apiResult struct {
|
|||||||
type editorClipboard struct {
|
type editorClipboard struct {
|
||||||
token string
|
token string
|
||||||
selection *ofdgo.ObjectSelection
|
selection *ofdgo.ObjectSelection
|
||||||
composite *ofdgo.CompositeSelection
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorInfo 编辑文档信息与操作状态
|
// editorInfo 编辑文档信息与操作状态
|
||||||
@@ -3430,9 +3429,9 @@ func (e *businessEngine) editorObjects(index int, text *ofdgo.PageText) ([]any,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
box, contours, err := e.currentSession.Renderer.AnnotationGeometry(annotation)
|
box, contours, err := e.currentSession.Renderer.AnnotationGeometry(annotation)
|
||||||
capabilities := map[string]any{"update": true, "transform": true, "arrange": true, "copy": true, "delete": true, "enter": len(annotation.Appearance.Objects) != 0}
|
capabilities := map[string]any{"update": true, "transform": true, "arrange": true, "copy": true, "delete": true, "children": len(annotation.Appearance.Objects) != 0}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
capabilities["transform"], capabilities["arrange"], capabilities["enter"] = false, false, false
|
capabilities["transform"], capabilities["arrange"], capabilities["children"] = false, false, false
|
||||||
capabilities["reason"], capabilities["reasonCode"] = err.Error(), "invalidObject"
|
capabilities["reason"], capabilities["reasonCode"] = err.Error(), "invalidObject"
|
||||||
}
|
}
|
||||||
if box.W <= 0 || box.H <= 0 {
|
if box.W <= 0 || box.H <= 0 {
|
||||||
@@ -3711,19 +3710,22 @@ func (e *businessEngine) splitEditorSelection(ids []string) ([]string, []string)
|
|||||||
return objects, annotations
|
return objects, annotations
|
||||||
}
|
}
|
||||||
|
|
||||||
// changeSelection 原子处理正文和注解混选,保持绘制顺序及统一历史
|
// changeSelection 原子处理正文、注解及内部对象混选,保持统一历史
|
||||||
// 入参: args 页面索引、标识列表、操作与参数
|
// 入参: args 页面索引、标识列表、操作与参数
|
||||||
// 返回: any 文档与新选区, error 错误信息
|
// 返回: any 文档与新选区, error 错误信息
|
||||||
func (e *businessEngine) changeSelection(args []platformValue) (any, error) {
|
func (e *businessEngine) changeSelection(args []platformValue) (any, error) {
|
||||||
var selected []string
|
var selected []string
|
||||||
result, err := e.changeAtomicObjects(func() error {
|
result, err := e.changeAtomicObjects(func() error {
|
||||||
page, operation := args[0].Int(), args[2].String()
|
page, operation := args[0].Int(), args[2].String()
|
||||||
objects, annotations := e.splitEditorSelection(e.stringsFromJS(args[1]))
|
paths, err := e.editorTargetPaths(e.stringsFromJS(args[1]))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
switch operation {
|
switch operation {
|
||||||
case "align", "distribute":
|
case "align", "distribute":
|
||||||
return e.arrangeEditorSelection(page, e.stringsFromJS(args[1]), operation, args[3].String())
|
return e.arrangeEditorSelection(page, e.stringsFromJS(args[1]), operation, args[3].String())
|
||||||
case "delete":
|
case "delete":
|
||||||
return e.currentEditor.DeleteSelection(page, objects, annotations)
|
return e.currentEditor.DeleteTargets(page, paths)
|
||||||
case "copy":
|
case "copy":
|
||||||
clipboard, err := e.captureEditorSelection(page, e.stringsFromJS(args[1]))
|
clipboard, err := e.captureEditorSelection(page, e.stringsFromJS(args[1]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3731,20 +3733,31 @@ func (e *businessEngine) changeSelection(args []platformValue) (any, error) {
|
|||||||
}
|
}
|
||||||
selected, err = e.pasteEditorSelection(page, clipboard, args[3].Float(), args[4].Float())
|
selected, err = e.pasteEditorSelection(page, clipboard, args[3].Float(), args[4].Float())
|
||||||
return err
|
return err
|
||||||
|
case "pasteStyle":
|
||||||
|
if e.copiedStyle == nil {
|
||||||
|
return fmt.Errorf("style clipboard is empty")
|
||||||
|
}
|
||||||
|
return e.currentEditor.CopyTargetsStyle(page, paths, *e.copiedStyle)
|
||||||
|
case "paint", "textColor":
|
||||||
|
style, err := e.editorPaintStyle(operation, args[3:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.currentEditor.StyleTargets(page, paths, style)
|
||||||
case "style":
|
case "style":
|
||||||
return e.currentEditor.StyleSelection(page, objects, annotations, e.objectStyle(args[3]))
|
return e.currentEditor.StyleTargets(page, paths, e.objectStyle(args[3]))
|
||||||
case "erase":
|
case "erase":
|
||||||
return e.currentEditor.EraseSelection(page, objects, annotations, ofdgo.Box{
|
return e.currentEditor.EraseTargets(page, paths, ofdgo.Box{
|
||||||
X: args[3].Float(), Y: args[4].Float(), W: args[5].Float(), H: args[6].Float(),
|
X: args[3].Float(), Y: args[4].Float(), W: args[5].Float(), H: args[6].Float(),
|
||||||
})
|
})
|
||||||
case "erasePath":
|
case "erasePath":
|
||||||
return e.currentEditor.EraseSelectionPath(page, objects, annotations, e.pointsFromJS(args[3]))
|
return e.currentEditor.EraseTargetsPath(page, paths, e.pointsFromJS(args[3]))
|
||||||
case "rotate":
|
case "rotate":
|
||||||
return e.currentEditor.RotateSelection(page, objects, annotations, args[3].Int())
|
return e.currentEditor.RotateTargets(page, paths, args[3].Int())
|
||||||
case "flip":
|
case "flip":
|
||||||
return e.currentEditor.FlipSelection(page, objects, annotations, args[3].String())
|
return e.currentEditor.FlipTargets(page, paths, args[3].String())
|
||||||
case "resize":
|
case "resize":
|
||||||
return e.currentEditor.ResizeSelection(page, objects, annotations, ofdgo.Box{
|
return e.currentEditor.ResizeTargets(page, paths, ofdgo.Box{
|
||||||
X: args[3].Float(), Y: args[4].Float(), W: args[5].Float(), H: args[6].Float(),
|
X: args[3].Float(), Y: args[4].Float(), W: args[5].Float(), H: args[6].Float(),
|
||||||
})
|
})
|
||||||
case "transform":
|
case "transform":
|
||||||
@@ -3753,7 +3766,7 @@ func (e *businessEngine) changeSelection(args []platformValue) (any, error) {
|
|||||||
return fmt.Errorf("scale must be positive")
|
return fmt.Errorf("scale must be positive")
|
||||||
}
|
}
|
||||||
matrix := ofdgo.NewMatrix(fmt.Sprintf("%g 0 0 %g %g %g", scale, scale, dx, dy))
|
matrix := ofdgo.NewMatrix(fmt.Sprintf("%g 0 0 %g %g %g", scale, scale, dx, dy))
|
||||||
return e.currentEditor.TransformSelection(page, objects, annotations, matrix)
|
return e.currentEditor.TransformTargets(page, paths, matrix)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported mixed selection operation %q", operation)
|
return fmt.Errorf("unsupported mixed selection operation %q", operation)
|
||||||
}
|
}
|
||||||
@@ -3764,30 +3777,42 @@ func (e *businessEngine) changeSelection(args []platformValue) (any, error) {
|
|||||||
return editorSelectionInfo{editorInfo: result.(editorInfo), SelectedIDs: selected}, nil
|
return editorSelectionInfo{editorInfo: result.(editorInfo), SelectedIDs: selected}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// arrangeEditorSelection 对正文和注解统一对齐或分布,不改变各自绘制层级
|
// arrangeEditorSelection 对跨容器选区统一对齐或分布,不改变各自绘制层级
|
||||||
// 入参: page 页面, ids 标识, operation 操作, direction 方向
|
// 入参: page 页面, ids 标识, operation 操作, direction 方向
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *businessEngine) arrangeEditorSelection(page int, ids []string, operation, direction string) error {
|
func (e *businessEngine) arrangeEditorSelection(page int, ids []string, operation, direction string) error {
|
||||||
objects, annotations := e.splitEditorSelection(ids)
|
paths, err := e.editorTargetPaths(ids)
|
||||||
switch operation {
|
if err != nil {
|
||||||
case "align":
|
return err
|
||||||
return e.currentEditor.AlignSelection(page, objects, annotations, direction)
|
|
||||||
case "distribute":
|
|
||||||
return e.currentEditor.DistributeSelection(page, objects, annotations, direction)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid arrangement operation %q", operation)
|
|
||||||
}
|
}
|
||||||
|
return e.currentEditor.ArrangeTargets(page, paths, operation == "distribute", direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
// captureEditorSelection 捕获正文与注解的独立副本
|
// editorTargetPaths 将画布标识解析为完整对象路径
|
||||||
|
// 入参: ids 画布标识
|
||||||
|
// 返回: []ofdgo.ObjectPath 对象路径, error 路径错误
|
||||||
|
func (e *businessEngine) editorTargetPaths(ids []string) ([]ofdgo.ObjectPath, error) {
|
||||||
|
paths := make([]ofdgo.ObjectPath, len(ids))
|
||||||
|
for i, id := range ids {
|
||||||
|
path, err := e.compositePath(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
paths[i] = path
|
||||||
|
}
|
||||||
|
return paths, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// captureEditorSelection 捕获正文、注解及内部对象的独立副本
|
||||||
// 入参: page 页面索引, ids 画布标识
|
// 入参: page 页面索引, ids 画布标识
|
||||||
// 返回: *editorClipboard 快照, error 错误信息
|
// 返回: *editorClipboard 快照, error 错误信息
|
||||||
func (e *businessEngine) captureEditorSelection(page int, ids []string) (*editorClipboard, error) {
|
func (e *businessEngine) captureEditorSelection(page int, ids []string) (*editorClipboard, error) {
|
||||||
objects, annotations := e.splitEditorSelection(ids)
|
paths, err := e.editorTargetPaths(ids)
|
||||||
result := &editorClipboard{}
|
if err != nil {
|
||||||
var err error
|
return nil, err
|
||||||
result.selection, err = e.currentEditor.CaptureSelection(page, objects, annotations)
|
}
|
||||||
return result, err
|
selection, err := e.currentEditor.CaptureTargets(page, paths)
|
||||||
|
return &editorClipboard{selection: selection}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// pasteEditorSelection 原子粘贴正文和注解,共享字体与图片数据
|
// pasteEditorSelection 原子粘贴正文和注解,共享字体与图片数据
|
||||||
@@ -3806,7 +3831,7 @@ func (e *businessEngine) pasteEditorSelection(page int, clipboard *editorClipboa
|
|||||||
// 返回: map[string]any 前端能力
|
// 返回: map[string]any 前端能力
|
||||||
func (e *businessEngine) editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) map[string]any {
|
func (e *businessEngine) editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) map[string]any {
|
||||||
result := map[string]any{"update": capability.Update, "paint": capability.Paint, "replaceFont": capability.ReplaceFont, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason, "reasonCode": string(capability.ReasonCode),
|
result := map[string]any{"update": capability.Update, "paint": capability.Paint, "replaceFont": capability.ReplaceFont, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason, "reasonCode": string(capability.ReasonCode),
|
||||||
"replaceImage": capability.ReplaceImage, "cropImage": capability.CropImage, "fitImage": capability.FitImage, "resetCrop": capability.ResetCrop, "ungroup": capability.Ungroup, "stretch": capability.Stretch, "enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
|
"replaceImage": capability.ReplaceImage, "cropImage": capability.CropImage, "fitImage": capability.FitImage, "resetCrop": capability.ResetCrop, "ungroup": capability.Ungroup, "stretch": capability.Stretch, "children": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
|
||||||
result["textContent"] = capability.TextContent
|
result["textContent"] = capability.TextContent
|
||||||
result["rewriteText"] = capability.RewriteText
|
result["rewriteText"] = capability.RewriteText
|
||||||
if missing := capability.MissingGlyphs; missing != nil {
|
if missing := capability.MissingGlyphs; missing != nil {
|
||||||
@@ -3963,7 +3988,7 @@ func (e *businessEngine) compositeObjects(args []platformValue) (any, error) {
|
|||||||
if box.W <= 0 || box.H <= 0 {
|
if box.W <= 0 || box.H <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
item := map[string]any{"id": fmt.Sprintf("%s/%d", key, i), "type": member.Object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": i, "position": member.Position.Index, "count": member.Position.Count, "container": key + ":" + member.Position.Container, "scoped": true, "contours": member.Contours,
|
item := map[string]any{"id": fmt.Sprintf("%s/%d", key, i), "type": member.Object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": i, "position": member.Position.Index, "count": member.Position.Count, "container": key + ":" + member.Position.Container, "scoped": true, "parent": key, "contours": member.Contours,
|
||||||
"capabilities": e.editorCapabilities(member.Capabilities, member.Object.Type)}
|
"capabilities": e.editorCapabilities(member.Capabilities, member.Object.Type)}
|
||||||
e.editorAppearance(item, member.Object, member.Capabilities.Paint, member.StrokeScale)
|
e.editorAppearance(item, member.Object, member.Capabilities.Paint, member.StrokeScale)
|
||||||
if member.Object.Type == "ImageObject" {
|
if member.Object.Type == "ImageObject" {
|
||||||
@@ -4128,35 +4153,9 @@ func (e *businessEngine) changeCompositeObjects(args []platformValue) (any, erro
|
|||||||
case "style":
|
case "style":
|
||||||
return e.currentEditor.StyleCompositeObjects(page, path, indexes, e.objectStyle(args[4]))
|
return e.currentEditor.StyleCompositeObjects(page, path, indexes, e.objectStyle(args[4]))
|
||||||
case "paint", "textColor":
|
case "paint", "textColor":
|
||||||
style := ofdgo.ObjectStyle{}
|
style, err := e.editorPaintStyle(operation, args[4:])
|
||||||
if operation == "textColor" {
|
if err != nil {
|
||||||
style.FillColor = &ofdgo.FillColor{}
|
return err
|
||||||
if err := e.setEditorColor(style.FillColor, args[4].String()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !args[4].IsNull() {
|
|
||||||
value := args[4].Bool()
|
|
||||||
style.Fill = &value
|
|
||||||
}
|
|
||||||
if !args[6].IsNull() {
|
|
||||||
value := args[6].Bool()
|
|
||||||
style.Stroke = &value
|
|
||||||
}
|
|
||||||
var colors [2]*ofdgo.FillColor
|
|
||||||
for i := range colors {
|
|
||||||
if !args[5+i*2].IsNull() {
|
|
||||||
colors[i] = &ofdgo.FillColor{}
|
|
||||||
if err := e.setEditorColor(colors[i], args[5+i*2].String()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
style.FillColor, style.StrokeColor = colors[0], (*ofdgo.StrokeColor)(colors[1])
|
|
||||||
if !args[8].IsNull() {
|
|
||||||
value := args[8].Float()
|
|
||||||
style.LineWidth = &value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return e.currentEditor.StyleCompositeObjects(page, path, indexes, style)
|
return e.currentEditor.StyleCompositeObjects(page, path, indexes, style)
|
||||||
default:
|
default:
|
||||||
@@ -4176,6 +4175,43 @@ func (e *businessEngine) changeCompositeObjects(args []platformValue) (any, erro
|
|||||||
return doc, nil
|
return doc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editorPaintStyle 解析绘制颜色与描边增量,供顶层和内部选区共用
|
||||||
|
// 入参: operation 操作, args 绘制参数
|
||||||
|
// 返回: ofdgo.ObjectStyle 样式增量, error 颜色错误
|
||||||
|
func (e *businessEngine) editorPaintStyle(operation string, args []platformValue) (ofdgo.ObjectStyle, error) {
|
||||||
|
style := ofdgo.ObjectStyle{}
|
||||||
|
if operation == "textColor" {
|
||||||
|
style.FillColor = &ofdgo.FillColor{}
|
||||||
|
if err := e.setEditorColor(style.FillColor, args[0].String()); err != nil {
|
||||||
|
return style, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !args[0].IsNull() {
|
||||||
|
value := args[0].Bool()
|
||||||
|
style.Fill = &value
|
||||||
|
}
|
||||||
|
if !args[2].IsNull() {
|
||||||
|
value := args[2].Bool()
|
||||||
|
style.Stroke = &value
|
||||||
|
}
|
||||||
|
var colors [2]*ofdgo.FillColor
|
||||||
|
for i := range colors {
|
||||||
|
if !args[1+i*2].IsNull() {
|
||||||
|
colors[i] = &ofdgo.FillColor{}
|
||||||
|
if err := e.setEditorColor(colors[i], args[1+i*2].String()); err != nil {
|
||||||
|
return style, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
style.FillColor, style.StrokeColor = colors[0], (*ofdgo.StrokeColor)(colors[1])
|
||||||
|
if !args[4].IsNull() {
|
||||||
|
value := args[4].Float()
|
||||||
|
style.LineWidth = &value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return style, nil
|
||||||
|
}
|
||||||
|
|
||||||
// compositeIndexes 将画布成员路径转换为同一范围的序号
|
// compositeIndexes 将画布成员路径转换为同一范围的序号
|
||||||
// 入参: key 父路径, ids 成员路径
|
// 入参: key 父路径, ids 成员路径
|
||||||
// 返回: []int 成员序号, error 错误信息
|
// 返回: []int 成员序号, error 错误信息
|
||||||
@@ -4849,7 +4885,7 @@ func (e *businessEngine) styleText(args []platformValue) (any, error) {
|
|||||||
}
|
}
|
||||||
var value strings.Builder
|
var value strings.Builder
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
object, err := e.currentEditor.Object(page, id)
|
object, err := e.editorObjectAt(page, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -4873,7 +4909,11 @@ func (e *businessEngine) styleText(args []platformValue) (any, error) {
|
|||||||
}
|
}
|
||||||
style.Color = color.Value
|
style.Color = color.Value
|
||||||
}
|
}
|
||||||
return e.currentEditor.StyleText(page, ids, style)
|
paths, err := e.editorTargetPaths(ids)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.currentEditor.StyleTargetsText(page, paths, style)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4911,7 +4951,7 @@ func (e *businessEngine) editorRichText(item map[string]any, object ofdgo.Graphi
|
|||||||
for _, key := range []string{"textContent", "rewriteText", "replaceFont", "reflow", "layoutKnown", "paint"} {
|
for _, key := range []string{"textContent", "rewriteText", "replaceFont", "reflow", "layoutKnown", "paint"} {
|
||||||
capabilities[key] = true
|
capabilities[key] = true
|
||||||
}
|
}
|
||||||
capabilities["enter"], capabilities["ungroup"] = false, false
|
capabilities["children"], capabilities["ungroup"] = false, false
|
||||||
capabilities["stretch"] = false
|
capabilities["stretch"] = false
|
||||||
if capabilities["reasonCode"] == string(ofdgo.EditUnsupportedObject) && capabilities["transform"] == true && capabilities["copy"] == true && object.CompositeGraphicUnit.Clips == nil {
|
if capabilities["reasonCode"] == string(ofdgo.EditUnsupportedObject) && capabilities["transform"] == true && capabilities["copy"] == true && object.CompositeGraphicUnit.Clips == nil {
|
||||||
delete(capabilities, "reason")
|
delete(capabilities, "reason")
|
||||||
@@ -5353,78 +5393,29 @@ func (e *businessEngine) copyObjects(args []platformValue) (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// captureObjects 保存独立选区快照,不修改文档或历史
|
// captureObjects 保存独立选区快照,不修改文档或历史
|
||||||
// 入参: args 页面索引、对象标识数组、剪贴板标识和可选父路径
|
// 入参: args 页面索引、对象路径数组和剪贴板标识
|
||||||
// 返回: any 空结果, error 错误信息
|
// 返回: any 空结果, error 错误信息
|
||||||
func (e *businessEngine) captureObjects(args []platformValue) (any, error) {
|
func (e *businessEngine) captureObjects(args []platformValue) (any, error) {
|
||||||
if e.currentEditor == nil {
|
if e.currentEditor == nil {
|
||||||
return nil, fmt.Errorf("no document is being edited")
|
return nil, fmt.Errorf("no document is being edited")
|
||||||
}
|
}
|
||||||
clipboard := &editorClipboard{token: args[2].String()}
|
clipboard, err := e.captureEditorSelection(args[0].Int(), e.stringsFromJS(args[1]))
|
||||||
var err error
|
|
||||||
if len(args) > 3 && args[3].String() != "" {
|
|
||||||
key := args[3].String()
|
|
||||||
path, pathErr := e.compositePath(key)
|
|
||||||
if pathErr != nil {
|
|
||||||
return nil, pathErr
|
|
||||||
}
|
|
||||||
indexes, indexErr := e.compositeIndexes(key, e.stringsFromJS(args[1]))
|
|
||||||
if indexErr != nil {
|
|
||||||
return nil, indexErr
|
|
||||||
}
|
|
||||||
clipboard.composite, err = e.currentEditor.CaptureCompositeObjects(args[0].Int(), path, indexes)
|
|
||||||
} else {
|
|
||||||
clipboard, err = e.captureEditorSelection(args[0].Int(), e.stringsFromJS(args[1]))
|
|
||||||
clipboard.token = args[2].String()
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
clipboard.token = args[2].String()
|
||||||
e.copiedObjects = clipboard
|
e.copiedObjects = clipboard
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pasteObjects 将当前文档的对象快照粘贴到目标页
|
// pasteObjects 将当前文档的对象快照粘贴到目标页
|
||||||
// 入参: args 目标页、剪贴板标识、横纵位移和可选父路径
|
// 入参: args 目标页、剪贴板标识和横纵位移
|
||||||
// 返回: any 文档信息及新选区, error 错误信息
|
// 返回: any 文档信息及新选区, error 错误信息
|
||||||
func (e *businessEngine) pasteObjects(args []platformValue) (any, error) {
|
func (e *businessEngine) pasteObjects(args []platformValue) (any, error) {
|
||||||
if e.currentEditor == nil || e.copiedObjects == nil || e.copiedObjects.token != args[1].String() {
|
if e.currentEditor == nil || e.copiedObjects == nil || e.copiedObjects.token != args[1].String() {
|
||||||
return nil, fmt.Errorf("object clipboard is no longer available")
|
return nil, fmt.Errorf("object clipboard is no longer available")
|
||||||
}
|
}
|
||||||
key := ""
|
ids, err := e.pasteEditorSelection(args[0].Int(), e.copiedObjects, args[2].Float(), args[3].Float())
|
||||||
if len(args) > 4 {
|
|
||||||
key = args[4].String()
|
|
||||||
}
|
|
||||||
page, dx, dy := args[0].Int(), args[2].Float(), args[3].Float()
|
|
||||||
if key != "" {
|
|
||||||
path, err := e.compositePath(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var indexes []int
|
|
||||||
if e.copiedObjects.composite != nil {
|
|
||||||
indexes, err = e.currentEditor.PasteCompositeObjects(page, path, e.copiedObjects.composite, dx, dy)
|
|
||||||
} else {
|
|
||||||
indexes, err = e.currentEditor.PasteSelectionToComposite(page, path, e.copiedObjects.selection, dx, dy)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
info, err := e.previewEditor(e.currentEditor, e.currentSession.Renderer.RenderAnnotations)
|
|
||||||
result := editorSelectionInfo{editorInfo: info}
|
|
||||||
for _, index := range indexes {
|
|
||||||
result.SelectedIDs = append(result.SelectedIDs, fmt.Sprintf("%s/%d", key, index))
|
|
||||||
}
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
if e.copiedObjects.composite != nil {
|
|
||||||
ids, err := e.currentEditor.PasteCompositeSelection(page, e.copiedObjects.composite, dx, dy)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
info, err := e.previewEditor(e.currentEditor, e.currentSession.Renderer.RenderAnnotations)
|
|
||||||
return editorSelectionInfo{info, ids}, err
|
|
||||||
}
|
|
||||||
ids, err := e.pasteEditorSelection(page, e.copiedObjects, dx, dy)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+454
-2
@@ -15,18 +15,26 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"slices"
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ObjectSelection 保存正文与注解的独立快照,共享当前编辑器的不可变资源
|
// ObjectSelection 保存正文、注解及组合成员的独立快照,共享当前编辑器的不可变资源
|
||||||
// 来源修改、删除或撤销不改变快照,不用于跨文档传输
|
// 来源修改、删除或撤销不改变快照,不用于跨文档传输
|
||||||
type ObjectSelection struct {
|
type ObjectSelection struct {
|
||||||
editor *Editor
|
editor *Editor
|
||||||
document editorDocumentIdentity
|
document editorDocumentIdentity
|
||||||
objects []GraphicObject
|
objects []GraphicObject
|
||||||
annotations *AnnotationSelection
|
annotations *AnnotationSelection
|
||||||
|
parts []editorSelectionPart
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorSelectionPart 保存按绘制顺序排列的顶层或内部对象快照
|
||||||
|
type editorSelectionPart struct {
|
||||||
|
objects []GraphicObject
|
||||||
|
composite *CompositeSelection
|
||||||
}
|
}
|
||||||
|
|
||||||
// SelectionBounds 获取正文与注解混合选区的实际范围,复用同一次度量的资源缓存
|
// SelectionBounds 获取正文与注解混合选区的实际范围,复用同一次度量的资源缓存
|
||||||
@@ -400,6 +408,18 @@ func (e *Editor) PasteSelection(page int, selection *ObjectSelection, dx, dy flo
|
|||||||
if err == nil && selection.annotations != nil {
|
if err == nil && selection.annotations != nil {
|
||||||
annotations, err = edit.PasteAnnotations(page, selection.annotations, dx, dy)
|
annotations, err = edit.PasteAnnotations(page, selection.annotations, dx, dy)
|
||||||
}
|
}
|
||||||
|
for _, part := range selection.parts {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var ids []string
|
||||||
|
if part.composite != nil {
|
||||||
|
ids, err = edit.PasteCompositeSelection(page, part.composite, dx, dy)
|
||||||
|
} else {
|
||||||
|
ids, err = edit.CopyObjects(page, part.objects, dx, dy)
|
||||||
|
}
|
||||||
|
objects = append(objects, ids...)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -418,5 +438,437 @@ func (e *Editor) PasteSelectionToComposite(page int, path ObjectPath, selection
|
|||||||
if selection.annotations != nil {
|
if selection.annotations != nil {
|
||||||
return nil, fmt.Errorf("annotations cannot be nested in a page object")
|
return nil, fmt.Errorf("annotations cannot be nested in a page object")
|
||||||
}
|
}
|
||||||
return e.CopyObjectsToComposite(page, path, selection.objects, dx, dy)
|
var indexes []int
|
||||||
|
err := e.Transaction(func(edit *Editor) error {
|
||||||
|
var err error
|
||||||
|
indexes, err = edit.CopyObjectsToComposite(page, path, selection.objects, dx, dy)
|
||||||
|
for _, part := range selection.parts {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var next []int
|
||||||
|
if part.composite != nil {
|
||||||
|
next, err = edit.PasteCompositeObjects(page, path, part.composite, dx, dy)
|
||||||
|
} else {
|
||||||
|
next, err = edit.CopyObjectsToComposite(page, path, part.objects, dx, dy)
|
||||||
|
}
|
||||||
|
indexes = append(indexes, next...)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return indexes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorTargetGroup 保存同一父路径的直接成员,结构操作按逆序处理父路径
|
||||||
|
type editorTargetGroup struct {
|
||||||
|
path ObjectPath
|
||||||
|
indexes []int
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorTargetGroups 分离顶层与内部选区,拒绝重复或相互包含的路径
|
||||||
|
// 入参: paths 完整对象路径
|
||||||
|
// 返回: []string 正文标识, []string 注解标识, []editorTargetGroup 内部成员, error 路径错误
|
||||||
|
func editorTargetGroups(paths []ObjectPath) ([]string, []string, []editorTargetGroup, error) {
|
||||||
|
var objects, annotations []string
|
||||||
|
var groups []editorTargetGroup
|
||||||
|
for i, path := range paths {
|
||||||
|
if (path.ID == "") == (path.Annotation == "") || slices.ContainsFunc(path.Children, func(index int) bool { return index < 0 }) {
|
||||||
|
return nil, nil, nil, fmt.Errorf("invalid object path")
|
||||||
|
}
|
||||||
|
for _, other := range paths[:i] {
|
||||||
|
length := min(len(path.Children), len(other.Children))
|
||||||
|
if path.ID == other.ID && path.Annotation == other.Annotation && slices.Equal(path.Children[:length], other.Children[:length]) {
|
||||||
|
return nil, nil, nil, fmt.Errorf("selection paths overlap")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(path.Children) == 0 {
|
||||||
|
if path.Annotation != "" {
|
||||||
|
annotations = append(annotations, path.Annotation)
|
||||||
|
} else {
|
||||||
|
objects = append(objects, path.ID)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
index := path.Children[len(path.Children)-1]
|
||||||
|
path.Children = slices.Clone(path.Children[:len(path.Children)-1])
|
||||||
|
group := slices.IndexFunc(groups, func(group editorTargetGroup) bool {
|
||||||
|
return path.ID == group.path.ID && path.Annotation == group.path.Annotation && slices.Equal(path.Children, group.path.Children)
|
||||||
|
})
|
||||||
|
if group < 0 {
|
||||||
|
groups = append(groups, editorTargetGroup{path: path, indexes: []int{index}})
|
||||||
|
} else {
|
||||||
|
groups[group].indexes = append(groups[group].indexes, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.SortFunc(groups, func(a, b editorTargetGroup) int {
|
||||||
|
if order := cmp.Compare(a.path.ID, b.path.ID); order != 0 {
|
||||||
|
return order
|
||||||
|
}
|
||||||
|
if order := cmp.Compare(a.path.Annotation, b.path.Annotation); order != 0 {
|
||||||
|
return order
|
||||||
|
}
|
||||||
|
return -slices.Compare(a.path.Children, b.path.Children)
|
||||||
|
})
|
||||||
|
return objects, annotations, groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// editTargets 原子处理跨容器选区,结构变化从后向前避免路径位移
|
||||||
|
// 入参: paths 完整对象路径, root 顶层处理, member 内部处理
|
||||||
|
// 返回: error 路径或编辑错误
|
||||||
|
func (e *Editor) editTargets(paths []ObjectPath, root func([]string, []string) error, member func(ObjectPath, []int) error) error {
|
||||||
|
objects, annotations, groups, err := editorTargetGroups(paths)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.Transaction(func(*Editor) error {
|
||||||
|
for _, group := range groups {
|
||||||
|
if err := member(group.path, group.indexes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(objects)+len(annotations) != 0 {
|
||||||
|
return root(objects, annotations)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureTargets 捕获跨容器选区,内部快照保留源变换与裁剪,源删除不影响粘贴
|
||||||
|
// 入参: page 页面索引, paths 完整对象路径
|
||||||
|
// 返回: *ObjectSelection 独立快照, error 路径或资源错误
|
||||||
|
func (e *Editor) CaptureTargets(page int, paths []ObjectPath) (*ObjectSelection, error) {
|
||||||
|
objects, annotations, groups, err := editorTargetGroups(paths)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(groups) == 0 {
|
||||||
|
return e.CaptureSelection(page, objects, annotations)
|
||||||
|
}
|
||||||
|
selection, err := e.CaptureSelection(page, nil, annotations)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
content, err := e.page(page)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
positions := e.objectPositions(content)
|
||||||
|
paths = slices.Clone(paths)
|
||||||
|
slices.SortStableFunc(paths, func(a, b ObjectPath) int {
|
||||||
|
if a.ID != b.ID {
|
||||||
|
if a.ID == "" {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if b.ID == "" {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return compareEditorPosition(positions[a.ID], positions[b.ID])
|
||||||
|
}
|
||||||
|
if a.Annotation != b.Annotation {
|
||||||
|
return cmp.Compare(a.Annotation, b.Annotation)
|
||||||
|
}
|
||||||
|
return slices.Compare(a.Children, b.Children)
|
||||||
|
})
|
||||||
|
for _, path := range paths {
|
||||||
|
if len(path.Children) == 0 {
|
||||||
|
if path.Annotation != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items, err := e.Objects(page, []string{path.ID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
selection.parts = append(selection.parts, editorSelectionPart{objects: items})
|
||||||
|
} else {
|
||||||
|
index := path.Children[len(path.Children)-1]
|
||||||
|
path.Children = path.Children[:len(path.Children)-1]
|
||||||
|
composite, err := e.CaptureCompositeObjects(page, path, []int{index})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
selection.parts = append(selection.parts, editorSelectionPart{composite: composite})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransformTargets 按页面坐标原子变换跨容器选区,保留组合结构
|
||||||
|
// 入参: page 页面索引, paths 完整对象路径, matrix 有限可逆变换
|
||||||
|
// 返回: error 路径、变换或编辑错误
|
||||||
|
func (e *Editor) TransformTargets(page int, paths []ObjectPath, matrix Matrix) error {
|
||||||
|
if _, ok := matrix.Invert(); !ok {
|
||||||
|
return fmt.Errorf("selection transform must be finite and invertible")
|
||||||
|
}
|
||||||
|
return e.editTargets(paths, func(objects, annotations []string) error {
|
||||||
|
return e.TransformSelection(page, objects, annotations, matrix)
|
||||||
|
}, func(path ObjectPath, indexes []int) error {
|
||||||
|
return e.changeCompositeObjects(page, path, indexes, func(Box) Matrix { return matrix })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTargets 原子删除跨容器选区,一次撤销恢复全部
|
||||||
|
// 入参: page 页面索引, paths 完整对象路径
|
||||||
|
// 返回: error 路径或编辑错误
|
||||||
|
func (e *Editor) DeleteTargets(page int, paths []ObjectPath) error {
|
||||||
|
return e.editTargets(paths, func(objects, annotations []string) error {
|
||||||
|
return e.DeleteSelection(page, objects, annotations)
|
||||||
|
}, func(path ObjectPath, indexes []int) error {
|
||||||
|
return e.DeleteCompositeObjects(page, path, indexes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleTargets 原子设置跨容器选区样式,一次撤销恢复全部
|
||||||
|
// 入参: page 页面索引, paths 完整对象路径, style 样式增量
|
||||||
|
// 返回: error 路径或编辑错误
|
||||||
|
func (e *Editor) StyleTargets(page int, paths []ObjectPath, style ObjectStyle) error {
|
||||||
|
return e.editTargets(paths, func(objects, annotations []string) error {
|
||||||
|
return e.StyleSelection(page, objects, annotations, style)
|
||||||
|
}, func(path ObjectPath, indexes []int) error {
|
||||||
|
return e.StyleCompositeObjects(page, path, indexes, style)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// EraseTargets 按页面矩形原子擦除跨容器选区,一次撤销恢复全部
|
||||||
|
// 入参: page 页面索引, paths 完整对象路径, box 页面毫米范围
|
||||||
|
// 返回: error 路径或编辑错误
|
||||||
|
func (e *Editor) EraseTargets(page int, paths []ObjectPath, box Box) error {
|
||||||
|
return e.editTargets(paths, func(objects, annotations []string) error {
|
||||||
|
return e.EraseSelection(page, objects, annotations, box)
|
||||||
|
}, func(path ObjectPath, indexes []int) error {
|
||||||
|
return e.EraseCompositeObjects(page, path, indexes, box)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// EraseTargetsPath 按页面多边形原子擦除跨容器选区,一次撤销恢复全部
|
||||||
|
// 入参: page 页面索引, paths 完整对象路径, points 页面毫米多边形
|
||||||
|
// 返回: error 路径或编辑错误
|
||||||
|
func (e *Editor) EraseTargetsPath(page int, paths []ObjectPath, points []Point) error {
|
||||||
|
return e.editTargets(paths, func(objects, annotations []string) error {
|
||||||
|
return e.EraseSelectionPath(page, objects, annotations, points)
|
||||||
|
}, func(path ObjectPath, indexes []int) error {
|
||||||
|
return e.EraseCompositeObjectsPath(page, path, indexes, points)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyTargetsStyle 原子复制样式至跨容器选区,保留各对象内容及位置
|
||||||
|
// 入参: page 页面索引, paths 完整对象路径, source 样式来源
|
||||||
|
// 返回: error 路径或样式错误
|
||||||
|
func (e *Editor) CopyTargetsStyle(page int, paths []ObjectPath, source GraphicObject) error {
|
||||||
|
return e.editTargets(paths, func(objects, annotations []string) error {
|
||||||
|
if err := e.CopyStyle(page, objects, source); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.editSelectionAnnotations(page, annotations, func(path ObjectPath, indexes []int) error {
|
||||||
|
return e.CopyCompositeStyle(page, path, indexes, source)
|
||||||
|
})
|
||||||
|
}, func(path ObjectPath, indexes []int) error {
|
||||||
|
return e.CopyCompositeStyle(page, path, indexes, source)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// targetGeometry 度量跨容器选区,保留输入顺序及独立宽高能力
|
||||||
|
// 入参: page 页面索引, paths 完整对象路径
|
||||||
|
// 返回: []Box 对象范围, bool 可独立缩放宽高, Box 页面范围, error 度量错误
|
||||||
|
func (e *Editor) targetGeometry(page int, paths []ObjectPath) ([]Box, bool, Box, error) {
|
||||||
|
if _, _, _, err := editorTargetGroups(paths); err != nil {
|
||||||
|
return nil, false, Box{}, err
|
||||||
|
}
|
||||||
|
_, _, paper, err := e.selectionGeometry(page, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, Box{}, err
|
||||||
|
}
|
||||||
|
boxes := make([]Box, len(paths))
|
||||||
|
stretch := true
|
||||||
|
for i, path := range paths {
|
||||||
|
if len(path.Children) != 0 {
|
||||||
|
index := path.Children[len(path.Children)-1]
|
||||||
|
path.Children = path.Children[:len(path.Children)-1]
|
||||||
|
member, err := e.CompositeObject(page, path, index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, Box{}, err
|
||||||
|
}
|
||||||
|
boxes[i] = member.Bounds
|
||||||
|
stretch = stretch && member.Capabilities.Stretch
|
||||||
|
} else {
|
||||||
|
var objects, annotations []string
|
||||||
|
if path.Annotation != "" {
|
||||||
|
annotations = []string{path.Annotation}
|
||||||
|
} else {
|
||||||
|
objects = []string{path.ID}
|
||||||
|
capability, err := e.ObjectCapabilities(page, path.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, Box{}, err
|
||||||
|
}
|
||||||
|
stretch = stretch && capability.Stretch
|
||||||
|
}
|
||||||
|
boxes[i], err = e.SelectionBounds(page, objects, annotations)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, Box{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return boxes, stretch, paper, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetsBounds 获取跨容器选区的共同可见范围
|
||||||
|
// 入参: page 页面索引, paths 完整对象路径
|
||||||
|
// 返回: Box 页面毫米范围, error 路径或度量错误
|
||||||
|
func (e *Editor) TargetsBounds(page int, paths []ObjectPath) (Box, error) {
|
||||||
|
objects, annotations, groups, err := editorTargetGroups(paths)
|
||||||
|
if err != nil {
|
||||||
|
return Box{}, err
|
||||||
|
}
|
||||||
|
if len(groups) == 0 {
|
||||||
|
return e.SelectionBounds(page, objects, annotations)
|
||||||
|
}
|
||||||
|
boxes, _, _, err := e.targetGeometry(page, paths)
|
||||||
|
if err != nil {
|
||||||
|
return Box{}, err
|
||||||
|
}
|
||||||
|
var bounds Box
|
||||||
|
for _, box := range boxes {
|
||||||
|
bounds = unionTextBox(bounds, box)
|
||||||
|
}
|
||||||
|
if bounds.W <= 0 || bounds.H <= 0 {
|
||||||
|
return Box{}, fmt.Errorf("selection has no visible bounds")
|
||||||
|
}
|
||||||
|
return bounds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// orientTargets 绕跨容器选区中心应用旋转或镜像
|
||||||
|
// 入参: page 页面索引, paths 完整对象路径, matrix 方向变换
|
||||||
|
// 返回: error 路径或变换错误
|
||||||
|
func (e *Editor) orientTargets(page int, paths []ObjectPath, matrix Matrix) error {
|
||||||
|
bounds, err := e.TargetsBounds(page, paths)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
x, y := bounds.X+bounds.W/2, bounds.Y+bounds.H/2
|
||||||
|
return e.TransformTargets(page, paths, TranslationMatrix(x, y).Multiply(matrix).Multiply(TranslationMatrix(-x, -y)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RotateTargets 绕共同中心旋转跨容器选区
|
||||||
|
// 入参: page 页面索引, paths 完整对象路径, degrees 顺时针直角角度
|
||||||
|
// 返回: error 路径或变换错误
|
||||||
|
func (e *Editor) RotateTargets(page int, paths []ObjectPath, degrees int) error {
|
||||||
|
matrix, err := editorRotation(degrees)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.orientTargets(page, paths, matrix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlipTargets 绕共同中心镜像跨容器选区
|
||||||
|
// 入参: page 页面索引, paths 完整对象路径, axis 为horizontal或vertical
|
||||||
|
// 返回: error 路径或变换错误
|
||||||
|
func (e *Editor) FlipTargets(page int, paths []ObjectPath, axis string) error {
|
||||||
|
matrix, err := editorFlip(axis)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.orientTargets(page, paths, matrix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResizeTargets 按共同范围调整跨容器选区,不重排文字
|
||||||
|
// 入参: page 页面索引, paths 完整对象路径, box 页面毫米目标范围
|
||||||
|
// 返回: error 路径、范围或编辑错误
|
||||||
|
func (e *Editor) ResizeTargets(page int, paths []ObjectPath, box Box) error {
|
||||||
|
objects, annotations, groups, err := editorTargetGroups(paths)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(groups) == 0 {
|
||||||
|
return e.ResizeSelection(page, objects, annotations, box)
|
||||||
|
}
|
||||||
|
if err := validateCreationBox(box); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
boxes, stretch, _, err := e.targetGeometry(page, paths)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var bounds Box
|
||||||
|
for _, member := range boxes {
|
||||||
|
bounds = unionTextBox(bounds, member)
|
||||||
|
}
|
||||||
|
if bounds.W <= 0 || bounds.H <= 0 {
|
||||||
|
return fmt.Errorf("selection has no visible bounds")
|
||||||
|
}
|
||||||
|
sx, sy := box.W/bounds.W, box.H/bounds.H
|
||||||
|
if math.Abs(sx-sy) > 1e-9*math.Max(sx, sy) {
|
||||||
|
if !stretch {
|
||||||
|
return fmt.Errorf("nonuniform resizing requires images or image-only groups")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sy = sx
|
||||||
|
}
|
||||||
|
return e.TransformTargets(page, paths, Matrix{a: sx, d: sy, e: box.X - bounds.X*sx, f: box.Y - bounds.Y*sy})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArrangeTargets 对齐或等距分布跨容器选区,单选对齐页面
|
||||||
|
// 入参: page 页面索引, paths 按绘制顺序排列的完整路径, distribute 是否分布, direction 对齐方向或分布轴
|
||||||
|
// 返回: error 路径、方向或编辑错误
|
||||||
|
func (e *Editor) ArrangeTargets(page int, paths []ObjectPath, distribute bool, direction string) error {
|
||||||
|
objects, annotations, groups, err := editorTargetGroups(paths)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(groups) == 0 {
|
||||||
|
if distribute {
|
||||||
|
return e.DistributeSelection(page, objects, annotations, direction)
|
||||||
|
}
|
||||||
|
return e.AlignSelection(page, objects, annotations, direction)
|
||||||
|
}
|
||||||
|
if distribute && direction != "horizontal" && direction != "vertical" || !distribute && !slices.Contains([]string{"left", "center", "right", "top", "middle", "bottom"}, direction) {
|
||||||
|
return fmt.Errorf("invalid arrangement direction %q", direction)
|
||||||
|
}
|
||||||
|
boxes, _, target, err := e.targetGeometry(page, paths)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
matrices := make([]Matrix, len(paths))
|
||||||
|
if distribute {
|
||||||
|
positions := make([]editorObjectPosition, len(paths))
|
||||||
|
for i := range positions {
|
||||||
|
positions[i].index = i
|
||||||
|
}
|
||||||
|
matrices, err = editorDistribution(boxes, positions, direction)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(paths) > 1 {
|
||||||
|
target = Box{}
|
||||||
|
for _, box := range boxes {
|
||||||
|
target = unionTextBox(target, box)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, box := range boxes {
|
||||||
|
matrices[i] = editorAlignment(box, target, direction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.Transaction(func(edit *Editor) error {
|
||||||
|
for i, matrix := range matrices {
|
||||||
|
if matrix != IdentityMatrix {
|
||||||
|
if err := edit.TransformTargets(page, paths[i:i+1], matrix); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleTargetsText 原子修改跨容器文字样式,保留各自定位及组合结构
|
||||||
|
// 入参: page 页面索引, paths 完整文字路径, style 字体、字号或颜色增量
|
||||||
|
// 返回: error 路径、文字或样式错误
|
||||||
|
func (e *Editor) StyleTargetsText(page int, paths []ObjectPath, style TextStyle) error {
|
||||||
|
return e.editTargets(paths, func(objects, annotations []string) error {
|
||||||
|
if len(annotations) != 0 {
|
||||||
|
return fmt.Errorf("annotation is not a text object")
|
||||||
|
}
|
||||||
|
return e.StyleText(page, objects, style)
|
||||||
|
}, func(path ObjectPath, indexes []int) error {
|
||||||
|
return e.StyleCompositeText(page, path, indexes, style)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user