From 07ad049cff38da149f9740258879671fc1405318 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Tue, 15 Sep 2026 15:33:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=88=9B=E5=BB=BA=E5=86=99=E5=85=A5):=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=AF=B9=E8=B1=A1=E5=A4=8D=E5=88=B6=E4=B8=8E?= =?UTF-8?q?=E5=B1=82=E7=BA=A7=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/index.html | 8 ++++++ assets/webui/ofdgo.css | 40 +++++++++++++++++++++++++----- assets/webui/ofdgo.js | 35 ++++++++++++++++++++++++-- internal/webui/wasm.go | 55 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 128 insertions(+), 10 deletions(-) diff --git a/assets/webui/index.html b/assets/webui/index.html index dda6a9f..ca2d7b6 100644 --- a/assets/webui/index.html +++ b/assets/webui/index.html @@ -58,7 +58,15 @@
+ + 对象
diff --git a/assets/webui/ofdgo.css b/assets/webui/ofdgo.css index dcdd4d4..ecb157e 100644 --- a/assets/webui/ofdgo.css +++ b/assets/webui/ofdgo.css @@ -104,6 +104,10 @@ a:focus-visible { border-right: 1px solid var(--line); } +.editor-group:has(#selectObjectButton) { + grid-template-columns: repeat(5, max-content); +} + .editor-group:has(#insertTextButton) { grid-template-columns: repeat(2, max-content); } @@ -116,12 +120,14 @@ a:focus-visible { border-right: 0; } -.editor-group > button { +.editor-group > button, +.editor-group > select { grid-row: 1; } .editor-group > span { grid-column: 1 / -1; + align-self: end; text-align: center; font-size: 12px; color: var(--text-muted); @@ -314,7 +320,8 @@ button:not(:disabled):hover { margin: 0; } -.format-select { +.format-select, +.order-select { width: 58px; height: var(--control-height); min-width: 0; @@ -424,8 +431,26 @@ button:not(:disabled):hover { } .form-row input[type="file"] { - height: auto; - padding: 4px; + padding: 0; + color: var(--text-muted); +} + +.form-row input[type="file"]::file-selector-button { + height: 100%; + margin: 0 8px 0 0; + padding: 0 8px; + border: 0; + border-right: 1px solid var(--line); + border-radius: 0; + background: var(--panel); + color: #1f2428; + font: inherit; + cursor: pointer; +} + +.form-row input[type="file"]::file-selector-button:hover { + background: #e4f3ef; + color: var(--accent-strong); } .form-unit { @@ -475,13 +500,16 @@ button:not(:disabled):hover { } .format-select:focus, -.format-select:focus-visible { +.format-select:focus-visible, +.order-select:focus, +.order-select:focus-visible { border-color: var(--accent); outline: 2px solid var(--accent); outline-offset: 2px; } -.format-select option:checked { +.format-select option:checked, +.order-select option:checked { background-color: #e4f3ef; color: var(--accent-strong); } diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 03415cb..8a625e1 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -1,4 +1,4 @@ -import { CanvasEditor } from "./ofdgo_edit.js"; +import { CanvasEditor, objectTransform } from "./ofdgo_edit.js"; import { FontManager } from "./ofdgo_font.js"; const MM_TO_PX = 96 / 25.4; @@ -87,6 +87,8 @@ const el = { selectObjectButton: document.querySelector("#selectObjectButton"), deleteObjectButton: document.querySelector("#deleteObjectButton"), editTextButton: document.querySelector("#editTextButton"), + copyObjectButton: document.querySelector("#copyObjectButton"), + objectOrder: document.querySelector("#objectOrder"), undoButton: document.querySelector("#undoButton"), redoButton: document.querySelector("#redoButton"), insertTextButton: document.querySelector("#insertTextButton"), @@ -247,6 +249,27 @@ el.editTextButton.addEventListener("click", () => { openInsertPanel(true, canvasEditor.selected); } }); +el.copyObjectButton.addEventListener("click", () => { + const item = canvasEditor.selected; + if (!item) { + return; + } + let change = objectTransform(item, item.page, 3, 3); + if (!change.x && !change.y) { + change = objectTransform(item, item.page, -3, -3); + } + return changeDocument("ofdgoCopyObject", item, change.x, change.y); +}); +el.objectOrder.addEventListener("change", () => { + const action = el.objectOrder.value; + el.objectOrder.value = ""; + const item = canvasEditor.selected; + if (!item || !action) { + return; + } + const targets = { up: item.order + 1, down: item.order - 1, top: item.count - 1, bottom: 0 }; + return changeDocument("ofdgoMoveObject", item, targets[action]); +}); el.undoButton.addEventListener("click", () => changeDocument("ofdgoUndo")); el.redoButton.addEventListener("click", () => changeDocument("ofdgoRedo")); el.addPageButton.addEventListener("click", () => { @@ -720,6 +743,9 @@ async function changeDocument(name, item, ...args) { if (!item || name === "ofdgoDeleteObject") { canvasEditor.clear(); } + if (name === "ofdgoCopyObject") { + canvasEditor.selectLast = true; + } openSeq = ++state.openSeq; if (item) { await refreshEditorPage(doc, item.index, openSeq); @@ -3771,8 +3797,13 @@ function updateEditorTools() { } function updateObjectControls(item) { - el.deleteObjectButton.disabled = !item || !state.ready || state.exporting; + const disabled = !item || !state.ready || state.exporting; + el.deleteObjectButton.disabled = el.copyObjectButton.disabled = disabled; el.editTextButton.disabled = !item || item.image || !state.ready || state.exporting; + el.objectOrder.disabled = disabled || item.count <= 1; + for (const option of el.objectOrder.options) { + option.disabled = disabled || (["up", "top"].includes(option.value) ? item.order === item.count - 1 : item.order === 0); + } } function updateAnnotationButton() { diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index 84f47a9..2c7b5c8 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -101,6 +101,8 @@ func RunWASM() { registerCallback("ofdgoUpdateText", updateText) registerCallback("ofdgoInsertImage", insertImage) registerCallback("ofdgoTransformObject", transformObject) + registerCallback("ofdgoCopyObject", copyObject) + registerCallback("ofdgoMoveObject", moveObject) registerCallback("ofdgoDeleteObject", deleteObject) registerCallback("ofdgoUndo", func([]js.Value) (any, error) { return restoreEditor(false) }) registerCallback("ofdgoRedo", func([]js.Value) (any, error) { return restoreEditor(true) }) @@ -589,7 +591,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) { fontNames[font.ID] = font.FontName } for _, layer := range page.Content.Layer { - for _, object := range layer.Objects { + for order, object := range layer.Objects { var id string var box ofdgo.Box switch object.Type { @@ -606,7 +608,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) { continue } if box.W > 0 && box.H > 0 { - item := map[string]any{"id": id, "image": object.Type == "ImageObject", "x": box.X, "y": box.Y, "width": box.W, "height": box.H} + item := map[string]any{"id": id, "image": object.Type == "ImageObject", "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": order, "count": len(layer.Objects)} if object.Type == "TextObject" { item["text"], item["font"], item["fontName"], item["size"] = textValues[id], object.TextObject.Font, fontNames[object.TextObject.Font], object.TextObject.Size } @@ -654,6 +656,55 @@ func transformObject(args []js.Value) (any, error) { return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations) } +// copyObject 复制选中对象并平移,复用字体和图片资源 +// 入参: args 页码、对象标识和位移 +// 返回: any 文档信息, error 错误信息 +func copyObject(args []js.Value) (any, error) { + if currentEditor == nil { + return nil, fmt.Errorf("no document is being created") + } + page := args[0].Int() + object, err := currentEditor.Object(page, args[1].String()) + if err != nil { + return nil, err + } + var boundary *string + switch object.Type { + case "TextObject": + boundary = &object.TextObject.Boundary + case "ImageObject": + boundary = &object.ImageObject.Boundary + default: + return nil, fmt.Errorf("unsupported object type %q", object.Type) + } + box, err := ofdgo.ParseBox(*boundary) + if err != nil { + return nil, err + } + *boundary = fmt.Sprintf("%g %g %g %g", box.X+args[2].Float(), box.Y+args[3].Float(), box.W, box.H) + if _, err := currentEditor.AddObject(page, object); err != nil { + return nil, err + } + return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations) +} + +// moveObject 调整选中对象的绘制顺序 +// 入参: args 页码、对象标识和目标索引 +// 返回: any 文档信息, error 错误信息 +func moveObject(args []js.Value) (any, error) { + if currentEditor == nil { + return nil, fmt.Errorf("no document is being created") + } + revision := currentEditor.Revision() + if err := currentEditor.MoveObject(args[0].Int(), args[1].String(), args[2].Int()); err != nil { + return nil, err + } + if currentEditor.Revision() == revision { + return editorSummary(), nil + } + return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations) +} + // deleteObject 删除创作画布中选中的对象 // 入参: args 页码和对象标识 // 返回: any 文档信息, error 错误信息