mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
This commit is contained in:
1 parent
89797af8d6
commit
11b3f38441
4 files changed
+179
-27
No files matched your search
+78
-3
@@ -6,6 +6,7 @@ const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)");
|
|||||||
const DEFAULT_IMAGE_DPI = 300;
|
const DEFAULT_IMAGE_DPI = 300;
|
||||||
const PAGE_CACHE_LIMIT = 16;
|
const PAGE_CACHE_LIMIT = 16;
|
||||||
const PAGE_CACHE_BYTES = 32 * 1024 * 1024;
|
const PAGE_CACHE_BYTES = 32 * 1024 * 1024;
|
||||||
|
const OBJECT_CLIPBOARD_TYPE = "application/x-ofdgo-objects";
|
||||||
const STATUS = {
|
const STATUS = {
|
||||||
ready: "选择 OFD 文件",
|
ready: "选择 OFD 文件",
|
||||||
opening: "正在打开文档",
|
opening: "正在打开文档",
|
||||||
@@ -37,6 +38,7 @@ const state = {
|
|||||||
editorInfo: null,
|
editorInfo: null,
|
||||||
savedRevision: null,
|
savedRevision: null,
|
||||||
insertObject: null,
|
insertObject: null,
|
||||||
|
objectClipboard: null,
|
||||||
textFonts: [],
|
textFonts: [],
|
||||||
textFontID: null,
|
textFontID: null,
|
||||||
textDefaults: { type: "TextObject", size: 12 * 25.4 / 72, color: "#000000", wrap: true, align: "left", paragraphHeight: 0, letterSpacing: 0 },
|
textDefaults: { type: "TextObject", size: 12 * 25.4 / 72, color: "#000000", wrap: true, align: "left", paragraphHeight: 0, letterSpacing: 0 },
|
||||||
@@ -503,6 +505,7 @@ el.panButton.addEventListener("click", togglePan);
|
|||||||
el.continuousButton.addEventListener("click", toggleContinuous);
|
el.continuousButton.addEventListener("click", toggleContinuous);
|
||||||
el.annotationButton.addEventListener("click", toggleAnnotations);
|
el.annotationButton.addEventListener("click", toggleAnnotations);
|
||||||
document.addEventListener("copy", copySelection);
|
document.addEventListener("copy", copySelection);
|
||||||
|
el.viewerPanel.addEventListener("paste", pasteEditorContent);
|
||||||
document.addEventListener("selectionchange", syncSelection);
|
document.addEventListener("selectionchange", syncSelection);
|
||||||
el.exportFormat.addEventListener("change", () => updateDPIControl());
|
el.exportFormat.addEventListener("change", () => updateDPIControl());
|
||||||
el.exportPageButton.addEventListener("click", () => exportFile(false));
|
el.exportPageButton.addEventListener("click", () => exportFile(false));
|
||||||
@@ -640,7 +643,7 @@ function handleKeyDown(event) {
|
|||||||
}
|
}
|
||||||
if (key === "Enter" && target === el.pageInput && !event.shiftKey) {
|
if (key === "Enter" && target === el.pageInput && !event.shiftKey) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
el.viewerPanel.focus({ preventScroll: true });
|
if (!event.repeat) el.pageInput.dispatchEvent(new Event("change"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key === "Enter" && target === el.searchInput && event.shiftKey && state.searchMatches.length) {
|
if (key === "Enter" && target === el.searchInput && event.shiftKey && state.searchMatches.length) {
|
||||||
@@ -742,6 +745,7 @@ async function createDocument(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.editing = true;
|
state.editing = true;
|
||||||
|
state.objectClipboard = null;
|
||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
delete state.textDefaults.fontChoice;
|
delete state.textDefaults.fontChoice;
|
||||||
updateTextFonts(null, true);
|
updateTextFonts(null, true);
|
||||||
@@ -1143,11 +1147,14 @@ async function changeDocument(name, item, ...args) {
|
|||||||
if (!item || name === "ofdgoDeleteObject" || name === "ofdgoDeleteObjects") {
|
if (!item || name === "ofdgoDeleteObject" || name === "ofdgoDeleteObjects") {
|
||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
}
|
}
|
||||||
if (name === "ofdgoCopyObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText") {
|
if (name === "ofdgoCopyObjects" || name === "ofdgoPasteObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
|
||||||
|
state.selectObjects = true;
|
||||||
|
canvasEditor.setTool("");
|
||||||
|
setPan(false);
|
||||||
canvasEditor.pendingSelection = { index: item?.index ?? args[0], ids: doc.selectedIDs };
|
canvasEditor.pendingSelection = { index: item?.index ?? args[0], ids: doc.selectedIDs };
|
||||||
}
|
}
|
||||||
openSeq = ++state.openSeq;
|
openSeq = ++state.openSeq;
|
||||||
if (item || name === "ofdgoInsertShape" || name === "ofdgoInsertText") {
|
if (item || name === "ofdgoPasteObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
|
||||||
await refreshEditorPage(doc, item ? item.index : args[0], openSeq);
|
await refreshEditorPage(doc, item ? item.index : args[0], openSeq);
|
||||||
} else {
|
} else {
|
||||||
const samePage = doc.pages.findIndex((page) => page.id === previous.id);
|
const samePage = doc.pages.findIndex((page) => page.id === previous.id);
|
||||||
@@ -1668,6 +1675,7 @@ async function openOFD(file) {
|
|||||||
state.ofdBytes = bytes;
|
state.ofdBytes = bytes;
|
||||||
state.fileName = file.name || "ofdgo.ofd";
|
state.fileName = file.name || "ofdgo.ofd";
|
||||||
state.editing = false;
|
state.editing = false;
|
||||||
|
state.objectClipboard = null;
|
||||||
state.editorInfo = null;
|
state.editorInfo = null;
|
||||||
state.savedRevision = null;
|
state.savedRevision = null;
|
||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
@@ -2816,11 +2824,78 @@ async function selectDocumentText() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyEditorSelection(event) {
|
||||||
|
if (!state.editing || !canvasEditor.enabled || !canvasEditor.selected || !el.viewerPanel.contains(event.target)
|
||||||
|
|| canvasEditor.input || canvasEditor.crop || canvasEditor.drag || document.body.hasAttribute("aria-busy")) return false;
|
||||||
|
const items = canvasEditor.items().slice().sort((a, b) => a.order - b.order);
|
||||||
|
const clipboard = { token: crypto.randomUUID(), page: state.doc.pages[items[0].index].id, offset: 0 };
|
||||||
|
event.clipboardData.setData(OBJECT_CLIPBOARD_TYPE, clipboard.token);
|
||||||
|
event.clipboardData.setData("text/plain", items.map(item => item.text || "").filter(Boolean).join("\n"));
|
||||||
|
event.preventDefault();
|
||||||
|
state.objectClipboard = clipboard;
|
||||||
|
clipboard.ready = callWASM("ofdgoCaptureObjects", items[0].index, items.map(item => item.id), clipboard.token)
|
||||||
|
.then(() => true, err => {
|
||||||
|
if (state.objectClipboard === clipboard) showError(err, false);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pasteEditorContent(event) {
|
||||||
|
if (!state.editing || event.target.closest?.("input, textarea, select, [contenteditable]") || formDialogOpen()) return;
|
||||||
|
event.preventDefault();
|
||||||
|
if (document.body.hasAttribute("aria-busy") || canvasEditor.input || canvasEditor.crop || canvasEditor.drag) return;
|
||||||
|
const token = event.clipboardData.getData(OBJECT_CLIPBOARD_TYPE);
|
||||||
|
const index = state.pageIndex, page = state.doc.pages[index];
|
||||||
|
if (token) {
|
||||||
|
const clipboard = state.objectClipboard;
|
||||||
|
if (!clipboard || token !== clipboard.token) {
|
||||||
|
setStatus("对象剪贴板已失效");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const openSeq = state.openSeq;
|
||||||
|
if (!await clipboard.ready || openSeq !== state.openSeq || index !== state.pageIndex || state.objectClipboard !== clipboard) return;
|
||||||
|
const offset = clipboard.page === page.id ? clipboard.offset + 3 : 0;
|
||||||
|
if (await changeDocument("ofdgoPasteObjects", null, index, token, offset, offset)) {
|
||||||
|
clipboard.page = page.id;
|
||||||
|
clipboard.offset = offset;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const files = Array.from(event.clipboardData.files);
|
||||||
|
const x = Math.min(20, page.width / 10), y = Math.min(20, page.height / 10);
|
||||||
|
if (files.length) {
|
||||||
|
if (files.length !== 1) {
|
||||||
|
setStatus("一次粘贴一张图片");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!/^image\/(png|jpeg)$/.test(files[0].type)) {
|
||||||
|
setStatus("图片仅支持 PNG、JPG");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await changeDocument("ofdgoInsertImage", null, index, files[0].arrayBuffer().then(bytes => new Uint8Array(bytes)), x, y, page.width - x * 2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const value = event.clipboardData.getData("text/plain").replace(/\t/g, " ");
|
||||||
|
if (!value.trim()) return;
|
||||||
|
const font = state.textFonts[Number(el.textFont.value)];
|
||||||
|
if (!font) {
|
||||||
|
setStatus("尚未添加字体");
|
||||||
|
el.textFontAdd.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const style = currentTextStyle();
|
||||||
|
const data = font.embedded ? callWASM("ofdgoEditorFont", font.id.slice(9)).then(result => result.bytes) : fontManager.read(font);
|
||||||
|
await changeDocument("ofdgoInsertText", null, index, value, data, x, y, style.size, style.color,
|
||||||
|
page.width - x * 2, style.wrap, style.align || "left", style.paragraphHeight || 0, style.letterSpacing || 0);
|
||||||
|
}
|
||||||
|
|
||||||
function copySelection(event) {
|
function copySelection(event) {
|
||||||
if (event.target.closest?.("input, textarea, [contenteditable]")) {
|
if (event.target.closest?.("input, textarea, [contenteditable]")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
|
if (selection.isCollapsed && copyEditorSelection(event)) return;
|
||||||
if (selection.isCollapsed || !selection.rangeCount) {
|
if (selection.isCollapsed || !selection.rangeCount) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-10
@@ -27,17 +27,19 @@ export function selectionBounds(items) {
|
|||||||
height: Math.max(...items.map(item => item.y + item.height)) - y };
|
height: Math.max(...items.map(item => item.y + item.height)) - y };
|
||||||
}
|
}
|
||||||
|
|
||||||
function alignmentGuides(box, targets, tolerance) {
|
function alignmentSnap(box, targets, tolerance) {
|
||||||
const lines = [];
|
const lines = [];
|
||||||
|
const offset = { x: 0, y: 0 };
|
||||||
for (const [axis, size, cross, span] of [["x", "width", "y", "height"], ["y", "height", "x", "width"]]) {
|
for (const [axis, size, cross, span] of [["x", "width", "y", "height"], ["y", "height", "x", "width"]]) {
|
||||||
let distance = Infinity, line;
|
let distance = Infinity, line;
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
for (const anchor of [0, 0.5, 1]) {
|
for (const anchor of [0, 0.5, 1]) {
|
||||||
const position = target[axis] + target[size] * anchor;
|
const position = target[axis] + target[size] * anchor;
|
||||||
for (const edge of [0, 0.5, 1]) {
|
for (const edge of [0, 0.5, 1]) {
|
||||||
const delta = Math.abs(box[axis] + box[size] * edge - position);
|
const delta = position - box[axis] - box[size] * edge;
|
||||||
if (delta > tolerance[axis] || delta >= distance) continue;
|
if (Math.abs(delta) > tolerance[axis] || Math.abs(delta) >= distance) continue;
|
||||||
distance = delta;
|
distance = Math.abs(delta);
|
||||||
|
offset[axis] = delta;
|
||||||
const from = Math.min(box[cross], target[cross]);
|
const from = Math.min(box[cross], target[cross]);
|
||||||
const to = Math.max(box[cross] + box[span], target[cross] + target[span]);
|
const to = Math.max(box[cross] + box[span], target[cross] + target[span]);
|
||||||
line = axis === "x" ? `M${position} ${from}V${to}` : `M${from} ${position}H${to}`;
|
line = axis === "x" ? `M${position} ${from}V${to}` : `M${from} ${position}H${to}`;
|
||||||
@@ -46,7 +48,7 @@ function alignmentGuides(box, targets, tolerance) {
|
|||||||
}
|
}
|
||||||
if (line) lines.push(line);
|
if (line) lines.push(line);
|
||||||
}
|
}
|
||||||
return lines.join("");
|
return { ...offset, path: lines.join("") };
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformedBox(box, matrix) {
|
function transformedBox(box, matrix) {
|
||||||
@@ -422,16 +424,18 @@ export class CanvasEditor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
drag.change = objectTransform(drag.item, to.x - from.x, to.y - from.y, drag.corner);
|
drag.change = objectTransform(drag.item, to.x - from.x, to.y - from.y, drag.corner);
|
||||||
|
if (!drag.corner) this.snap(drag, event.altKey);
|
||||||
this.place(drag.item, drag.change);
|
this.place(drag.item, drag.change);
|
||||||
if (!drag.corner) this.showGuides(drag);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showGuides(drag) {
|
snap(drag, bypass = false) {
|
||||||
const { item, rect, rotation, change } = drag;
|
const { item, rect, rotation, change } = drag;
|
||||||
const path = alignmentGuides({ ...item, x: item.x + change.x, y: item.y + change.y }, drag.targets, {
|
const { x, y, path } = bypass ? { x: 0, y: 0, path: "" } : alignmentSnap({ ...item, x: item.x + change.x, y: item.y + change.y }, drag.targets, {
|
||||||
x: 4 * item.page.width / (rotation % 180 ? rect.height : rect.width),
|
x: 4 * item.page.width / (rotation % 180 ? rect.height : rect.width),
|
||||||
y: 4 * item.page.height / (rotation % 180 ? rect.width : rect.height),
|
y: 4 * item.page.height / (rotation % 180 ? rect.width : rect.height),
|
||||||
});
|
});
|
||||||
|
change.x += x;
|
||||||
|
change.y += y;
|
||||||
if (!drag.guides && path) {
|
if (!drag.guides && path) {
|
||||||
drag.guides = this.createPreview(item.surface, item.page, "path", {
|
drag.guides = this.createPreview(item.surface, item.page, "path", {
|
||||||
fill: false, stroke: true, strokeColor: "var(--accent)", lineWidth: 1,
|
fill: false, stroke: true, strokeColor: "var(--accent)", lineWidth: 1,
|
||||||
@@ -620,9 +624,9 @@ export class CanvasEditor {
|
|||||||
|
|
||||||
modifierChange(event) {
|
modifierChange(event) {
|
||||||
const pointer = this.drag?.pointer;
|
const pointer = this.drag?.pointer;
|
||||||
if (event.key === "Shift" && pointer) {
|
if ((event.key === "Shift" || event.key === "Alt") && pointer) {
|
||||||
this.move({ pointerId: pointer.pointerId, clientX: pointer.clientX, clientY: pointer.clientY,
|
this.move({ pointerId: pointer.pointerId, clientX: pointer.clientX, clientY: pointer.clientY,
|
||||||
shiftKey: event.shiftKey, preventDefault() {} });
|
shiftKey: event.shiftKey, altKey: event.altKey, preventDefault() {} });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+52
-1
@@ -114,6 +114,8 @@ func RunWASM() {
|
|||||||
registerCallback("ofdgoAlignObjects", alignObjects)
|
registerCallback("ofdgoAlignObjects", alignObjects)
|
||||||
registerCallback("ofdgoDeleteObjects", deleteObjects)
|
registerCallback("ofdgoDeleteObjects", deleteObjects)
|
||||||
registerCallback("ofdgoCopyObjects", copyObjects)
|
registerCallback("ofdgoCopyObjects", copyObjects)
|
||||||
|
registerCallback("ofdgoCaptureObjects", captureObjects)
|
||||||
|
registerCallback("ofdgoPasteObjects", pasteObjects)
|
||||||
registerCallback("ofdgoOrderObjects", orderObjects)
|
registerCallback("ofdgoOrderObjects", orderObjects)
|
||||||
registerCallback("ofdgoDistributeObjects", distributeObjects)
|
registerCallback("ofdgoDistributeObjects", distributeObjects)
|
||||||
registerCallback("ofdgoRotateObjects", rotateObjects)
|
registerCallback("ofdgoRotateObjects", rotateObjects)
|
||||||
@@ -197,6 +199,7 @@ func openDocument(args []js.Value) (any, error) {
|
|||||||
}
|
}
|
||||||
renderAnnotations := args[2].Bool()
|
renderAnnotations := args[2].Bool()
|
||||||
currentEditor = nil
|
currentEditor = nil
|
||||||
|
copiedObjects = nil
|
||||||
if currentSession != nil {
|
if currentSession != nil {
|
||||||
_ = currentSession.Close()
|
_ = currentSession.Close()
|
||||||
currentSession = nil
|
currentSession = nil
|
||||||
@@ -530,6 +533,15 @@ func encodeResult(result apiResult) string {
|
|||||||
// currentEditor 当前新建文档
|
// currentEditor 当前新建文档
|
||||||
var currentEditor *ofdgo.Editor
|
var currentEditor *ofdgo.Editor
|
||||||
|
|
||||||
|
// editorClipboard 当前创作文档中的对象快照与剪贴板标识。
|
||||||
|
type editorClipboard struct {
|
||||||
|
token string
|
||||||
|
objects []ofdgo.GraphicObject
|
||||||
|
}
|
||||||
|
|
||||||
|
// copiedObjects 当前对象剪贴板,不保存字体或图片的重复数据。
|
||||||
|
var copiedObjects *editorClipboard
|
||||||
|
|
||||||
// editorInfo 创作文档信息与操作状态
|
// editorInfo 创作文档信息与操作状态
|
||||||
type editorInfo struct {
|
type editorInfo struct {
|
||||||
DocumentInfo
|
DocumentInfo
|
||||||
@@ -814,7 +826,43 @@ func copyObjects(args []js.Value) (any, error) {
|
|||||||
if currentEditor == nil {
|
if currentEditor == nil {
|
||||||
return nil, fmt.Errorf("no document is being created")
|
return nil, fmt.Errorf("no document is being created")
|
||||||
}
|
}
|
||||||
ids, err := currentEditor.CopyObjects(args[0].Int(), stringsFromJS(args[1]), args[2].Float(), args[3].Float())
|
objects, err := currentEditor.Objects(args[0].Int(), stringsFromJS(args[1]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return copyEditorObjects(args[0].Int(), objects, args[2].Float(), args[3].Float())
|
||||||
|
}
|
||||||
|
|
||||||
|
// captureObjects 保存独立选区快照,不修改文档或历史。
|
||||||
|
// 入参: args 页面索引、对象标识数组和剪贴板标识
|
||||||
|
// 返回: any 空结果, error 错误信息
|
||||||
|
func captureObjects(args []js.Value) (any, error) {
|
||||||
|
if currentEditor == nil {
|
||||||
|
return nil, fmt.Errorf("no document is being created")
|
||||||
|
}
|
||||||
|
objects, err := currentEditor.Objects(args[0].Int(), stringsFromJS(args[1]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
copiedObjects = &editorClipboard{args[2].String(), objects}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pasteObjects 将当前文档的对象快照粘贴到目标页。
|
||||||
|
// 入参: args 目标页、剪贴板标识和横纵位移
|
||||||
|
// 返回: any 文档信息及新选区, error 错误信息
|
||||||
|
func pasteObjects(args []js.Value) (any, error) {
|
||||||
|
if currentEditor == nil || copiedObjects == nil || copiedObjects.token != args[1].String() {
|
||||||
|
return nil, fmt.Errorf("object clipboard is no longer available")
|
||||||
|
}
|
||||||
|
return copyEditorObjects(args[0].Int(), copiedObjects.objects, args[2].Float(), args[3].Float())
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyEditorObjects 复制快照并更新预览,返回新对象标识。
|
||||||
|
// 入参: page 目标页, objects 对象快照, dx、dy 毫米位移
|
||||||
|
// 返回: any 文档信息及新选区, error 错误信息
|
||||||
|
func copyEditorObjects(page int, objects []ofdgo.GraphicObject, dx, dy float64) (any, error) {
|
||||||
|
ids, err := currentEditor.CopyObjects(page, objects, dx, dy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -928,6 +976,9 @@ func previewEditor(editor *ofdgo.Editor, annotations bool) (editorInfo, error) {
|
|||||||
_ = currentSession.Close()
|
_ = currentSession.Close()
|
||||||
}
|
}
|
||||||
currentSession = session
|
currentSession = session
|
||||||
|
if currentEditor != editor {
|
||||||
|
copiedObjects = nil
|
||||||
|
}
|
||||||
currentEditor = editor
|
currentEditor = editor
|
||||||
return editorSummary(), nil
|
return editorSummary(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-13
@@ -24,23 +24,46 @@ import (
|
|||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CopyObjects 按原绘制顺序复制同页对象并平移,复用资源,提交一次撤销记录。
|
// Objects 按绘制顺序获取选区的独立快照,保留编辑中的段落信息。
|
||||||
// 入参: page 页面索引, ids 对象标识, dx、dy 毫米位移
|
// 入参: page 页面索引, ids 对象标识,不得重复
|
||||||
// 返回: []string 按绘制顺序排列的新对象标识, error 错误信息
|
// 返回: []GraphicObject 独立对象副本, error 错误信息
|
||||||
func (e *Editor) CopyObjects(page int, ids []string, dx, dy float64) ([]string, error) {
|
func (e *Editor) Objects(page int, ids []string) ([]GraphicObject, error) {
|
||||||
if !finite(dx) || !finite(dy) {
|
|
||||||
return nil, fmt.Errorf("copy requires finite offsets")
|
|
||||||
}
|
|
||||||
_, indexes, err := e.selectedObjects(page, ids)
|
_, indexes, err := e.selectedObjects(page, ids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
slices.Sort(indexes)
|
slices.Sort(indexes)
|
||||||
before := e.pages[page].Content.Layer[0].Objects
|
|
||||||
objects := make([]GraphicObject, len(indexes))
|
objects := make([]GraphicObject, len(indexes))
|
||||||
result := make([]string, len(indexes))
|
|
||||||
for i, index := range indexes {
|
for i, index := range indexes {
|
||||||
object, err := cloneEditorObject(before[index])
|
objects[i], err = cloneEditorObject(e.pages[page].Content.Layer[0].Objects[index])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return objects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyObjects 将对象快照按输入顺序复制到目标页并平移,分配新ID,提交一次撤销记录。
|
||||||
|
// 对象引用当前Editor已注册的资源,保留段落信息,不修改输入快照。
|
||||||
|
// 入参: page 目标页面索引, objects 对象快照, dx、dy 毫米位移
|
||||||
|
// 返回: []string 按绘制顺序排列的新对象标识, error 错误信息
|
||||||
|
func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64) ([]string, error) {
|
||||||
|
if !finite(dx) || !finite(dy) {
|
||||||
|
return nil, fmt.Errorf("copy requires finite offsets")
|
||||||
|
}
|
||||||
|
content, err := e.page(page)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(objects) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
before := content.Content.Layer[0].Objects
|
||||||
|
prepared := make([]GraphicObject, len(objects))
|
||||||
|
result := make([]string, len(objects))
|
||||||
|
for i, object := range objects {
|
||||||
|
result[i] = strconv.Itoa(e.maxID + i + 1)
|
||||||
|
object, err := e.prepareObject(result[i], object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -48,14 +71,13 @@ func (e *Editor) CopyObjects(page int, ids []string, dx, dy float64) ([]string,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result[i] = strconv.Itoa(e.maxID + i + 1)
|
prepared[i], err = e.prepareObject(result[i], object)
|
||||||
objects[i], err = e.prepareObject(result[i], object)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
e.maxID += len(objects)
|
e.maxID += len(objects)
|
||||||
e.replaceObjects(page, append(slices.Clone(before), objects...))
|
e.replaceObjects(page, append(slices.Clone(before), prepared...))
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user