mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
This commit is contained in:
1 parent
896b00148a
commit
47562c4e7a
5 files changed
+521
-123
No files matched your search
+14
-5
@@ -1005,7 +1005,7 @@ function openObjectStyle() {
|
|||||||
const alpha = shared("alpha", 255), dash = shared("dashPattern", "");
|
const alpha = shared("alpha", 255), dash = shared("dashPattern", "");
|
||||||
el.objectOpacity.value = alpha === null ? "" : String(Math.round((1 - alpha / 255) * 10000) / 100);
|
el.objectOpacity.value = alpha === null ? "" : String(Math.round((1 - alpha / 255) * 10000) / 100);
|
||||||
el.objectOpacity.placeholder = alpha === null ? "混合" : "";
|
el.objectOpacity.placeholder = alpha === null ? "混合" : "";
|
||||||
el.objectStrokeFields.hidden = !items.every(item => item.type === "PathObject");
|
el.objectStrokeFields.hidden = Boolean(state.composite) || !items.every(item => item.type === "PathObject");
|
||||||
el.objectDash.value = dash === null ? "mixed" : dash === "" ? "solid" : "custom";
|
el.objectDash.value = dash === null ? "mixed" : dash === "" ? "solid" : "custom";
|
||||||
el.objectDashPattern.value = dash || "";
|
el.objectDashPattern.value = dash || "";
|
||||||
el.objectDashRow.hidden = el.objectDash.value !== "custom";
|
el.objectDashRow.hidden = el.objectDash.value !== "custom";
|
||||||
@@ -1704,6 +1704,13 @@ async function changeTextStyle(color) {
|
|||||||
updateObjectControls(canvasEditor.selected, true);
|
updateObjectControls(canvasEditor.selected, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (state.composite) {
|
||||||
|
if (color && fill !== null && canEditObject(item, "paint")) {
|
||||||
|
await changeDocument("ofdgoCompositeTextColor", item, fill);
|
||||||
|
}
|
||||||
|
updateObjectControls(canvasEditor.selected, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (item.items) {
|
if (item.items) {
|
||||||
if (color ? !canEditObject(item, "paint") : !canEditObject(item, "layoutKnown") || !canEditObject(item, "reflow")) return;
|
if (color ? !canEditObject(item, "paint") : !canEditObject(item, "layoutKnown") || !canEditObject(item, "reflow")) return;
|
||||||
if (!color && !el.textSize.value) return;
|
if (!color && !el.textSize.value) return;
|
||||||
@@ -1823,7 +1830,8 @@ async function changeDocument(name, item, ...args) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const scope = state.composite;
|
const scope = state.composite;
|
||||||
const operation = { ofdgoTransformObject: "transform", ofdgoTransformObjects: "transform", ofdgoRotateObjects: "rotate", ofdgoFlipObjects: "flip", ofdgoResizeObjects: "resize" }[name];
|
const operation = { ofdgoTransformObject: "transform", ofdgoTransformObjects: "transform", ofdgoRotateObjects: "rotate", ofdgoFlipObjects: "flip", ofdgoResizeObjects: "resize",
|
||||||
|
ofdgoAlignObject: "align", ofdgoAlignObjects: "align", ofdgoDistributeObjects: "distribute", ofdgoStyleObjects: "style", ofdgoUpdatePathStyle: "paint", ofdgoCompositeTextColor: "textColor" }[name];
|
||||||
const members = item?.items || (item ? [item] : []);
|
const members = item?.items || (item ? [item] : []);
|
||||||
const scoped = scope && members.length && members.every(member => member.scoped);
|
const scoped = scope && members.length && members.every(member => member.scoped);
|
||||||
if (!item) resetCompositeScope();
|
if (!item) resetCompositeScope();
|
||||||
@@ -5498,7 +5506,7 @@ function updateControls() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateEditorTools() {
|
function updateEditorTools() {
|
||||||
el.editButton.disabled = !state.doc || !state.ready || state.exporting;
|
el.editButton.disabled = !state.doc || !state.ready || state.exporting || document.body.hasAttribute("aria-busy");
|
||||||
el.editButton.setAttribute("aria-pressed", String(state.editing));
|
el.editButton.setAttribute("aria-pressed", String(state.editing));
|
||||||
el.editButton.title = state.editing ? "阅读" : "编辑";
|
el.editButton.title = state.editing ? "阅读" : "编辑";
|
||||||
el.editButton.setAttribute("aria-label", el.editButton.title);
|
el.editButton.setAttribute("aria-label", el.editButton.title);
|
||||||
@@ -5532,7 +5540,7 @@ function updateObjectControls(item, reset = false) {
|
|||||||
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");
|
el.copyObjectButton.disabled = disabled || !canEditObject(item, "copy");
|
||||||
el.objectStyleButton.disabled = disabled || Boolean(state.composite) || !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");
|
||||||
@@ -5551,7 +5559,7 @@ function updateObjectControls(item, reset = false) {
|
|||||||
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;
|
||||||
const textDisabled = !state.editing || !state.ready || state.exporting || Boolean(state.composite) || Boolean(item?.items && !selection);
|
const textDisabled = !state.editing || !state.ready || state.exporting || Boolean(state.composite && !selection) || Boolean(item?.items && !selection);
|
||||||
fontPicker.setDisabled(textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "replaceFont"));
|
fontPicker.setDisabled(textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "replaceFont"));
|
||||||
el.textSize.disabled = textDisabled || text !== state.textDefaults && !item.draft && (!canEditObject(item, "reflow") || Boolean(item?.items) && !canEditObject(item, "layoutKnown"));
|
el.textSize.disabled = textDisabled || text !== state.textDefaults && !item.draft && (!canEditObject(item, "reflow") || Boolean(item?.items) && !canEditObject(item, "layoutKnown"));
|
||||||
el.textColor.disabled = textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "paint");
|
el.textColor.disabled = textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "paint");
|
||||||
@@ -5659,6 +5667,7 @@ function showError(err, empty = !state.doc) {
|
|||||||
|
|
||||||
function setBusy(busy, text = "", percent = 0, status = "") {
|
function setBusy(busy, text = "", percent = 0, status = "") {
|
||||||
document.body.toggleAttribute("aria-busy", busy);
|
document.body.toggleAttribute("aria-busy", busy);
|
||||||
|
el.editButton.disabled = busy || !state.doc || !state.ready || state.exporting;
|
||||||
el.createForm.inert = busy;
|
el.createForm.inert = busy;
|
||||||
el.insertForm.inert = busy;
|
el.insertForm.inert = busy;
|
||||||
el.pageForm.inert = busy;
|
el.pageForm.inert = busy;
|
||||||
|
|||||||
+114
-41
@@ -761,34 +761,10 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
item := map[string]any{"id": id, "type": object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": order, "position": position.Index, "count": position.Count, "container": position.Container,
|
item := map[string]any{"id": id, "type": object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": order, "position": position.Index, "count": position.Count, "container": position.Container,
|
||||||
"capabilities": 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)}}
|
"capabilities": editorCapabilities(capability, object.Type)}
|
||||||
if missing := capability.MissingGlyphs; missing != nil {
|
editorAppearance(item, object, capability.Paint, editorPathScale(object.PathObject))
|
||||||
item["capabilities"].(map[string]any)["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
|
|
||||||
}
|
|
||||||
item["capabilities"].(map[string]any)["enter"] = capability.Transform && (object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit")
|
|
||||||
var alpha *int
|
|
||||||
switch object.Type {
|
|
||||||
case "TextObject":
|
|
||||||
alpha = object.TextObject.Alpha
|
|
||||||
case "ImageObject":
|
|
||||||
alpha = object.ImageObject.Alpha
|
|
||||||
item["imageBorder"] = object.ImageObject.Border != nil
|
|
||||||
case "PathObject":
|
|
||||||
alpha = object.PathObject.Alpha
|
|
||||||
case "CompositeObject", "CompositeGraphicUnit":
|
|
||||||
alpha = object.CompositeGraphicUnit.Alpha
|
|
||||||
}
|
|
||||||
item["alpha"] = 255
|
|
||||||
if alpha != nil {
|
|
||||||
item["alpha"] = *alpha
|
|
||||||
}
|
|
||||||
if object.Type == "PathObject" {
|
if object.Type == "PathObject" {
|
||||||
path := object.PathObject
|
path := object.PathObject
|
||||||
item["dashPattern"], item["cap"], item["join"] = path.DashPattern, path.Cap, path.Join
|
|
||||||
item["dashOffset"] = 0.0
|
|
||||||
if path.DashOffset != nil {
|
|
||||||
item["dashOffset"] = *path.DashOffset
|
|
||||||
}
|
|
||||||
kind, geometry := path.Shape()
|
kind, geometry := path.Shape()
|
||||||
if kind != "" && capability.Update {
|
if kind != "" && capability.Update {
|
||||||
item["shape"] = string(kind)
|
item["shape"] = string(kind)
|
||||||
@@ -796,12 +772,6 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
|||||||
} else if outline, err := path.Outline(); err == nil {
|
} else if outline, err := path.Outline(); err == nil {
|
||||||
item["outline"] = outline
|
item["outline"] = outline
|
||||||
}
|
}
|
||||||
if capability.Paint {
|
|
||||||
item["fill"], item["stroke"] = path.Fill != nil && *path.Fill, path.Stroke == nil || *path.Stroke
|
|
||||||
item["fillColor"] = editorColorHex(path.FillColor)
|
|
||||||
item["strokeColor"] = editorColorHex((*ofdgo.FillColor)(path.StrokeColor))
|
|
||||||
item["lineWidth"] = path.LineWidth * editorPathScale(path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if object.Type == "PathObject" || object.Type == "ImageObject" || object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
|
if object.Type == "PathObject" || object.Type == "ImageObject" || object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
|
||||||
paths := make([]any, len(contours))
|
paths := make([]any, len(contours))
|
||||||
@@ -849,9 +819,6 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
|||||||
second, _ := strconv.ParseFloat(codes[1].Y, 64)
|
second, _ := strconv.ParseFloat(codes[1].Y, 64)
|
||||||
item["lineHeight"] = second - first
|
item["lineHeight"] = second - first
|
||||||
}
|
}
|
||||||
if capability.Paint {
|
|
||||||
item["color"] = editorColorHex(object.TextObject.FillColor)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
objects = append(objects, item)
|
objects = append(objects, item)
|
||||||
}
|
}
|
||||||
@@ -860,6 +827,52 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
|||||||
return objects, nil
|
return objects, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editorCapabilities 统一顶层与内部对象的操作能力和受限原因
|
||||||
|
// 入参: capability 库层能力, kind 对象类型
|
||||||
|
// 返回: map[string]any 前端能力
|
||||||
|
func 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),
|
||||||
|
"enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
|
||||||
|
if missing := capability.MissingGlyphs; missing != nil {
|
||||||
|
result["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorAppearance 提供对象自身透明度及有效绘制外观,不混入父对象透明度
|
||||||
|
// 入参: item 前端对象, object 有效样式, paint 是否可改色, scale 描边页面倍率
|
||||||
|
func editorAppearance(item map[string]any, object ofdgo.GraphicObject, paint bool, scale float64) {
|
||||||
|
var alpha *int
|
||||||
|
switch object.Type {
|
||||||
|
case "TextObject":
|
||||||
|
alpha = object.TextObject.Alpha
|
||||||
|
if paint {
|
||||||
|
item["color"] = editorColorHex(object.TextObject.FillColor)
|
||||||
|
}
|
||||||
|
case "ImageObject":
|
||||||
|
alpha = object.ImageObject.Alpha
|
||||||
|
item["imageBorder"] = object.ImageObject.Border != nil
|
||||||
|
case "PathObject":
|
||||||
|
path := object.PathObject
|
||||||
|
alpha = path.Alpha
|
||||||
|
item["dashPattern"], item["cap"], item["join"], item["dashOffset"] = path.DashPattern, path.Cap, path.Join, 0.0
|
||||||
|
if path.DashOffset != nil {
|
||||||
|
item["dashOffset"] = *path.DashOffset
|
||||||
|
}
|
||||||
|
if paint {
|
||||||
|
item["fill"], item["stroke"] = path.Fill != nil && *path.Fill, path.Stroke == nil || *path.Stroke
|
||||||
|
item["fillColor"], item["strokeColor"] = editorColorHex(path.FillColor), editorColorHex((*ofdgo.FillColor)(path.StrokeColor))
|
||||||
|
item["lineWidth"] = path.LineWidth * scale
|
||||||
|
}
|
||||||
|
case "CompositeObject", "CompositeGraphicUnit":
|
||||||
|
alpha = object.CompositeGraphicUnit.Alpha
|
||||||
|
}
|
||||||
|
item["alpha"] = 255
|
||||||
|
if alpha != nil {
|
||||||
|
item["alpha"] = *alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// compositePath 解析画布复合对象路径
|
// compositePath 解析画布复合对象路径
|
||||||
// 入参: value 顶层标识与逐层序号
|
// 入参: value 顶层标识与逐层序号
|
||||||
// 返回: ofdgo.ObjectPath 库层路径, error 错误信息
|
// 返回: ofdgo.ObjectPath 库层路径, error 错误信息
|
||||||
@@ -892,14 +905,29 @@ func compositeObjects(args []js.Value) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
fonts, err := currentSession.Reader.Fonts()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fontNames := make(map[string]string, len(fonts))
|
||||||
|
for _, font := range fonts {
|
||||||
|
fontNames[font.ID] = font.FontName
|
||||||
|
}
|
||||||
objects := make([]any, 0, len(members))
|
objects := make([]any, 0, len(members))
|
||||||
for i, member := range members {
|
for i, member := range members {
|
||||||
box := member.Bounds
|
box := member.Bounds
|
||||||
if box.W <= 0 || box.H <= 0 {
|
if box.W <= 0 || box.H <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
objects = append(objects, 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": i, "count": len(members), "container": key, "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": i, "count": len(members), "container": key, "scoped": true, "contours": member.Contours,
|
||||||
"capabilities": map[string]any{"transform": member.Transform, "enter": member.Object.Type == "CompositeObject" || member.Object.Type == "CompositeGraphicUnit"}})
|
"capabilities": editorCapabilities(member.Capabilities, member.Object.Type)}
|
||||||
|
editorAppearance(item, member.Object, member.Capabilities.Paint, member.StrokeScale)
|
||||||
|
if member.Object.Type == "TextObject" {
|
||||||
|
object := member.Object.TextObject
|
||||||
|
item["text"], _ = object.TextLayout()
|
||||||
|
item["font"], item["fontName"], item["size"] = object.Font, fontNames[object.Font], object.Size
|
||||||
|
}
|
||||||
|
objects = append(objects, item)
|
||||||
}
|
}
|
||||||
return objects, nil
|
return objects, nil
|
||||||
}
|
}
|
||||||
@@ -927,7 +955,8 @@ func changeCompositeObjects(args []js.Value) (any, error) {
|
|||||||
indexes = append(indexes, i)
|
indexes = append(indexes, i)
|
||||||
}
|
}
|
||||||
page := args[0].Int()
|
page := args[0].Int()
|
||||||
switch args[3].String() {
|
operation := args[3].String()
|
||||||
|
switch operation {
|
||||||
case "transform":
|
case "transform":
|
||||||
return currentEditor.TransformCompositeObjects(page, path, indexes, args[4].Float(), args[5].Float(), args[6].Float())
|
return currentEditor.TransformCompositeObjects(page, path, indexes, args[4].Float(), args[5].Float(), args[6].Float())
|
||||||
case "rotate":
|
case "rotate":
|
||||||
@@ -936,6 +965,44 @@ func changeCompositeObjects(args []js.Value) (any, error) {
|
|||||||
return currentEditor.FlipCompositeObjects(page, path, indexes, args[4].String())
|
return currentEditor.FlipCompositeObjects(page, path, indexes, args[4].String())
|
||||||
case "resize":
|
case "resize":
|
||||||
return currentEditor.ResizeCompositeObjects(page, path, indexes, ofdgo.Box{X: args[4].Float(), Y: args[5].Float(), W: args[6].Float(), H: args[7].Float()})
|
return currentEditor.ResizeCompositeObjects(page, path, indexes, ofdgo.Box{X: args[4].Float(), Y: args[5].Float(), W: args[6].Float(), H: args[7].Float()})
|
||||||
|
case "align":
|
||||||
|
return currentEditor.AlignCompositeObjects(page, path, indexes, args[4].String())
|
||||||
|
case "distribute":
|
||||||
|
return currentEditor.DistributeCompositeObjects(page, path, indexes, args[4].String())
|
||||||
|
case "style":
|
||||||
|
return currentEditor.StyleCompositeObjects(page, path, indexes, objectStyle(args[4]))
|
||||||
|
case "paint", "textColor":
|
||||||
|
style := ofdgo.ObjectStyle{}
|
||||||
|
if operation == "textColor" {
|
||||||
|
style.FillColor = &ofdgo.FillColor{}
|
||||||
|
if err := 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 := 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 currentEditor.StyleCompositeObjects(page, path, indexes, style)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported composite operation")
|
return fmt.Errorf("unsupported composite operation")
|
||||||
}
|
}
|
||||||
@@ -1321,8 +1388,15 @@ func changeOutline(args []js.Value) (any, error) {
|
|||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func styleObjects(args []js.Value) (any, error) {
|
func styleObjects(args []js.Value) (any, error) {
|
||||||
return changeObjects(func() error {
|
return changeObjects(func() error {
|
||||||
|
return currentEditor.StyleObjects(args[0].Int(), stringsFromJS(args[1]), objectStyle(args[2]))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// objectStyle 读取高级面板提交的外观字段,省略字段保持原值
|
||||||
|
// 入参: value 前端样式
|
||||||
|
// 返回: ofdgo.ObjectStyle 库层样式
|
||||||
|
func objectStyle(value js.Value) ofdgo.ObjectStyle {
|
||||||
style := ofdgo.ObjectStyle{}
|
style := ofdgo.ObjectStyle{}
|
||||||
value := args[2]
|
|
||||||
if field := value.Get("alpha"); !field.IsUndefined() {
|
if field := value.Get("alpha"); !field.IsUndefined() {
|
||||||
v := field.Int()
|
v := field.Int()
|
||||||
style.Alpha = &v
|
style.Alpha = &v
|
||||||
@@ -1343,8 +1417,7 @@ func styleObjects(args []js.Value) (any, error) {
|
|||||||
v := field.String()
|
v := field.String()
|
||||||
style.Join = &v
|
style.Join = &v
|
||||||
}
|
}
|
||||||
return currentEditor.StyleObjects(args[0].Int(), stringsFromJS(args[1]), style)
|
return style
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clearImport 释放待插页文档
|
// clearImport 释放待插页文档
|
||||||
|
|||||||
+66
-28
@@ -268,6 +268,39 @@ func (e *Editor) DistributeObjects(page int, ids []string, axis string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
matrices, err := editorDistribution(boxes, indexes, axis)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var updates []GraphicObject
|
||||||
|
for i, matrix := range matrices {
|
||||||
|
if matrix == IdentityMatrix {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
object, err := cloneEditorObject(objects[i])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
object, err = e.transformObject(object, matrix.e, matrix.f, 1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updates = append(updates, object)
|
||||||
|
}
|
||||||
|
return e.updateObjects(page, updates, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorDistribution 计算等间距平移,位置相同时按原绘制顺序排序
|
||||||
|
// 入参: boxes 可见范围, indexes 绘制位置, axis 分布方向
|
||||||
|
// 返回: []Matrix 页面平移, error 错误信息
|
||||||
|
func editorDistribution(boxes []Box, indexes []editorObjectPosition, axis string) ([]Matrix, error) {
|
||||||
|
matrices := make([]Matrix, len(boxes))
|
||||||
|
for i := range matrices {
|
||||||
|
matrices[i] = IdentityMatrix
|
||||||
|
}
|
||||||
|
if len(boxes) < 3 {
|
||||||
|
return matrices, nil
|
||||||
|
}
|
||||||
positions, sizes := make([]float64, len(boxes)), make([]float64, len(boxes))
|
positions, sizes := make([]float64, len(boxes)), make([]float64, len(boxes))
|
||||||
order := make([]int, len(boxes))
|
order := make([]int, len(boxes))
|
||||||
total := 0.0
|
total := 0.0
|
||||||
@@ -293,11 +326,10 @@ func (e *Editor) DistributeObjects(page int, ids []string, axis string) error {
|
|||||||
step = 0
|
step = 0
|
||||||
}
|
}
|
||||||
if step < 0 || step == 0 && compareEditorPosition(indexes[index], indexes[order[i+1]]) > 0 {
|
if step < 0 || step == 0 && compareEditorPosition(indexes[index], indexes[order[i+1]]) > 0 {
|
||||||
return fmt.Errorf("overlap prevents ordered equal-gap distribution")
|
return nil, fmt.Errorf("overlap prevents ordered equal-gap distribution")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
next := positions[first] + sizes[first] + gap
|
next := positions[first] + sizes[first] + gap
|
||||||
updates := make([]GraphicObject, 0, len(order)-2)
|
|
||||||
for _, index := range order[1 : len(order)-1] {
|
for _, index := range order[1 : len(order)-1] {
|
||||||
position := next
|
position := next
|
||||||
next += sizes[index] + gap
|
next += sizes[index] + gap
|
||||||
@@ -308,17 +340,9 @@ func (e *Editor) DistributeObjects(page int, ids []string, axis string) error {
|
|||||||
if axis == "vertical" {
|
if axis == "vertical" {
|
||||||
dx, dy = 0, dx
|
dx, dy = 0, dx
|
||||||
}
|
}
|
||||||
object, err := cloneEditorObject(objects[index])
|
matrices[index] = TranslationMatrix(dx, dy)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
object, err = e.transformObject(object, dx, dy, 1)
|
return matrices, nil
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
updates = append(updates, object)
|
|
||||||
}
|
|
||||||
return e.updateObjects(page, updates, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// replaceLayers 替换图层容器,隔离历史快照与后续的增删、排序操作
|
// replaceLayers 替换图层容器,隔离历史快照与后续的增删、排序操作
|
||||||
@@ -504,26 +528,12 @@ func (e *Editor) AlignObjects(page int, ids []string, alignment string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, box := range boxes {
|
for i, box := range boxes {
|
||||||
dx, dy := 0.0, 0.0
|
matrix := editorAlignment(box, target, alignment)
|
||||||
switch alignment {
|
|
||||||
case "left":
|
|
||||||
dx = target.X - box.X
|
|
||||||
case "center":
|
|
||||||
dx = target.X + (target.W-box.W)/2 - box.X
|
|
||||||
case "right":
|
|
||||||
dx = target.X + target.W - box.W - box.X
|
|
||||||
case "top":
|
|
||||||
dy = target.Y - box.Y
|
|
||||||
case "middle":
|
|
||||||
dy = target.Y + (target.H-box.H)/2 - box.Y
|
|
||||||
case "bottom":
|
|
||||||
dy = target.Y + target.H - box.H - box.Y
|
|
||||||
}
|
|
||||||
objects[i], err = cloneEditorObject(objects[i])
|
objects[i], err = cloneEditorObject(objects[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
objects[i], err = e.transformObject(objects[i], dx, dy, 1)
|
objects[i], err = e.transformObject(objects[i], matrix.e, matrix.f, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -531,6 +541,34 @@ func (e *Editor) AlignObjects(page int, ids []string, alignment string) error {
|
|||||||
return e.updateObjects(page, objects, true)
|
return e.updateObjects(page, objects, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editorAlignment 计算对象范围到目标边缘或中心的平移
|
||||||
|
// 入参: box 对象范围, target 目标范围, alignment 对齐方式
|
||||||
|
// 返回: Matrix 页面平移
|
||||||
|
func editorAlignment(box, target Box, alignment string) Matrix {
|
||||||
|
matrix := IdentityMatrix
|
||||||
|
switch alignment {
|
||||||
|
case "left":
|
||||||
|
matrix.e = target.X - box.X
|
||||||
|
case "center":
|
||||||
|
matrix.e = target.X + (target.W-box.W)/2 - box.X
|
||||||
|
case "right":
|
||||||
|
matrix.e = target.X + target.W - box.W - box.X
|
||||||
|
case "top":
|
||||||
|
matrix.f = target.Y - box.Y
|
||||||
|
case "middle":
|
||||||
|
matrix.f = target.Y + (target.H-box.H)/2 - box.Y
|
||||||
|
case "bottom":
|
||||||
|
matrix.f = target.Y + target.H - box.H - box.Y
|
||||||
|
}
|
||||||
|
if canvas.Equal(matrix.e, 0) {
|
||||||
|
matrix.e = 0
|
||||||
|
}
|
||||||
|
if canvas.Equal(matrix.f, 0) {
|
||||||
|
matrix.f = 0
|
||||||
|
}
|
||||||
|
return matrix
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteObjects 原子删除同页对象,保留其余对象顺序,一次撤销恢复全部
|
// DeleteObjects 原子删除同页对象,保留其余对象顺序,一次撤销恢复全部
|
||||||
// 入参: page 页面索引, ids 对象标识
|
// 入参: page 页面索引, ids 对象标识
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
|
|||||||
+231
-36
@@ -32,11 +32,14 @@ type ObjectPath struct {
|
|||||||
Children []int `json:"children,omitempty"`
|
Children []int `json:"children,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompositeMember 复合对象的直接成员快照,Bounds与Contours使用页面毫米坐标
|
// CompositeMember 复合对象的直接成员快照,Object包含有效样式,不用于整体替换原对象
|
||||||
|
// Bounds与Contours使用页面毫米坐标,StrokeScale为对象描边到页面的倍率
|
||||||
type CompositeMember struct {
|
type CompositeMember struct {
|
||||||
Object GraphicObject
|
Object GraphicObject
|
||||||
Bounds Box
|
Bounds Box
|
||||||
Contours []ObjectContour
|
Contours []ObjectContour
|
||||||
|
StrokeScale float64
|
||||||
|
Capabilities ObjectCapabilities
|
||||||
Transform bool
|
Transform bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,8 +51,8 @@ type editorCompositeNode struct {
|
|||||||
parent Matrix
|
parent Matrix
|
||||||
boundaryInCTM bool
|
boundaryInCTM bool
|
||||||
defaults *DrawParam
|
defaults *DrawParam
|
||||||
|
drawParams []string
|
||||||
clip *canvas.Path
|
clip *canvas.Path
|
||||||
alpha *int
|
|
||||||
visible bool
|
visible bool
|
||||||
ref *editorCompositeNode
|
ref *editorCompositeNode
|
||||||
children []*editorCompositeNode
|
children []*editorCompositeNode
|
||||||
@@ -67,7 +70,7 @@ func (e *Editor) CompositeObjects(page int, path ObjectPath) ([]CompositeMember,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
return measureCompositeMembers(renderer, members), nil
|
return e.measureCompositeMembers(renderer, members), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransformCompositeObjects 在页面坐标中等比缩放并平移内部选区,只隔离被修改实例的资源
|
// TransformCompositeObjects 在页面坐标中等比缩放并平移内部选区,只隔离被修改实例的资源
|
||||||
@@ -177,6 +180,7 @@ func (e *Editor) compositeScope(page int, path ObjectPath) (*Reader, *Renderer,
|
|||||||
}
|
}
|
||||||
renderer := NewRenderer(reader, WithFontFS(e.fontFS...))
|
renderer := NewRenderer(reader, WithFontFS(e.fontFS...))
|
||||||
root.parent, root.visible, root.defaults = IdentityMatrix, true, renderer.drawParamDefaults(layer.DrawParam, nil)
|
root.parent, root.visible, root.defaults = IdentityMatrix, true, renderer.drawParamDefaults(layer.DrawParam, nil)
|
||||||
|
root.drawParams = []string{layer.DrawParam}
|
||||||
scope := root
|
scope := root
|
||||||
for depth := 0; ; depth++ {
|
for depth := 0; ; depth++ {
|
||||||
var members []*editorCompositeNode
|
var members []*editorCompositeNode
|
||||||
@@ -243,7 +247,7 @@ func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiti
|
|||||||
}
|
}
|
||||||
clip := intersectClipPath(n.clip, renderer.buildClipPath(clips, 0, 0, 0, clipMatrix))
|
clip := intersectClipPath(n.clip, renderer.buildClipPath(clips, 0, 0, 0, clipMatrix))
|
||||||
defaults := renderer.drawParamDefaults(c.DrawParam, n.defaults)
|
defaults := renderer.drawParamDefaults(c.DrawParam, n.defaults)
|
||||||
alpha := mergeAlpha(c.Alpha, n.alpha)
|
drawParams := append(slices.Clone(n.drawParams), c.DrawParam)
|
||||||
visible := n.visible && (c.Visible == nil || *c.Visible)
|
visible := n.visible && (c.Visible == nil || *c.Visible)
|
||||||
if c.ResourceID != "" {
|
if c.ResourceID != "" {
|
||||||
data, err := reader.readFile(reader.resourceFiles[c.ResourceID])
|
data, err := reader.readFile(reader.resourceFiles[c.ResourceID])
|
||||||
@@ -274,7 +278,8 @@ func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiti
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
n.ref.parent, n.ref.boundaryInCTM, n.ref.defaults, n.ref.clip, n.ref.alpha, n.ref.visible = matrix, true, defaults, clip, alpha, visible
|
n.ref.parent, n.ref.boundaryInCTM, n.ref.defaults, n.ref.clip, n.ref.visible = matrix, true, defaults, clip, visible
|
||||||
|
n.ref.drawParams = drawParams
|
||||||
}
|
}
|
||||||
var collect func(*editorXML) error
|
var collect func(*editorXML) error
|
||||||
collect = func(container *editorXML) error {
|
collect = func(container *editorXML) error {
|
||||||
@@ -293,7 +298,8 @@ func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiti
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
item.parent, item.boundaryInCTM, item.defaults, item.clip, item.alpha, item.visible, item.span = matrix, n.boundaryInCTM, defaults, clip, alpha, visible, child
|
item.parent, item.boundaryInCTM, item.defaults, item.clip, item.visible, item.span = matrix, n.boundaryInCTM, defaults, clip, visible, child
|
||||||
|
item.drawParams = drawParams
|
||||||
n.children = append(n.children, item)
|
n.children = append(n.children, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,53 +327,223 @@ func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiti
|
|||||||
return append(result, n.children...), nil
|
return append(result, n.children...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// measureCompositeMembers 按实际父变换、裁剪和继承样式度量成员
|
// measureCompositeMembers 按父变换、裁剪和继承样式度量成员,透明对象保留选区,未着色路径按轮廓度量
|
||||||
// 入参: renderer 渲染器, nodes 成员
|
// 入参: renderer 渲染器, nodes 成员
|
||||||
// 返回: []CompositeMember 独立快照及操作能力
|
// 返回: []CompositeMember 独立快照及操作能力
|
||||||
func measureCompositeMembers(renderer *Renderer, nodes []*editorCompositeNode) []CompositeMember {
|
func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorCompositeNode) []CompositeMember {
|
||||||
result := make([]CompositeMember, len(nodes))
|
result := make([]CompositeMember, len(nodes))
|
||||||
for i, node := range nodes {
|
for i, node := range nodes {
|
||||||
bounds := &boundsRenderer{collect: true}
|
bounds := &boundsRenderer{collect: true}
|
||||||
object := mergeGraphicObjectAlpha(node.object, node.alpha)
|
object := node.object
|
||||||
|
object.TextObject.Alpha, object.PathObject.Alpha = nil, nil
|
||||||
|
object.ImageObject.Alpha, object.CompositeGraphicUnit.Alpha = nil, nil
|
||||||
|
if object.Type == "PathObject" && object.PathObject.Stroke != nil && !*object.PathObject.Stroke && (object.PathObject.Fill == nil || !*object.PathObject.Fill) {
|
||||||
|
object.PathObject.Stroke = nil
|
||||||
|
}
|
||||||
if node.visible {
|
if node.visible {
|
||||||
renderer.renderObject(canvas.NewContext(bounds), &object, 0, node.defaults, &node.parent, node.boundaryInCTM, node.clip)
|
renderer.renderObject(canvas.NewContext(bounds), &object, 0, node.defaults, &node.parent, node.boundaryInCTM, node.clip)
|
||||||
}
|
}
|
||||||
_, invertible := node.parent.Invert()
|
_, invertible := node.parent.Invert()
|
||||||
borderActions := node.object.Type == "ImageObject" && node.object.ImageObject.Border != nil && len(node.object.ImageObject.Actions) != 0
|
borderActions := node.object.Type == "ImageObject" && node.object.ImageObject.Border != nil && len(node.object.ImageObject.Actions) != 0
|
||||||
result[i] = CompositeMember{Object: cloneEditorData(node.object), Bounds: bounds.box, Contours: bounds.contours, Transform: invertible && editorXMLTransformable(node.node) && validateEditorGeometry(node.object) == nil && !borderActions}
|
transform := invertible && editorXMLTransformable(node.node) && validateEditorGeometry(node.object) == nil && !borderActions
|
||||||
|
capability := ObjectCapabilities{Transform: transform, Arrange: transform && bounds.box.W > 0 && bounds.box.H > 0}
|
||||||
|
style, err := e.compositeMemberStyle(node)
|
||||||
|
if !transform {
|
||||||
|
capability.ReasonCode, capability.Reason = EditUnsupportedContainer, "composite member cannot be transformed"
|
||||||
|
} else if err != nil {
|
||||||
|
capability.ReasonCode, capability.Reason = editReason(err), err.Error()
|
||||||
|
} else {
|
||||||
|
capability.Paint = e.editorPaintable(style)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
style = cloneEditorData(node.object)
|
||||||
|
}
|
||||||
|
_, ctm := editorGeometry(node.object)
|
||||||
|
matrix := node.parent.Multiply(NewMatrix(ctm))
|
||||||
|
result[i] = CompositeMember{Object: style, Bounds: bounds.box, Contours: bounds.contours, StrokeScale: math.Sqrt(math.Abs(matrix.a*matrix.d - matrix.b*matrix.c)), Capabilities: capability, Transform: transform}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compositeMemberStyle 解析成员的继承样式,缺失或循环参数不开放改色
|
||||||
|
// 入参: node 成员节点
|
||||||
|
// 返回: GraphicObject 有效外观, error 错误信息
|
||||||
|
func (e *Editor) compositeMemberStyle(node *editorCompositeNode) (GraphicObject, error) {
|
||||||
|
for _, id := range node.drawParams {
|
||||||
|
if _, err := e.editorDrawParam(id, make(map[string]bool)); err != nil {
|
||||||
|
return GraphicObject{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base := node.defaults
|
||||||
|
if base == nil {
|
||||||
|
base = &DrawParam{}
|
||||||
|
}
|
||||||
|
object, err := e.resolveEditorStyleDefaults(node.object, base)
|
||||||
|
if err == nil && object.Type == "PathObject" && object.PathObject.LineWidth == 0 {
|
||||||
|
object.PathObject.LineWidth = defaultPathLineWidth
|
||||||
|
}
|
||||||
|
return object, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleCompositeObjects 原子修改成员透明度、文字颜色及路径填充、描边和线宽
|
||||||
|
// 不重排文字,不改写继承参数;暂不支持虚线及端点样式
|
||||||
|
// 入参: page 页面索引, path 父路径, indexes 成员序号, style 待修改属性,线宽使用页面毫米
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) StyleCompositeObjects(page int, path ObjectPath, indexes []int, style ObjectStyle) error {
|
||||||
|
if style.DashPattern != nil || style.DashOffset != nil || style.Cap != nil || style.Join != nil {
|
||||||
|
return fmt.Errorf("composite stroke parameters are not supported")
|
||||||
|
}
|
||||||
|
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
|
paint := style.Fill != nil || style.Stroke != nil || style.FillColor != nil || style.StrokeColor != nil || style.LineWidth != nil
|
||||||
|
for i, node := range nodes {
|
||||||
|
capability := members[i].Capabilities
|
||||||
|
if !capability.Transform || paint && !capability.Paint {
|
||||||
|
return fmt.Errorf("composite member %d cannot accept this style: %w", indexes[i], capability.editError())
|
||||||
|
}
|
||||||
|
local := style
|
||||||
|
switch members[i].Object.Type {
|
||||||
|
case "TextObject":
|
||||||
|
local.FillColor = editorStyleColor(style.FillColor, members[i].Object.TextObject.FillColor)
|
||||||
|
case "PathObject":
|
||||||
|
p := members[i].Object.PathObject
|
||||||
|
local.FillColor = editorStyleColor(style.FillColor, p.FillColor)
|
||||||
|
local.StrokeColor = (*StrokeColor)(editorStyleColor((*FillColor)(style.StrokeColor), (*FillColor)(p.StrokeColor)))
|
||||||
|
if style.LineWidth != nil {
|
||||||
|
width := *style.LineWidth / members[i].StrokeScale
|
||||||
|
local.LineWidth = &width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object, err := e.styleObject(node.object, local)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data, err := editorXMLObject(node.data, node.node, node.object, object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if bytes.Equal(data, node.data) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
updated, err := newEditorCompositeNode(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
node.data, node.node, node.object, node.changed = data, updated.node, updated.object, true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlignCompositeObjects 将内部多选成员对齐至选区边界,单选时对齐页面
|
||||||
|
// 入参: page 页面索引, path 父路径, indexes 成员序号, alignment 为left、center、right、top、middle或bottom
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) AlignCompositeObjects(page int, path ObjectPath, indexes []int, alignment string) error {
|
||||||
|
if !slices.Contains([]string{"left", "center", "right", "top", "middle", "bottom"}, alignment) {
|
||||||
|
return fmt.Errorf("invalid alignment %q", alignment)
|
||||||
|
}
|
||||||
|
return e.arrangeCompositeObjects(page, path, indexes, func(boxes []Box) ([]Matrix, error) {
|
||||||
|
target, _ := ParseBox(e.pages[page].Area.PhysicalBox)
|
||||||
|
if len(boxes) > 1 {
|
||||||
|
target = Box{}
|
||||||
|
for _, box := range boxes {
|
||||||
|
target = unionTextBox(target, box)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matrices := make([]Matrix, len(boxes))
|
||||||
|
for i, box := range boxes {
|
||||||
|
matrices[i] = editorAlignment(box, target, alignment)
|
||||||
|
}
|
||||||
|
return matrices, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistributeCompositeObjects 按可见范围等距分布内部成员,固定两端并保留绘制顺序
|
||||||
|
// 入参: page 页面索引, path 父路径, indexes 成员序号, axis 为horizontal或vertical
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) DistributeCompositeObjects(page int, path ObjectPath, indexes []int, axis string) error {
|
||||||
|
if axis != "horizontal" && axis != "vertical" {
|
||||||
|
return fmt.Errorf("invalid distribution axis %q", axis)
|
||||||
|
}
|
||||||
|
return e.arrangeCompositeObjects(page, path, indexes, func(boxes []Box) ([]Matrix, error) {
|
||||||
|
positions := make([]editorObjectPosition, len(indexes))
|
||||||
|
for i, index := range indexes {
|
||||||
|
positions[i].index = index
|
||||||
|
}
|
||||||
|
return editorDistribution(boxes, positions, axis)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// arrangeCompositeObjects 按成员实际轮廓计算独立页面变换并提交一次历史记录
|
||||||
|
// 入参: page 页面索引, path 父路径, indexes 成员序号, arrange 排列计算
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) arrangeCompositeObjects(page int, path ObjectPath, indexes []int, arrange func([]Box) ([]Matrix, error)) error {
|
||||||
|
return e.editCompositeObjects(page, path, indexes, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
|
boxes := make([]Box, len(members))
|
||||||
|
for i, member := range members {
|
||||||
|
if !member.Capabilities.Arrange {
|
||||||
|
return fmt.Errorf("composite member has no editable bounds")
|
||||||
|
}
|
||||||
|
boxes[i] = member.Bounds
|
||||||
|
}
|
||||||
|
matrices, err := arrange(boxes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, matrix := range matrices {
|
||||||
|
if err := e.transformCompositeMember(renderer, nodes[i], matrix); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// changeCompositeObjects 原子变换内部选区,沿改动路径复制共享资源
|
// changeCompositeObjects 原子变换内部选区,沿改动路径复制共享资源
|
||||||
// 入参: page 页面索引, path 父路径, indexes 成员序号, transform 页面变换生成器
|
// 入参: page 页面索引, path 父路径, indexes 成员序号, transform 页面变换生成器
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) changeCompositeObjects(page int, path ObjectPath, indexes []int, transform func(Box) Matrix) (err error) {
|
func (e *Editor) changeCompositeObjects(page int, path ObjectPath, indexes []int, transform func(Box) Matrix) error {
|
||||||
|
return e.editCompositeObjects(page, path, indexes, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
|
var box Box
|
||||||
|
for i := range nodes {
|
||||||
|
if !members[i].Capabilities.Transform {
|
||||||
|
return members[i].Capabilities.editError()
|
||||||
|
}
|
||||||
|
box = unionTextBox(box, members[i].Bounds)
|
||||||
|
}
|
||||||
|
matrix := transform(box)
|
||||||
|
for _, node := range nodes {
|
||||||
|
if err := e.transformCompositeMember(renderer, node, matrix); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// editCompositeObjects 校验选区并原子提交成员改动,失败时回收本次分配的资源
|
||||||
|
// 入参: page 页面索引, path 父路径, indexes 成员序号, edit 修改回调
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) editCompositeObjects(page int, path ObjectPath, indexes []int, edit func(*Renderer, []*editorCompositeNode, []CompositeMember) error) (err error) {
|
||||||
reader, renderer, root, nodes, err := e.compositeScope(page, path)
|
reader, renderer, root, nodes, err := e.compositeScope(page, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
members := measureCompositeMembers(renderer, nodes)
|
members := e.measureCompositeMembers(renderer, nodes)
|
||||||
selected := make(map[int]bool)
|
selected := make(map[int]bool)
|
||||||
var box Box
|
var selectedNodes []*editorCompositeNode
|
||||||
|
var selectedMembers []CompositeMember
|
||||||
for _, i := range indexes {
|
for _, i := range indexes {
|
||||||
if i < 0 || i >= len(nodes) || selected[i] {
|
if i < 0 || i >= len(nodes) || selected[i] {
|
||||||
return fmt.Errorf("invalid composite selection")
|
return fmt.Errorf("invalid composite selection")
|
||||||
}
|
}
|
||||||
if !members[i].Transform {
|
|
||||||
return fmt.Errorf("composite member %d cannot be transformed", i)
|
|
||||||
}
|
|
||||||
selected[i] = true
|
selected[i] = true
|
||||||
box = unionTextBox(box, members[i].Bounds)
|
selectedNodes = append(selectedNodes, nodes[i])
|
||||||
|
selectedMembers = append(selectedMembers, members[i])
|
||||||
}
|
}
|
||||||
if len(selected) == 0 {
|
if len(selected) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
matrix := transform(box)
|
|
||||||
if matrix == IdentityMatrix {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
count, maximum := len(e.resources), e.maxID
|
count, maximum := len(e.resources), e.maxID
|
||||||
ready := e.source.idsReady
|
ready := e.source.idsReady
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -380,8 +556,30 @@ func (e *Editor) changeCompositeObjects(page int, path ObjectPath, indexes []int
|
|||||||
if err = e.prepareSourceIDs(); err != nil {
|
if err = e.prepareSourceIDs(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for i := range selected {
|
if err = edit(renderer, selectedNodes, selectedMembers); err != nil {
|
||||||
n := nodes[i]
|
return err
|
||||||
|
}
|
||||||
|
data, changed, err := e.writeCompositeNode(root)
|
||||||
|
if err != nil || !changed {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
next, err := newEditorCompositeNode(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
origin := e.objectOrigin(path.ID)
|
||||||
|
next.node.parent = origin.node.parent
|
||||||
|
updated := &editorObjectOrigin{page: origin.page, data: data, node: next.node, object: next.object}
|
||||||
|
return e.updateObjectOrigins(page, []GraphicObject{next.object}, true, map[string]*editorObjectOrigin{path.ID: updated})
|
||||||
|
}
|
||||||
|
|
||||||
|
// transformCompositeMember 将页面变换换算至成员坐标,保留边框及旧式边界约定
|
||||||
|
// 入参: renderer 渲染器, n 成员, matrix 页面变换
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) transformCompositeMember(renderer *Renderer, n *editorCompositeNode, matrix Matrix) (err error) {
|
||||||
|
if matrix == IdentityMatrix {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
object := cloneEditorData(n.object)
|
object := cloneEditorData(n.object)
|
||||||
inverse, _ := n.parent.Invert()
|
inverse, _ := n.parent.Invert()
|
||||||
basic := object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit"
|
basic := object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit"
|
||||||
@@ -419,19 +617,7 @@ func (e *Editor) changeCompositeObjects(page int, path ObjectPath, indexes []int
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
n.node, n.object, n.changed = updated.node, updated.object, true
|
n.node, n.object, n.changed = updated.node, updated.object, true
|
||||||
}
|
return nil
|
||||||
data, _, err := e.writeCompositeNode(root)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
next, err := newEditorCompositeNode(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
origin := e.objectOrigin(path.ID)
|
|
||||||
next.node.parent = origin.node.parent
|
|
||||||
updated := &editorObjectOrigin{page: origin.page, data: data, node: next.node, object: next.object}
|
|
||||||
return e.updateObjectOrigins(page, []GraphicObject{next.object}, true, map[string]*editorObjectOrigin{path.ID: updated})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// transformCompositeOffsets 变换旧式内联复合成员的页面偏移,资源引用保持原坐标约定
|
// transformCompositeOffsets 变换旧式内联复合成员的页面偏移,资源引用保持原坐标约定
|
||||||
@@ -632,14 +818,23 @@ func (e *Editor) compositeResource(id string, data []byte) (editorResource, erro
|
|||||||
func collectCompositeReferences(composite CompositeGraphicUnit, used map[string]bool) {
|
func collectCompositeReferences(composite CompositeGraphicUnit, used map[string]bool) {
|
||||||
used[composite.ResourceID] = true
|
used[composite.ResourceID] = true
|
||||||
for _, object := range composite.Objects {
|
for _, object := range composite.Objects {
|
||||||
|
var fill, stroke *FillColor
|
||||||
switch object.Type {
|
switch object.Type {
|
||||||
case "TextObject":
|
case "TextObject":
|
||||||
used[object.TextObject.Font] = true
|
used[object.TextObject.Font] = true
|
||||||
|
fill, stroke = object.TextObject.FillColor, (*FillColor)(object.TextObject.StrokeColor)
|
||||||
|
case "PathObject":
|
||||||
|
fill, stroke = object.PathObject.FillColor, (*FillColor)(object.PathObject.StrokeColor)
|
||||||
case "ImageObject":
|
case "ImageObject":
|
||||||
used[object.ImageObject.ResourceID] = true
|
used[object.ImageObject.ResourceID] = true
|
||||||
used[object.ImageObject.ImageMask] = true
|
used[object.ImageObject.ImageMask] = true
|
||||||
case "CompositeObject", "CompositeGraphicUnit":
|
case "CompositeObject", "CompositeGraphicUnit":
|
||||||
collectCompositeReferences(object.CompositeGraphicUnit, used)
|
collectCompositeReferences(object.CompositeGraphicUnit, used)
|
||||||
}
|
}
|
||||||
|
for _, color := range []*FillColor{fill, stroke} {
|
||||||
|
if color != nil {
|
||||||
|
used[color.ColorSpace] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+96
-13
@@ -66,6 +66,22 @@ func (e *Editor) resolveEditorStyle(object GraphicObject, layer string) (Graphic
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return GraphicObject{}, err
|
return GraphicObject{}, err
|
||||||
}
|
}
|
||||||
|
return e.resolveEditorStyleDefaults(object, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveEditorStyleDefaults 合并对象参数与继承外观,仅生成编辑快照
|
||||||
|
// 入参: object 原对象, base 继承参数
|
||||||
|
// 返回: GraphicObject 有效样式快照, error 错误信息
|
||||||
|
func (e *Editor) resolveEditorStyleDefaults(object GraphicObject, base *DrawParam) (GraphicObject, error) {
|
||||||
|
var id string
|
||||||
|
switch object.Type {
|
||||||
|
case "TextObject":
|
||||||
|
id = object.TextObject.DrawParam
|
||||||
|
case "PathObject":
|
||||||
|
id = object.PathObject.DrawParam
|
||||||
|
default:
|
||||||
|
return cloneEditorData(object), nil
|
||||||
|
}
|
||||||
dp, err := e.editorDrawParam(id, make(map[string]bool))
|
dp, err := e.editorDrawParam(id, make(map[string]bool))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return GraphicObject{}, err
|
return GraphicObject{}, err
|
||||||
@@ -101,37 +117,77 @@ func (e *Editor) resolveEditorStyle(object GraphicObject, layer string) (Graphic
|
|||||||
return cloneEditorObject(object)
|
return cloneEditorObject(object)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectStyle 对象外观,nil字段保持原值,长度单位为对象坐标系中的毫米
|
// ObjectStyle 对象外观,nil字段保持原值,颜色的Alpha为nil时保留原颜色透明度
|
||||||
|
// StyleObjects使用对象坐标毫米,StyleCompositeObjects的LineWidth使用页面毫米
|
||||||
type ObjectStyle struct {
|
type ObjectStyle struct {
|
||||||
Alpha *int
|
Alpha *int
|
||||||
|
Fill *bool
|
||||||
|
Stroke *bool
|
||||||
|
FillColor *FillColor
|
||||||
|
StrokeColor *StrokeColor
|
||||||
|
LineWidth *float64
|
||||||
DashPattern *string
|
DashPattern *string
|
||||||
DashOffset *float64
|
DashOffset *float64
|
||||||
Cap *string
|
Cap *string
|
||||||
Join *string
|
Join *string
|
||||||
}
|
}
|
||||||
|
|
||||||
// StyleObjects 原子更新文字、图片、路径与复合对象透明度,路径另支持描边样式
|
// StyleObjects 原子更新对象透明度、文字颜色及路径填充和描边样式
|
||||||
// 入参: page 页面索引, ids 对象标识, style 待修改属性
|
// 入参: page 页面索引, ids 对象标识, style 待修改属性
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error {
|
func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error {
|
||||||
if style.Alpha != nil && (*style.Alpha < 0 || *style.Alpha > 255) {
|
|
||||||
return fmt.Errorf("alpha must be between 0 and 255")
|
|
||||||
}
|
|
||||||
objects, _, err := e.selectedObjects(page, ids)
|
objects, _, err := e.selectedObjects(page, ids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
stroke := style.DashPattern != nil || style.DashOffset != nil || style.Cap != nil || style.Join != nil
|
|
||||||
for i := range objects {
|
for i := range objects {
|
||||||
object := cloneEditorData(objects[i])
|
if style.Fill != nil || style.Stroke != nil || style.FillColor != nil || style.StrokeColor != nil || style.LineWidth != nil {
|
||||||
if stroke && object.Type != "PathObject" {
|
capability, err := e.ObjectCapabilities(page, ids[i])
|
||||||
return fmt.Errorf("stroke style requires path objects")
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !capability.Paint {
|
||||||
|
return fmt.Errorf("object %q cannot be painted: %w", ids[i], capability.editError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
objects[i], err = e.styleObject(objects[i], style)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.updateObjects(page, objects, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// styleObject 仅替换请求的外观字段,保留内容、定位及未修改样式
|
||||||
|
// 入参: object 原对象, style 待修改属性
|
||||||
|
// 返回: GraphicObject 修改后的独立对象, error 错误信息
|
||||||
|
func (e *Editor) styleObject(object GraphicObject, style ObjectStyle) (GraphicObject, error) {
|
||||||
|
if style.Alpha != nil && (*style.Alpha < 0 || *style.Alpha > 255) {
|
||||||
|
return GraphicObject{}, fmt.Errorf("alpha must be between 0 and 255")
|
||||||
|
}
|
||||||
|
stroke := style.DashPattern != nil || style.DashOffset != nil || style.Cap != nil || style.Join != nil
|
||||||
|
paint := style.Fill != nil || style.Stroke != nil || style.StrokeColor != nil || style.LineWidth != nil
|
||||||
|
if (stroke || paint) && object.Type != "PathObject" {
|
||||||
|
return GraphicObject{}, fmt.Errorf("stroke style requires path objects")
|
||||||
|
}
|
||||||
|
if style.FillColor != nil && object.Type != "TextObject" && object.Type != "PathObject" {
|
||||||
|
return GraphicObject{}, fmt.Errorf("fill color requires text or path objects")
|
||||||
|
}
|
||||||
|
for _, value := range []*FillColor{style.FillColor, (*FillColor)(style.StrokeColor)} {
|
||||||
|
if value != nil {
|
||||||
|
if err := e.editorColor(value); err != nil {
|
||||||
|
return GraphicObject{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
switch object.Type {
|
switch object.Type {
|
||||||
case "TextObject":
|
case "TextObject":
|
||||||
if style.Alpha != nil {
|
if style.Alpha != nil {
|
||||||
object.TextObject.Alpha = style.Alpha
|
object.TextObject.Alpha = style.Alpha
|
||||||
}
|
}
|
||||||
|
if style.FillColor != nil {
|
||||||
|
object.TextObject.FillColor = editorStyleColor(style.FillColor, object.TextObject.FillColor)
|
||||||
|
}
|
||||||
case "ImageObject":
|
case "ImageObject":
|
||||||
if style.Alpha != nil {
|
if style.Alpha != nil {
|
||||||
object.ImageObject.Alpha = style.Alpha
|
object.ImageObject.Alpha = style.Alpha
|
||||||
@@ -145,6 +201,24 @@ func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error {
|
|||||||
if style.Alpha != nil {
|
if style.Alpha != nil {
|
||||||
path.Alpha = style.Alpha
|
path.Alpha = style.Alpha
|
||||||
}
|
}
|
||||||
|
if style.Fill != nil {
|
||||||
|
path.Fill = style.Fill
|
||||||
|
}
|
||||||
|
if style.Stroke != nil {
|
||||||
|
path.Stroke = style.Stroke
|
||||||
|
}
|
||||||
|
if style.FillColor != nil {
|
||||||
|
path.FillColor = editorStyleColor(style.FillColor, path.FillColor)
|
||||||
|
}
|
||||||
|
if style.StrokeColor != nil {
|
||||||
|
path.StrokeColor = (*StrokeColor)(editorStyleColor((*FillColor)(style.StrokeColor), (*FillColor)(path.StrokeColor)))
|
||||||
|
}
|
||||||
|
if style.LineWidth != nil {
|
||||||
|
if !finite(*style.LineWidth) || *style.LineWidth <= 0 {
|
||||||
|
return GraphicObject{}, fmt.Errorf("line width must be positive")
|
||||||
|
}
|
||||||
|
path.LineWidth = *style.LineWidth
|
||||||
|
}
|
||||||
if style.DashPattern != nil {
|
if style.DashPattern != nil {
|
||||||
path.DashPattern = *style.DashPattern
|
path.DashPattern = *style.DashPattern
|
||||||
}
|
}
|
||||||
@@ -159,15 +233,24 @@ func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error {
|
|||||||
}
|
}
|
||||||
if stroke {
|
if stroke {
|
||||||
if err := validateEditorStroke(*path); err != nil {
|
if err := validateEditorStroke(*path); err != nil {
|
||||||
return err
|
return GraphicObject{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported object type %q", object.Type)
|
return GraphicObject{}, fmt.Errorf("unsupported object type %q", object.Type)
|
||||||
}
|
}
|
||||||
objects[i] = object
|
return cloneEditorData(object), nil
|
||||||
}
|
}
|
||||||
return e.updateObjects(page, objects, true)
|
|
||||||
|
// editorStyleColor 替换色值时保留未指定的颜色透明度
|
||||||
|
// 入参: color 新颜色, before 原有效颜色
|
||||||
|
// 返回: *FillColor 独立颜色
|
||||||
|
func editorStyleColor(color, before *FillColor) *FillColor {
|
||||||
|
result := cloneEditorData(color)
|
||||||
|
if result != nil && result.Alpha == nil && before != nil {
|
||||||
|
result.Alpha = cloneEditorData(before.Alpha)
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyStyle 将同类型对象的外观复制到选区,不复制内容、位置、资源数据或动作
|
// CopyStyle 将同类型对象的外观复制到选区,不复制内容、位置、资源数据或动作
|
||||||
|
|||||||
Reference in new issue
Block a user