Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×

Rotate everything on a page 90 degrees?…

Is there a way to rotate everything on a page (map, DL lines, doors, etc.) 90 degrees so it all still lines up?…
No there is not. I've copied the page, rotated the map, then gone to the lighting layer, selected everything, then rotated and carefully moved it into position. I'm successful about half the time (which is why I first make a copy so I can start over when I mess it up). There is no way to do this to doors and windows though.
1786561863
The Aaron
Roll20 Production Team
API Scripter
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.`); } }); });
Thanks. I have never messed around with API/macros before. I just created a macro in the game with your title and text above. How do I run it on a page?…
1786567466
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Hi RPGeezer! The file above goes into a new script field on your Mods page. When you save it, that should restart the sandbox, maeing the code accessible to the game. Then enter your game, navigate to the page you want to rotate and type  !rotate-page  in chat.
1786567649
The Aaron
Roll20 Production Team
API Scripter
Here's a good starter video by Nick Olivio:&nbsp; <a href="https://www.youtube.com/watch?v=jam2yx8btaQ" rel="nofollow">https://www.youtube.com/watch?v=jam2yx8btaQ</a> It's should get you up and running quickly.
Thanks. I’ll check it out…
Got it working, then realized the map was the right way after all. But now I have it in my toolbox. Thanks again!…
Thank you for this! This needs to be in the 1-click.&nbsp; The Aaron said: Just to check, you want everything on every layer to be rotated around the page center by 90 degrees?&nbsp; Here's a script to do that.&nbsp; Be sure to use it on a copy of your source page just in case something doesn't workout...