Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/containers/paper-canvas.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
clampViewBounds, resetZoom, setWorkspaceBounds, zoomToFit, resizeCrosshair
} from '../helper/view';
import {ensureClockwise, scaleWithStrokes} from '../helper/math';
import {pivotImportOptions, restorePivotsToItems} from '../helper/pivot-data';
import {clearHoveredItem} from '../reducers/hover';
import {clearPasteOffset} from '../reducers/clipboard';
import {changeFormat} from '../reducers/format';
Expand Down Expand Up @@ -231,7 +232,7 @@ class PaperCanvas extends React.Component {
}
}

paper.project.importSVG(svg, {
paper.project.importSVG(svg, Object.assign({
expandShapes: true,
insert: false,
onLoad: function (item) {
Expand All @@ -251,7 +252,7 @@ class PaperCanvas extends React.Component {
paperCanvas.initializeSvg(item, rotationCenterX, rotationCenterY, viewBox);
});
}
});
}, pivotImportOptions()));
}
initializeSvg (item, rotationCenterX, rotationCenterY, viewBox) {
setImportingImage(false);
Expand Down Expand Up @@ -315,6 +316,10 @@ class PaperCanvas extends React.Component {
ungroupItems([item]);
}

// Tree's done moving around now, so the anchors can finally go back on. Do NOT
// move this earlier, initializeSvg will shove everything around and ruin it.
restorePivotsToItems();

performSnapshot(this.props.undoSnapshot, Formats.VECTOR_SKIP_CONVERT);
this.maybeZoomToFit();
}
Expand Down
9 changes: 9 additions & 0 deletions src/helper/group.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import paper from '@turbowarp/paper';
import {getRootItem, isGroupItem} from './item';
import {clearSelection, getSelectedRootItems, setItemSelection} from './selection';
import {clearSelectionPivot, getSelectionPivot, setPivot} from './pivot';

const isGroup = function (item) {
return isGroupItem(item);
Expand All @@ -16,7 +17,15 @@ const isGroup = function (item) {
*/
const groupItems = function (items, clearSelectedItems, setSelectedItems, onUpdateImage) {
if (items.length > 0) {
// Grab the anchor BEFORE grouping. The selection is about to become one item and
// we're not about to silently throw away where they put it. ASSHOLE!
const pivot = getSelectionPivot(items);
const group = new paper.Group(items);
if (pivot) {
// Members give theirs up so the group is the only one talking. One answer, not five.
clearSelectionPivot(items);
setPivot(group, pivot);
}
clearSelection(clearSelectedItems);
setItemSelection(group, true);
for (let i = 0; i < group.children.length; i++) {
Expand Down
10 changes: 6 additions & 4 deletions src/helper/guides.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ const hoverItem = function (item) {
clone.closed = true;
}
clone.parent = getGuideLayer();
clone.position = item.position;
// Use the matrix, NOT position. Once an item has an anchor, `position` IS the anchor,
// so the hover outline wanders off somewhere stupid. bruh....
clone.matrix = item.globalMatrix;
clone.strokeColor = GUIDE_BLUE;
clone.fillColor = null;
clone.data.isHelperItem = true;
Expand All @@ -36,7 +38,7 @@ const hoverBounds = function (item, expandBy) {
bounds = bounds.expand(expandBy);
}
const rect = new paper.Path.Rectangle(bounds);
rect.matrix = item.matrix;
rect.matrix = item.globalMatrix;
setDefaultGuideStyle(rect);
rect.parent = getGuideLayer();
rect.strokeColor = GUIDE_BLUE;
Expand Down Expand Up @@ -91,11 +93,11 @@ const _removePaperItemsByTags = function (tags) {
};

const removeBoundsPath = function () {
_removePaperItemsByDataTags(['isSelectionBound', 'isRotHandle', 'isScaleHandle']);
_removePaperItemsByDataTags(['isSelectionBound', 'isRotHandle', 'isScaleHandle', 'isPivotHandle']);
};

const removeBoundsHandles = function () {
_removePaperItemsByDataTags(['isRotHandle', 'isScaleHandle']);
_removePaperItemsByDataTags(['isRotHandle', 'isScaleHandle', 'isPivotHandle']);
};

const removeAllGuides = function () {
Expand Down
178 changes: 178 additions & 0 deletions src/helper/pivot-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* SERIALIZATION STUFF
* Saving the transform anchor directly onto the SVG element as `data-pivot`.
*
* This uses two states:
* `item.pivot` - live editor state in local space. Volatile!
* `item.data.persistedPivot` - inert JSON copy in art-board space that survives SVG export/import. Sturdy!
*/
import paper from '@turbowarp/paper';

import {getPivot, setPivot} from './pivot';
import {CENTER} from './view';

const ATTRIBUTE = 'data-pivot';
const PRECISION = 4;

const _round = function (n) {
const factor = Math.pow(10, PRECISION);
return Math.round(n * factor) / factor;
};

/**
* @param {paper.Point} point A point in paper space. Smooth and inoffensive.
* @return {Array<number>} [x, y] relative to the art board center, rounded.
*/
const toPersistedArray = function (point) {
return [_round(point.x - CENTER.x), _round(point.y - CENTER.y)];
};

/**
* @param {Array<number>} array [x, y] relative to the art board center.
* @return {?paper.Point} The point in paper space, or null if malformed.
*/
const fromPersistedArray = function (array) {
if (!Array.isArray(array) || array.length < 2) return null;
const x = Number(array[0]);
const y = Number(array[1]);
if (isNaN(x) || isNaN(y)) return null;
return new paper.Point(x + CENTER.x, y + CENTER.y);
};

/**
* @param {Array<number>} array [x, y].
* @return {string} Serialized attribute value.
*/
const serializePivot = function (array) {
return array.join(',');
};

/**
* @param {string} string An attribute value.
* @return {?Array<number>} [x, y], or null if invalid.
*/
const parsePivot = function (string) {
if (typeof string !== 'string') return null;
const parts = string.split(',');
if (parts.length !== 2) return null;
const x = Number(parts[0]);
const y = Number(parts[1]);
return isNaN(x) || isNaN(y) ? null : [x, y];
};

/**
* Walk the painting layer, dodging guides and clip masks.
* @param {function} callback Called with each valid item.
*/
const walkPaintingLayer = function (callback) {
let layer = null;
for (const child of paper.project.layers) {
if (child.data && child.data.isPaintingLayer) {
layer = child;
break;
}
}
if (!layer) return;
const recurse = function (item) {
if (item.guide || item.isClipMask()) return;
callback(item);
if (item.children) {
for (const child of item.children) recurse(child);
}
};
for (const child of layer.children) recurse(child);
};

/**
* Copy live anchor to inert data before export scales things.
* @param {paper.Item} item Any item.
*/
const syncPivotData = function (item) {
const pivot = getPivot(item);
if (!item.data) item.data = {};
if (pivot) {
item.data.persistedPivot = toPersistedArray(pivot);
} else {
delete item.data.persistedPivot;
}
};

/**
* Apply inert data back to the live anchor after import finishes positioning...
* @param {paper.Item} item Any item.
*/
const applyPivotData = function (item) {
if (!item.data || !item.data.persistedPivot) return;
const point = fromPersistedArray(item.data.persistedPivot);
if (point) setPivot(item, point);
};

/**
* Stamps the anchor attribute onto the exported SVG node.
* @param {paper.Item} item The item being exported.
* @param {Element} node The SVG node paper produced.
* @return {Element} The same node.
*/
const stampPivotOnExport = function (item, node) {
if (!node || !node.setAttribute) return node;
const value = item.data && item.data.persistedPivot;
if (value) {
node.setAttribute(ATTRIBUTE, serializePivot(value));
} else if (node.removeAttribute) {
node.removeAttribute(ATTRIBUTE);
}
return node;
};

/**
* Stashes the anchor inertly on import so it survives shape expansion.
* @param {Element} node The incoming SVG node.
* @param {paper.Item} item The paper item born from it.
* @return {paper.Item} The same item.
*/
const readPivotOnImport = function (node, item) {
if (!item || !node || !node.getAttribute) return item;
const value = parsePivot(node.getAttribute(ATTRIBUTE));
if (!value) return item;
if (!item.data) item.data = {};
item.data.persistedPivot = value;
return item;
};

/**
* @return {object} Options for `paper.project.exportSVG`.
*/
const pivotExportOptions = function () {
return {onExport: stampPivotOnExport};
};

/**
* @return {object} Options for `paper.project.importSVG`.
*/
const pivotImportOptions = function () {
return {onImport: readPivotOnImport};
};

const syncPivotsFromItems = function () {
walkPaintingLayer(syncPivotData);
};

const restorePivotsToItems = function () {
walkPaintingLayer(applyPivotData);
};

export {
ATTRIBUTE,
toPersistedArray,
fromPersistedArray,
serializePivot,
parsePivot,
syncPivotData,
applyPivotData,
stampPivotOnExport,
readPivotOnImport,
pivotExportOptions,
pivotImportOptions,
syncPivotsFromItems,
restorePivotsToItems
};
Loading