diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 934247e..ce9ac4f 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -110,6 +110,7 @@ function createDocumentState() { styleOriginal: null, objectClipboardToken: null, styleClipboard: null, + objectDistribution: null, outlineExpanded: new Map(), textFonts: [], textFontID: null, @@ -1260,7 +1261,7 @@ el.objectAlign.addEventListener("change", () => { el.objectDistribute.addEventListener("change", () => { const axis = el.objectDistribute.value; el.objectDistribute.value = ""; - if (axis && canvasEditor.items().length >= 3) { + if (axis && !el.objectDistribute.disabled && state.objectDistribution?.axes?.includes(axis)) { return changeDocument("ofdgoDistributeObjects", canvasEditor.selected, axis); } }); @@ -12746,6 +12747,30 @@ function updateEditorTools() { updateObjectControls(canvasEditor.selected); } +function updateObjectDistribution(item, disabled) { + const tab = documentTab(), state = tab.state; + const ids = item?.items?.slice().sort((a, b) => a.order - b.order).map(member => member.id) || []; + if (disabled || ids.length < 3) state.objectDistribution = null; + else { + const { openSeq, wasmSeq } = state, revision = state.editorInfo?.revision; + const key = JSON.stringify([openSeq, wasmSeq, revision, item.index, ids]); + if (state.objectDistribution?.key !== key) { + const request = state.objectDistribution = { key, axes: [] }; + const current = () => !tab.closed && state.objectDistribution === request && state.openSeq === openSeq && state.wasmSeq === wasmSeq && state.editorInfo?.revision === revision; + callTabWASM(tab, "ofdgoDistributionAxes", item.index, ids).then(axes => { + if (!current()) return; + request.axes = axes; + if (isForegroundDocument(tab)) updateObjectControls(canvasEditor.selected); + }).catch(error => { + if (current() && isForegroundDocument(tab)) showError(error, false); + }); + } + } + const axes = state.objectDistribution?.axes || []; + el.objectDistribute.disabled = disabled || !axes.length; + for (const option of el.objectDistribute.options) if (option.value) option.disabled = !axes.includes(option.value); +} + function updateObjectControls(item, reset = false) { const members = item?.items || (item ? [item] : []); const disabled = !item || Boolean(item.draft) || !state.ready || state.exporting; @@ -12763,7 +12788,7 @@ function updateObjectControls(item, reset = false) { el.cropImageButton.setAttribute("data-pressed", String(cropping)); el.resetCropButton.textContent = cropping ? "取消" : "还原"; 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; + updateObjectDistribution(item, el.objectAlign.disabled); el.editObjectButton.disabled = disabled || (item.items ? !item.items.every(member => member.type === "TextObject" && !member.richText) : !canEditObject(item, "children") && (item.type === "PathObject" || !(item.type === "TextObject" && canEditObject(item, "rewriteText")) && !canEditObject(item, item.type === "TextObject" ? "textContent" : item.type === "ImageObject" ? "replaceImage" : "update"))); diff --git a/internal/webui/webui.go b/internal/webui/webui.go index 95f7ca4..2060cf8 100644 --- a/internal/webui/webui.go +++ b/internal/webui/webui.go @@ -5703,6 +5703,20 @@ func (e *businessEngine) orderObjects(args []platformValue) (any, error) { }) } +// distributionAxes 获取画布选区可用的等间距分布方向 +// 入参: args 页面索引和画布对象标识数组 +// 返回: any 可用方向, error 路径或度量错误 +func (e *businessEngine) distributionAxes(args []platformValue) (any, error) { + if e.currentEditor == nil { + return nil, fmt.Errorf("no document is being edited") + } + paths, err := e.editorTargetPaths(e.stringsFromJS(args[1])) + if err != nil { + return nil, err + } + return e.currentEditor.DistributionAxes(args[0].Int(), paths) +} + // distributeObjects 按指定轴等距分布选区 // 入参: args 页面索引、对象标识数组和分布轴 // 返回: any 文档信息, error 错误信息 @@ -8159,6 +8173,7 @@ func (e *businessEngine) businessCommands() map[string]func([]platformValue) (an "ofdgoImportObjects": e.importObjects, "ofdgoOrderObjects": e.orderObjects, "ofdgoDistributeObjects": e.distributeObjects, + "ofdgoDistributionAxes": e.distributionAxes, "ofdgoRotateObjects": e.rotateObjects, "ofdgoFlipObjects": e.flipObjects, "ofdgoCropImage": e.cropImage, diff --git a/ofdgo_editor_selection.go b/ofdgo_editor_selection.go index 85507ca..5cdd4ae 100644 --- a/ofdgo_editor_selection.go +++ b/ofdgo_editor_selection.go @@ -808,6 +808,40 @@ func (e *Editor) ResizeTargets(page int, paths []ObjectPath, box Box) error { return e.TransformTargets(page, paths, Matrix{a: sx, d: sy, e: box.X - bounds.X*sx, f: box.Y - bounds.Y*sy}) } +// DistributionAxes 获取选区可保持位置顺序的等间距分布方向,不修改文档或历史 +// 入参: page 页面索引, paths 按绘制顺序排列的完整路径 +// 返回: []string 可用的horizontal和vertical方向, error 路径或度量错误 +func (e *Editor) DistributionAxes(page int, paths []ObjectPath) ([]string, error) { + objects, annotations, groups, err := editorTargetGroups(paths) + if err != nil { + return nil, err + } + var boxes []Box + var positions []editorObjectPosition + if len(groups) == 0 { + boxes, positions, _, err = e.selectionGeometry(page, objects, annotations) + } else { + boxes, _, _, err = e.targetGeometry(page, paths) + positions = make([]editorObjectPosition, len(paths)) + for i := range positions { + positions[i].index = i + } + } + if err != nil { + return nil, err + } + axes := []string{} + if len(boxes) < 3 { + return axes, nil + } + for _, axis := range []string{"horizontal", "vertical"} { + if _, err := editorDistribution(boxes, positions, axis); err == nil { + axes = append(axes, axis) + } + } + return axes, nil +} + // ArrangeTargets 对齐或等距分布跨容器选区,单选对齐页面 // 入参: page 页面索引, paths 按绘制顺序排列的完整路径, distribute 是否分布, direction 对齐方向或分布轴 // 返回: error 路径、方向或编辑错误