mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
This commit is contained in:
1 parent
4a6174c2ae
commit
7bf9f7b3b7
14 files changed
+762
-79
No files matched your search
+18
-1
@@ -103,6 +103,7 @@
|
|||||||
<button id="drawLineButton" class="button" type="button" aria-label="绘制直线" aria-pressed="false">直线</button>
|
<button id="drawLineButton" class="button" type="button" aria-label="绘制直线" aria-pressed="false">直线</button>
|
||||||
<button id="drawRectangleButton" class="button" type="button" aria-label="绘制矩形" aria-pressed="false">矩形</button>
|
<button id="drawRectangleButton" class="button" type="button" aria-label="绘制矩形" aria-pressed="false">矩形</button>
|
||||||
<button id="drawEllipseButton" class="button" type="button" aria-label="绘制椭圆" aria-pressed="false">椭圆</button>
|
<button id="drawEllipseButton" class="button" type="button" aria-label="绘制椭圆" aria-pressed="false">椭圆</button>
|
||||||
|
<select id="arrowTool" aria-label="绘制箭头" title="绘制箭头"><option value="" hidden>箭头</option><option value="arrow">单向</option><option value="double-arrow">双向</option></select>
|
||||||
<span>插入</span>
|
<span>插入</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-group">
|
<div class="editor-group">
|
||||||
@@ -152,7 +153,7 @@
|
|||||||
<option value="horizontal">水平</option>
|
<option value="horizontal">水平</option>
|
||||||
<option value="vertical">垂直</option>
|
<option value="vertical">垂直</option>
|
||||||
</select>
|
</select>
|
||||||
<span>变换</span>
|
<button id="objectBoundsButton" class="editor-group-action" type="button" title="位置尺寸" aria-label="位置尺寸" aria-haspopup="dialog" aria-controls="objectBoundsPanel" disabled>变换</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-group">
|
<div class="editor-group">
|
||||||
<select id="addPageButton" aria-label="新增页面" title="新增页面"><option value="" hidden>新增</option><option value="blank">空白</option><option value="import">插页</option></select>
|
<select id="addPageButton" aria-label="新增页面" title="新增页面"><option value="" hidden>新增</option><option value="blank">空白</option><option value="import">插页</option></select>
|
||||||
@@ -173,6 +174,7 @@
|
|||||||
</dialog>
|
</dialog>
|
||||||
<dialog id="objectStylePanel" class="form-dialog" aria-label="对象样式">
|
<dialog id="objectStylePanel" class="form-dialog" aria-label="对象样式">
|
||||||
<form id="objectStyleForm">
|
<form id="objectStyleForm">
|
||||||
|
<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 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">
|
<div id="objectStrokeFields">
|
||||||
<div class="form-row"><label for="objectDash">线型</label><select id="objectDash"><option value="mixed" hidden>混合</option><option value="solid">实线</option><option value="dash">虚线</option><option value="dot">点线</option><option value="dashdot">点划</option><option value="custom">自定</option></select></div>
|
<div class="form-row"><label for="objectDash">线型</label><select id="objectDash"><option value="mixed" hidden>混合</option><option value="solid">实线</option><option value="dash">虚线</option><option value="dot">点线</option><option value="dashdot">点划</option><option value="custom">自定</option></select></div>
|
||||||
@@ -191,9 +193,24 @@
|
|||||||
<div class="form-row"><label for="outlineTitle">标题</label><input id="outlineTitle" type="text" required autofocus></div>
|
<div class="form-row"><label for="outlineTitle">标题</label><input id="outlineTitle" type="text" required autofocus></div>
|
||||||
<div class="form-row"><label for="outlineParent">上级</label><select id="outlineParent"></select></div>
|
<div class="form-row"><label for="outlineParent">上级</label><select id="outlineParent"></select></div>
|
||||||
<div class="form-row"><label for="outlinePage">页码</label><input id="outlinePage" type="number" min="1" step="1" placeholder="无" aria-label="目标页码,留空表示无目标"></div>
|
<div class="form-row"><label for="outlinePage">页码</label><input id="outlinePage" type="number" min="1" step="1" placeholder="无" aria-label="目标页码,留空表示无目标"></div>
|
||||||
|
<div id="outlinePositionRow" class="form-row" hidden><label for="outlinePosition">顺序</label><input id="outlinePosition" type="number" min="1" step="1" required value="1"></div>
|
||||||
<div class="form-actions"><div id="outlineStatus" class="form-status" role="status"></div><button id="outlineCancel" class="button" type="button">取消</button><button id="outlineSubmit" class="button" type="submit">新增</button></div>
|
<div class="form-actions"><div id="outlineStatus" class="form-status" role="status"></div><button id="outlineCancel" class="button" type="button">取消</button><button id="outlineSubmit" class="button" type="submit">新增</button></div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
<dialog id="objectBoundsPanel" class="form-dialog" aria-label="位置尺寸">
|
||||||
|
<form id="objectBoundsForm">
|
||||||
|
<div class="form-pair">
|
||||||
|
<div class="form-row"><label for="objectX">横向</label><div class="form-unit"><input id="objectX" type="number" step="any" required autofocus><span>mm</span></div></div>
|
||||||
|
<div class="form-row"><label for="objectY">纵向</label><div class="form-unit"><input id="objectY" type="number" step="any" required><span>mm</span></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-pair">
|
||||||
|
<div class="form-row"><label for="objectWidth">宽度</label><div class="form-unit"><input id="objectWidth" type="number" min="0" step="any" required><span>mm</span></div></div>
|
||||||
|
<div class="form-row"><label for="objectHeight">高度</label><div class="form-unit"><input id="objectHeight" type="number" min="0" step="any" required><span>mm</span></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row"><span>比例</span><label class="form-check"><input id="objectAspect" type="checkbox" checked>锁定</label></div>
|
||||||
|
<div class="form-actions"><div id="objectBoundsStatus" class="form-status" role="status"></div><button id="objectBoundsCancel" class="button" type="button">取消</button><button class="button" type="submit">确定</button></div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
<dialog id="createPanel" class="form-dialog" aria-label="新建文档">
|
<dialog id="createPanel" class="form-dialog" aria-label="新建文档">
|
||||||
<form id="createForm">
|
<form id="createForm">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
|
|||||||
@@ -1511,6 +1511,21 @@ button.outline-link:hover {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-format {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-format > button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.outline-link.selected {
|
.outline-link.selected {
|
||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
|||||||
+168
-13
@@ -1,4 +1,4 @@
|
|||||||
import { CanvasEditor, canEditObject, editTextValue, missingGlyphMessage, objectEditReason, selectedText } from "./ofdgo_edit.js";
|
import { CanvasEditor, canEditObject, editTextValue, missingGlyphMessage, objectEditReason, selectedText, lineShape, selectionBounds } from "./ofdgo_edit.js";
|
||||||
import { FontManager, FontPicker } from "./ofdgo_font.js";
|
import { FontManager, FontPicker } from "./ofdgo_font.js";
|
||||||
|
|
||||||
const MM_TO_PX = 96 / 25.4;
|
const MM_TO_PX = 96 / 25.4;
|
||||||
@@ -43,6 +43,8 @@ const state = {
|
|||||||
outlineAction: "add",
|
outlineAction: "add",
|
||||||
styleOriginal: null,
|
styleOriginal: null,
|
||||||
objectClipboard: null,
|
objectClipboard: null,
|
||||||
|
styleClipboard: null,
|
||||||
|
outlineExpanded: new Map(),
|
||||||
textFonts: [],
|
textFonts: [],
|
||||||
textFontID: null,
|
textFontID: null,
|
||||||
textDefaults: { type: "TextObject", size: 12 * 25.4 / 72, color: "#000000", wrap: true, align: "left", paragraphHeight: 0, letterSpacing: 0 },
|
textDefaults: { type: "TextObject", size: 12 * 25.4 / 72, color: "#000000", wrap: true, align: "left", paragraphHeight: 0, letterSpacing: 0 },
|
||||||
@@ -147,6 +149,7 @@ const el = {
|
|||||||
textSize: document.querySelector("#textSize"),
|
textSize: document.querySelector("#textSize"),
|
||||||
textColor: document.querySelector("#textColor"),
|
textColor: document.querySelector("#textColor"),
|
||||||
drawLineButton: document.querySelector("#drawLineButton"),
|
drawLineButton: document.querySelector("#drawLineButton"),
|
||||||
|
arrowTool: document.querySelector("#arrowTool"),
|
||||||
drawRectangleButton: document.querySelector("#drawRectangleButton"),
|
drawRectangleButton: document.querySelector("#drawRectangleButton"),
|
||||||
drawEllipseButton: document.querySelector("#drawEllipseButton"),
|
drawEllipseButton: document.querySelector("#drawEllipseButton"),
|
||||||
eraseObjectButton: document.querySelector("#eraseObjectButton"),
|
eraseObjectButton: document.querySelector("#eraseObjectButton"),
|
||||||
@@ -176,6 +179,18 @@ const el = {
|
|||||||
batchPagesCancel: document.querySelector("#batchPagesCancel"),
|
batchPagesCancel: document.querySelector("#batchPagesCancel"),
|
||||||
batchPagesSubmit: document.querySelector("#batchPagesSubmit"),
|
batchPagesSubmit: document.querySelector("#batchPagesSubmit"),
|
||||||
objectStyleButton: document.querySelector("#objectStyleButton"),
|
objectStyleButton: document.querySelector("#objectStyleButton"),
|
||||||
|
copyStyleButton: document.querySelector("#copyStyleButton"),
|
||||||
|
pasteStyleButton: document.querySelector("#pasteStyleButton"),
|
||||||
|
objectBoundsButton: document.querySelector("#objectBoundsButton"),
|
||||||
|
objectBoundsPanel: document.querySelector("#objectBoundsPanel"),
|
||||||
|
objectBoundsForm: document.querySelector("#objectBoundsForm"),
|
||||||
|
objectBoundsCancel: document.querySelector("#objectBoundsCancel"),
|
||||||
|
objectBoundsStatus: document.querySelector("#objectBoundsStatus"),
|
||||||
|
objectX: document.querySelector("#objectX"),
|
||||||
|
objectY: document.querySelector("#objectY"),
|
||||||
|
objectWidth: document.querySelector("#objectWidth"),
|
||||||
|
objectHeight: document.querySelector("#objectHeight"),
|
||||||
|
objectAspect: document.querySelector("#objectAspect"),
|
||||||
objectStylePanel: document.querySelector("#objectStylePanel"),
|
objectStylePanel: document.querySelector("#objectStylePanel"),
|
||||||
objectStyleForm: document.querySelector("#objectStyleForm"),
|
objectStyleForm: document.querySelector("#objectStyleForm"),
|
||||||
objectOpacity: document.querySelector("#objectOpacity"),
|
objectOpacity: document.querySelector("#objectOpacity"),
|
||||||
@@ -192,6 +207,8 @@ const el = {
|
|||||||
outlineForm: document.querySelector("#outlineForm"),
|
outlineForm: document.querySelector("#outlineForm"),
|
||||||
outlineTitle: document.querySelector("#outlineTitle"),
|
outlineTitle: document.querySelector("#outlineTitle"),
|
||||||
outlineParent: document.querySelector("#outlineParent"),
|
outlineParent: document.querySelector("#outlineParent"),
|
||||||
|
outlinePosition: document.querySelector("#outlinePosition"),
|
||||||
|
outlinePositionRow: document.querySelector("#outlinePositionRow"),
|
||||||
outlinePage: document.querySelector("#outlinePage"),
|
outlinePage: document.querySelector("#outlinePage"),
|
||||||
outlineStatus: document.querySelector("#outlineStatus"),
|
outlineStatus: document.querySelector("#outlineStatus"),
|
||||||
outlineCancel: document.querySelector("#outlineCancel"),
|
outlineCancel: document.querySelector("#outlineCancel"),
|
||||||
@@ -598,6 +615,51 @@ el.batchPagesForm.addEventListener("submit", async event => {
|
|||||||
if (await changeDocument("ofdgoBatchPages", null, el.batchPageAction.value, el.batchPageRange.value)) el.batchPagesPanel.close();
|
if (await changeDocument("ofdgoBatchPages", null, el.batchPageAction.value, el.batchPageRange.value)) el.batchPagesPanel.close();
|
||||||
});
|
});
|
||||||
editorClick(el.objectStyleButton, openObjectStyle);
|
editorClick(el.objectStyleButton, openObjectStyle);
|
||||||
|
editorClick(el.objectBoundsButton, openObjectBounds);
|
||||||
|
el.arrowTool.addEventListener("change", async () => {
|
||||||
|
const tool = el.arrowTool.value;
|
||||||
|
el.arrowTool.value = "";
|
||||||
|
if (await canvasEditor.commitText() && await canvasEditor.commitCrop()) {
|
||||||
|
setPan(false);
|
||||||
|
state.selectObjects = true;
|
||||||
|
canvasEditor.setTool(tool);
|
||||||
|
updateControls();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
el.copyStyleButton.addEventListener("click", async () => {
|
||||||
|
const item = canvasEditor.selected, seq = state.openSeq;
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await callWASM("ofdgoCaptureStyle", item.index, item.id);
|
||||||
|
if (seq === state.openSeq) { state.styleClipboard = item.type; el.objectStylePanel.close(); }
|
||||||
|
} catch (err) { if (seq === state.openSeq) el.objectStyleStatus.textContent = err.message; }
|
||||||
|
finally { if (seq === state.openSeq) setBusy(false); }
|
||||||
|
});
|
||||||
|
el.pasteStyleButton.addEventListener("click", async () => {
|
||||||
|
const item = canvasEditor.selected;
|
||||||
|
if (await changeDocument("ofdgoPasteStyle", { ...item, id: canvasEditor.items().map(member => member.id) })) el.objectStylePanel.close();
|
||||||
|
});
|
||||||
|
el.objectBoundsCancel.addEventListener("click", () => el.objectBoundsPanel.close());
|
||||||
|
for (const input of [el.objectWidth, el.objectHeight]) input.addEventListener("input", () => {
|
||||||
|
if (!el.objectAspect.checked || !input.value || el.objectWidth.disabled || el.objectHeight.disabled) return;
|
||||||
|
const box = state.boundsOriginal;
|
||||||
|
if (input === el.objectWidth) setObjectDimension(el.objectHeight, Number(input.value) * box.height / box.width);
|
||||||
|
else setObjectDimension(el.objectWidth, Number(input.value) * box.width / box.height);
|
||||||
|
});
|
||||||
|
el.objectAspect.addEventListener("change", () => {
|
||||||
|
if (el.objectAspect.checked) setObjectDimension(el.objectHeight, objectDimension(el.objectWidth) * state.boundsOriginal.height / state.boundsOriginal.width);
|
||||||
|
});
|
||||||
|
el.objectBoundsForm.addEventListener("submit", async event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const item = canvasEditor.selected;
|
||||||
|
const values = [el.objectX, el.objectY, el.objectWidth, el.objectHeight].map(objectDimension);
|
||||||
|
let saved;
|
||||||
|
if (item.shape) {
|
||||||
|
if (lineShape(item.shape)) { values[2] *= Math.sign(item.geometry.width); values[3] *= Math.sign(item.geometry.height); }
|
||||||
|
saved = await changeDocument("ofdgoReshapeObject", item, ...values);
|
||||||
|
} else saved = await changeDocument("ofdgoResizeObjects", { ...item, id: canvasEditor.items().map(member => member.id) }, ...values);
|
||||||
|
if (saved) el.objectBoundsPanel.close();
|
||||||
|
});
|
||||||
el.objectStyleCancel.addEventListener("click", () => el.objectStylePanel.close());
|
el.objectStyleCancel.addEventListener("click", () => el.objectStylePanel.close());
|
||||||
el.objectDash.addEventListener("change", () => {
|
el.objectDash.addEventListener("change", () => {
|
||||||
el.objectDashRow.hidden = el.objectDash.value !== "custom";
|
el.objectDashRow.hidden = el.objectDash.value !== "custom";
|
||||||
@@ -614,8 +676,12 @@ el.outlineForm.addEventListener("submit", async event => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const path = state.outlineAction === "add" ? JSON.parse(el.outlineParent.value) : state.outlineSelection;
|
const path = state.outlineAction === "add" ? JSON.parse(el.outlineParent.value) : state.outlineSelection;
|
||||||
const page = el.outlinePage.value === "" ? -1 : Number(el.outlinePage.value) - 1;
|
const page = el.outlinePage.value === "" ? -1 : Number(el.outlinePage.value) - 1;
|
||||||
if (await changeDocument("ofdgoChangeOutline", null, state.outlineAction, path, el.outlineTitle.value, page)) el.outlinePanel.close();
|
const saved = state.outlineAction === "move"
|
||||||
|
? await changeDocument("ofdgoMoveOutline", null, state.outlineSelection, JSON.parse(el.outlineParent.value), Number(el.outlinePosition.value) - 1)
|
||||||
|
: await changeDocument("ofdgoChangeOutline", null, state.outlineAction, path, el.outlineTitle.value, page);
|
||||||
|
if (saved) el.outlinePanel.close();
|
||||||
});
|
});
|
||||||
|
el.outlineParent.addEventListener("change", updateOutlinePosition);
|
||||||
el.pageCancel.addEventListener("click", () => el.pagePanel.close());
|
el.pageCancel.addEventListener("click", () => el.pagePanel.close());
|
||||||
el.pageWidth.addEventListener("input", updatePageDirection);
|
el.pageWidth.addEventListener("input", updatePageDirection);
|
||||||
el.pageHeight.addEventListener("input", updatePageDirection);
|
el.pageHeight.addEventListener("input", updatePageDirection);
|
||||||
@@ -865,12 +931,37 @@ function handleKeyDown(event) {
|
|||||||
|
|
||||||
function formDialogOpen() {
|
function formDialogOpen() {
|
||||||
return el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open
|
return el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open
|
||||||
|| el.batchPagesPanel.open || el.objectStylePanel.open || el.outlinePanel.open;
|
|| el.batchPagesPanel.open || el.objectStylePanel.open || el.outlinePanel.open || el.objectBoundsPanel.open;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setObjectDimension(input, value) {
|
||||||
|
input.value = String(Number(value.toFixed(2)));
|
||||||
|
input.defaultValue = input.value;
|
||||||
|
input.dataset.precise = String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectDimension(input) {
|
||||||
|
return Number(input.value === input.defaultValue ? input.dataset.precise : input.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openObjectBounds() {
|
||||||
|
const item = canvasEditor.selected;
|
||||||
|
const box = item.geometry || selectionBounds((item.items || [item]).map(member => member.bounds || member));
|
||||||
|
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 = !item.shape && (item.items || [item]).some(member => member.type !== "ImageObject") || 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 = "";
|
||||||
|
el.objectBoundsPanel.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openObjectStyle() {
|
function openObjectStyle() {
|
||||||
const items = canvasEditor.items();
|
const items = canvasEditor.items();
|
||||||
if (!items.length) return;
|
if (!items.length) return;
|
||||||
|
el.copyStyleButton.disabled = items.length !== 1 || !canEditObject(items[0], "update");
|
||||||
|
el.pasteStyleButton.disabled = !state.styleClipboard || !items.every(item => item.type === state.styleClipboard && canEditObject(item, "update"));
|
||||||
const shared = (key, fallback) => items.every(item => (item[key] ?? fallback) === (items[0][key] ?? fallback)) ? items[0][key] ?? fallback : null;
|
const shared = (key, fallback) => items.every(item => (item[key] ?? fallback) === (items[0][key] ?? fallback)) ? items[0][key] ?? fallback : null;
|
||||||
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);
|
||||||
@@ -921,18 +1012,59 @@ function openOutlinePanel(action) {
|
|||||||
addOption([], "无");
|
addOption([], "无");
|
||||||
const walk = (items, path = []) => items.forEach((item, index) => {
|
const walk = (items, path = []) => items.forEach((item, index) => {
|
||||||
const next = [...path, index];
|
const next = [...path, index];
|
||||||
|
if (action === "move" && state.outlineSelection.every((part, i) => next[i] === part)) return;
|
||||||
addOption(next, `${" ".repeat(path.length)}${item.title}`);
|
addOption(next, `${" ".repeat(path.length)}${item.title}`);
|
||||||
walk(item.children || [], next);
|
walk(item.children || [], next);
|
||||||
});
|
});
|
||||||
walk(state.doc.outlines || []);
|
walk(state.doc.outlines || []);
|
||||||
el.outlineParent.value = JSON.stringify(action === "add" ? state.outlineSelection || [] : state.outlineSelection.slice(0, -1));
|
el.outlineParent.value = JSON.stringify(action === "add" ? state.outlineSelection || [] : state.outlineSelection.slice(0, -1));
|
||||||
el.outlineParent.disabled = action !== "add";
|
el.outlineParent.disabled = action !== "add" && action !== "move";
|
||||||
el.outlineTitle.disabled = el.outlinePage.disabled = action === "delete";
|
el.outlineTitle.disabled = el.outlinePage.disabled = action === "delete" || action === "move";
|
||||||
el.outlineSubmit.textContent = { add: "新增", update: "确定", delete: "删除" }[action];
|
el.outlinePositionRow.hidden = action !== "move";
|
||||||
|
el.outlinePosition.disabled = action !== "move";
|
||||||
|
updateOutlinePosition();
|
||||||
|
el.outlineSubmit.textContent = { add: "新增", update: "确定", delete: "删除", move: "确定" }[action];
|
||||||
el.outlineStatus.textContent = action === "delete" && outline.children?.length ? "同时删除子目录" : "";
|
el.outlineStatus.textContent = action === "delete" && outline.children?.length ? "同时删除子目录" : "";
|
||||||
el.outlinePanel.showModal();
|
el.outlinePanel.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function outlineChildren(outlines, path) {
|
||||||
|
for (const index of path) outlines = outlines[index].children || [];
|
||||||
|
return outlines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOutlinePosition() {
|
||||||
|
if (state.outlineAction !== "move") return;
|
||||||
|
const parent = JSON.parse(el.outlineParent.value);
|
||||||
|
const same = JSON.stringify(parent) === JSON.stringify(state.outlineSelection.slice(0, -1));
|
||||||
|
const count = outlineChildren(state.doc.outlines, parent).length + (same ? 0 : 1);
|
||||||
|
el.outlinePosition.max = String(count);
|
||||||
|
el.outlinePosition.value = String(same ? state.outlineSelection.at(-1) + 1 : count);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remapOutlineExpansion(name, args, next) {
|
||||||
|
const roots = state.doc.outlines || [], expanded = new Map();
|
||||||
|
const walk = (items, visit, path = []) => items.forEach((item, index) => {
|
||||||
|
const current = [...path, index];
|
||||||
|
visit(item, JSON.stringify(current));
|
||||||
|
walk(item.children || [], visit, current);
|
||||||
|
});
|
||||||
|
walk(roots, (item, key) => expanded.set(item, state.outlineExpanded.get(key) ?? item.expanded));
|
||||||
|
const moving = name === "ofdgoMoveOutline", action = moving ? "move" : args[0], source = moving ? args[0] : args[1];
|
||||||
|
let item;
|
||||||
|
if (action === "move" || action === "delete") item = outlineChildren(roots, source.slice(0, -1)).splice(source.at(-1), 1)[0];
|
||||||
|
if (action === "move" || action === "add") {
|
||||||
|
let children = roots;
|
||||||
|
for (const index of next.slice(0, -1)) {
|
||||||
|
expanded.set(children[index], true);
|
||||||
|
children = children[index].children ||= [];
|
||||||
|
}
|
||||||
|
children.splice(next.at(-1), 0, item || {});
|
||||||
|
}
|
||||||
|
state.outlineExpanded.clear();
|
||||||
|
walk(roots, (item, key) => state.outlineExpanded.set(key, Boolean(expanded.get(item))));
|
||||||
|
}
|
||||||
|
|
||||||
function openImportPanel() {
|
function openImportPanel() {
|
||||||
if (!state.editing || document.body.hasAttribute("aria-busy")) return;
|
if (!state.editing || document.body.hasAttribute("aria-busy")) return;
|
||||||
state.importPageCount = 0;
|
state.importPageCount = 0;
|
||||||
@@ -1050,6 +1182,7 @@ async function toggleEditor() {
|
|||||||
state.selectObjects = true;
|
state.selectObjects = true;
|
||||||
state.ofdBytes = null;
|
state.ofdBytes = null;
|
||||||
state.objectClipboard = null;
|
state.objectClipboard = null;
|
||||||
|
state.styleClipboard = null;
|
||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
setPan(false);
|
setPan(false);
|
||||||
await openDocument({ doc, openSeq, skipAutoFonts: true, pageIndex, fitMode, scale,
|
await openDocument({ doc, openSeq, skipAutoFonts: true, pageIndex, fitMode, scale,
|
||||||
@@ -1097,6 +1230,7 @@ async function createDocument(event) {
|
|||||||
}
|
}
|
||||||
state.editing = true;
|
state.editing = true;
|
||||||
state.objectClipboard = null;
|
state.objectClipboard = null;
|
||||||
|
state.styleClipboard = null;
|
||||||
state.fontRenderPending = false;
|
state.fontRenderPending = false;
|
||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
delete state.textDefaults.fontChoice;
|
delete state.textDefaults.fontChoice;
|
||||||
@@ -1516,12 +1650,13 @@ async function changeShapeStyle() {
|
|||||||
function updateDrawingControls() {
|
function updateDrawingControls() {
|
||||||
const tool = canvasEditor.tool;
|
const tool = canvasEditor.tool;
|
||||||
el.insertTextButton.setAttribute("aria-pressed", String(tool === "text"));
|
el.insertTextButton.setAttribute("aria-pressed", String(tool === "text"));
|
||||||
const line = tool === "line" || !tool && canvasEditor.selected?.shape === "line";
|
const line = lineShape(tool) || !tool && lineShape(canvasEditor.selected?.shape);
|
||||||
if (line) {
|
if (line) {
|
||||||
el.shapeFill.checked = false;
|
el.shapeFill.checked = false;
|
||||||
el.shapeStroke.checked = true;
|
el.shapeStroke.checked = true;
|
||||||
}
|
}
|
||||||
const disabled = !state.editing || !state.ready || state.exporting;
|
const disabled = !state.editing || !state.ready || state.exporting;
|
||||||
|
el.arrowTool.disabled = disabled || !pageCan("insert");
|
||||||
for (const [button, name] of [[el.drawLineButton, "line"], [el.drawRectangleButton, "rectangle"], [el.drawEllipseButton, "ellipse"]]) {
|
for (const [button, name] of [[el.drawLineButton, "line"], [el.drawRectangleButton, "rectangle"], [el.drawEllipseButton, "ellipse"]]) {
|
||||||
button.disabled = disabled || !pageCan("insert");
|
button.disabled = disabled || !pageCan("insert");
|
||||||
button.setAttribute("aria-pressed", String(tool === name));
|
button.setAttribute("aria-pressed", String(tool === name));
|
||||||
@@ -1560,6 +1695,10 @@ async function changeDocument(name, item, ...args) {
|
|||||||
if (doc.revision === state.editorInfo?.revision) {
|
if (doc.revision === state.editorInfo?.revision) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if ((name === "ofdgoUndo" || name === "ofdgoRedo") && JSON.stringify(doc.outlines) !== JSON.stringify(state.doc.outlines)) {
|
||||||
|
state.outlineSelection = null;
|
||||||
|
state.outlineExpanded.clear();
|
||||||
|
}
|
||||||
setEditorInfo(doc);
|
setEditorInfo(doc);
|
||||||
if (name === "ofdgoUpdateInfo") {
|
if (name === "ofdgoUpdateInfo") {
|
||||||
Object.assign(state.doc, { title: doc.title, author: doc.author, subject: doc.subject });
|
Object.assign(state.doc, { title: doc.title, author: doc.author, subject: doc.subject });
|
||||||
@@ -1567,8 +1706,10 @@ async function changeDocument(name, item, ...args) {
|
|||||||
updateControls();
|
updateControls();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (name === "ofdgoChangeOutline") {
|
if (name === "ofdgoChangeOutline" || name === "ofdgoMoveOutline") {
|
||||||
|
remapOutlineExpansion(name, args, doc.outlinePath);
|
||||||
state.doc.outlines = doc.outlines;
|
state.doc.outlines = doc.outlines;
|
||||||
|
state.outlineSelection = doc.outlinePath;
|
||||||
renderOutlines(false);
|
renderOutlines(false);
|
||||||
showNavigation(el.outlinesTab);
|
showNavigation(el.outlinesTab);
|
||||||
updateControls();
|
updateControls();
|
||||||
@@ -1603,6 +1744,8 @@ async function changeDocument(name, item, ...args) {
|
|||||||
el.batchPagesStatus.textContent = err.message;
|
el.batchPagesStatus.textContent = err.message;
|
||||||
} else if (el.objectStylePanel.open) {
|
} else if (el.objectStylePanel.open) {
|
||||||
el.objectStyleStatus.textContent = err.message;
|
el.objectStyleStatus.textContent = err.message;
|
||||||
|
} else if (el.objectBoundsPanel.open) {
|
||||||
|
el.objectBoundsStatus.textContent = err.message;
|
||||||
} else if (el.outlinePanel.open) {
|
} else if (el.outlinePanel.open) {
|
||||||
el.outlineStatus.textContent = err.message;
|
el.outlineStatus.textContent = err.message;
|
||||||
} else if (el.infoPanel.open) {
|
} else if (el.infoPanel.open) {
|
||||||
@@ -2121,6 +2264,7 @@ async function openOFD(file) {
|
|||||||
state.fileName = file.name || "ofdgo.ofd";
|
state.fileName = file.name || "ofdgo.ofd";
|
||||||
state.editing = false;
|
state.editing = false;
|
||||||
state.objectClipboard = null;
|
state.objectClipboard = null;
|
||||||
|
state.styleClipboard = null;
|
||||||
state.editorInfo = null;
|
state.editorInfo = null;
|
||||||
state.fontRenderPending = false;
|
state.fontRenderPending = false;
|
||||||
state.savedRevision = null;
|
state.savedRevision = null;
|
||||||
@@ -2134,6 +2278,7 @@ async function openOFD(file) {
|
|||||||
el.importPanel.close();
|
el.importPanel.close();
|
||||||
el.batchPagesPanel.close();
|
el.batchPagesPanel.close();
|
||||||
el.objectStylePanel.close();
|
el.objectStylePanel.close();
|
||||||
|
el.objectBoundsPanel.close();
|
||||||
el.outlinePanel.close();
|
el.outlinePanel.close();
|
||||||
updateControls();
|
updateControls();
|
||||||
await openDocument({ pageIndex: 0, resetScroll: true, openSeq });
|
await openDocument({ pageIndex: 0, resetScroll: true, openSeq });
|
||||||
@@ -3668,23 +3813,26 @@ function showNavigation(selected) {
|
|||||||
function renderOutlines(reset = true) {
|
function renderOutlines(reset = true) {
|
||||||
const selected = !reset && [el.pagesTab, el.outlinesTab, el.searchTab].find(tab => tab.getAttribute("aria-selected") === "true");
|
const selected = !reset && [el.pagesTab, el.outlinesTab, el.searchTab].find(tab => tab.getAttribute("aria-selected") === "true");
|
||||||
const scrollTop = reset ? 0 : el.pageListPanel.scrollTop;
|
const scrollTop = reset ? 0 : el.pageListPanel.scrollTop;
|
||||||
if (reset) state.navigationScroll.clear();
|
if (reset) {
|
||||||
|
state.navigationScroll.clear();
|
||||||
|
state.outlineExpanded.clear();
|
||||||
|
state.outlineSelection = null;
|
||||||
|
}
|
||||||
const outlines = state.doc.outlines || [];
|
const outlines = state.doc.outlines || [];
|
||||||
el.pageListTitle.hidden = true;
|
el.pageListTitle.hidden = true;
|
||||||
el.navigationTabs.hidden = false;
|
el.navigationTabs.hidden = false;
|
||||||
el.outlinesTab.hidden = outlines.length === 0 && !state.editing;
|
el.outlinesTab.hidden = outlines.length === 0 && !state.editing;
|
||||||
el.outlineList.replaceChildren();
|
el.outlineList.replaceChildren();
|
||||||
state.outlineSelection = null;
|
|
||||||
if (state.editing) {
|
if (state.editing) {
|
||||||
const tools = document.createElement("div");
|
const tools = document.createElement("div");
|
||||||
tools.className = "outline-tools";
|
tools.className = "outline-tools";
|
||||||
for (const [action, label] of [["add", "新增"], ["update", "修改"], ["delete", "删除"]]) {
|
for (const [action, label] of [["add", "新增"], ["update", "修改"], ["move", "移动"], ["delete", "删除"]]) {
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.className = "button";
|
button.className = "button";
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
button.textContent = label;
|
button.textContent = label;
|
||||||
button.dataset.outlineAction = action;
|
button.dataset.outlineAction = action;
|
||||||
button.disabled = action !== "add";
|
button.disabled = action !== "add" && !state.outlineSelection?.length;
|
||||||
editorClick(button, () => openOutlinePanel(action));
|
editorClick(button, () => openOutlinePanel(action));
|
||||||
tools.append(button);
|
tools.append(button);
|
||||||
}
|
}
|
||||||
@@ -3885,6 +4033,7 @@ function createOutlineList(outlines, path = []) {
|
|||||||
}
|
}
|
||||||
const link = document.createElement(outline.page || state.editing ? "button" : "span");
|
const link = document.createElement(outline.page || state.editing ? "button" : "span");
|
||||||
link.className = "outline-link";
|
link.className = "outline-link";
|
||||||
|
link.classList.toggle("selected", state.editing && JSON.stringify(current) === JSON.stringify(state.outlineSelection));
|
||||||
const title = document.createElement("span");
|
const title = document.createElement("span");
|
||||||
title.textContent = outline.title;
|
title.textContent = outline.title;
|
||||||
link.append(title);
|
link.append(title);
|
||||||
@@ -3911,7 +4060,11 @@ function createOutlineList(outlines, path = []) {
|
|||||||
row.append(link);
|
row.append(link);
|
||||||
if (hasChildren) {
|
if (hasChildren) {
|
||||||
const details = document.createElement("details");
|
const details = document.createElement("details");
|
||||||
details.open = outline.expanded;
|
const key = JSON.stringify(current);
|
||||||
|
details.open = state.outlineExpanded.get(key) ?? outline.expanded;
|
||||||
|
details.addEventListener("toggle", () => {
|
||||||
|
if (details.isConnected) state.outlineExpanded.set(key, details.open);
|
||||||
|
});
|
||||||
details.append(row, createOutlineList(outline.children, current));
|
details.append(row, createOutlineList(outline.children, current));
|
||||||
item.append(details);
|
item.append(details);
|
||||||
} else {
|
} else {
|
||||||
@@ -4940,6 +5093,7 @@ function updateObjectControls(item, reset = false) {
|
|||||||
el.copyObjectButton.disabled = disabled || !canEditObject(item, "copy");
|
el.copyObjectButton.disabled = disabled || !canEditObject(item, "copy");
|
||||||
el.objectStyleButton.disabled = disabled || !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.objectAlign.disabled = disabled || cropping || !canEditObject(item, "arrange");
|
el.objectAlign.disabled = disabled || cropping || !canEditObject(item, "arrange");
|
||||||
el.objectRotate.disabled = el.objectFlip.disabled = el.objectAlign.disabled;
|
el.objectRotate.disabled = el.objectFlip.disabled = el.objectAlign.disabled;
|
||||||
el.cropImageButton.disabled = disabled || item.type !== "ImageObject" || !canEditObject(item, "update");
|
el.cropImageButton.disabled = disabled || item.type !== "ImageObject" || !canEditObject(item, "update");
|
||||||
@@ -5063,6 +5217,7 @@ function setBusy(busy, text = "", percent = 0, status = "") {
|
|||||||
el.importForm.inert = busy;
|
el.importForm.inert = busy;
|
||||||
el.batchPagesForm.inert = busy;
|
el.batchPagesForm.inert = busy;
|
||||||
el.objectStyleForm.inert = busy;
|
el.objectStyleForm.inert = busy;
|
||||||
|
el.objectBoundsForm.inert = busy;
|
||||||
el.outlineForm.inert = busy;
|
el.outlineForm.inert = busy;
|
||||||
el.outlineList.inert = busy;
|
el.outlineList.inert = busy;
|
||||||
el.navigationTabs.inert = busy;
|
el.navigationTabs.inert = busy;
|
||||||
|
|||||||
+90
-28
@@ -32,6 +32,34 @@ export function objectTransform(box, dx, dy, corner = "") {
|
|||||||
return { x: left ? box.width * (1 - scale) : 0, y: top ? box.height * (1 - scale) : 0, scale };
|
return { x: left ? box.width * (1 - scale) : 0, y: top ? box.height * (1 - scale) : 0, scale };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function textTransform(box, dx, dy, corner) {
|
||||||
|
if (!corner) return objectTransform(box, dx, dy);
|
||||||
|
const left = corner.includes("w"), top = corner.includes("n");
|
||||||
|
const horizontal = (left ? -dx : dx) / box.width;
|
||||||
|
const vertical = (top ? -dy : dy) / box.height;
|
||||||
|
const delta = corner.length === 1 || Math.abs(vertical) > Math.abs(horizontal) ? vertical : horizontal;
|
||||||
|
const scale = Math.max(Math.min(1, 1 / box.height), 1 + delta);
|
||||||
|
return { x: corner.length === 1 ? box.width * (1 - scale) / 2 : left ? box.width * (1 - scale) : 0,
|
||||||
|
y: top ? box.height * (1 - scale) : 0, scale };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lineShape(shape) {
|
||||||
|
return shape === "line" || shape === "arrow" || shape === "double-arrow";
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrowPath(shape, box) {
|
||||||
|
const { x, y, width: w, height: h } = box, length = Math.hypot(w, h);
|
||||||
|
let path = `M${x} ${y}L${x + w} ${y + h}`;
|
||||||
|
if (!length) return path;
|
||||||
|
const head = Math.min(4, length / 3), ux = w / length, uy = h / length;
|
||||||
|
for (const reverse of shape === "double-arrow" ? [false, true] : [false]) {
|
||||||
|
const px = reverse ? x : x + w, py = reverse ? y : y + h, direction = reverse ? -1 : 1;
|
||||||
|
const bx = px - ux * head * direction, by = py - uy * head * direction;
|
||||||
|
path += `M${bx - uy * head * .45} ${by + ux * head * .45}L${px} ${py}L${bx + uy * head * .45} ${by - ux * head * .45}`;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
export function selectionBounds(items) {
|
export function selectionBounds(items) {
|
||||||
const x = Math.min(...items.map(item => item.x)), y = Math.min(...items.map(item => item.y));
|
const x = Math.min(...items.map(item => item.x)), y = Math.min(...items.map(item => item.y));
|
||||||
return { x, y, width: Math.max(...items.map(item => item.x + item.width)) - x,
|
return { x, y, width: Math.max(...items.map(item => item.x + item.width)) - x,
|
||||||
@@ -96,7 +124,7 @@ function cropBox(box, bounds, dx, dy, corner) {
|
|||||||
|
|
||||||
export function constrainedPoint(from, to, shape, shift) {
|
export function constrainedPoint(from, to, shape, shift) {
|
||||||
let dx = to.x - from.x, dy = to.y - from.y;
|
let dx = to.x - from.x, dy = to.y - from.y;
|
||||||
if (shift && shape === "line") {
|
if (shift && lineShape(shape)) {
|
||||||
const directions = [[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]];
|
const directions = [[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]];
|
||||||
const [x, y] = directions[(Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) + 8) % 8];
|
const [x, y] = directions[(Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) + 8) % 8];
|
||||||
const length = (dx * x + dy * y) / (x * x + y * y);
|
const length = (dx * x + dy * y) / (x * x + y * y);
|
||||||
@@ -113,7 +141,7 @@ export function constrainedPoint(from, to, shape, shift) {
|
|||||||
export function reshapeBox(item, dx, dy, handle, shift) {
|
export function reshapeBox(item, dx, dy, handle, shift) {
|
||||||
const box = item.geometry;
|
const box = item.geometry;
|
||||||
if (!dx && !dy && !shift) return { ...box };
|
if (!dx && !dy && !shift) return { ...box };
|
||||||
if (item.shape === "line") {
|
if (lineShape(item.shape)) {
|
||||||
const start = { x: box.x, y: box.y }, end = { x: box.x + box.width, y: box.y + box.height };
|
const start = { x: box.x, y: box.y }, end = { x: box.x + box.width, y: box.y + box.height };
|
||||||
const fixed = handle === "start" ? end : start, moving = handle === "start" ? start : end;
|
const fixed = handle === "start" ? end : start, moving = handle === "start" ? start : end;
|
||||||
const point = constrainedPoint(fixed, { x: moving.x + dx, y: moving.y + dy }, "line", shift);
|
const point = constrainedPoint(fixed, { x: moving.x + dx, y: moving.y + dy }, "line", shift);
|
||||||
@@ -136,7 +164,8 @@ export function reshapeBox(item, dx, dy, handle, shift) {
|
|||||||
|
|
||||||
function paintShape(node, shape, box) {
|
function paintShape(node, shape, box) {
|
||||||
const { x, y, width, height } = box;
|
const { x, y, width, height } = box;
|
||||||
const attributes = shape === "line" ? { x1: x, y1: y, x2: x + width, y2: y + height }
|
const attributes = lineShape(shape) && shape !== "line" ? { d: arrowPath(shape, box) }
|
||||||
|
: shape === "line" ? { x1: x, y1: y, x2: x + width, y2: y + height }
|
||||||
: shape === "ellipse" ? { cx: x + width / 2, cy: y + height / 2, rx: width / 2, ry: height / 2 }
|
: shape === "ellipse" ? { cx: x + width / 2, cy: y + height / 2, rx: width / 2, ry: height / 2 }
|
||||||
: { x, y, width, height };
|
: { x, y, width, height };
|
||||||
for (const [key, value] of Object.entries(attributes)) {
|
for (const [key, value] of Object.entries(attributes)) {
|
||||||
@@ -237,18 +266,21 @@ export class CanvasEditor {
|
|||||||
this.pages.set(surface, { index, width: page.width, height: page.height });
|
this.pages.set(surface, { index, width: page.width, height: page.height });
|
||||||
const layer = document.createElement("div");
|
const layer = document.createElement("div");
|
||||||
layer.className = "edit-layer";
|
layer.className = "edit-layer";
|
||||||
for (const object of page.objects) {
|
const artwork = new Map([...surface.querySelectorAll("[data-ofd-object]")].map((node, order) => [node.getAttribute("data-ofd-object"), { node, order }]));
|
||||||
|
const objects = [...page.objects].sort((a, b) => (artwork.get(a.id)?.order ?? 0) - (artwork.get(b.id)?.order ?? 0));
|
||||||
|
for (const object of objects) {
|
||||||
const node = document.createElement("div");
|
const node = document.createElement("div");
|
||||||
node.className = "edit-object";
|
node.className = "edit-object";
|
||||||
node.classList.toggle("read-only", !canEditObject(object, "transform"));
|
node.classList.toggle("read-only", !canEditObject(object, "transform"));
|
||||||
node.title = objectEditReason(object);
|
node.title = objectEditReason(object);
|
||||||
if (object.shape === "line") {
|
if (lineShape(object.shape)) {
|
||||||
node.classList.add("edit-line");
|
node.classList.add("edit-line");
|
||||||
}
|
}
|
||||||
node.tabIndex = 0;
|
node.tabIndex = 0;
|
||||||
node.setAttribute("role", "button");
|
node.setAttribute("role", "button");
|
||||||
node.setAttribute("aria-label", { ImageObject: "图片对象", TextObject: "文字对象", PathObject: "图形对象" }[object.type]);
|
node.setAttribute("aria-label", { ImageObject: "图片对象", TextObject: "文字对象", PathObject: "图形对象" }[object.type]);
|
||||||
const item = { ...object, index, page: { width: page.width, height: page.height }, node, surface };
|
const item = { ...object, index, page: { width: page.width, height: page.height }, node, surface };
|
||||||
|
item.artwork = artwork.get(object.id)?.node;
|
||||||
if (object.type === "PathObject" && (object.shape || object.outline)) {
|
if (object.type === "PathObject" && (object.shape || object.outline)) {
|
||||||
node.classList.add("edit-contour");
|
node.classList.add("edit-contour");
|
||||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
@@ -263,10 +295,10 @@ export class CanvasEditor {
|
|||||||
}
|
}
|
||||||
this.nodes.set(node, item);
|
this.nodes.set(node, item);
|
||||||
node.addEventListener("focus", () => this.select(item));
|
node.addEventListener("focus", () => this.select(item));
|
||||||
const handles = !canEditObject(object, "transform") ? [] : object.shape === "line" ? ["start", "end"]
|
const handles = !canEditObject(object, "transform") ? [] : lineShape(object.shape) ? ["start", "end"]
|
||||||
: object.shape ? ["nw", "n", "ne", "e", "se", "s", "sw", "w"]
|
: object.shape ? ["nw", "n", "ne", "e", "se", "s", "sw", "w"]
|
||||||
: object.type === "TextObject" && canEditObject(object, "reflow") && canEditObject(object, "layoutKnown")
|
: object.type === "TextObject" && canEditObject(object, "reflow") && canEditObject(object, "layoutKnown")
|
||||||
? ["nw", "ne", "e", "se", "sw", "w"] : ["nw", "ne", "sw", "se"];
|
? ["nw", "n", "ne", "e", "se", "s", "sw", "w"] : ["nw", "ne", "sw", "se"];
|
||||||
for (const corner of handles) {
|
for (const corner of handles) {
|
||||||
const handle = document.createElement("span");
|
const handle = document.createElement("span");
|
||||||
handle.className = `edit-handle edit-${corner}`;
|
handle.className = `edit-handle edit-${corner}`;
|
||||||
@@ -299,6 +331,11 @@ export class CanvasEditor {
|
|||||||
y: change.y + (member.y - item.y) * (change.scale - 1), scale: change.scale,
|
y: change.y + (member.y - item.y) * (change.scale - 1), scale: change.scale,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (item.artwork) {
|
||||||
|
const scale = change.scale, x = item.x * (1 - scale) + change.x, y = item.y * (1 - scale) + change.y;
|
||||||
|
if (x || y || scale !== 1) item.artwork.setAttribute("transform", `translate(${x} ${y}) scale(${scale})`);
|
||||||
|
else item.artwork.removeAttribute("transform");
|
||||||
|
}
|
||||||
if (item.shape) {
|
if (item.shape) {
|
||||||
this.placeShape(item, { x: item.geometry.x + change.x + (item.geometry.x - item.x) * (change.scale - 1),
|
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),
|
y: item.geometry.y + change.y + (item.geometry.y - item.y) * (change.scale - 1),
|
||||||
@@ -328,7 +365,7 @@ export class CanvasEditor {
|
|||||||
left: `${x * PX_PER_MM}px`, top: `${y * PX_PER_MM}px`,
|
left: `${x * PX_PER_MM}px`, top: `${y * PX_PER_MM}px`,
|
||||||
width: `${Math.abs(box.width) * PX_PER_MM}px`, height: `${Math.abs(box.height) * PX_PER_MM}px`,
|
width: `${Math.abs(box.width) * PX_PER_MM}px`, height: `${Math.abs(box.height) * PX_PER_MM}px`,
|
||||||
});
|
});
|
||||||
if (item.shape === "line") {
|
if (lineShape(item.shape)) {
|
||||||
for (const handle of item.node.children) {
|
for (const handle of item.node.children) {
|
||||||
if (!handle.dataset.corner) continue;
|
if (!handle.dataset.corner) continue;
|
||||||
const end = handle.dataset.corner === "end";
|
const end = handle.dataset.corner === "end";
|
||||||
@@ -340,7 +377,8 @@ export class CanvasEditor {
|
|||||||
const { svg, path } = item.contour;
|
const { svg, path } = item.contour;
|
||||||
const width = Math.abs(box.width), height = Math.abs(box.height);
|
const width = Math.abs(box.width), height = Math.abs(box.height);
|
||||||
svg.setAttribute("viewBox", `0 0 ${width || 1} ${height || 1}`);
|
svg.setAttribute("viewBox", `0 0 ${width || 1} ${height || 1}`);
|
||||||
const values = item.shape === "line" ? { x1: box.x - x, y1: box.y - y, x2: box.x + box.width - x, y2: box.y + box.height - y }
|
const values = lineShape(item.shape) && item.shape !== "line" ? { d: arrowPath(item.shape, { x: box.x - x, y: box.y - y, width: box.width, height: box.height }) }
|
||||||
|
: item.shape === "line" ? { x1: box.x - x, y1: box.y - y, x2: box.x + box.width - x, y2: box.y + box.height - y }
|
||||||
: item.shape === "ellipse" ? { cx: width / 2, cy: height / 2, rx: width / 2, ry: height / 2 }
|
: item.shape === "ellipse" ? { cx: width / 2, cy: height / 2, rx: width / 2, ry: height / 2 }
|
||||||
: { x: 0, y: 0, width, height };
|
: { x: 0, y: 0, width, height };
|
||||||
for (const [key, value] of Object.entries(values)) path.setAttribute(key, String(value));
|
for (const [key, value] of Object.entries(values)) path.setAttribute(key, String(value));
|
||||||
@@ -430,12 +468,12 @@ export class CanvasEditor {
|
|||||||
this.cycleSelection(event);
|
this.cycleSelection(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const node = event.target.closest(".edit-object, .edit-selection");
|
let node = event.target.closest(".edit-object, .edit-selection");
|
||||||
if (node) {
|
const direct = this.nodes.get(node);
|
||||||
event.preventDefault();
|
const target = event.target.dataset.corner || direct && !direct.contours ? direct : this.itemsAt(event)[0];
|
||||||
}
|
if (node || target) event.preventDefault();
|
||||||
|
node = target?.node;
|
||||||
this.viewer.focus({ preventScroll: true });
|
this.viewer.focus({ preventScroll: true });
|
||||||
const target = node ? this.nodes.get(node) : null;
|
|
||||||
const additive = event.shiftKey || event.ctrlKey || event.metaKey || this.multiple;
|
const additive = event.shiftKey || event.ctrlKey || event.metaKey || this.multiple;
|
||||||
if (!target) {
|
if (!target) {
|
||||||
this.startMarquee(event, additive);
|
this.startMarquee(event, additive);
|
||||||
@@ -462,7 +500,7 @@ export class CanvasEditor {
|
|||||||
.map(node => this.nodes.get(node)).filter(member => !selected.includes(member));
|
.map(node => this.nodes.get(node)).filter(member => !selected.includes(member));
|
||||||
this.drag.targets.push({ x: 0, y: 0, ...item.page });
|
this.drag.targets.push({ x: 0, y: 0, ...item.page });
|
||||||
}
|
}
|
||||||
if (item.shape === "line" && this.drag.corner) {
|
if (lineShape(item.shape) && this.drag.corner) {
|
||||||
Object.assign(this.drag, this.createPreview(item.surface, item.page, item.shape, {
|
Object.assign(this.drag, this.createPreview(item.surface, item.page, item.shape, {
|
||||||
fill: false, stroke: true, strokeColor: "var(--accent)", lineWidth: item.lineWidth,
|
fill: false, stroke: true, strokeColor: "var(--accent)", lineWidth: item.lineWidth,
|
||||||
}));
|
}));
|
||||||
@@ -470,13 +508,28 @@ export class CanvasEditor {
|
|||||||
this.viewer.setPointerCapture(event.pointerId);
|
this.viewer.setPointerCapture(event.pointerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
cycleSelection(event) {
|
contains(item, point, tolerance = 0) {
|
||||||
|
if (point.x < item.x - tolerance || point.x > item.x + item.width + tolerance || point.y < item.y - tolerance || point.y > item.y + item.height + tolerance) return false;
|
||||||
|
if (!item.contours) return point.x >= item.x && point.x <= item.x + item.width && point.y >= item.y && point.y <= item.y + item.height;
|
||||||
|
this.hitContext ||= document.createElement("canvas").getContext("2d");
|
||||||
|
item.hitPaths ||= item.contours.map(contour => ({ path: new Path2D(contour.path), rule: contour.evenOdd ? "evenodd" : "nonzero" }));
|
||||||
|
this.hitContext.lineWidth = tolerance * 2;
|
||||||
|
return item.hitPaths.some(({ path, rule }) => this.hitContext.isPointInPath(path, point.x, point.y, rule)
|
||||||
|
|| tolerance > 0 && this.hitContext.isPointInStroke(path, point.x, point.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsAt(event) {
|
||||||
const surface = event.target.closest(".page-surface");
|
const surface = event.target.closest(".page-surface");
|
||||||
const page = this.pages.get(surface);
|
const page = this.pages.get(surface);
|
||||||
if (!page) return;
|
if (!page) return [];
|
||||||
const point = pagePoint(event.clientX, event.clientY, surface.getBoundingClientRect(), page, this.options.rotation());
|
const rect = surface.getBoundingClientRect(), rotation = this.options.rotation();
|
||||||
const items = [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node)).reverse()
|
const point = pagePoint(event.clientX, event.clientY, rect, page, rotation);
|
||||||
.filter(item => point.x >= item.x && point.x <= item.x + item.width && point.y >= item.y && point.y <= item.y + item.height);
|
const tolerance = 3 * page.width / (rotation % 180 ? rect.height : rect.width);
|
||||||
|
return [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node)).reverse().filter(item => this.contains(item, point, tolerance));
|
||||||
|
}
|
||||||
|
|
||||||
|
cycleSelection(event) {
|
||||||
|
const items = this.itemsAt(event);
|
||||||
if (!items.length) return;
|
if (!items.length) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.viewer.focus({ preventScroll: true });
|
this.viewer.focus({ preventScroll: true });
|
||||||
@@ -514,6 +567,15 @@ export class CanvasEditor {
|
|||||||
}
|
}
|
||||||
const from = pagePoint(drag.clientX, drag.clientY, drag.rect, drag.item.page, drag.rotation);
|
const from = pagePoint(drag.clientX, drag.clientY, drag.rect, drag.item.page, drag.rotation);
|
||||||
const to = pagePoint(event.clientX, event.clientY, drag.rect, drag.item.page, drag.rotation);
|
const to = pagePoint(event.clientX, event.clientY, drag.rect, drag.item.page, drag.rotation);
|
||||||
|
if (!drag.box && !drag.change && drag.item.type === "TextObject" && (drag.corner === "w" || drag.corner === "e")) {
|
||||||
|
const scale = (drag.rotation % 180 ? drag.rect.width : drag.rect.height) / drag.item.page.height;
|
||||||
|
const dx = to.x - from.x, dy = to.y - from.y;
|
||||||
|
const frame = drag.item.textFrame;
|
||||||
|
if ((!frame || frame.matrix[0] > 0 && frame.matrix[3] > 0)
|
||||||
|
&& drag.item.height * scale < 28 && Math.abs(dy) * scale >= 3 && Math.abs(dy) > Math.abs(dx) / 2) {
|
||||||
|
drag.corner = (dy < 0 ? "n" : "s") + drag.corner;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (drag.item.type === "TextObject" && (drag.corner === "w" || drag.corner === "e")) {
|
if (drag.item.type === "TextObject" && (drag.corner === "w" || drag.corner === "e")) {
|
||||||
if (drag.item.textFrame) {
|
if (drag.item.textFrame) {
|
||||||
const frame = drag.item.textFrame, [a, b, c, d] = frame.matrix;
|
const frame = drag.item.textFrame, [a, b, c, d] = frame.matrix;
|
||||||
@@ -535,7 +597,8 @@ export class CanvasEditor {
|
|||||||
if (drag.node) paintShape(drag.node, drag.item.shape, drag.box || drag.item.geometry);
|
if (drag.node) paintShape(drag.node, drag.item.shape, drag.box || drag.item.geometry);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
drag.change = objectTransform(drag.item, to.x - from.x, to.y - from.y, drag.corner);
|
const transform = drag.item.type === "TextObject" ? textTransform : objectTransform;
|
||||||
|
drag.change = transform(drag.item, to.x - from.x, to.y - from.y, drag.corner);
|
||||||
if (!drag.corner) this.snap(drag, event.altKey);
|
if (!drag.corner) this.snap(drag, event.altKey);
|
||||||
this.place(drag.item, drag.change);
|
this.place(drag.item, drag.change);
|
||||||
}
|
}
|
||||||
@@ -681,8 +744,7 @@ export class CanvasEditor {
|
|||||||
drag.node.setAttribute("d", drag.points.map((point, index) => `${index ? "L" : "M"}${point.x} ${point.y}`).join(" ") + " Z");
|
drag.node.setAttribute("d", drag.points.map((point, index) => `${index ? "L" : "M"}${point.x} ${point.y}`).join(" ") + " Z");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const item = [...drag.items].reverse().find(item => point.x >= item.x && point.x <= item.x + item.width
|
const item = [...drag.items].reverse().find(item => this.contains(item, point, 3 * drag.page.width / (drag.rotation % 180 ? drag.rect.height : drag.rect.width)));
|
||||||
&& point.y >= item.y && point.y <= item.y + item.height);
|
|
||||||
if (item) { drag.erased.add(item); item.node.classList.add("erasing"); }
|
if (item) { drag.erased.add(item); item.node.classList.add("erasing"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -729,7 +791,7 @@ export class CanvasEditor {
|
|||||||
this.viewer.focus({ preventScroll: true });
|
this.viewer.focus({ preventScroll: true });
|
||||||
const shape = this.tool;
|
const shape = this.tool;
|
||||||
const style = this.options.drawStyle();
|
const style = this.options.drawStyle();
|
||||||
if (shape === "line") {
|
if (lineShape(shape)) {
|
||||||
style.fill = false;
|
style.fill = false;
|
||||||
style.stroke = true;
|
style.stroke = true;
|
||||||
}
|
}
|
||||||
@@ -758,7 +820,7 @@ export class CanvasEditor {
|
|||||||
const preview = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
const preview = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
preview.classList.add("edit-preview");
|
preview.classList.add("edit-preview");
|
||||||
preview.setAttribute("viewBox", `0 0 ${page.width} ${page.height}`);
|
preview.setAttribute("viewBox", `0 0 ${page.width} ${page.height}`);
|
||||||
const node = document.createElementNS("http://www.w3.org/2000/svg", shape === "rectangle" ? "rect" : shape);
|
const node = document.createElementNS("http://www.w3.org/2000/svg", lineShape(shape) && shape !== "line" ? "path" : shape === "rectangle" ? "rect" : shape);
|
||||||
node.setAttribute("fill", style.fill ? style.fillColor : "none");
|
node.setAttribute("fill", style.fill ? style.fillColor : "none");
|
||||||
node.setAttribute("stroke", style.stroke ? style.strokeColor : "none");
|
node.setAttribute("stroke", style.stroke ? style.strokeColor : "none");
|
||||||
node.setAttribute("stroke-width", style.lineWidth);
|
node.setAttribute("stroke-width", style.lineWidth);
|
||||||
@@ -777,11 +839,11 @@ export class CanvasEditor {
|
|||||||
const x = Math.min(from.x, to.x), y = Math.min(from.y, to.y);
|
const x = Math.min(from.x, to.x), y = Math.min(from.y, to.y);
|
||||||
const width = Math.abs(to.x - from.x), height = drag.shape === "text" ? Math.max(5, Math.abs(to.y - from.y)) : Math.abs(to.y - from.y);
|
const width = Math.abs(to.x - from.x), height = drag.shape === "text" ? Math.max(5, Math.abs(to.y - from.y)) : Math.abs(to.y - from.y);
|
||||||
const valid = Math.hypot(event.clientX - drag.clientX, event.clientY - drag.clientY) >= 3
|
const valid = Math.hypot(event.clientX - drag.clientX, event.clientY - drag.clientY) >= 3
|
||||||
&& (drag.shape === "line" ? width > 0 || height > 0 : width > 0 && height > 0);
|
&& (lineShape(drag.shape) ? width > 0 || height > 0 : width > 0 && height > 0);
|
||||||
drag.box = valid ? drag.shape === "line"
|
drag.box = valid ? lineShape(drag.shape)
|
||||||
? { x: from.x, y: from.y, width: to.x - from.x, height: to.y - from.y }
|
? { x: from.x, y: from.y, width: to.x - from.x, height: to.y - from.y }
|
||||||
: { x, y, width, height } : null;
|
: { x, y, width, height } : null;
|
||||||
paintShape(drag.node, drag.shape === "text" ? "rectangle" : drag.shape, drag.shape === "line"
|
paintShape(drag.node, drag.shape === "text" ? "rectangle" : drag.shape, lineShape(drag.shape)
|
||||||
? { x: from.x, y: from.y, width: to.x - from.x, height: to.y - from.y } : { x, y, width, height });
|
? { x: from.x, y: from.y, width: to.x - from.x, height: to.y - from.y } : { x, y, width, height });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+102
-7
@@ -80,6 +80,9 @@ var currentSession *Session
|
|||||||
// pendingImport 待插页文档,仅保留文件数据与索引,不创建渲染会话
|
// pendingImport 待插页文档,仅保留文件数据与索引,不创建渲染会话
|
||||||
var pendingImport *ofdgo.Reader
|
var pendingImport *ofdgo.Reader
|
||||||
|
|
||||||
|
// copiedStyle 当前文档中的独立样式快照
|
||||||
|
var copiedStyle *ofdgo.GraphicObject
|
||||||
|
|
||||||
// apiResult 浏览器接口返回结果
|
// apiResult 浏览器接口返回结果
|
||||||
type apiResult struct {
|
type apiResult struct {
|
||||||
OK bool `json:"ok"`
|
OK bool `json:"ok"`
|
||||||
@@ -113,6 +116,10 @@ func RunWASM() {
|
|||||||
registerCallback("ofdgoBatchPages", batchPages)
|
registerCallback("ofdgoBatchPages", batchPages)
|
||||||
registerCallback("ofdgoChangeOutline", changeOutline)
|
registerCallback("ofdgoChangeOutline", changeOutline)
|
||||||
registerCallback("ofdgoStyleObjects", styleObjects)
|
registerCallback("ofdgoStyleObjects", styleObjects)
|
||||||
|
registerCallback("ofdgoMoveOutline", moveOutline)
|
||||||
|
registerCallback("ofdgoCaptureStyle", captureStyle)
|
||||||
|
registerCallback("ofdgoPasteStyle", pasteStyle)
|
||||||
|
registerCallback("ofdgoResizeObjects", resizeObjects)
|
||||||
registerCallback("ofdgoLoadImport", loadImport)
|
registerCallback("ofdgoLoadImport", loadImport)
|
||||||
registerCallback("ofdgoImportPages", importPages)
|
registerCallback("ofdgoImportPages", importPages)
|
||||||
registerCallback("ofdgoInsertText", insertText)
|
registerCallback("ofdgoInsertText", insertText)
|
||||||
@@ -229,6 +236,7 @@ func openDocument(args []js.Value) (any, error) {
|
|||||||
clearImport()
|
clearImport()
|
||||||
currentEditor = nil
|
currentEditor = nil
|
||||||
copiedObjects = nil
|
copiedObjects = nil
|
||||||
|
copiedStyle = nil
|
||||||
if currentSession != nil {
|
if currentSession != nil {
|
||||||
_ = currentSession.Close()
|
_ = currentSession.Close()
|
||||||
currentSession = nil
|
currentSession = nil
|
||||||
@@ -759,6 +767,17 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
|||||||
item["lineWidth"] = path.LineWidth * editorPathScale(path)
|
item["lineWidth"] = path.LineWidth * editorPathScale(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if object.Type == "PathObject" || object.Type == "ImageObject" {
|
||||||
|
contours, err := currentSession.Renderer.ObjectContours(object, layer.DrawParam)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
paths := make([]any, len(contours))
|
||||||
|
for i, contour := range contours {
|
||||||
|
paths[i] = map[string]any{"path": contour.Path, "evenOdd": contour.EvenOdd}
|
||||||
|
}
|
||||||
|
item["contours"] = paths
|
||||||
|
}
|
||||||
if object.Type == "ImageObject" && capability.Update {
|
if object.Type == "ImageObject" && capability.Update {
|
||||||
full, err := object.ImageObject.ImageBounds()
|
full, err := object.ImageObject.ImageBounds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -767,6 +786,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
|||||||
item["imageBounds"] = editorBox(full)
|
item["imageBounds"] = editorBox(full)
|
||||||
}
|
}
|
||||||
if object.Type == "TextObject" {
|
if object.Type == "TextObject" {
|
||||||
|
item["bounds"] = editorBox(box)
|
||||||
value, layout := object.TextObject.TextLayout()
|
value, layout := object.TextObject.TextLayout()
|
||||||
item["text"], item["font"], item["fontName"], item["size"] = value, object.TextObject.Font, fontNames[object.TextObject.Font], object.TextObject.Size
|
item["text"], item["font"], item["fontName"], item["size"] = value, object.TextObject.Font, fontNames[object.TextObject.Font], object.TextObject.Size
|
||||||
item["wrap"], item["align"], item["paragraphHeight"] = layout.Wrap, layout.Align, layout.LineHeight
|
item["wrap"], item["align"], item["paragraphHeight"] = layout.Wrap, layout.Align, layout.LineHeight
|
||||||
@@ -1071,27 +1091,100 @@ func batchPages(args []js.Value) (any, error) {
|
|||||||
return editorPageInfo{info.(editorInfo), index}, nil
|
return editorPageInfo{info.(editorInfo), index}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editorOutlineInfo 目录操作后的位置
|
||||||
|
type editorOutlineInfo struct {
|
||||||
|
editorInfo
|
||||||
|
OutlinePath []int `json:"outlinePath"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// indexesFromJS 读取整数索引列表
|
||||||
|
// 入参: value JavaScript数组
|
||||||
|
// 返回: []int 索引列表
|
||||||
|
func indexesFromJS(value js.Value) []int {
|
||||||
|
indexes := make([]int, value.Length())
|
||||||
|
for i := range indexes {
|
||||||
|
indexes[i] = value.Index(i).Int()
|
||||||
|
}
|
||||||
|
return indexes
|
||||||
|
}
|
||||||
|
|
||||||
|
// moveOutline 调整目录顺序或层级
|
||||||
|
// 入参: args 源路径、目标父路径及插入索引
|
||||||
|
// 返回: any 文档及新路径, error 错误信息
|
||||||
|
func moveOutline(args []js.Value) (any, error) {
|
||||||
|
var path []int
|
||||||
|
info, err := changeObjects(func() error {
|
||||||
|
var err error
|
||||||
|
path, err = currentEditor.MoveOutline(indexesFromJS(args[0]), indexesFromJS(args[1]), args[2].Int())
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return editorOutlineInfo{info.(editorInfo), path}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// captureStyle 保存单个对象样式来源,不改写文档
|
||||||
|
// 入参: args 页面索引和对象标识
|
||||||
|
// 返回: any 空结果, error 错误信息
|
||||||
|
func captureStyle(args []js.Value) (any, error) {
|
||||||
|
if currentEditor == nil {
|
||||||
|
return nil, fmt.Errorf("no document is being edited")
|
||||||
|
}
|
||||||
|
object, err := currentEditor.Object(args[0].Int(), args[1].String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
copiedStyle = &object
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pasteStyle 将保存的样式应用到同类型选区
|
||||||
|
// 入参: args 页面索引和对象标识列表
|
||||||
|
// 返回: any 文档信息, error 错误信息
|
||||||
|
func pasteStyle(args []js.Value) (any, error) {
|
||||||
|
return changeObjects(func() error {
|
||||||
|
if copiedStyle == nil {
|
||||||
|
return fmt.Errorf("style clipboard is empty")
|
||||||
|
}
|
||||||
|
return currentEditor.CopyStyle(args[0].Int(), stringsFromJS(args[1]), *copiedStyle)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// resizeObjects 设置选区位置及尺寸
|
||||||
|
// 入参: args 页面索引、对象标识、横纵坐标及宽高
|
||||||
|
// 返回: any 文档信息, error 错误信息
|
||||||
|
func resizeObjects(args []js.Value) (any, error) {
|
||||||
|
return changeObjects(func() error {
|
||||||
|
return currentEditor.ResizeObjects(args[0].Int(), stringsFromJS(args[1]), ofdgo.Box{X: args[2].Float(), Y: args[3].Float(), W: args[4].Float(), H: args[5].Float()})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// changeOutline 新增、修改或删除层级目录
|
// changeOutline 新增、修改或删除层级目录
|
||||||
// 入参: args 操作、层级索引、标题及目标页面索引
|
// 入参: args 操作、层级索引、标题及目标页面索引
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func changeOutline(args []js.Value) (any, error) {
|
func changeOutline(args []js.Value) (any, error) {
|
||||||
return changeObjects(func() error {
|
path := indexesFromJS(args[1])
|
||||||
path := make([]int, args[1].Length())
|
info, err := changeObjects(func() error {
|
||||||
for i := range path {
|
|
||||||
path[i] = args[1].Index(i).Int()
|
|
||||||
}
|
|
||||||
switch args[0].String() {
|
switch args[0].String() {
|
||||||
case "add":
|
case "add":
|
||||||
_, err := currentEditor.AddOutline(path, args[2].String(), args[3].Int())
|
var err error
|
||||||
|
path, err = currentEditor.AddOutline(path, args[2].String(), args[3].Int())
|
||||||
return err
|
return err
|
||||||
case "update":
|
case "update":
|
||||||
return currentEditor.UpdateOutline(path, args[2].String(), args[3].Int())
|
return currentEditor.UpdateOutline(path, args[2].String(), args[3].Int())
|
||||||
case "delete":
|
case "delete":
|
||||||
return currentEditor.DeleteOutline(path)
|
err := currentEditor.DeleteOutline(path)
|
||||||
|
path = nil
|
||||||
|
return err
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported outline action %q", args[0].String())
|
return fmt.Errorf("unsupported outline action %q", args[0].String())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return editorOutlineInfo{info.(editorInfo), path}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// styleObjects 原子更新对象透明度和路径描边属性
|
// styleObjects 原子更新对象透明度和路径描边属性
|
||||||
@@ -1257,8 +1350,10 @@ func previewEditor(editor *ofdgo.Editor, annotations bool) (editorInfo, error) {
|
|||||||
_ = currentSession.Close()
|
_ = currentSession.Close()
|
||||||
}
|
}
|
||||||
currentSession = session
|
currentSession = session
|
||||||
|
session.editing = true
|
||||||
if currentEditor != editor {
|
if currentEditor != editor {
|
||||||
copiedObjects = nil
|
copiedObjects = nil
|
||||||
|
copiedStyle = nil
|
||||||
}
|
}
|
||||||
currentEditor = editor
|
currentEditor = editor
|
||||||
return editorSummary(), nil
|
return editorSummary(), nil
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ type Session struct {
|
|||||||
signatures []SignatureInfo
|
signatures []SignatureInfo
|
||||||
signatureError error
|
signatureError error
|
||||||
signaturesRead bool
|
signaturesRead bool
|
||||||
|
editing bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// DocumentInfo 文档信息
|
// DocumentInfo 文档信息
|
||||||
@@ -455,7 +456,11 @@ func (s *Session) RenderPageSVG(index int) (PageSVG, error) {
|
|||||||
s.textCache[index] = text
|
s.textCache[index] = text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resources, err := renderer.RenderToSVGWithResources(page, &buf)
|
renderSVG := renderer.RenderToSVGWithResources
|
||||||
|
if s.editing {
|
||||||
|
renderSVG = renderer.RenderToSVGWithObjects
|
||||||
|
}
|
||||||
|
resources, err := renderSVG(page, &buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PageSVG{}, err
|
return PageSVG{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,6 +187,62 @@ func (e *Editor) DeleteOutline(path []int) error {
|
|||||||
return e.setOutlineXML(editorPatchXML(data, []editorXMLPatch{{node.start, node.end, nil}}))
|
return e.setOutlineXML(editorPatchXML(data, []editorXMLPatch{{node.start, node.end, nil}}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MoveOutline 移动目录及其子树,保留原始动作和扩展字段,一次撤销恢复
|
||||||
|
// 入参: path 节点路径, parent 移动前的目标父路径,空路径表示根, index 移除源节点后在目标父节点中的插入索引
|
||||||
|
// 返回: []int 移动后的路径, error 错误信息
|
||||||
|
func (e *Editor) MoveOutline(path, parent []int, index int) ([]int, error) {
|
||||||
|
if len(path) == 0 || len(parent) >= len(path) && slices.Equal(path, parent[:len(path)]) {
|
||||||
|
return nil, fmt.Errorf("cannot move an outline into itself")
|
||||||
|
}
|
||||||
|
data, err := e.outlineXML()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
node, err := editorOutlineNode(root, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
target, err := editorOutlineNode(root, parent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
children := editorOutlineChildren(target)
|
||||||
|
same := slices.Equal(path[:len(path)-1], parent)
|
||||||
|
if same {
|
||||||
|
children = slices.Delete(children, path[len(path)-1], path[len(path)-1]+1)
|
||||||
|
}
|
||||||
|
if index < 0 || index > len(children) {
|
||||||
|
return nil, fmt.Errorf("outline insertion index %d out of range", index)
|
||||||
|
}
|
||||||
|
result := slices.Clone(parent)
|
||||||
|
if len(result) >= len(path) && slices.Equal(path[:len(path)-1], result[:len(path)-1]) && result[len(path)-1] > path[len(path)-1] {
|
||||||
|
result[len(path)-1]--
|
||||||
|
}
|
||||||
|
result = append(result, index)
|
||||||
|
if same && path[len(path)-1] == index {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
value, err := editorXMLStandalone(data[node.start:node.end], node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
patches := []editorXMLPatch{{node.start, node.end, nil}}
|
||||||
|
if target.open == target.end {
|
||||||
|
patches = append(patches, editorXMLContent(data, target, value))
|
||||||
|
} else {
|
||||||
|
at := target.close
|
||||||
|
if index < len(children) {
|
||||||
|
at = children[index].start
|
||||||
|
}
|
||||||
|
patches = append(patches, editorXMLPatch{at, at, value})
|
||||||
|
}
|
||||||
|
return result, e.setOutlineXML(editorPatchXML(data, patches))
|
||||||
|
}
|
||||||
|
|
||||||
// validateOutline 校验目录标题和目标页面
|
// validateOutline 校验目录标题和目标页面
|
||||||
// 入参: title 标题, page 页面索引
|
// 入参: title 标题, page 页面索引
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
|
|||||||
+98
-3
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -149,9 +150,6 @@ func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error {
|
|||||||
if err := validateEditorStroke(*path); err != nil {
|
if err := validateEditorStroke(*path); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if path.Join != "" && path.Join != "Miter" && path.Join != "Round" && path.Join != "Bevel" {
|
|
||||||
return fmt.Errorf("invalid line join %q", path.Join)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported object type %q", object.Type)
|
return fmt.Errorf("unsupported object type %q", object.Type)
|
||||||
@@ -161,10 +159,107 @@ func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error {
|
|||||||
return e.updateObjects(page, objects, true)
|
return e.updateObjects(page, objects, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CopyStyle 将同类型对象的外观复制到选区,不复制内容、位置、资源数据或动作
|
||||||
|
// 文字复制字体、字号、颜色及透明度,沿用目标段落设置;字体标识须属于当前文档
|
||||||
|
// 路径按页面中的实际线宽和虚线长度复制,保持目标变换
|
||||||
|
// 入参: page 目标页面索引, ids 目标对象标识, source 样式来源快照
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) CopyStyle(page int, ids []string, source GraphicObject) error {
|
||||||
|
var alpha *int
|
||||||
|
switch source.Type {
|
||||||
|
case "TextObject":
|
||||||
|
alpha = source.TextObject.Alpha
|
||||||
|
case "PathObject":
|
||||||
|
alpha = source.PathObject.Alpha
|
||||||
|
case "ImageObject":
|
||||||
|
alpha = source.ImageObject.Alpha
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported style source %q", source.Type)
|
||||||
|
}
|
||||||
|
if alpha != nil && (*alpha < 0 || *alpha > 255) {
|
||||||
|
return fmt.Errorf("alpha must be between 0 and 255")
|
||||||
|
}
|
||||||
|
objects, _, err := e.selectedObjects(page, ids)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, object := range objects {
|
||||||
|
if object.Type != source.Type {
|
||||||
|
return fmt.Errorf("style requires objects of the same type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if source.Type == "TextObject" {
|
||||||
|
objects, err = e.styleTextObjects(objects, TextStyle{Font: source.TextObject.Font, Size: source.TextObject.Size})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range objects {
|
||||||
|
objects[i].TextObject.FillColor = cloneEditorData(source.TextObject.FillColor)
|
||||||
|
objects[i].TextObject.Alpha = cloneEditorData(source.TextObject.Alpha)
|
||||||
|
}
|
||||||
|
return e.UpdateObjects(page, objects)
|
||||||
|
}
|
||||||
|
if source.Type == "PathObject" {
|
||||||
|
from := source.PathObject
|
||||||
|
if from.DrawParam != "" {
|
||||||
|
return fmt.Errorf("style source must have resolved draw parameters")
|
||||||
|
}
|
||||||
|
if err := validateEditorStroke(from); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, color := range []*FillColor{from.FillColor, (*FillColor)(from.StrokeColor)} {
|
||||||
|
if err := creationColor(color); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, object := range objects {
|
||||||
|
object = cloneEditorData(object)
|
||||||
|
switch source.Type {
|
||||||
|
case "PathObject":
|
||||||
|
from, to := source.PathObject, &object.PathObject
|
||||||
|
to.Fill, to.Stroke, to.Alpha = from.Fill, from.Stroke, from.Alpha
|
||||||
|
to.FillColor, to.StrokeColor = from.FillColor, from.StrokeColor
|
||||||
|
to.LineWidth, to.Cap, to.Join, to.MiterLimit = from.LineWidth, from.Cap, from.Join, from.MiterLimit
|
||||||
|
to.DashPattern, to.DashOffset = from.DashPattern, from.DashOffset
|
||||||
|
scale := editorStrokeScale(from.CTM) / editorStrokeScale(to.CTM)
|
||||||
|
if scale != 1 {
|
||||||
|
if to.LineWidth == 0 {
|
||||||
|
to.LineWidth = defaultPathLineWidth
|
||||||
|
}
|
||||||
|
to.LineWidth *= scale
|
||||||
|
to.DashPattern = scaleTextNumbers(to.DashPattern, scale)
|
||||||
|
if to.DashOffset != nil {
|
||||||
|
value := *to.DashOffset * scale
|
||||||
|
to.DashOffset = &value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "ImageObject":
|
||||||
|
object.ImageObject.Alpha = source.ImageObject.Alpha
|
||||||
|
}
|
||||||
|
objects[i] = object
|
||||||
|
}
|
||||||
|
return e.updateObjects(page, objects, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorStrokeScale 获取路径在页面坐标中的描边倍率,与渲染器保持一致
|
||||||
|
// 入参: ctm 对象变换
|
||||||
|
// 返回: float64 描边倍率
|
||||||
|
func editorStrokeScale(ctm string) float64 {
|
||||||
|
m := NewMatrix(ctm)
|
||||||
|
if scale := math.Sqrt(math.Abs(m.a*m.d - m.b*m.c)); scale > 0 {
|
||||||
|
return scale
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
// validateEditorStroke 校验路径描边尺寸、端点和虚线
|
// validateEditorStroke 校验路径描边尺寸、端点和虚线
|
||||||
// 入参: path 路径对象
|
// 入参: path 路径对象
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func validateEditorStroke(path PathObject) error {
|
func validateEditorStroke(path PathObject) error {
|
||||||
|
if path.Join != "" && path.Join != "Miter" && path.Join != "Round" && path.Join != "Bevel" {
|
||||||
|
return fmt.Errorf("invalid line join %q", path.Join)
|
||||||
|
}
|
||||||
if !finite(path.LineWidth) || path.LineWidth < 0 || !finite(path.MiterLimit) || path.MiterLimit < 0 {
|
if !finite(path.LineWidth) || path.LineWidth < 0 || !finite(path.MiterLimit) || path.MiterLimit < 0 {
|
||||||
return fmt.Errorf("invalid path stroke dimensions")
|
return fmt.Errorf("invalid path stroke dimensions")
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-4
@@ -87,11 +87,22 @@ func (e *Editor) StyleText(page int, ids []string, style TextStyle) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
updates, err := e.styleTextObjects(objects, style)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.UpdateObjects(page, updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// styleTextObjects 生成文字样式副本,不提交文档或历史
|
||||||
|
// 入参: objects 原文字对象, style 样式增量
|
||||||
|
// 返回: []GraphicObject 更新对象, error 错误信息
|
||||||
|
func (e *Editor) styleTextObjects(objects []GraphicObject, style TextStyle) ([]GraphicObject, error) {
|
||||||
updates := make([]GraphicObject, len(objects))
|
updates := make([]GraphicObject, len(objects))
|
||||||
for i, object := range objects {
|
for i, object := range objects {
|
||||||
id := editorObjectID(object)
|
id := editorObjectID(object)
|
||||||
if object.Type != "TextObject" {
|
if object.Type != "TextObject" {
|
||||||
return fmt.Errorf("object %q is not text", id)
|
return nil, fmt.Errorf("object %q is not text", id)
|
||||||
}
|
}
|
||||||
updates[i] = cloneEditorData(object)
|
updates[i] = cloneEditorData(object)
|
||||||
text := &updates[i].TextObject
|
text := &updates[i].TextObject
|
||||||
@@ -100,7 +111,7 @@ func (e *Editor) StyleText(page int, ids []string, style TextStyle) error {
|
|||||||
fontChanged := style.Font != "" && style.Font != text.Font
|
fontChanged := style.Font != "" && style.Font != text.Font
|
||||||
sizeChanged := style.Size != 0 && style.Size != text.Size
|
sizeChanged := style.Size != 0 && style.Size != text.Size
|
||||||
if sizeChanged && !known {
|
if sizeChanged && !known {
|
||||||
return &EditError{Code: EditUnsupportedObject, Err: fmt.Errorf("object %q requires explicit paragraph layout before changing size", id)}
|
return nil, &EditError{Code: EditUnsupportedObject, Err: fmt.Errorf("object %q requires explicit paragraph layout before changing size", id)}
|
||||||
}
|
}
|
||||||
if fontChanged {
|
if fontChanged {
|
||||||
text.Font = style.Font
|
text.Font = style.Font
|
||||||
@@ -110,7 +121,7 @@ func (e *Editor) StyleText(page int, ids []string, style TextStyle) error {
|
|||||||
}
|
}
|
||||||
if known && (fontChanged || sizeChanged) {
|
if known && (fontChanged || sizeChanged) {
|
||||||
if err := e.LayoutText(text, value, layout); err != nil {
|
if err := e.LayoutText(text, value, layout); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if style.Color != "" {
|
if style.Color != "" {
|
||||||
@@ -120,7 +131,7 @@ func (e *Editor) StyleText(page int, ids []string, style TextStyle) error {
|
|||||||
text.FillColor.Value = style.Color
|
text.FillColor.Value = style.Color
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return e.UpdateObjects(page, updates)
|
return updates, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// textLayout 保存编辑中的原文与选项,不写入OFD,也不参与渲染
|
// textLayout 保存编辑中的原文与选项,不写入OFD,也不参与渲染
|
||||||
|
|||||||
@@ -64,6 +64,49 @@ func (e *Editor) FlipObjects(page int, ids []string, axis string) error {
|
|||||||
return e.orientObjects(page, ids, m)
|
return e.orientObjects(page, ids, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResizeObjects 将选区可见范围移动并缩放到指定矩形,文字与路径保持比例,图片支持独立宽高
|
||||||
|
// 不重排文字或重采样图片,一次撤销恢复全部;基本图形独立调整宽高可使用PathObject.Reshape
|
||||||
|
// 入参: page 页面索引, ids 对象标识, box 页面毫米坐标中的目标范围
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) ResizeObjects(page int, ids []string, box Box) error {
|
||||||
|
if _, err := creationBox(editorBoxString(box)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
objects, _, err := e.selectedObjects(page, ids)
|
||||||
|
if err != nil || len(objects) == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
boxes, err := e.objectBounds(page, objects)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var before Box
|
||||||
|
for _, bounds := range boxes {
|
||||||
|
before = unionTextBox(before, bounds)
|
||||||
|
}
|
||||||
|
if before.W <= 0 || before.H <= 0 {
|
||||||
|
return fmt.Errorf("selection has no visible bounds")
|
||||||
|
}
|
||||||
|
sx, sy := box.W/before.W, box.H/before.H
|
||||||
|
uniform := math.Abs(sx-sy) <= 1e-9*math.Max(sx, sy)
|
||||||
|
matrix := Matrix{a: sx, d: sy, e: box.X - before.X*sx, f: box.Y - before.Y*sy}
|
||||||
|
for i, object := range objects {
|
||||||
|
object = cloneEditorData(object)
|
||||||
|
if uniform {
|
||||||
|
object, err = transformEditorObject(object, matrix.e, matrix.f, sx)
|
||||||
|
} else if object.Type == "ImageObject" {
|
||||||
|
object = transformEditorMatrix(object, matrix)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("nonuniform resizing requires image objects")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
objects[i] = object
|
||||||
|
}
|
||||||
|
return e.updateObjects(page, objects, true)
|
||||||
|
}
|
||||||
|
|
||||||
// orientObjects 更新Boundary与CTM,不重排文字或重采样图片
|
// orientObjects 更新Boundary与CTM,不重排文字或重采样图片
|
||||||
// 入参: page 页面索引, ids 对象标识, matrix 直角旋转或镜像矩阵
|
// 入参: page 页面索引, ids 对象标识, matrix 直角旋转或镜像矩阵
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
@@ -83,6 +126,15 @@ func (e *Editor) orientObjects(page int, ids []string, matrix Matrix) error {
|
|||||||
x, y := box.X+box.W/2, box.Y+box.H/2
|
x, y := box.X+box.W/2, box.Y+box.H/2
|
||||||
matrix = TranslationMatrix(x, y).Multiply(matrix).Multiply(TranslationMatrix(-x, -y))
|
matrix = TranslationMatrix(x, y).Multiply(matrix).Multiply(TranslationMatrix(-x, -y))
|
||||||
for i, object := range objects {
|
for i, object := range objects {
|
||||||
|
objects[i] = transformEditorMatrix(object, matrix)
|
||||||
|
}
|
||||||
|
return e.updateObjects(page, objects, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// transformEditorMatrix 在页面坐标中变换基本对象,保留局部绘制数据和裁剪
|
||||||
|
// 入参: object 对象, matrix 页面变换
|
||||||
|
// 返回: GraphicObject 变换后的独立对象
|
||||||
|
func transformEditorMatrix(object GraphicObject, matrix Matrix) GraphicObject {
|
||||||
var boundary, ctm *string
|
var boundary, ctm *string
|
||||||
switch object.Type {
|
switch object.Type {
|
||||||
case "TextObject":
|
case "TextObject":
|
||||||
@@ -91,8 +143,6 @@ func (e *Editor) orientObjects(page int, ids []string, matrix Matrix) error {
|
|||||||
boundary, ctm = &object.PathObject.Boundary, &object.PathObject.CTM
|
boundary, ctm = &object.PathObject.Boundary, &object.PathObject.CTM
|
||||||
case "ImageObject":
|
case "ImageObject":
|
||||||
boundary, ctm = &object.ImageObject.Boundary, &object.ImageObject.CTM
|
boundary, ctm = &object.ImageObject.Boundary, &object.ImageObject.CTM
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported object type %q", object.Type)
|
|
||||||
}
|
}
|
||||||
before, _ := ParseBox(*boundary)
|
before, _ := ParseBox(*boundary)
|
||||||
if object.Type == "ImageObject" && *ctm == "" {
|
if object.Type == "ImageObject" && *ctm == "" {
|
||||||
@@ -105,9 +155,7 @@ func (e *Editor) orientObjects(page int, ids []string, matrix Matrix) error {
|
|||||||
if clips := editorObjectClips(&object); *clips != nil && (*clips).TransFlag != nil && !*(*clips).TransFlag {
|
if clips := editorObjectClips(&object); *clips != nil && (*clips).TransFlag != nil && !*(*clips).TransFlag {
|
||||||
*clips = transformObjectClips(*clips, local)
|
*clips = transformObjectClips(*clips, local)
|
||||||
}
|
}
|
||||||
objects[i] = object
|
return object
|
||||||
}
|
|
||||||
return e.updateObjects(page, objects, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// transformObjectClips 变换裁剪区域,不修改原始裁剪数据
|
// transformObjectClips 变换裁剪区域,不修改原始裁剪数据
|
||||||
|
|||||||
@@ -28,6 +28,32 @@ import (
|
|||||||
// 入参: object 图形对象, drawParam 图层绘制参数标识,无继承时为空
|
// 入参: object 图形对象, drawParam 图层绘制参数标识,无继承时为空
|
||||||
// 返回: Box 毫米坐标范围, error 错误信息
|
// 返回: Box 毫米坐标范围, error 错误信息
|
||||||
func (r *Renderer) ObjectBounds(object GraphicObject, drawParam string) (Box, error) {
|
func (r *Renderer) ObjectBounds(object GraphicObject, drawParam string) (Box, error) {
|
||||||
|
return r.measureObject(object, drawParam, &boundsRenderer{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectContour 页面坐标中的填充轮廓,Path为SVG路径,EvenOdd表示奇偶填充
|
||||||
|
type ObjectContour struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
EvenOdd bool `json:"evenOdd,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectContours 获取路径或图片的实际绘制区域,包含描边、虚线、变换及对象裁剪
|
||||||
|
// 图片不解码像素,底纹不展开图案单元;不包含页面边界或父级变换
|
||||||
|
// 入参: object 路径或图片对象, drawParam 图层绘制参数标识
|
||||||
|
// 返回: []ObjectContour 可用于点选的轮廓, error 错误信息
|
||||||
|
func (r *Renderer) ObjectContours(object GraphicObject, drawParam string) ([]ObjectContour, error) {
|
||||||
|
if object.Type != "PathObject" && object.Type != "ImageObject" {
|
||||||
|
return nil, fmt.Errorf("contours require a path or image object")
|
||||||
|
}
|
||||||
|
bounds := &boundsRenderer{collect: true}
|
||||||
|
_, err := r.measureObject(object, drawParam, bounds)
|
||||||
|
return bounds.contours, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// measureObject 复用渲染逻辑收集对象范围与可选轮廓
|
||||||
|
// 入参: object 对象, drawParam 图层绘制参数标识, bounds 度量目标
|
||||||
|
// 返回: Box 毫米范围, error 错误信息
|
||||||
|
func (r *Renderer) measureObject(object GraphicObject, drawParam string, bounds *boundsRenderer) (Box, error) {
|
||||||
if object.Type != "TextObject" && object.Type != "PathObject" && object.Type != "ImageObject" {
|
if object.Type != "TextObject" && object.Type != "PathObject" && object.Type != "ImageObject" {
|
||||||
return Box{}, fmt.Errorf("unsupported object type %q", object.Type)
|
return Box{}, fmt.Errorf("unsupported object type %q", object.Type)
|
||||||
}
|
}
|
||||||
@@ -44,7 +70,6 @@ func (r *Renderer) ObjectBounds(object GraphicObject, drawParam string) (Box, er
|
|||||||
renderer.pageText = nil
|
renderer.pageText = nil
|
||||||
renderer.textOnly = false
|
renderer.textOnly = false
|
||||||
defaults := renderer.drawParamDefaults(drawParam, nil)
|
defaults := renderer.drawParamDefaults(drawParam, nil)
|
||||||
bounds := &boundsRenderer{}
|
|
||||||
ctx := canvas.NewContext(bounds)
|
ctx := canvas.NewContext(bounds)
|
||||||
switch object.Type {
|
switch object.Type {
|
||||||
case "TextObject":
|
case "TextObject":
|
||||||
@@ -107,6 +132,8 @@ func boundsColor(color *FillColor) *FillColor {
|
|||||||
// boundsRenderer 收集绘制范围,不分配页面像素或合并独立图形轮廓
|
// boundsRenderer 收集绘制范围,不分配页面像素或合并独立图形轮廓
|
||||||
type boundsRenderer struct {
|
type boundsRenderer struct {
|
||||||
box Box
|
box Box
|
||||||
|
collect bool
|
||||||
|
contours []ObjectContour
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size 返回不限定边界的度量画布尺寸
|
// Size 返回不限定边界的度量画布尺寸
|
||||||
@@ -118,18 +145,27 @@ func (r *boundsRenderer) Size() (float64, float64) {
|
|||||||
// add 合并路径的精确曲线范围并转换为向下的纵轴
|
// add 合并路径的精确曲线范围并转换为向下的纵轴
|
||||||
// 入参: path 绘制路径
|
// 入参: path 绘制路径
|
||||||
func (r *boundsRenderer) add(path *canvas.Path) {
|
func (r *boundsRenderer) add(path *canvas.Path) {
|
||||||
|
r.addContour(path, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addContour 合并范围并按需保留独立填充区域
|
||||||
|
// 入参: path 绘制路径, evenOdd 是否使用奇偶规则
|
||||||
|
func (r *boundsRenderer) addContour(path *canvas.Path, evenOdd bool) {
|
||||||
if path.Empty() {
|
if path.Empty() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rect := path.Bounds()
|
rect := path.Bounds()
|
||||||
r.box = unionTextBox(r.box, Box{X: rect.X0, Y: -rect.Y1, W: rect.W(), H: rect.H()})
|
r.box = unionTextBox(r.box, Box{X: rect.X0, Y: -rect.Y1, W: rect.W(), H: rect.H()})
|
||||||
|
if r.collect {
|
||||||
|
r.contours = append(r.contours, ObjectContour{Path: path.Copy().Transform(canvas.Matrix{{1, 0, 0}, {0, -1, 0}}).ToSVG(), EvenOdd: evenOdd})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderPath 收集填充与描边范围,保持线帽、连接和虚线语义
|
// RenderPath 收集填充与描边范围,保持线帽、连接和虚线语义
|
||||||
// 入参: path 路径, style 绘制样式, m 变换矩阵
|
// 入参: path 路径, style 绘制样式, m 变换矩阵
|
||||||
func (r *boundsRenderer) RenderPath(path *canvas.Path, style canvas.Style, m canvas.Matrix) {
|
func (r *boundsRenderer) RenderPath(path *canvas.Path, style canvas.Style, m canvas.Matrix) {
|
||||||
if style.HasFill() {
|
if style.HasFill() {
|
||||||
r.add(path.Copy().Transform(m))
|
r.addContour(path.Copy().Transform(m), style.FillRule == canvas.EvenOdd)
|
||||||
}
|
}
|
||||||
if style.HasStroke() {
|
if style.HasStroke() {
|
||||||
p := path.Dash(style.DashOffset, style.Dashes...).Stroke(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.Tolerance)
|
p := path.Dash(style.DashOffset, style.Dashes...).Stroke(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.Tolerance)
|
||||||
|
|||||||
@@ -169,6 +169,12 @@ func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu Composite
|
|||||||
// renderObject 渲染图形对象
|
// renderObject 渲染图形对象
|
||||||
// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaults 默认绘制参数, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径
|
// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaults 默认绘制参数, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径
|
||||||
func (r *Renderer) renderObject(ctx *canvas.Context, obj *GraphicObject, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
|
func (r *Renderer) renderObject(ctx *canvas.Context, obj *GraphicObject, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
|
||||||
|
if groups, ok := ctx.Renderer.(interface {
|
||||||
|
beginObject(*GraphicObject) bool
|
||||||
|
endObject()
|
||||||
|
}); ok && groups.beginObject(obj) {
|
||||||
|
defer groups.endObject()
|
||||||
|
}
|
||||||
switch obj.Type {
|
switch obj.Type {
|
||||||
case "TextObject":
|
case "TextObject":
|
||||||
r.renderText(ctx, obj.TextObject, pageH, defaults, parentCTM, boundaryInCTM, parentClip)
|
r.renderText(ctx, obj.TextObject, pageH, defaults, parentCTM, boundaryInCTM, parentClip)
|
||||||
|
|||||||
+49
-6
@@ -19,6 +19,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html"
|
||||||
"image"
|
"image"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
@@ -61,13 +62,14 @@ type svgResourceRenderer struct {
|
|||||||
seen map[string]bool
|
seen map[string]bool
|
||||||
styles strings.Builder
|
styles strings.Builder
|
||||||
err error
|
err error
|
||||||
|
objects map[*GraphicObject]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderToSVGWithFonts 渲染为SVG并返回页面引用的字体资源,不在SVG中嵌入字体
|
// RenderToSVGWithFonts 渲染为SVG并返回页面引用的字体资源,不在SVG中嵌入字体
|
||||||
// 入参: page 页面内容, writer 输出流
|
// 入参: page 页面内容, writer 输出流
|
||||||
// 返回: []SVGFont 字体资源,可按Name复用并按Weight、Style注册,error 错误信息
|
// 返回: []SVGFont 字体资源,可按Name复用并按Weight、Style注册,error 错误信息
|
||||||
func (r *Renderer) RenderToSVGWithFonts(page *PageContent, writer io.Writer) ([]SVGFont, error) {
|
func (r *Renderer) RenderToSVGWithFonts(page *PageContent, writer io.Writer) ([]SVGFont, error) {
|
||||||
resources, err := r.renderSVGResources(page, writer, false)
|
resources, err := r.renderSVGResources(page, writer, false, false)
|
||||||
return resources.Fonts, err
|
return resources.Fonts, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,14 +77,21 @@ func (r *Renderer) RenderToSVGWithFonts(page *PageContent, writer io.Writer) ([]
|
|||||||
// 入参: page 页面内容, writer 输出流
|
// 入参: page 页面内容, writer 输出流
|
||||||
// 返回: SVGResources 可跨页面复用的资源, error 错误信息
|
// 返回: SVGResources 可跨页面复用的资源, error 错误信息
|
||||||
func (r *Renderer) RenderToSVGWithResources(page *PageContent, writer io.Writer) (SVGResources, error) {
|
func (r *Renderer) RenderToSVGWithResources(page *PageContent, writer io.Writer) (SVGResources, error) {
|
||||||
return r.renderSVGResources(page, writer, true)
|
return r.renderSVGResources(page, writer, true, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderToSVGWithObjects 渲染SVG并为页面直接对象添加data-ofd-object分组,保持原绘制顺序和分离资源
|
||||||
|
// 入参: page 页面内容, writer 输出流
|
||||||
|
// 返回: SVGResources 字体和图片资源, error 错误信息
|
||||||
|
func (r *Renderer) RenderToSVGWithObjects(page *PageContent, writer io.Writer) (SVGResources, error) {
|
||||||
|
return r.renderSVGResources(page, writer, true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderSVGResources 渲染SVG并收集外部资源
|
// renderSVGResources 渲染SVG并收集外部资源
|
||||||
// 入参: page 页面内容, writer 输出流, images 是否分离图片
|
// 入参: page 页面内容, writer 输出流, images 是否分离图片, objects 是否标识页面直接对象
|
||||||
// 返回: SVGResources 外部资源, error 错误信息
|
// 返回: SVGResources 外部资源, error 错误信息
|
||||||
func (r *Renderer) renderSVGResources(page *PageContent, writer io.Writer, images bool) (SVGResources, error) {
|
func (r *Renderer) renderSVGResources(page *PageContent, writer io.Writer, images, objects bool) (SVGResources, error) {
|
||||||
c, err := r.renderPage(page)
|
box, err := r.GetPageBox(page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return SVGResources{}, err
|
return SVGResources{}, err
|
||||||
}
|
}
|
||||||
@@ -90,7 +99,7 @@ func (r *Renderer) renderSVGResources(page *PageContent, writer io.Writer, image
|
|||||||
options.EmbedFonts = false
|
options.EmbedFonts = false
|
||||||
buffer := bufio.NewWriter(writer)
|
buffer := bufio.NewWriter(writer)
|
||||||
s := &svgResourceRenderer{
|
s := &svgResourceRenderer{
|
||||||
SVG: svg.New(buffer, c.W, c.H, &options),
|
SVG: svg.New(buffer, box.W, box.H, &options),
|
||||||
renderer: r,
|
renderer: r,
|
||||||
writer: buffer,
|
writer: buffer,
|
||||||
seen: make(map[string]bool),
|
seen: make(map[string]bool),
|
||||||
@@ -98,7 +107,24 @@ func (r *Renderer) renderSVGResources(page *PageContent, writer io.Writer, image
|
|||||||
if images {
|
if images {
|
||||||
s.imageNames = make(map[image.Image]string)
|
s.imageNames = make(map[image.Image]string)
|
||||||
}
|
}
|
||||||
|
if objects {
|
||||||
|
s.objects = make(map[*GraphicObject]string)
|
||||||
|
for i := range page.Content.Layer {
|
||||||
|
for j := range page.Content.Layer[i].Objects {
|
||||||
|
object := &page.Content.Layer[i].Objects[j]
|
||||||
|
s.objects[object] = editorObjectID(*object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := r.renderPageToContext(canvas.NewContext(s), page, true); err != nil {
|
||||||
|
return SVGResources{}, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c, err := r.renderPage(page)
|
||||||
|
if err != nil {
|
||||||
|
return SVGResources{}, err
|
||||||
|
}
|
||||||
c.RenderTo(s)
|
c.RenderTo(s)
|
||||||
|
}
|
||||||
if s.err != nil {
|
if s.err != nil {
|
||||||
return SVGResources{}, s.err
|
return SVGResources{}, s.err
|
||||||
}
|
}
|
||||||
@@ -109,6 +135,23 @@ func (r *Renderer) renderSVGResources(page *PageContent, writer io.Writer, image
|
|||||||
return SVGResources{Fonts: s.fonts, Images: s.images}, buffer.Flush()
|
return SVGResources{Fonts: s.fonts, Images: s.images}, buffer.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// beginObject 开始标记当前页面直接对象,模板、注释和内部复合对象不单独标记
|
||||||
|
// 入参: object 图形对象
|
||||||
|
// 返回: bool 是否写入分组
|
||||||
|
func (s *svgResourceRenderer) beginObject(object *GraphicObject) bool {
|
||||||
|
id := s.objects[object]
|
||||||
|
if id == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
fmt.Fprintf(s.writer, `<g data-ofd-object="%s">`, html.EscapeString(id))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// endObject 结束对象分组
|
||||||
|
func (s *svgResourceRenderer) endObject() {
|
||||||
|
fmt.Fprint(s.writer, `</g>`)
|
||||||
|
}
|
||||||
|
|
||||||
// RenderImage 绘制图片,分离资源时保持原始编码、尺寸和变换
|
// RenderImage 绘制图片,分离资源时保持原始编码、尺寸和变换
|
||||||
// 入参: img 图片对象, m 变换矩阵
|
// 入参: img 图片对象, m 变换矩阵
|
||||||
func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) {
|
func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) {
|
||||||
|
|||||||
+48
-9
@@ -48,6 +48,8 @@ type ShapeKind string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
ShapeLine ShapeKind = "line"
|
ShapeLine ShapeKind = "line"
|
||||||
|
ShapeArrow ShapeKind = "arrow"
|
||||||
|
ShapeDoubleArrow ShapeKind = "double-arrow"
|
||||||
ShapeRectangle ShapeKind = "rectangle"
|
ShapeRectangle ShapeKind = "rectangle"
|
||||||
ShapeEllipse ShapeKind = "ellipse"
|
ShapeEllipse ShapeKind = "ellipse"
|
||||||
)
|
)
|
||||||
@@ -63,13 +65,38 @@ func NewShape(kind ShapeKind, box Box) (PathObject, error) {
|
|||||||
}
|
}
|
||||||
object := PathObject{LineWidth: defaultPathLineWidth}
|
object := PathObject{LineWidth: defaultPathLineWidth}
|
||||||
switch kind {
|
switch kind {
|
||||||
case ShapeLine:
|
case ShapeLine, ShapeArrow, ShapeDoubleArrow:
|
||||||
if box.W == 0 && box.H == 0 {
|
if box.W == 0 && box.H == 0 {
|
||||||
return PathObject{}, fmt.Errorf("line endpoints must differ")
|
return PathObject{}, fmt.Errorf("line endpoints must differ")
|
||||||
}
|
}
|
||||||
x, y := math.Min(0, box.W)-defaultPathLineWidth/2, math.Min(0, box.H)-defaultPathLineWidth/2
|
points := []Point{{}, {X: box.W, Y: box.H}}
|
||||||
object.AbbreviatedData = fmt.Sprintf("M %g %g L %g %g", -x, -y, box.W-x, box.H-y)
|
if kind != ShapeLine {
|
||||||
box = Box{X: box.X + x, Y: box.Y + y, W: math.Abs(box.W) + defaultPathLineWidth, H: math.Abs(box.H) + defaultPathLineWidth}
|
length := math.Hypot(box.W, box.H)
|
||||||
|
head := math.Min(4, length/3)
|
||||||
|
x, y := box.W/length*head, box.H/length*head
|
||||||
|
points = append(points, Point{X: box.W - x - y*0.45, Y: box.H - y + x*0.45}, Point{X: box.W, Y: box.H}, Point{X: box.W - x + y*0.45, Y: box.H - y - x*0.45})
|
||||||
|
if kind == ShapeDoubleArrow {
|
||||||
|
points = append(points, Point{X: x - y*0.45, Y: y + x*0.45}, Point{}, Point{X: x + y*0.45, Y: y - x*0.45})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
left, top, right, bottom := 0.0, 0.0, 0.0, 0.0
|
||||||
|
for _, point := range points {
|
||||||
|
left, top, right, bottom = math.Min(left, point.X), math.Min(top, point.Y), math.Max(right, point.X), math.Max(bottom, point.Y)
|
||||||
|
}
|
||||||
|
left, top, right, bottom = left-defaultPathLineWidth/2, top-defaultPathLineWidth/2, right+defaultPathLineWidth/2, bottom+defaultPathLineWidth/2
|
||||||
|
var data strings.Builder
|
||||||
|
for i, point := range points {
|
||||||
|
command := "L"
|
||||||
|
if i == 0 || i == 2 || i == 5 {
|
||||||
|
command = "M"
|
||||||
|
}
|
||||||
|
if i > 0 {
|
||||||
|
data.WriteByte(' ')
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&data, "%s %g %g", command, point.X-left, point.Y-top)
|
||||||
|
}
|
||||||
|
object.AbbreviatedData = data.String()
|
||||||
|
box = Box{X: box.X + left, Y: box.Y + top, W: right - left, H: bottom - top}
|
||||||
case ShapeRectangle, ShapeEllipse:
|
case ShapeRectangle, ShapeEllipse:
|
||||||
if box.W <= 0 || box.H <= 0 {
|
if box.W <= 0 || box.H <= 0 {
|
||||||
return PathObject{}, fmt.Errorf("shape dimensions must be positive")
|
return PathObject{}, fmt.Errorf("shape dimensions must be positive")
|
||||||
@@ -108,6 +135,16 @@ func (p PathObject) Shape() (ShapeKind, Box) {
|
|||||||
var kind ShapeKind
|
var kind ShapeKind
|
||||||
var box Box
|
var box Box
|
||||||
switch {
|
switch {
|
||||||
|
case (commands == "MLMLL" && len(values) == 10) || (commands == "MLMLLMLL" && len(values) == 16):
|
||||||
|
kind = ShapeArrow
|
||||||
|
if commands == "MLMLLMLL" {
|
||||||
|
kind = ShapeDoubleArrow
|
||||||
|
}
|
||||||
|
box = Box{X: values[0], Y: values[1], W: values[2] - values[0], H: values[3] - values[1]}
|
||||||
|
shape, err := NewShape(kind, Box{W: box.W, H: box.H})
|
||||||
|
if err != nil || !sameShapePath(tokens, strings.Fields(shape.AbbreviatedData)) {
|
||||||
|
return "", Box{}
|
||||||
|
}
|
||||||
case commands == "ML" && len(values) == 4 && len(tokens) == 6 && tokens[0] == "M" && tokens[3] == "L":
|
case commands == "ML" && len(values) == 4 && len(tokens) == 6 && tokens[0] == "M" && tokens[3] == "L":
|
||||||
kind, box = ShapeLine, Box{X: values[0], Y: values[1], W: values[2] - values[0], H: values[3] - values[1]}
|
kind, box = ShapeLine, Box{X: values[0], Y: values[1], W: values[2] - values[0], H: values[3] - values[1]}
|
||||||
case commands == "MLLLC" && len(values) == 8:
|
case commands == "MLLLC" && len(values) == 8:
|
||||||
@@ -117,7 +154,8 @@ func (p PathObject) Shape() (ShapeKind, Box) {
|
|||||||
default:
|
default:
|
||||||
return "", Box{}
|
return "", Box{}
|
||||||
}
|
}
|
||||||
if kind != ShapeLine {
|
line := kind == ShapeLine || kind == ShapeArrow || kind == ShapeDoubleArrow
|
||||||
|
if !line {
|
||||||
shape, err := NewShape(kind, box)
|
shape, err := NewShape(kind, box)
|
||||||
if err != nil || !sameShapePath(tokens, strings.Fields(shape.AbbreviatedData)) {
|
if err != nil || !sameShapePath(tokens, strings.Fields(shape.AbbreviatedData)) {
|
||||||
return "", Box{}
|
return "", Box{}
|
||||||
@@ -135,7 +173,7 @@ func (p PathObject) Shape() (ShapeKind, Box) {
|
|||||||
if err != nil || !axisAlignedMatrix(m) {
|
if err != nil || !axisAlignedMatrix(m) {
|
||||||
return "", Box{}
|
return "", Box{}
|
||||||
}
|
}
|
||||||
if kind == ShapeLine {
|
if line {
|
||||||
x, y := m.Transform(box.X, box.Y)
|
x, y := m.Transform(box.X, box.Y)
|
||||||
w, h := matrixVector(m, box.W, box.H)
|
w, h := matrixVector(m, box.W, box.H)
|
||||||
box = Box{X: boundary.X + x, Y: boundary.Y + y, W: w, H: h}
|
box = Box{X: boundary.X + x, Y: boundary.Y + y, W: w, H: h}
|
||||||
@@ -161,7 +199,7 @@ func sameShapePath(a, b []string) bool {
|
|||||||
}
|
}
|
||||||
x, errX := strconv.ParseFloat(token, 64)
|
x, errX := strconv.ParseFloat(token, 64)
|
||||||
y, errY := strconv.ParseFloat(b[i], 64)
|
y, errY := strconv.ParseFloat(b[i], 64)
|
||||||
if errX != nil || errY != nil || x != y {
|
if errX != nil || errY != nil || !finite(x) || !finite(y) || math.Abs(x-y) > 1e-10*math.Max(1, math.Max(math.Abs(x), math.Abs(y))) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,7 +228,8 @@ func (p PathObject) Reshape(box Box) (PathObject, error) {
|
|||||||
} else {
|
} else {
|
||||||
w, h = box.H/m.b, box.W/m.c
|
w, h = box.H/m.b, box.W/m.c
|
||||||
}
|
}
|
||||||
if kind != ShapeLine {
|
line := kind == ShapeLine || kind == ShapeArrow || kind == ShapeDoubleArrow
|
||||||
|
if !line {
|
||||||
w, h = math.Abs(w), math.Abs(h)
|
w, h = math.Abs(w), math.Abs(h)
|
||||||
}
|
}
|
||||||
shape, err := NewShape(kind, Box{W: w, H: h})
|
shape, err := NewShape(kind, Box{W: w, H: h})
|
||||||
@@ -203,7 +242,7 @@ func (p PathObject) Reshape(box Box) (PathObject, error) {
|
|||||||
} else {
|
} else {
|
||||||
m.e, m.f = 0, 0
|
m.e, m.f = 0, 0
|
||||||
var x, y float64
|
var x, y float64
|
||||||
if kind == ShapeLine {
|
if line {
|
||||||
x, y = m.Transform(-boundary.X, -boundary.Y)
|
x, y = m.Transform(-boundary.X, -boundary.Y)
|
||||||
} else {
|
} else {
|
||||||
bounds := m.TransformBox(boundary)
|
bounds := m.TransformBox(boundary)
|
||||||
|
|||||||
Reference in new issue
Block a user