mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
feat(编辑能力): 支持组合内容直接编辑
This commit is contained in:
1 parent
9ee47731c9
commit
6dae7bdc20
7 files changed
+698
-242
No files matched your search
+107
-116
@@ -359,7 +359,6 @@ type apiResult struct {
|
||||
type editorClipboard struct {
|
||||
token string
|
||||
selection *ofdgo.ObjectSelection
|
||||
composite *ofdgo.CompositeSelection
|
||||
}
|
||||
|
||||
// editorInfo 编辑文档信息与操作状态
|
||||
@@ -3430,9 +3429,9 @@ func (e *businessEngine) editorObjects(index int, text *ofdgo.PageText) ([]any,
|
||||
continue
|
||||
}
|
||||
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 {
|
||||
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"
|
||||
}
|
||||
if box.W <= 0 || box.H <= 0 {
|
||||
@@ -3711,19 +3710,22 @@ func (e *businessEngine) splitEditorSelection(ids []string) ([]string, []string)
|
||||
return objects, annotations
|
||||
}
|
||||
|
||||
// changeSelection 原子处理正文和注解混选,保持绘制顺序及统一历史
|
||||
// changeSelection 原子处理正文、注解及内部对象混选,保持统一历史
|
||||
// 入参: args 页面索引、标识列表、操作与参数
|
||||
// 返回: any 文档与新选区, error 错误信息
|
||||
func (e *businessEngine) changeSelection(args []platformValue) (any, error) {
|
||||
var selected []string
|
||||
result, err := e.changeAtomicObjects(func() error {
|
||||
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 {
|
||||
case "align", "distribute":
|
||||
return e.arrangeEditorSelection(page, e.stringsFromJS(args[1]), operation, args[3].String())
|
||||
case "delete":
|
||||
return e.currentEditor.DeleteSelection(page, objects, annotations)
|
||||
return e.currentEditor.DeleteTargets(page, paths)
|
||||
case "copy":
|
||||
clipboard, err := e.captureEditorSelection(page, e.stringsFromJS(args[1]))
|
||||
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())
|
||||
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":
|
||||
return e.currentEditor.StyleSelection(page, objects, annotations, e.objectStyle(args[3]))
|
||||
return e.currentEditor.StyleTargets(page, paths, e.objectStyle(args[3]))
|
||||
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(),
|
||||
})
|
||||
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":
|
||||
return e.currentEditor.RotateSelection(page, objects, annotations, args[3].Int())
|
||||
return e.currentEditor.RotateTargets(page, paths, args[3].Int())
|
||||
case "flip":
|
||||
return e.currentEditor.FlipSelection(page, objects, annotations, args[3].String())
|
||||
return e.currentEditor.FlipTargets(page, paths, args[3].String())
|
||||
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(),
|
||||
})
|
||||
case "transform":
|
||||
@@ -3753,7 +3766,7 @@ func (e *businessEngine) changeSelection(args []platformValue) (any, error) {
|
||||
return fmt.Errorf("scale must be positive")
|
||||
}
|
||||
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:
|
||||
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
|
||||
}
|
||||
|
||||
// arrangeEditorSelection 对正文和注解统一对齐或分布,不改变各自绘制层级
|
||||
// arrangeEditorSelection 对跨容器选区统一对齐或分布,不改变各自绘制层级
|
||||
// 入参: page 页面, ids 标识, operation 操作, direction 方向
|
||||
// 返回: error 错误信息
|
||||
func (e *businessEngine) arrangeEditorSelection(page int, ids []string, operation, direction string) error {
|
||||
objects, annotations := e.splitEditorSelection(ids)
|
||||
switch operation {
|
||||
case "align":
|
||||
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)
|
||||
paths, err := e.editorTargetPaths(ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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 画布标识
|
||||
// 返回: *editorClipboard 快照, error 错误信息
|
||||
func (e *businessEngine) captureEditorSelection(page int, ids []string) (*editorClipboard, error) {
|
||||
objects, annotations := e.splitEditorSelection(ids)
|
||||
result := &editorClipboard{}
|
||||
var err error
|
||||
result.selection, err = e.currentEditor.CaptureSelection(page, objects, annotations)
|
||||
return result, err
|
||||
paths, err := e.editorTargetPaths(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selection, err := e.currentEditor.CaptureTargets(page, paths)
|
||||
return &editorClipboard{selection: selection}, err
|
||||
}
|
||||
|
||||
// pasteEditorSelection 原子粘贴正文和注解,共享字体与图片数据
|
||||
@@ -3806,7 +3831,7 @@ func (e *businessEngine) pasteEditorSelection(page int, clipboard *editorClipboa
|
||||
// 返回: 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),
|
||||
"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["rewriteText"] = capability.RewriteText
|
||||
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 {
|
||||
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)}
|
||||
e.editorAppearance(item, member.Object, member.Capabilities.Paint, member.StrokeScale)
|
||||
if member.Object.Type == "ImageObject" {
|
||||
@@ -4128,35 +4153,9 @@ func (e *businessEngine) changeCompositeObjects(args []platformValue) (any, erro
|
||||
case "style":
|
||||
return e.currentEditor.StyleCompositeObjects(page, path, indexes, e.objectStyle(args[4]))
|
||||
case "paint", "textColor":
|
||||
style := ofdgo.ObjectStyle{}
|
||||
if operation == "textColor" {
|
||||
style.FillColor = &ofdgo.FillColor{}
|
||||
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
|
||||
}
|
||||
style, err := e.editorPaintStyle(operation, args[4:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.currentEditor.StyleCompositeObjects(page, path, indexes, style)
|
||||
default:
|
||||
@@ -4176,6 +4175,43 @@ func (e *businessEngine) changeCompositeObjects(args []platformValue) (any, erro
|
||||
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 将画布成员路径转换为同一范围的序号
|
||||
// 入参: key 父路径, ids 成员路径
|
||||
// 返回: []int 成员序号, error 错误信息
|
||||
@@ -4849,7 +4885,7 @@ func (e *businessEngine) styleText(args []platformValue) (any, error) {
|
||||
}
|
||||
var value strings.Builder
|
||||
for _, id := range ids {
|
||||
object, err := e.currentEditor.Object(page, id)
|
||||
object, err := e.editorObjectAt(page, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -4873,7 +4909,11 @@ func (e *businessEngine) styleText(args []platformValue) (any, error) {
|
||||
}
|
||||
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"} {
|
||||
capabilities[key] = true
|
||||
}
|
||||
capabilities["enter"], capabilities["ungroup"] = false, false
|
||||
capabilities["children"], capabilities["ungroup"] = false, false
|
||||
capabilities["stretch"] = false
|
||||
if capabilities["reasonCode"] == string(ofdgo.EditUnsupportedObject) && capabilities["transform"] == true && capabilities["copy"] == true && object.CompositeGraphicUnit.Clips == nil {
|
||||
delete(capabilities, "reason")
|
||||
@@ -5353,78 +5393,29 @@ func (e *businessEngine) copyObjects(args []platformValue) (any, error) {
|
||||
}
|
||||
|
||||
// captureObjects 保存独立选区快照,不修改文档或历史
|
||||
// 入参: args 页面索引、对象标识数组、剪贴板标识和可选父路径
|
||||
// 入参: args 页面索引、对象路径数组和剪贴板标识
|
||||
// 返回: any 空结果, error 错误信息
|
||||
func (e *businessEngine) captureObjects(args []platformValue) (any, error) {
|
||||
if e.currentEditor == nil {
|
||||
return nil, fmt.Errorf("no document is being edited")
|
||||
}
|
||||
clipboard := &editorClipboard{token: args[2].String()}
|
||||
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()
|
||||
}
|
||||
clipboard, err := e.captureEditorSelection(args[0].Int(), e.stringsFromJS(args[1]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clipboard.token = args[2].String()
|
||||
e.copiedObjects = clipboard
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// pasteObjects 将当前文档的对象快照粘贴到目标页
|
||||
// 入参: args 目标页、剪贴板标识、横纵位移和可选父路径
|
||||
// 入参: args 目标页、剪贴板标识和横纵位移
|
||||
// 返回: any 文档信息及新选区, error 错误信息
|
||||
func (e *businessEngine) pasteObjects(args []platformValue) (any, error) {
|
||||
if e.currentEditor == nil || e.copiedObjects == nil || e.copiedObjects.token != args[1].String() {
|
||||
return nil, fmt.Errorf("object clipboard is no longer available")
|
||||
}
|
||||
key := ""
|
||||
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)
|
||||
ids, err := e.pasteEditorSelection(args[0].Int(), e.copiedObjects, args[2].Float(), args[3].Float())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user