mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
1 parent
4826496e98
commit
9f2ef7e212
18 files changed
+674
-79
No files matched your search
@@ -175,6 +175,7 @@
|
||||
</dialog>
|
||||
<dialog id="objectStylePanel" class="form-dialog" aria-label="对象样式">
|
||||
<form id="objectStyleForm">
|
||||
<div class="form-row"><span>组合</span><div class="form-format"><button id="groupObjectsButton" class="button" type="button">组合</button><button id="ungroupObjectButton" class="button" type="button">解组</button></div></div>
|
||||
<div class="form-row"><span>格式</span><div class="form-format"><button id="copyStyleButton" class="button" type="button" aria-label="复制格式">复制</button><button id="pasteStyleButton" class="button" type="button" aria-label="粘贴格式">粘贴</button></div></div>
|
||||
<div class="form-row"><label for="objectOpacity">透明</label><div class="form-unit"><input id="objectOpacity" type="number" min="0" max="100" step="any" aria-label="透明度" autofocus><span>%</span></div></div>
|
||||
<div id="objectStrokeFields">
|
||||
|
||||
+15
-3
@@ -203,6 +203,8 @@ const el = {
|
||||
objectAspect: document.querySelector("#objectAspect"),
|
||||
objectStylePanel: document.querySelector("#objectStylePanel"),
|
||||
objectStyleForm: document.querySelector("#objectStyleForm"),
|
||||
groupObjectsButton: document.querySelector("#groupObjectsButton"),
|
||||
ungroupObjectButton: document.querySelector("#ungroupObjectButton"),
|
||||
objectOpacity: document.querySelector("#objectOpacity"),
|
||||
objectStrokeFields: document.querySelector("#objectStrokeFields"),
|
||||
objectDash: document.querySelector("#objectDash"),
|
||||
@@ -366,7 +368,8 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||
updatePendingChanges();
|
||||
el.undoButton.disabled = !state.ready || state.exporting || !state.editorInfo?.canUndo && !canvasEditor.nudgeChanged();
|
||||
},
|
||||
onReshape: (item, box) => changeDocument("ofdgoReshapeObject", item, box.x, box.y, box.width, box.height),
|
||||
onReshape: (item, box) => changeDocument("ofdgoReshapeObject", item, box.x, box.y, box.width, box.height, Boolean(item.oriented)),
|
||||
onResize: (item, box) => changeDocument("ofdgoResizeObjects", { ...item, id: [item.id] }, box.x, box.y, box.width, box.height),
|
||||
onTextWidth: (item, box) => changeDocument("ofdgoLayoutText", item, box.x - item.x, box.width, true, item.align || "", item.paragraphHeight || 0, item.letterSpacing || 0, ...textIndents(item)),
|
||||
onCrop: (item, box) => changeDocument("ofdgoCropImage", item, box.x, box.y, box.width, box.height),
|
||||
onCropChange: () => {
|
||||
@@ -658,6 +661,12 @@ el.batchPagesForm.addEventListener("submit", async event => {
|
||||
});
|
||||
editorClick(el.objectStyleButton, openObjectStyle);
|
||||
editorClick(el.objectBoundsButton, openObjectBounds);
|
||||
for (const [button, ungroup] of [[el.groupObjectsButton, false], [el.ungroupObjectButton, true]]) {
|
||||
button.addEventListener("click", async () => {
|
||||
const item = canvasEditor.selected;
|
||||
if (await changeDocument("ofdgoGroupObjects", { ...item, id: canvasEditor.items().map(member => member.id) }, ungroup)) el.objectStylePanel.close();
|
||||
});
|
||||
}
|
||||
el.arrowTool.addEventListener("change", async () => {
|
||||
const tool = el.arrowTool.value;
|
||||
el.arrowTool.value = "";
|
||||
@@ -999,7 +1008,7 @@ function openObjectBounds() {
|
||||
state.boundsOriginal = { x: box.x, y: box.y, width: Math.abs(box.width), height: Math.abs(box.height) };
|
||||
for (const [input, key] of [[el.objectX,"x"], [el.objectY,"y"], [el.objectWidth,"width"], [el.objectHeight,"height"]]) setObjectDimension(input, state.boundsOriginal[key]);
|
||||
el.objectAspect.checked = true;
|
||||
el.objectAspect.disabled = Boolean(state.composite) || !item.shape && (item.items || [item]).some(member => member.type !== "ImageObject" || member.imageBorder) || lineShape(item.shape) && (!box.width || !box.height);
|
||||
el.objectAspect.disabled = !item.shape && (item.items || [item]).some(member => member.type !== "ImageObject" || member.imageBorder) || lineShape(item.shape) && (!box.width || !box.height);
|
||||
el.objectWidth.disabled = lineShape(item.shape) && !box.width;
|
||||
el.objectHeight.disabled = lineShape(item.shape) && !box.height;
|
||||
el.objectBoundsStatus.textContent = "";
|
||||
@@ -1016,6 +1025,9 @@ function canCopyStyle(item) {
|
||||
function openObjectStyle() {
|
||||
const items = canvasEditor.items();
|
||||
if (!items.length) return;
|
||||
const ordered = [...items].sort((a, b) => a.position - b.position);
|
||||
el.groupObjectsButton.disabled = Boolean(state.composite) || items.length < 2 || !ordered.every((item, index) => canEditObject(item, "copy") && canEditObject(item, "order") && item.container === ordered[0].container && item.position === ordered[0].position + index);
|
||||
el.ungroupObjectButton.disabled = Boolean(state.composite) || items.length !== 1 || !canEditObject(items[0], "ungroup");
|
||||
el.copyStyleButton.disabled = items.length !== 1 || !canCopyStyle(items[0]);
|
||||
el.pasteStyleButton.disabled = !state.styleClipboard || !items.every(item => item.type === state.styleClipboard && canCopyStyle(item));
|
||||
const shared = (key, fallback) => items.every(item => (item[key] ?? fallback) === (items[0][key] ?? fallback)) ? items[0][key] ?? fallback : null;
|
||||
@@ -1926,7 +1938,7 @@ async function changeDocument(name, item, ...args) {
|
||||
return true;
|
||||
}
|
||||
const clearSelection = !item || ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoEraseObjects", "ofdgoEraseObjectsPath"].includes(name);
|
||||
if (name === "ofdgoCopyObjects" || name === "ofdgoPasteObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
|
||||
if (name === "ofdgoCopyObjects" || name === "ofdgoPasteObjects" || name === "ofdgoGroupObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
|
||||
state.selectObjects = true;
|
||||
canvasEditor.setTool("");
|
||||
setPan(false);
|
||||
|
||||
@@ -159,6 +159,11 @@ export function reshapeBox(item, dx, dy, handle, shift) {
|
||||
return { x, y, width: right - x, height: bottom - y };
|
||||
}
|
||||
|
||||
export function reshapeFrame(frame, dx, dy, handle, shift) {
|
||||
const [a, b, c, d] = frame.matrix, determinant = a * d - b * c;
|
||||
return reshapeBox({ geometry: frame.box }, (d * dx - c * dy) / determinant, (a * dy - b * dx) / determinant, handle, shift);
|
||||
}
|
||||
|
||||
function paintShape(node, shape, box) {
|
||||
const { x, y, width, height } = box;
|
||||
const attributes = lineShape(shape) && shape !== "line" ? { d: arrowPath(shape, box) }
|
||||
@@ -289,9 +294,10 @@ export class CanvasEditor {
|
||||
svg.setAttribute("aria-hidden", "true");
|
||||
svg.setAttribute("preserveAspectRatio", "none");
|
||||
let contour;
|
||||
if (object.type === "PathObject" && (!object.scoped || object.shape)) {
|
||||
contour = document.createElementNS("http://www.w3.org/2000/svg", object.shape === "line" ? "line" : object.shape === "ellipse" ? "ellipse" : object.shape === "rectangle" ? "rect" : "path");
|
||||
if (!object.shape) contour.setAttribute("d", object.outline);
|
||||
if (object.type === "PathObject" && (!object.scoped || object.shape || object.oriented)) {
|
||||
const shape = object.shape || object.oriented?.kind;
|
||||
contour = document.createElementNS("http://www.w3.org/2000/svg", shape === "line" ? "line" : shape === "ellipse" ? "ellipse" : shape === "rectangle" ? "rect" : "path");
|
||||
if (!shape) contour.setAttribute("d", object.outline);
|
||||
svg.append(contour);
|
||||
} else {
|
||||
for (const path of object.contours) {
|
||||
@@ -306,7 +312,7 @@ export class CanvasEditor {
|
||||
this.nodes.set(node, item);
|
||||
node.addEventListener("focus", () => this.select(item));
|
||||
const handles = !canEditObject(object, "transform") ? [] : lineShape(object.shape) ? ["start", "end"]
|
||||
: object.shape ? ["nw", "n", "ne", "e", "se", "s", "sw", "w"]
|
||||
: object.shape || object.oriented || object.type === "ImageObject" && !object.imageBorder ? ["nw", "n", "ne", "e", "se", "s", "sw", "w"]
|
||||
: object.type === "TextObject" && canEditObject(object, "reflow") && canEditObject(object, "layoutKnown")
|
||||
? ["nw", "n", "ne", "e", "se", "s", "sw", "w"] : ["nw", "ne", "sw", "se"];
|
||||
for (const corner of handles) {
|
||||
@@ -346,6 +352,10 @@ export class CanvasEditor {
|
||||
if (x || y || scale !== 1) item.artwork.setAttribute("transform", `translate(${x} ${y}) scale(${scale})`);
|
||||
else item.artwork.removeAttribute("transform");
|
||||
}
|
||||
if (item.oriented) {
|
||||
this.placeOriented(item, item.oriented.box, change);
|
||||
return;
|
||||
}
|
||||
if (item.shape) {
|
||||
this.placeShape(item, { x: item.geometry.x + change.x + (item.geometry.x - item.x) * (change.scale - 1),
|
||||
y: item.geometry.y + change.y + (item.geometry.y - item.y) * (change.scale - 1),
|
||||
@@ -369,6 +379,28 @@ export class CanvasEditor {
|
||||
}
|
||||
}
|
||||
|
||||
placeOriented(item, box, change = { x: 0, y: 0, scale: 1 }) {
|
||||
const frame = item.oriented, bounds = transformedBox(box, frame.matrix), scale = change.scale;
|
||||
const [a, b, c, d, e, f] = frame.matrix;
|
||||
Object.assign(item.node.style, {
|
||||
left: `${(item.x + change.x + (bounds.x - item.x) * scale) * PX_PER_MM}px`,
|
||||
top: `${(item.y + change.y + (bounds.y - item.y) * scale) * PX_PER_MM}px`,
|
||||
width: `${bounds.width * scale * PX_PER_MM}px`, height: `${bounds.height * scale * PX_PER_MM}px`,
|
||||
});
|
||||
for (const handle of item.node.children) {
|
||||
const corner = handle.dataset.corner;
|
||||
if (!corner) continue;
|
||||
const x = box.x + box.width * (corner.includes("w") ? 0 : corner.includes("e") ? 1 : .5);
|
||||
const y = box.y + box.height * (corner.includes("n") ? 0 : corner.includes("s") ? 1 : .5);
|
||||
handle.style.left = `${(a * x + c * y + e - bounds.x) * scale * PX_PER_MM}px`;
|
||||
handle.style.top = `${(b * x + d * y + f - bounds.y) * scale * PX_PER_MM}px`;
|
||||
}
|
||||
const { svg, path } = item.contour;
|
||||
svg.setAttribute("viewBox", `${bounds.x} ${bounds.y} ${bounds.width} ${bounds.height}`);
|
||||
path.setAttribute("transform", `matrix(${frame.matrix.join(" ")})`);
|
||||
paintShape(path, frame.kind, box);
|
||||
}
|
||||
|
||||
placeShape(item, box) {
|
||||
const x = Math.min(box.x, box.x + box.width), y = Math.min(box.y, box.y + box.height);
|
||||
Object.assign(item.node.style, {
|
||||
@@ -597,6 +629,23 @@ export class CanvasEditor {
|
||||
Object.assign(drag.item.node.style, { left: `${drag.box.x * PX_PER_MM}px`, width: `${drag.box.width * PX_PER_MM}px` });
|
||||
return;
|
||||
}
|
||||
if (drag.item.type === "ImageObject" && !drag.item.imageBorder && drag.corner.length === 1) {
|
||||
drag.box = reshapeBox({ geometry: drag.item }, to.x - from.x, to.y - from.y, drag.corner, false);
|
||||
const sx = drag.box.width / drag.item.width, sy = drag.box.height / drag.item.height;
|
||||
Object.assign(drag.item.node.style, { left: `${drag.box.x * PX_PER_MM}px`, top: `${drag.box.y * PX_PER_MM}px`,
|
||||
width: `${drag.box.width * PX_PER_MM}px`, height: `${drag.box.height * PX_PER_MM}px` });
|
||||
drag.item.artwork?.setAttribute("transform", `translate(${drag.box.x - drag.item.x * sx} ${drag.box.y - drag.item.y * sy}) scale(${sx} ${sy})`);
|
||||
return;
|
||||
}
|
||||
if (drag.item.oriented && drag.corner) {
|
||||
const frame = drag.item.oriented;
|
||||
drag.box = reshapeFrame(frame, to.x - from.x, to.y - from.y, drag.corner, event.shiftKey);
|
||||
this.placeOriented(drag.item, drag.box);
|
||||
const matrix = new DOMMatrix(frame.matrix), sx = drag.box.width / frame.box.width, sy = drag.box.height / frame.box.height;
|
||||
const transform = matrix.translate(drag.box.x - frame.box.x * sx, drag.box.y - frame.box.y * sy).scale(sx, sy).multiply(matrix.inverse());
|
||||
drag.item.artwork?.setAttribute("transform", `matrix(${[transform.a, transform.b, transform.c, transform.d, transform.e, transform.f].join(" ")})`);
|
||||
return;
|
||||
}
|
||||
if (drag.item.shape && drag.corner) {
|
||||
drag.box = reshapeBox(drag.item, to.x - from.x, to.y - from.y, drag.corner, event.shiftKey);
|
||||
this.placeShape(drag.item, drag.box || drag.item.geometry);
|
||||
@@ -709,12 +758,12 @@ export class CanvasEditor {
|
||||
.finally(() => drag.preview.remove());
|
||||
return;
|
||||
}
|
||||
if ((drag.item.shape || drag.item.type === "TextObject") && drag.corner && drag.box) {
|
||||
if (drag.corner && drag.box) {
|
||||
this.drag = null;
|
||||
this.viewer.releasePointerCapture(drag.pointerID);
|
||||
const geometry = drag.item.geometry || (drag.item.textFrame ? { x: drag.item.x, width: drag.item.textFrame.width } : drag.item);
|
||||
const geometry = drag.item.oriented?.box || drag.item.geometry || (drag.item.textFrame ? { x: drag.item.x, width: drag.item.textFrame.width } : drag.item);
|
||||
const changed = Object.keys(drag.box).some((key) => drag.box[key] !== geometry[key]);
|
||||
const apply = drag.item.shape ? this.options.onReshape : this.options.onTextWidth;
|
||||
const apply = drag.item.shape || drag.item.oriented ? this.options.onReshape : drag.item.type === "ImageObject" ? this.options.onResize : this.options.onTextWidth;
|
||||
Promise.resolve(changed && apply(drag.item, drag.box)).finally(() => {
|
||||
drag.preview?.remove();
|
||||
this.place(drag.item);
|
||||
|
||||
+48
-12
@@ -120,6 +120,7 @@ func RunWASM() {
|
||||
registerCallback("ofdgoStyleObjects", styleObjects)
|
||||
registerCallback("ofdgoMoveOutline", moveOutline)
|
||||
registerCallback("ofdgoCaptureStyle", captureStyle)
|
||||
registerCallback("ofdgoGroupObjects", groupObjects)
|
||||
registerCallback("ofdgoPasteStyle", pasteStyle)
|
||||
registerCallback("ofdgoResizeObjects", resizeObjects)
|
||||
registerCallback("ofdgoLoadImport", loadImport)
|
||||
@@ -775,6 +776,11 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
||||
} else if outline, err := path.Outline(); err == nil {
|
||||
item["outline"] = outline
|
||||
}
|
||||
if kind == "" && capability.Update {
|
||||
if kind, box, matrix := path.ShapeFrame(); kind != "" {
|
||||
item["oriented"] = map[string]any{"kind": string(kind), "box": editorBox(box), "matrix": editorMatrix(matrix)}
|
||||
}
|
||||
}
|
||||
}
|
||||
if object.Type == "PathObject" || object.Type == "ImageObject" || object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
|
||||
paths := make([]any, len(contours))
|
||||
@@ -835,7 +841,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
||||
// 返回: 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),
|
||||
"replaceImage": capability.ReplaceImage, "cropImage": capability.CropImage, "fitImage": capability.FitImage, "resetCrop": capability.ResetCrop, "enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
|
||||
"replaceImage": capability.ReplaceImage, "cropImage": capability.CropImage, "fitImage": capability.FitImage, "resetCrop": capability.ResetCrop, "ungroup": capability.Ungroup, "enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
|
||||
if missing := capability.MissingGlyphs; missing != nil {
|
||||
result["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
|
||||
}
|
||||
@@ -927,6 +933,8 @@ func compositeObjects(args []js.Value) (any, error) {
|
||||
editorAppearance(item, member.Object, member.Capabilities.Paint, member.StrokeScale)
|
||||
if kind, geometry := member.Shape(); kind != "" {
|
||||
item["shape"], item["geometry"] = string(kind), editorBox(geometry)
|
||||
} else if kind, box, matrix := member.ShapeFrame(); kind != "" {
|
||||
item["oriented"] = map[string]any{"kind": string(kind), "box": editorBox(box), "matrix": editorMatrix(matrix)}
|
||||
}
|
||||
if member.Object.Type == "TextObject" {
|
||||
object := member.Object.TextObject
|
||||
@@ -996,6 +1004,9 @@ func changeCompositeObjects(args []js.Value) (any, error) {
|
||||
if operation == "line" {
|
||||
return currentEditor.ReshapeCompositeLine(page, path, indexes[0], ofdgo.ShapeKind(args[8].String()), box)
|
||||
}
|
||||
if len(args) > 8 && args[8].Bool() {
|
||||
return currentEditor.ReshapeCompositeFrame(page, path, indexes[0], box)
|
||||
}
|
||||
return currentEditor.ReshapeCompositeObject(page, path, indexes[0], box)
|
||||
case "erase":
|
||||
return currentEditor.EraseCompositeObjects(page, path, indexes, ofdgo.Box{X: args[4].Float(), Y: args[5].Float(), W: args[6].Float(), H: args[7].Float()})
|
||||
@@ -1019,14 +1030,11 @@ func changeCompositeObjects(args []js.Value) (any, error) {
|
||||
if !args[4].IsNull() {
|
||||
return currentEditor.ResizeCompositeTextFrame(page, path, indexes[0], args[4].Float(), args[5].Float(), layout)
|
||||
}
|
||||
members, err := currentEditor.CompositeObjects(page, path)
|
||||
member, err := currentEditor.CompositeObject(page, path, indexes[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if indexes[0] >= len(members) {
|
||||
return fmt.Errorf("text member is unavailable")
|
||||
}
|
||||
value, _ := members[indexes[0]].Object.TextObject.TextLayout()
|
||||
value, _ := member.Object.TextObject.TextLayout()
|
||||
return currentEditor.LayoutCompositeText(page, path, indexes[0], value, layout)
|
||||
case "fit", "resetCrop":
|
||||
if len(indexes) != 1 {
|
||||
@@ -1274,7 +1282,12 @@ func reshapeObject(args []js.Value) (any, error) {
|
||||
if object.Type != "PathObject" {
|
||||
return nil, fmt.Errorf("object is not a path")
|
||||
}
|
||||
object.PathObject, err = object.PathObject.Reshape(ofdgo.Box{X: args[2].Float(), Y: args[3].Float(), W: args[4].Float(), H: args[5].Float()})
|
||||
box := ofdgo.Box{X: args[2].Float(), Y: args[3].Float(), W: args[4].Float(), H: args[5].Float()}
|
||||
if len(args) > 6 && args[6].Bool() {
|
||||
object.PathObject, err = object.PathObject.ReshapeFrame(box)
|
||||
} else {
|
||||
object.PathObject, err = object.PathObject.Reshape(box)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1412,6 +1425,32 @@ func copyEditorObjects(page int, objects []ofdgo.GraphicObject, dx, dy float64)
|
||||
return editorSelectionInfo{info, ids}, err
|
||||
}
|
||||
|
||||
// groupObjects 组合或解组并返回新的选区,复用单次历史提交
|
||||
// 入参: args 页面索引、对象标识列表和是否解组
|
||||
// 返回: any 文档及选区, error 错误信息
|
||||
func groupObjects(args []js.Value) (any, error) {
|
||||
var ids []string
|
||||
info, err := changeObjects(func() error {
|
||||
page, selected := args[0].Int(), stringsFromJS(args[1])
|
||||
var err error
|
||||
if args[2].Bool() {
|
||||
if len(selected) != 1 {
|
||||
return fmt.Errorf("ungrouping requires one object")
|
||||
}
|
||||
ids, err = currentEditor.UngroupObject(page, selected[0])
|
||||
} else {
|
||||
var id string
|
||||
id, err = currentEditor.GroupObjects(page, selected)
|
||||
ids = []string{id}
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return editorSelectionInfo{info.(editorInfo), ids}, nil
|
||||
}
|
||||
|
||||
// orderObjects 调整选区的绘制顺序
|
||||
// 入参: args 页码、对象标识列表和层级动作
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
@@ -1587,14 +1626,11 @@ func captureStyle(args []js.Value) (any, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
members, err := currentEditor.CompositeObjects(args[0].Int(), path)
|
||||
member, err := currentEditor.CompositeObject(args[0].Int(), path, indexes[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if indexes[0] >= len(members) {
|
||||
return nil, fmt.Errorf("style source is unavailable")
|
||||
}
|
||||
object := members[indexes[0]].Style()
|
||||
object := member.Style()
|
||||
copiedStyle = &object
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
+24
-3
@@ -56,6 +56,7 @@ type Editor struct {
|
||||
revision uint64
|
||||
serial uint64
|
||||
source *editorSource
|
||||
origins map[string]*editorObjectOrigin
|
||||
outlines []byte
|
||||
}
|
||||
|
||||
@@ -252,15 +253,21 @@ func (e *Editor) CopyPages(indexes []int) ([]int, error) {
|
||||
}
|
||||
}
|
||||
result := make([]int, len(pages))
|
||||
maximum := e.maxID
|
||||
origins := make(map[string]*editorObjectOrigin)
|
||||
for n := range pages {
|
||||
page := &pages[n]
|
||||
page.ID = e.nextID()
|
||||
maximum++
|
||||
page.ID = strconv.Itoa(maximum)
|
||||
for i := range page.Content.Layer {
|
||||
layer := &page.Content.Layer[i]
|
||||
layer.ID = e.nextID()
|
||||
maximum++
|
||||
layer.ID = strconv.Itoa(maximum)
|
||||
for j := range layer.Objects {
|
||||
object := &layer.Objects[j]
|
||||
id := e.nextID()
|
||||
before := *object
|
||||
maximum++
|
||||
id := strconv.Itoa(maximum)
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
object.TextObject.ID = id
|
||||
@@ -268,11 +275,25 @@ func (e *Editor) CopyPages(indexes []int) ([]int, error) {
|
||||
object.PathObject.ID = id
|
||||
case "ImageObject":
|
||||
object.ImageObject.ID = id
|
||||
case "CompositeObject", "CompositeGraphicUnit":
|
||||
object.CompositeGraphicUnit.ID = id
|
||||
}
|
||||
copied, origin, err := e.copyObjectOrigin(before, *object, &maximum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
*object = copied
|
||||
if origin != nil {
|
||||
origins[id] = origin
|
||||
}
|
||||
}
|
||||
}
|
||||
result[n] = at + n
|
||||
}
|
||||
e.maxID = maximum
|
||||
for id, origin := range origins {
|
||||
e.setObjectOrigin(id, origin)
|
||||
}
|
||||
e.pages = append(e.pages, pages...)
|
||||
if change := e.recordChange(); change != nil {
|
||||
for i := range pages {
|
||||
|
||||
+15
-3
@@ -113,7 +113,7 @@ func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64)
|
||||
e.maxID = maximum
|
||||
for i, origin := range origins {
|
||||
if origin != nil {
|
||||
e.source.origins[result[i]] = origin
|
||||
e.setObjectOrigin(result[i], origin)
|
||||
}
|
||||
}
|
||||
layers := copyEditorPage(e.pages[page]).Content.Layer
|
||||
@@ -173,14 +173,26 @@ func (e *Editor) objectOrderIndexes(page int, layer *Layer, id string) (*editorX
|
||||
var nodes map[string]*editorXML
|
||||
var parent *editorXML
|
||||
if e.originalPage(page) {
|
||||
nodes = e.source.pages[e.pages[page].ID].nodes
|
||||
source := e.source.pages[e.pages[page].ID]
|
||||
if slices.ContainsFunc(source.original.Content.Layer, func(original Layer) bool { return original.ID == layer.ID }) {
|
||||
nodes = source.nodes
|
||||
}
|
||||
}
|
||||
if nodes != nil {
|
||||
if node := nodes[id]; node != nil {
|
||||
parent = node.parent
|
||||
} else if origin := e.objectOrigin(id); origin != nil {
|
||||
parent = origin.node.parent
|
||||
}
|
||||
}
|
||||
var indexes []int
|
||||
for i, object := range layer.Objects {
|
||||
node := nodes[editorObjectID(object)]
|
||||
if node == nil && nodes != nil {
|
||||
if origin := e.objectOrigin(editorObjectID(object)); origin != nil {
|
||||
node = origin.node
|
||||
}
|
||||
}
|
||||
if node == nil && parent == nil || node != nil && node.parent == parent {
|
||||
indexes = append(indexes, i)
|
||||
}
|
||||
@@ -469,7 +481,7 @@ func (e *Editor) updateObjectOrigins(page int, objects []GraphicObject, preserve
|
||||
e.pages[page].Content.Layer[index.layer].Objects[index.index] = objects[i]
|
||||
}
|
||||
for id, origin := range origins {
|
||||
e.source.origins[id] = origin
|
||||
e.setObjectOrigin(id, origin)
|
||||
}
|
||||
}
|
||||
apply(e, after, afterOrigins)
|
||||
|
||||
@@ -158,6 +158,36 @@ func (e *Editor) CompositeObjects(page int, path ObjectPath) ([]CompositeMember,
|
||||
return e.measureCompositeMembers(renderer, members), nil
|
||||
}
|
||||
|
||||
// CompositeObject 读取指定内部成员,仅度量该成员并保留完整容器位置
|
||||
// 入参: page 页面索引, path 父复合路径, index 成员序号
|
||||
// 返回: CompositeMember 独立成员快照, error 错误信息
|
||||
func (e *Editor) CompositeObject(page int, path ObjectPath, index int) (CompositeMember, error) {
|
||||
reader, renderer, _, nodes, err := e.compositeScope(page, path)
|
||||
if err != nil {
|
||||
return CompositeMember{}, err
|
||||
}
|
||||
defer reader.Close()
|
||||
if index < 0 || index >= len(nodes) {
|
||||
return CompositeMember{}, fmt.Errorf("composite member index %d out of range", index)
|
||||
}
|
||||
member := e.measureCompositeMembers(renderer, nodes[index:index+1])[0]
|
||||
position := ObjectPosition{}
|
||||
for i, node := range nodes {
|
||||
if node.span.parent != nodes[index].span.parent {
|
||||
continue
|
||||
}
|
||||
if position.Count == 0 {
|
||||
position.Container = fmt.Sprint(i)
|
||||
}
|
||||
if i == index {
|
||||
position.Index = position.Count
|
||||
}
|
||||
position.Count++
|
||||
}
|
||||
member.Position = position
|
||||
return member, nil
|
||||
}
|
||||
|
||||
// TransformCompositeObjects 在页面坐标中等比缩放并平移内部选区,只隔离被修改实例的资源
|
||||
// 入参: page 页面索引, path 父复合对象路径, indexes 成员序号, dx、dy 位移, scale 正缩放比例
|
||||
// 返回: error 错误信息
|
||||
@@ -195,7 +225,7 @@ func (e *Editor) FlipCompositeObjects(page int, path ObjectPath, indexes []int,
|
||||
return e.changeCompositeObjects(page, path, indexes, func(box Box) Matrix { return compositeOrientation(box, m) })
|
||||
}
|
||||
|
||||
// ResizeCompositeObjects 等比缩放内部选区到目标范围,不重排文字或重采样图片
|
||||
// ResizeCompositeObjects 缩放内部选区到目标范围,无边框图片支持独立宽高,其他对象保持比例
|
||||
// 入参: page 页面索引, path 父复合对象路径, indexes 成员序号, box 页面毫米目标范围
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) ResizeCompositeObjects(page int, path ObjectPath, indexes []int, box Box) error {
|
||||
@@ -214,10 +244,17 @@ func (e *Editor) ResizeCompositeObjects(page int, path ObjectPath, indexes []int
|
||||
return fmt.Errorf("selection has no visible bounds")
|
||||
}
|
||||
sx, sy := box.W/before.W, box.H/before.H
|
||||
if math.Abs(sx-sy) > 1e-9*math.Max(sx, sy) {
|
||||
return fmt.Errorf("composite members require proportional resizing")
|
||||
uniform := math.Abs(sx-sy) <= 1e-9*math.Max(sx, sy)
|
||||
if !uniform {
|
||||
for _, node := range nodes {
|
||||
if node.object.Type != "ImageObject" || node.object.ImageObject.Border != nil {
|
||||
return fmt.Errorf("nonuniform resizing requires images without borders")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sy = sx
|
||||
}
|
||||
matrix := Matrix{a: sx, d: sx, e: box.X - before.X*sx, f: box.Y - before.Y*sx}
|
||||
matrix := Matrix{a: sx, d: sy, e: box.X - before.X*sx, f: box.Y - before.Y*sy}
|
||||
for _, node := range nodes {
|
||||
if err := e.transformCompositeMember(renderer, node, matrix); err != nil {
|
||||
return err
|
||||
@@ -687,7 +724,7 @@ func (e *Editor) editCompositeScope(page int, path ObjectPath, edit func(*Render
|
||||
}
|
||||
defer reader.Close()
|
||||
count, maximum := len(e.resources), e.maxID
|
||||
ready := e.source.idsReady
|
||||
ready := e.source != nil && e.source.idsReady
|
||||
defer func() {
|
||||
if err != nil {
|
||||
removed := make(map[string]bool)
|
||||
@@ -700,7 +737,9 @@ func (e *Editor) editCompositeScope(page int, path ObjectPath, edit func(*Render
|
||||
maps.DeleteFunc(e.resourceID, func(_ editorResourceKey, id string) bool { return removed[id] })
|
||||
clear(e.resources[count:])
|
||||
e.resources, e.maxID = e.resources[:count], maximum
|
||||
e.source.idsReady = ready
|
||||
if e.source != nil {
|
||||
e.source.idsReady = ready
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err = e.prepareSourceIDs(); err != nil {
|
||||
|
||||
@@ -63,11 +63,15 @@ func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste fu
|
||||
if selection == nil || selection.editor != e {
|
||||
return fmt.Errorf("composite selection belongs to another editor")
|
||||
}
|
||||
count, maximum, ready := len(e.resources), e.maxID, e.source.idsReady
|
||||
count, maximum := len(e.resources), e.maxID
|
||||
ready := e.source != nil && e.source.idsReady
|
||||
defer func() {
|
||||
if err != nil {
|
||||
clear(e.resources[count:])
|
||||
e.resources, e.maxID, e.source.idsReady = e.resources[:count], maximum, ready
|
||||
e.resources, e.maxID = e.resources[:count], maximum
|
||||
if e.source != nil {
|
||||
e.source.idsReady = ready
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err := e.prepareSourceIDs(); err != nil {
|
||||
@@ -85,12 +89,22 @@ func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste fu
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
style, styleErr := e.compositeMemberStyle(source)
|
||||
if styleErr != nil {
|
||||
if source.object.Type != "PathObject" || editReason(styleErr) != EditUnsupportedColor {
|
||||
return styleErr
|
||||
}
|
||||
copy, err = e.wrapCompositePath(copy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
copy.setStates(source.states)
|
||||
copy.defaults = source.defaults
|
||||
copy.drawParams = source.drawParams
|
||||
object, err := e.compositeMemberStyle(source)
|
||||
if err != nil {
|
||||
return err
|
||||
object := cloneEditorData(copy.object)
|
||||
if styleErr == nil && object.Type != "PathObject" {
|
||||
object = style
|
||||
}
|
||||
object = mergeGraphicObjectAlpha(object, source.alpha)
|
||||
if !source.visible || source.clip != nil && source.clip.Empty() {
|
||||
@@ -138,7 +152,7 @@ func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste fu
|
||||
}
|
||||
}
|
||||
copy.node.parent = &editorXML{name: xml.Name{Local: "Layer"}, parent: &editorXML{name: xml.Name{Local: "Content"}}}
|
||||
object, err = e.resolveEditorStyle(copy.object, "")
|
||||
object, err = e.copiedObjectStyle(copy.object, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -149,7 +163,7 @@ func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste fu
|
||||
object.CompositeGraphicUnit.states = maps.Clone(copy.states)
|
||||
copy.object.CompositeGraphicUnit.states = object.CompositeGraphicUnit.states
|
||||
}
|
||||
object.origin = &editorObjectOrigin{page: selection.source, data: copy.data, node: copy.node, object: copy.object}
|
||||
object.origin = &editorObjectOrigin{editor: e, page: selection.source, data: copy.data, node: copy.node, object: copy.object}
|
||||
objects = append(objects, object)
|
||||
}
|
||||
return paste(objects)
|
||||
|
||||
@@ -110,17 +110,11 @@ func appendCompositeClip(node *editorCompositeNode, shape PathObject, matrix Mat
|
||||
}
|
||||
var patch editorXMLPatch
|
||||
if original := node.node.child("Clips"); original != nil {
|
||||
clip, err := editorXMLStandalone(data[root.children[0].start:root.children[0].end], root.children[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clip := editorXMLEncodedFragment(data, root.children[0])
|
||||
content := append(bytes.Clone(node.data[original.open:original.close]), clip...)
|
||||
patch = editorXMLContent(node.data, original, content)
|
||||
} else {
|
||||
data, err = editorXMLStandalone(data, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = editorXMLEncodedFragment(data, root)
|
||||
content := append(data, node.data[node.node.open:node.node.close]...)
|
||||
patch = editorXMLContent(node.data, node.node, content)
|
||||
}
|
||||
|
||||
@@ -92,13 +92,22 @@ func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []Gra
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if node.object.Type == "PathObject" {
|
||||
_, styleErr := e.resolveEditorStyle(node.object, e.copiedLayerStyle(source))
|
||||
if styleErr != nil && editReason(styleErr) == EditUnsupportedColor || !e.editorPaintable(node.object) {
|
||||
node, err = e.wrapCompositePath(node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
node.parent, node.boundaryInCTM = IdentityMatrix, true
|
||||
node.setStates(object.CompositeGraphicUnit.states)
|
||||
node.drawParams = []string{e.copiedLayerStyle(source)}
|
||||
if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
|
||||
if err := node.convertCoordinates(IdentityMatrix, true); err != nil {
|
||||
return err
|
||||
}
|
||||
node.drawParams = []string{e.copiedLayerStyle(source)}
|
||||
}
|
||||
if err := e.isolateCompositeStyle(node); err != nil {
|
||||
return err
|
||||
@@ -149,11 +158,26 @@ func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []Gra
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// wrapCompositePath 通过独立容器变换复杂路径,保持渐变坐标与原始颜色节点
|
||||
// 入参: node 复杂路径
|
||||
// 返回: *editorCompositeNode 包装节点, error 错误信息
|
||||
func (e *Editor) wrapCompositePath(node *editorCompositeNode) (*editorCompositeNode, error) {
|
||||
content, err := editorXMLContainer("Content", nil, bytes.TrimPrefix(node.data, []byte(xml.Header)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := editorXMLContainer("CompositeObject", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: e.nextID()}, {Name: xml.Name{Local: "Boundary"}, Value: "0 0 1 1"}}, bytes.TrimPrefix(content, []byte(xml.Header)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newEditorCompositeNode(data)
|
||||
}
|
||||
|
||||
// isolateCompositeStyle 固定对象的绘制参数,避免目标容器改变颜色、虚线和线帽
|
||||
// 入参: node 页面坐标中的对象节点
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) isolateCompositeStyle(node *editorCompositeNode) error {
|
||||
if node.object.Type == "CompositeObject" || node.object.Type == "CompositeGraphicUnit" {
|
||||
if node.object.Type == "CompositeObject" || node.object.Type == "CompositeGraphicUnit" || node.object.Type == "PathObject" {
|
||||
id, err := e.neutralDrawParam()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -164,29 +188,32 @@ func (e *Editor) isolateCompositeStyle(node *editorCompositeNode) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
id, err = e.copyDrawParam(node.object.CompositeGraphicUnit.DrawParam, id, make(map[string]bool))
|
||||
parameter := node.object.CompositeGraphicUnit.DrawParam
|
||||
if node.object.Type == "PathObject" {
|
||||
parameter = node.object.PathObject.DrawParam
|
||||
}
|
||||
id, err = e.copyDrawParam(parameter, id, make(map[string]bool))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
object := node.object
|
||||
object.CompositeGraphicUnit.DrawParam = id
|
||||
if object.Type == "PathObject" {
|
||||
object.PathObject.DrawParam = id
|
||||
} else {
|
||||
object.CompositeGraphicUnit.DrawParam = id
|
||||
}
|
||||
return node.update(object)
|
||||
}
|
||||
object, err := e.resolveEditorStyleDefaults(node.object, &DrawParam{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object.Type == "TextObject" || object.Type == "PathObject" {
|
||||
if object.Type == "TextObject" {
|
||||
id, err := e.neutralDrawParam()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object.Type == "TextObject" {
|
||||
object.TextObject.DrawParam = id
|
||||
} else {
|
||||
object.PathObject.DrawParam = id
|
||||
object.PathObject.dashPatternSet = true
|
||||
}
|
||||
object.TextObject.DrawParam = id
|
||||
}
|
||||
return node.update(object)
|
||||
}
|
||||
|
||||
@@ -206,6 +206,49 @@ func (m CompositeMember) Shape() (ShapeKind, Box) {
|
||||
return kind, parent.TransformBox(box)
|
||||
}
|
||||
|
||||
// ShapeFrame 获取内部矩形或椭圆的局部范围及页面变换
|
||||
// 返回: ShapeKind 图形类型, Box 局部范围, Matrix 页面变换
|
||||
func (m CompositeMember) ShapeFrame() (ShapeKind, Box, Matrix) {
|
||||
if m.Object.Type != "PathObject" || !m.Capabilities.Transform {
|
||||
return "", Box{}, Matrix{}
|
||||
}
|
||||
kind, box, _ := m.Object.PathObject.ShapeFrame()
|
||||
if _, ok := m.Matrix.Invert(); !ok {
|
||||
return "", Box{}, Matrix{}
|
||||
}
|
||||
return kind, box, m.Matrix
|
||||
}
|
||||
|
||||
// ReshapeCompositeFrame 沿内部矩形或椭圆自身方向拉伸,不修改共享实例
|
||||
// 入参: page 页面索引, path 父路径, index 成员序号, box ShapeFrame局部目标范围
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) ReshapeCompositeFrame(page int, path ObjectPath, index int, box Box) error {
|
||||
return e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||
kind, previous, _ := members[0].ShapeFrame()
|
||||
if kind == "" {
|
||||
return fmt.Errorf("composite member is not a framed shape")
|
||||
}
|
||||
if box == previous {
|
||||
return nil
|
||||
}
|
||||
node := nodes[0]
|
||||
object := cloneEditorData(node.object)
|
||||
if !node.boundaryInCTM {
|
||||
object = compositeBoundary(object, node.parent, true)
|
||||
}
|
||||
var err error
|
||||
object.PathObject, err = object.PathObject.ReshapeFrame(box)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !node.boundaryInCTM {
|
||||
object = compositeBoundary(object, node.parent, false)
|
||||
}
|
||||
object.PathObject.Clips = node.object.PathObject.Clips
|
||||
return node.update(object)
|
||||
})
|
||||
}
|
||||
|
||||
// shapeParent 获取路径所在坐标到页面的变换,兼容旧式内联边界
|
||||
// 返回: Matrix 路径父坐标变换, bool 是否可逆
|
||||
func (m CompositeMember) shapeParent() (Matrix, bool) {
|
||||
|
||||
+21
-4
@@ -21,12 +21,23 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// copiedObjectStyle 保留复杂路径的原始颜色引用,普通对象沿用有效样式快照
|
||||
// 入参: object 原对象, layer 图层绘制参数
|
||||
// 返回: GraphicObject 复制快照, error 错误信息
|
||||
func (e *Editor) copiedObjectStyle(object GraphicObject, layer string) (GraphicObject, error) {
|
||||
result, err := e.resolveEditorStyle(object, layer)
|
||||
if err != nil && object.Type == "PathObject" && editReason(err) == EditUnsupportedColor {
|
||||
return cloneEditorData(object), nil
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
// snapshotOrigin 获取当前文档内捕获时的原文来源,外部快照按普通对象校验
|
||||
// 入参: object 对象快照
|
||||
// 返回: *editorObjectOrigin 原文来源
|
||||
func (e *Editor) snapshotOrigin(object GraphicObject) *editorObjectOrigin {
|
||||
if object.origin != nil {
|
||||
if e.source != nil && e.source.pages[object.origin.page.ref.ID] == object.origin.page {
|
||||
if object.origin.editor == e || object.origin.page != nil && e.source != nil && e.source.pages[object.origin.page.ref.ID] == object.origin.page {
|
||||
return object.origin
|
||||
}
|
||||
return nil
|
||||
@@ -199,15 +210,21 @@ func editorObjectReference(name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// copiedLayerStyle 获取复合对象需保留的图层绘制参数,不向基本对象重新施加继承样式
|
||||
// copiedLayerStyle 保留复合对象和复杂路径的图层参数,普通对象使用已解析样式
|
||||
// 入参: object 对象
|
||||
// 返回: string 绘制参数标识
|
||||
func (e *Editor) copiedLayerStyle(object GraphicObject) string {
|
||||
if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
|
||||
if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" || object.Type == "PathObject" {
|
||||
if origin := e.snapshotOrigin(object); origin != nil {
|
||||
for parent := origin.node.parent; parent != nil; parent = parent.parent {
|
||||
if parent.name.Local == "Layer" {
|
||||
return parent.attr("DrawParam")
|
||||
id := parent.attr("DrawParam")
|
||||
if object.Type == "PathObject" {
|
||||
if _, err := e.resolveEditorStyle(object, id); err == nil || editReason(err) != EditUnsupportedColor {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// GroupObjects 将同一容器内连续的对象组合,保留标识、绘制顺序及原始内容
|
||||
// 入参: page 页面索引, ids 至少两个对象标识
|
||||
// 返回: string 组合标识, error 错误信息
|
||||
func (e *Editor) GroupObjects(page int, ids []string) (string, error) {
|
||||
_, indexes, err := e.selectedObjects(page, ids)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(indexes) < 2 {
|
||||
return "", fmt.Errorf("grouping requires at least two objects")
|
||||
}
|
||||
slices.SortFunc(indexes, compareEditorPosition)
|
||||
layer := &e.pages[page].Content.Layer[indexes[0].layer]
|
||||
parent, siblings := e.objectOrderIndexes(page, layer, editorObjectID(layer.Objects[indexes[0].index]))
|
||||
first := slices.Index(siblings, indexes[0].index)
|
||||
for i, index := range indexes {
|
||||
id := editorObjectID(e.pages[page].Content.Layer[index.layer].Objects[index.index])
|
||||
capability, err := e.ObjectCapabilities(page, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if index.layer != indexes[0].layer || first+i >= len(siblings) || siblings[first+i] != index.index || !capability.Copy || !capability.Order {
|
||||
return "", fmt.Errorf("grouping requires consecutive objects in the same container")
|
||||
}
|
||||
}
|
||||
var content []byte
|
||||
var source *editorSourcePage
|
||||
states := make(map[string]editorCompositeState)
|
||||
for _, index := range indexes {
|
||||
object := layer.Objects[index.index]
|
||||
var data []byte
|
||||
if origin := e.objectOrigin(editorObjectID(object)); origin != nil {
|
||||
source = origin.page
|
||||
data, err = editorXMLObject(origin.data, origin.node, origin.object, object)
|
||||
if err == nil {
|
||||
data, err = editorXMLStandalone(data, origin.node)
|
||||
}
|
||||
} else {
|
||||
data, err = editorObjectXML(object)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
||||
maps.Copy(states, object.CompositeGraphicUnit.states)
|
||||
state := object.state
|
||||
if object.Type == "TextObject" && object.TextObject.layout != nil {
|
||||
state.layout = object.TextObject.layout
|
||||
state.origin[0], _ = strconv.ParseFloat(object.TextObject.TextCode[0].X, 64)
|
||||
state.origin[1], _ = strconv.ParseFloat(object.TextObject.TextCode[0].Y, 64)
|
||||
}
|
||||
states[editorObjectID(object)] = state
|
||||
}
|
||||
if err := e.prepareSourceIDs(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
id := strconv.Itoa(e.maxID + 1)
|
||||
content, err = editorXMLContainer("Content", nil, content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err := editorXMLContainer("CompositeObject", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id}, {Name: xml.Name{Local: "Boundary"}, Value: "0 0 1 1"}}, bytes.TrimPrefix(content, []byte(xml.Header)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
group, err := newEditorCompositeNode(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if parent == nil {
|
||||
parent = &editorXML{name: xml.Name{Local: "Layer"}, attrs: []xml.Attr{{Name: xml.Name{Local: "ID"}, Value: layer.ID}}, parent: &editorXML{name: xml.Name{Local: "Content"}}}
|
||||
}
|
||||
group.node.parent = parent
|
||||
group.object.CompositeGraphicUnit.states = states
|
||||
origin := &editorObjectOrigin{page: source, data: data, node: group.node, object: group.object}
|
||||
e.maxID++
|
||||
e.replaceGroup(page, indexes, []GraphicObject{group.object}, map[string]*editorObjectOrigin{id: origin})
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// UngroupObject 展开内联组合,保留页面外观与裁剪,含容器行为或扩展的组合不拆解
|
||||
// 入参: page 页面索引, id 组合标识
|
||||
// 返回: []string 展开后的对象标识, error 错误信息
|
||||
func (e *Editor) UngroupObject(page int, id string) ([]string, error) {
|
||||
objects, indexes, err := e.selectedObjects(page, []string{id})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
origin := e.objectOrigin(id)
|
||||
if origin == nil || !editorUngroupable(origin.node) || !editorContainerOrderable(origin.node.parent) || objects[0].CompositeGraphicUnit.ResourceID != "" {
|
||||
return nil, fmt.Errorf("group container cannot be removed without losing attributes")
|
||||
}
|
||||
reader, _, _, nodes, err := e.compositeScope(page, ObjectPath{ID: id})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader.Close()
|
||||
if len(nodes) == 0 {
|
||||
return nil, fmt.Errorf("group has no objects")
|
||||
}
|
||||
members := make([]int, len(nodes))
|
||||
for i := range members {
|
||||
members[i] = i
|
||||
}
|
||||
selection, err := e.CaptureCompositeObjects(page, ObjectPath{ID: id}, members)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result []string
|
||||
err = e.pasteCompositeSelection(selection, func(objects []GraphicObject) error {
|
||||
origins := make(map[string]*editorObjectOrigin, len(objects))
|
||||
for i := range objects {
|
||||
object := &objects[i]
|
||||
copy := *object.origin
|
||||
copy.node.parent = origin.node.parent
|
||||
object.origin = nil
|
||||
id := editorObjectID(*object)
|
||||
origins[id] = ©
|
||||
result = append(result, id)
|
||||
}
|
||||
e.replaceGroup(page, indexes, objects, origins)
|
||||
return nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// editorUngroupable 判断移除内联容器是否会丢失对象行为或未知属性
|
||||
// 入参: node 容器节点
|
||||
// 返回: bool 是否可解组
|
||||
func editorUngroupable(node *editorXML) bool {
|
||||
if node.name.Local != "CompositeObject" && node.name.Local != "CompositeGraphicUnit" {
|
||||
return false
|
||||
}
|
||||
if !editorXMLAttributes(node, "ID Boundary CTM DrawParam Alpha Visible") {
|
||||
return false
|
||||
}
|
||||
for _, child := range node.children {
|
||||
switch child.name.Local {
|
||||
case "Content":
|
||||
if !editorXMLAttributes(child, "") {
|
||||
return false
|
||||
}
|
||||
for _, member := range child.children {
|
||||
if member.name.Space != "" && member.name.Space != ofdNamespace && member.name.Space != "http://www.ofdspec.org" || !slices.Contains([]string{"TextObject", "PathObject", "ImageObject", "CompositeObject", "CompositeGraphicUnit"}, member.name.Local) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
case "Clips":
|
||||
if !editorXMLSupported(child) {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// replaceGroup 原子替换同容器连续成员及原文来源,提交一次撤销记录
|
||||
// 入参: page 页面索引, indexes 原对象位置, objects 新成员, origins 新成员原文
|
||||
func (e *Editor) replaceGroup(page int, indexes []editorObjectPosition, objects []GraphicObject, origins map[string]*editorObjectOrigin) {
|
||||
before := copyEditorPage(e.pages[page]).Content.Layer
|
||||
after := copyEditorPage(e.pages[page]).Content.Layer
|
||||
layer := &after[indexes[0].layer]
|
||||
layer.Objects = slices.Replace(layer.Objects, indexes[0].index, indexes[len(indexes)-1].index+1, objects...)
|
||||
previous := make(map[string]*editorObjectOrigin, len(origins))
|
||||
for id := range origins {
|
||||
previous[id] = e.objectOrigin(id)
|
||||
}
|
||||
apply := func(e *Editor, layers []Layer, origins map[string]*editorObjectOrigin) {
|
||||
e.pages[page].Content.Layer = copyEditorPage(PageContent{Content: Content{Layer: layers}}).Content.Layer
|
||||
for id, origin := range origins {
|
||||
e.setObjectOrigin(id, origin)
|
||||
}
|
||||
}
|
||||
apply(e, after, origins)
|
||||
if change := e.recordChange(); change != nil {
|
||||
change.undo = func(e *Editor) { apply(e, before, previous) }
|
||||
change.redo = func(e *Editor) { apply(e, after, origins) }
|
||||
}
|
||||
}
|
||||
+11
-4
@@ -219,13 +219,17 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
||||
preview := *e
|
||||
preview.source, preview.pages = next, added
|
||||
for i, index := range indexes {
|
||||
layouts := make(map[string]*textLayout)
|
||||
states := make(map[string]editorCompositeState)
|
||||
groups := make(map[string]map[string]editorCompositeState)
|
||||
for _, layer := range e.pages[index].Content.Layer {
|
||||
for _, object := range layer.Objects {
|
||||
id := m.ids[editorObjectID(object)]
|
||||
state := object.state
|
||||
if layout := object.TextObject.layout; object.Type == "TextObject" && layout != nil {
|
||||
value := *layout
|
||||
layouts[m.ids[object.TextObject.ID]] = &value
|
||||
state.layout = layout
|
||||
}
|
||||
states[id] = state
|
||||
groups[id] = remapCompositeStates(object.CompositeGraphicUnit.states, m.ids)
|
||||
}
|
||||
}
|
||||
if err := preview.loadSourcePage(i); err != nil {
|
||||
@@ -234,9 +238,12 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
||||
for j := range added[i].Content.Layer {
|
||||
for k := range added[i].Content.Layer[j].Objects {
|
||||
object := &added[i].Content.Layer[j].Objects[k]
|
||||
id := editorObjectID(*object)
|
||||
object.state = states[id]
|
||||
if object.Type == "TextObject" {
|
||||
object.TextObject.layout = layouts[object.TextObject.ID]
|
||||
object.TextObject.layout = object.state.layout
|
||||
}
|
||||
object.CompositeGraphicUnit.states = groups[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-3
@@ -42,6 +42,7 @@ type editorSource struct {
|
||||
|
||||
// editorObjectOrigin 保留原对象的XML语义,供复制和局部更新复用
|
||||
type editorObjectOrigin struct {
|
||||
editor *Editor
|
||||
page *editorSourcePage
|
||||
data []byte
|
||||
node *editorXML
|
||||
@@ -66,6 +67,7 @@ type editorSourcePage struct {
|
||||
// Paint表示可独立修改纯色填充和描边,不要求重新排版文字
|
||||
// ReplaceImage表示可替换图片数据,CropImage表示可裁剪图片;内部裁剪与原裁剪取交集
|
||||
// MissingGlyphs提供缺字导致操作受限时的结构化诊断
|
||||
// Ungroup表示可移除内联组合容器并保留内部内容
|
||||
type ObjectCapabilities struct {
|
||||
Update bool
|
||||
Paint bool
|
||||
@@ -81,6 +83,7 @@ type ObjectCapabilities struct {
|
||||
Copy bool
|
||||
Delete bool
|
||||
Order bool
|
||||
Ungroup bool
|
||||
Reason string
|
||||
ReasonCode EditReason
|
||||
MissingGlyphs *MissingGlyphError
|
||||
@@ -382,7 +385,8 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
|
||||
return ObjectCapabilities{Reason: err.Error(), ReasonCode: EditInvalidObject}, nil
|
||||
}
|
||||
all.Order = editorContainerOrderable(node.parent)
|
||||
all.Copy = editorXMLCopyable(node) && origin.reason == nil
|
||||
all.Ungroup = all.Order && editorUngroupable(node) && len(object.CompositeGraphicUnit.Objects) != 0
|
||||
all.Copy = editorXMLCopyable(node) && (origin.reason == nil || object.Type == "PathObject" && editReason(origin.reason) == EditUnsupportedColor)
|
||||
if origin.reason != nil {
|
||||
all.Update, all.Reflow, all.ReplaceFont, all.Paint = false, false, false, false
|
||||
all.Reason, all.ReasonCode = origin.reason.Error(), editReason(origin.reason)
|
||||
@@ -415,12 +419,29 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
|
||||
// 入参: id 对象标识
|
||||
// 返回: *editorObjectOrigin 原始对象,无来源时为nil
|
||||
func (e *Editor) objectOrigin(id string) *editorObjectOrigin {
|
||||
if origin := e.origins[id]; origin != nil {
|
||||
return origin
|
||||
}
|
||||
if e.source == nil {
|
||||
return nil
|
||||
}
|
||||
return e.source.origins[id]
|
||||
}
|
||||
|
||||
// setObjectOrigin 保存当前编辑器的对象原文,支持新建文档中的组合对象
|
||||
// 入参: id 对象标识, origin 原文来源,nil恢复原始来源
|
||||
func (e *Editor) setObjectOrigin(id string, origin *editorObjectOrigin) {
|
||||
if e.origins == nil {
|
||||
e.origins = make(map[string]*editorObjectOrigin)
|
||||
}
|
||||
if origin != nil && origin.page == nil && origin.editor == nil {
|
||||
copy := *origin
|
||||
copy.editor = e
|
||||
origin = ©
|
||||
}
|
||||
e.origins[id] = origin
|
||||
}
|
||||
|
||||
// editorPreservedObject 判断对象是否仅改变标识、几何和可独立写回的外观字段
|
||||
// 入参: before 原始快照, after 新快照
|
||||
// 返回: bool 是否保留原内容
|
||||
@@ -475,7 +496,7 @@ func (e *Editor) prepareCopiedObject(id string, object GraphicObject) (GraphicOb
|
||||
if origin == nil {
|
||||
return e.prepareObject(id, object)
|
||||
}
|
||||
if !editorXMLTransformable(origin.node) || !editorXMLContainersSupported(origin.node) || !editorXMLCopyable(origin.node) || origin.reason != nil {
|
||||
if !editorXMLTransformable(origin.node) || !editorXMLContainersSupported(origin.node) || !editorXMLCopyable(origin.node) || origin.reason != nil && (object.Type != "PathObject" || editReason(origin.reason) != EditUnsupportedColor) {
|
||||
return GraphicObject{}, fmt.Errorf("object cannot be copied without changing its original structure")
|
||||
}
|
||||
var drawParam string
|
||||
@@ -485,7 +506,7 @@ func (e *Editor) prepareCopiedObject(id string, object GraphicObject) (GraphicOb
|
||||
break
|
||||
}
|
||||
}
|
||||
before, err := e.resolveEditorStyle(origin.object, drawParam)
|
||||
before, err := e.copiedObjectStyle(origin.object, drawParam)
|
||||
if err != nil {
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
|
||||
+23
-2
@@ -250,7 +250,13 @@ func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, err
|
||||
for _, object := range current.Objects {
|
||||
id := editorObjectID(object)
|
||||
after[id] = object
|
||||
if node := source.nodes[id]; node != nil {
|
||||
node := source.nodes[id]
|
||||
if node == nil {
|
||||
if origin := e.objectOrigin(id); origin != nil {
|
||||
node = origin.node
|
||||
}
|
||||
}
|
||||
if node != nil {
|
||||
ordered[node.parent] = append(ordered[node.parent], object)
|
||||
}
|
||||
}
|
||||
@@ -279,7 +285,7 @@ func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, err
|
||||
if exists {
|
||||
nextID := editorObjectID(next)
|
||||
origin := source.nodes[nextID]
|
||||
if reflect.DeepEqual(before[nextID], next) {
|
||||
if origin != nil && reflect.DeepEqual(before[nextID], next) {
|
||||
encoded = data[origin.start:origin.end]
|
||||
} else {
|
||||
var err error
|
||||
@@ -294,6 +300,21 @@ func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, err
|
||||
patches = append(patches, editorXMLPatch{node.start, node.end, encoded})
|
||||
}
|
||||
}
|
||||
for parent, members := range ordered {
|
||||
if !orderable[parent] || positions[parent] >= len(members) {
|
||||
continue
|
||||
}
|
||||
var added []byte
|
||||
for _, object := range members[positions[parent]:] {
|
||||
origin := e.objectOrigin(editorObjectID(object))
|
||||
encoded, err := editorXMLObject(origin.data, origin.node, origin.object, object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
added = append(added, encoded...)
|
||||
}
|
||||
patches = append(patches, editorXMLPatch{parent.close, parent.close, added})
|
||||
}
|
||||
}
|
||||
var added []byte
|
||||
for _, layer := range page.Content.Layer[len(source.original.Content.Layer):] {
|
||||
|
||||
@@ -187,6 +187,56 @@ func (p PathObject) Shape() (ShapeKind, Box) {
|
||||
return kind, box
|
||||
}
|
||||
|
||||
// ShapeFrame 获取矩形或椭圆的局部几何及到所在坐标系的变换
|
||||
// 返回: ShapeKind 图形类型, Box 局部范围, Matrix 坐标变换,非基本图形返回空类型
|
||||
func (p PathObject) ShapeFrame() (ShapeKind, Box, Matrix) {
|
||||
if p.CTM != "" {
|
||||
if _, err := creationNumbers(p.CTM, 6); err != nil {
|
||||
return "", Box{}, Matrix{}
|
||||
}
|
||||
}
|
||||
boundary, err := ParseBox(p.Boundary)
|
||||
matrix := TranslationMatrix(boundary.X, boundary.Y).Multiply(NewMatrix(p.CTM))
|
||||
if _, ok := matrix.Invert(); err != nil || !ok {
|
||||
return "", Box{}, Matrix{}
|
||||
}
|
||||
p.Boundary, p.CTM = "0 0 1 1", ""
|
||||
kind, box := p.Shape()
|
||||
if kind != ShapeRectangle && kind != ShapeEllipse {
|
||||
return "", Box{}, Matrix{}
|
||||
}
|
||||
return kind, box, matrix
|
||||
}
|
||||
|
||||
// ReshapeFrame 沿矩形或椭圆自身方向调整局部范围,保留样式、变换方向和裁剪
|
||||
// 入参: box ShapeFrame坐标系中的目标范围
|
||||
// 返回: PathObject 调整后的路径, error 错误信息
|
||||
func (p PathObject) ReshapeFrame(box Box) (PathObject, error) {
|
||||
kind, previous, matrix := p.ShapeFrame()
|
||||
if kind == "" {
|
||||
return PathObject{}, fmt.Errorf("path is not a supported framed shape")
|
||||
}
|
||||
if box == previous {
|
||||
return p, nil
|
||||
}
|
||||
if _, err := creationBox(editorBoxString(box)); err != nil {
|
||||
return PathObject{}, err
|
||||
}
|
||||
shape, err := NewShape(kind, Box{W: box.W, H: box.H})
|
||||
if err != nil {
|
||||
return PathObject{}, err
|
||||
}
|
||||
matrix = matrix.Multiply(TranslationMatrix(box.X, box.Y))
|
||||
bounds := matrix.TransformBox(Box{W: box.W, H: box.H})
|
||||
if _, err := creationBox(editorBoxString(bounds)); err != nil {
|
||||
return PathObject{}, err
|
||||
}
|
||||
p.Boundary = editorBoxString(bounds)
|
||||
p.CTM = TranslationMatrix(-bounds.X, -bounds.Y).Multiply(matrix).String()
|
||||
p.AbbreviatedData = shape.AbbreviatedData
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// sameShapePath 比较基本路径的命令与数值,忽略数值格式差异
|
||||
// 入参: a、b 路径词元
|
||||
// 返回: bool 是否相同
|
||||
|
||||
+31
-6
@@ -202,7 +202,7 @@ func (e *Editor) validate() error {
|
||||
// 入参: write 条目写入方法, progress 保存进度回调,预览时为nil
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) writeParts(write func(string, []byte, bool) error, progress editorProgress) error {
|
||||
fonts, images, spaces, _ := e.usedResources()
|
||||
fonts, images, spaces, definitions := e.usedResources()
|
||||
writeXML := func(name string, encode func(*ofdXML)) error {
|
||||
data, err := encodeOFDXML(encode)
|
||||
if err != nil {
|
||||
@@ -257,6 +257,9 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error, progress edi
|
||||
if len(fonts)+len(images)+len(spaces) != 0 {
|
||||
x.text("DocumentRes", "DocumentRes.xml")
|
||||
}
|
||||
for _, name := range definitions {
|
||||
x.text("DocumentRes", "/"+name)
|
||||
}
|
||||
x.end("CommonData")
|
||||
x.start("Pages", nil)
|
||||
for i, page := range e.pages {
|
||||
@@ -280,10 +283,19 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error, progress edi
|
||||
if err := progress.report("pages", i, len(e.pages)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeXML(fmt.Sprintf("Doc_0/Pages/%d/Content.xml", i+1), func(x *ofdXML) {
|
||||
x.page(page)
|
||||
}); err != nil {
|
||||
return err
|
||||
name := fmt.Sprintf("Doc_0/Pages/%d/Content.xml", i+1)
|
||||
if len(e.origins) == 0 {
|
||||
if err := writeXML(name, func(x *ofdXML) { x.page(page) }); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
data, err := e.sourceNewPageXML(page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := write(name, data, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := progress.report("pages", len(e.pages), len(e.pages)); err != nil {
|
||||
@@ -296,6 +308,13 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error, progress edi
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, resource := range e.resources {
|
||||
if resource.definition() != "" && slices.Contains(definitions, resource.name) {
|
||||
if err := write(resource.name, resource.data, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -320,7 +339,7 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
|
||||
for _, layer := range page.Content.Layer {
|
||||
for _, object := range layer.Objects {
|
||||
before, exists := original[editorObjectID(object)]
|
||||
if origin := e.objectOrigin(editorObjectID(object)); origin != nil && (!exists || before.Type != object.Type || !reflect.DeepEqual(before.CompositeGraphicUnit, object.CompositeGraphicUnit)) {
|
||||
if origin := e.objectOrigin(editorObjectID(object)); origin != nil && origin.page != nil && (!exists || before.Type != object.Type || !reflect.DeepEqual(before.CompositeGraphicUnit, object.CompositeGraphicUnit)) {
|
||||
for _, name := range origin.page.original.PageRes {
|
||||
files[e.source.reader.ResPath(resolveResourcePath(origin.page.ref.BaseLoc, "", name))] = true
|
||||
}
|
||||
@@ -336,6 +355,9 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
|
||||
switch object.Type {
|
||||
case "CompositeObject", "CompositeGraphicUnit":
|
||||
collectCompositeReferences(object.CompositeGraphicUnit, used)
|
||||
if e.source != nil && (!exists || !reflect.DeepEqual(before.CompositeGraphicUnit, object.CompositeGraphicUnit)) {
|
||||
collectCompositeReferences(object.CompositeGraphicUnit, promoted)
|
||||
}
|
||||
case "TextObject":
|
||||
used[object.TextObject.DrawParam] = true
|
||||
used[object.TextObject.Font] = true
|
||||
@@ -364,6 +386,9 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
|
||||
if resource.definition() != "" && used[resource.definition()] {
|
||||
for _, id := range resource.references {
|
||||
used[id] = true
|
||||
if e.source != nil {
|
||||
promoted[id] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user