Just to check, you want everything on every layer to be rotated around the page center by 90 degrees? Here's a script to do that. Be sure to use it on a copy of your source page just in case something doesn't workout... Command: !rotate-page Code: on('ready',()=>{
const ANGLE = 90;
const getPageForPlayer = (playerid) => {
let player = getObj('player',playerid);
if(playerIsGM(playerid)){
return player.get('lastpage') || Campaign().get('playerpageid');
}
let psp = Campaign().get('playerspecificpages');
if(psp[playerid]){
return psp[playerid];
}
return Campaign().get('playerpageid');
};
const simpleObj = (o) => JSON.parse(JSON.stringify(o));
// Positive degrees match Roll20 rotation (clockwise on the Y-down tabletop).
const rotateAround = (x, y, cx, cy, degrees) => {
const t = degrees * Math.PI / 180;
const cos = Math.cos(t);
const sin = Math.sin(t);
const dx = x - cx;
const dy = y - cy;
return {
x: cx + cos * dx - sin * dy,
y: cy + sin * dx + cos * dy
};
};
// 90° CW around page center + translate so content fits swapped width/height.
// Equivalent to (x, y) -> (oldHeightPx - y, x).
const transformPagePoint = (x, y, oldHeightPx) => ({
x: oldHeightPx - y,
y: x
});
const transformPageOffset = (ox, oy) => ({
x: -oy,
y: ox
});
const swapHexGridType = (gridType) => {
if('hex' === gridType){
return 'hexr';
}
if('hexr' === gridType){
return 'hex';
}
return gridType;
};
const asPathData = (pathProp) => {
if('string' === typeof pathProp){
try {
return JSON.parse(pathProp);
} catch(e){
return pathProp;
}
}
return pathProp;
};
const roll20PathProps = (path) => {
let props = simpleObj(path);
props.pageid = props._pageid;
props.path = props._path;
delete props._id;
delete props._type;
delete props._path;
delete props._pageid;
return props;
};
const rotateGraphicOrText = (obj, oldHeightPx) => {
const p = transformPagePoint(obj.get('left'), obj.get('top'), oldHeightPx);
obj.set({
left: p.x,
top: p.y,
rotation: (parseFloat(obj.get('rotation')) || 0) + ANGLE
});
};
const rotatePin = (obj, oldHeightPx) => {
const p = transformPagePoint(obj.get('x'), obj.get('y'), oldHeightPx);
obj.set({
x: p.x,
y: p.y
});
};
const rotateDoorOrWindow = (obj, oldHeightPx) => {
const path = asPathData(obj.get('path')) || {};
const h0 = path.handle0 || {x:0,y:0};
const h1 = path.handle1 || {x:0,y:0};
const ox = obj.get('x');
const oy = obj.get('y');
const center = transformPagePoint(ox, -oy, oldHeightPx);
const map0 = transformPagePoint(ox + h0.x, -(oy + h0.y), oldHeightPx);
const map1 = transformPagePoint(ox + h1.x, -(oy + h1.y), oldHeightPx);
const newX = center.x;
const newY = -center.y;
obj.set({
x: newX,
y: newY,
path: {
handle0: {
x: map0.x - newX,
y: -map0.y - newY
},
handle1: {
x: map1.x - newX,
y: -map1.y - newY
}
}
});
};
const rotateLegacyPath = (path, oldHeightPx) => {
const cmds = JSON.parse(path.get('path') || '[]');
const left = path.get('left');
const top = path.get('top');
const w = path.get('width');
const h = path.get('height');
const scaleX = path.get('scaleX') || 1;
const scaleY = path.get('scaleY') || 1;
const rot = parseFloat(path.get('rotation')) || 0;
const toMap = (px, py) => {
let x = scaleX * px + left - (scaleX * w / 2);
let y = scaleY * py + top - (scaleY * h / 2);
if(rot){
({x, y} = rotateAround(x, y, left, top, rot));
}
return transformPagePoint(x, y, oldHeightPx);
};
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
const mapped = cmds.map((cmd) => {
const out = [cmd[0]];
if('Z' === cmd[0] || 'z' === cmd[0]){
return out;
}
for(let i = 1; i < cmd.length; i += 2){
const p = toMap(cmd[i], cmd[i + 1]);
out.push(p.x, p.y);
minX = Math.min(minX, p.x);
minY = Math.min(minY, p.y);
maxX = Math.max(maxX, p.x);
maxY = Math.max(maxY, p.y);
}
return out;
});
if(!isFinite(minX)){
const p = transformPagePoint(left, top, oldHeightPx);
path.set({
left: p.x,
top: p.y,
rotation: rot + ANGLE
});
return;
}
const newW = maxX - minX;
const newH = maxY - minY;
const normalized = mapped.map((cmd) => {
if(1 === cmd.length){
return cmd;
}
const out = [cmd[0]];
for(let i = 1; i < cmd.length; i += 2){
out.push(cmd[i] - minX, cmd[i + 1] - minY);
}
return out;
});
const props = roll20PathProps(path);
props.path = JSON.stringify(normalized);
props.width = newW;
props.height = newH;
props.left = minX + newW / 2;
props.top = minY + newH / 2;
props.rotation = 0;
props.scaleX = 1;
props.scaleY = 1;
const neu = createObj('path', props);
if(neu){
path.remove();
}
};
const pathV2MapPoints = (path) => {
const shape = path.get('shape') || 'pol';
const points = JSON.parse(path.get('points') || '[]');
const px = path.get('x');
const py = path.get('y');
const rot = parseFloat(path.get('rotation')) || 0;
let mapPts = [];
if('rec' === shape || 'eli' === shape){
if(!points.length){
return {shape, mapPts, px, py, rot};
}
const p1 = points[0];
const p2 = points[points.length - 1];
const midX = (p1[0] + p2[0]) / 2;
const midY = (p1[1] + p2[1]) / 2;
if(Math.abs(midX) < 0.1 && Math.abs(midY) < 0.1){
mapPts = [
[px + p1[0], py + p1[1]],
[px + p2[0], py + p2[1]]
];
} else {
const w = Math.abs(p2[0] - p1[0]);
const h = Math.abs(p2[1] - p1[1]);
mapPts = [
[px - w / 2 + p1[0], py - h / 2 + p1[1]],
[px - w / 2 + p2[0], py - h / 2 + p2[1]]
];
}
} else {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
points.forEach(([x, y]) => {
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
});
const midX = (minX + maxX) / 2;
const midY = (minY + maxY) / 2;
mapPts = points.map(([x, y]) => [px + x - midX, py + y - midY]);
}
if(rot){
mapPts = mapPts.map(([x, y]) => {
const p = rotateAround(x, y, px, py, rot);
return [p.x, p.y];
});
}
return {shape, mapPts};
};
const rotatePathV2 = (path, oldHeightPx) => {
const {shape, mapPts} = pathV2MapPoints(path);
if(!mapPts.length){
const p = transformPagePoint(path.get('x'), path.get('y'), oldHeightPx);
path.set({
x: p.x,
y: p.y,
rotation: (parseFloat(path.get('rotation')) || 0) + ANGLE
});
return;
}
const rotated = mapPts.map(([x, y]) => {
const p = transformPagePoint(x, y, oldHeightPx);
return [p.x, p.y];
});
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
rotated.forEach(([x, y]) => {
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
});
const newX = (minX + maxX) / 2;
const newY = (minY + maxY) / 2;
const w = maxX - minX;
const h = maxY - minY;
let points;
if('rec' === shape || 'eli' === shape){
points = [[-w / 2, -h / 2], [w / 2, h / 2]];
} else {
points = rotated.map(([x, y]) => [x - newX, y - newY]);
}
path.set({
x: newX,
y: newY,
points: JSON.stringify(points),
rotation: 0
});
};
const rotatePage = (pageid) => {
const page = getObj('page', pageid);
if(!page){
return 0;
}
const oldWidth = page.get('width');
const oldHeight = page.get('height');
const oldHeightPx = oldHeight * 70;
const oldOffsetX = parseFloat(page.get('grid_offset_x')) || 0;
const oldOffsetY = parseFloat(page.get('grid_offset_y')) || 0;
const newOffset = transformPageOffset(oldOffsetX, oldOffsetY);
let count = 0;
findObjs({type: 'graphic', pageid}).forEach((o) => {
rotateGraphicOrText(o, oldHeightPx);
++count;
});
findObjs({type: 'text', pageid}).forEach((o) => {
rotateGraphicOrText(o, oldHeightPx);
++count;
});
findObjs({type: 'pin', pageid}).forEach((o) => {
rotatePin(o, oldHeightPx);
++count;
});
findObjs({type: 'door', pageid}).forEach((o) => {
rotateDoorOrWindow(o, oldHeightPx);
++count;
});
findObjs({type: 'window', pageid}).forEach((o) => {
rotateDoorOrWindow(o, oldHeightPx);
++count;
});
findObjs({type: 'path', pageid}).forEach((o) => {
rotateLegacyPath(o, oldHeightPx);
++count;
});
findObjs({type: 'pathv2', pageid}).forEach((o) => {
rotatePathV2(o, oldHeightPx);
++count;
});
page.set({
width: oldHeight,
height: oldWidth,
grid_offset_x: newOffset.x,
grid_offset_y: newOffset.y,
grid_type: swapHexGridType(page.get('grid_type'))
});
return count;
};
on('chat:message', (msg) => {
if('api' === msg.type && /^!rotate-page(\b\s|$)/i.test(msg.content) && playerIsGM(msg.playerid)){
const who = (getObj('player', msg.playerid) || {get: () => 'API'}).get('_displayname');
const pageid = getPageForPlayer(msg.playerid);
const n = rotatePage(pageid);
sendChat('', `/w "${who}" Rotated ${n} object${1 === n ? '' : 's'} 90° on the current page.`);
}
});
});