I'd like a script for battle-maps

1507296386
So I upload the image, I figure out the number of pixels of the image and get an exact measure of the image and all that stuff. I try to line it up but not so exact... I'M PRETTY SURE i'm doing it correctly but at the same time it's a lot of faff.... Could I get a script where I right click the uploaded image and tell it "Make a page out of this that's accurately measured for me"?
1507300890
The Aaron
Pro
API Scripter
The API doesn't know anything about the native size of images.  You can write a script that would adjust the page so that it's always large enough for everything that's on it.  Then you could right click the image and tell it to be the size you know it should be, and the page would grow to match it.
1507322968
The Aaron said: The API doesn't know anything about the native size of images.  You can write a script that would adjust the page so that it's always large enough for everything that's on it.  Then you could right click the image and tell it to be the size you know it should be, and the page would grow to match it. That would be neat, how would  go about doing that? 
1507485545

Edited 1507517217
The Aaron
Pro
API Scripter
I'd do it like this: on('ready',()=>{     const minPageX=10;     const minPageY=10;     const mapOnlyBounds=true;     const fuzzFactor=1.0e-4;     const bKeys=['left','top','width','height','rotation'];     const d2r = (degrees) => degrees * (Math.PI/180);     //const r2d = (radians) => radians * (180/Math.PI);     const rotateOPA = (o,p,a) => {         const r = d2r(a);         const s = Math.sin(r);         const c = Math.cos(r);         let pp = {             x: p.x-o.x,             y: p.y-o.y         };         return {             x: (pp.x * c - pp.y * s)+o.x,             y: (pp.x * s + pp.y * c)+o.y         };     };     const oBounds = (o)=>{         let oB = _.reduce(bKeys,(mB,k)=>Object.assign(mB,{[k]:o.get(k)}),{});         const oC = {x: oB.left, y: oB.top};         const oR = o.get('rotation');         oB = {             left: oB.left-Math.floor(oB.width/2),             top: oB.top-Math.floor(oB.height/2),             right: oB.left+Math.ceil(oB.width/2),             bottom: oB.top+Math.ceil(oB.height/2)         };         if(oR % 180){             let p1 = rotateOPA(oC, {x:oB.left, y:oB.top}, oR),                 p2 = rotateOPA(oC, {x:oB.right, y:oB.top}, oR),                 p3 = rotateOPA(oC, {x:oB.left, y:oB.bottom}, oR),                 p4 = rotateOPA(oC, {x:oB.right, y:oB.bottom}, oR);             oB = {                 left: Math.min(p1.x,p2.x,p3.x,p4.x),                 top: Math.min(p1.y,p2.y,p3.y,p4.y),                 right: Math.max(p1.x,p2.x,p3.x,p4.x),                 bottom: Math.max(p1.y,p2.y,p3.y,p4.y)             };         }         return oB;     };     const translateObjs = (objs,transform) => objs.forEach(         (o) => o.set(Object.keys(transform).reduce(             (m,k) => Object.assign(m, {[k]:o.get(k)-transform[k]}), {})         )     );     const resizePage = _.debounce((pageid) =>{         const page = getObj('page',pageid);         let count = 0;         if(page){             const objs = findObjs({pageid}); // will only be Graphics, Paths, Text             let bounds = _.reduce(objs,(m,o)=>{                 if(mapOnlyBounds && o.get('layer')!=='map'){                     return m;                 }                 count++;                 let oB = oBounds(o);                 switch(o.get('type')){                     case 'text':                     case 'path':                     case 'graphic':                         return {                             left: Math.min(m.left,oB.left),                             top: Math.min(m.top,oB.top),                             right: Math.max(m.right,oB.right),                             bottom: Math.max(m.bottom,oB.bottom)                         };                 }                 return m;             },{left:Infinity,top:Infinity,right:-Infinity,bottom:-Infinity});             if(count){                 if(bounds.left || bounds.top){                     translateObjs(objs, { left: bounds.left, top: bounds.top});                     bounds={                         left: 0,                         top: 0,                         right: bounds.right-bounds.left,                         bottom: bounds.bottom-bounds.top,                     };                 }                 let uX=Math.max(minPageX,Math.ceil((bounds.right-fuzzFactor)/70));                 let uY=Math.max(minPageY,Math.ceil((bounds.bottom-fuzzFactor)/70));                 if(uX !== page.get('width') || uY !== page.get('height') ){                     page.set({                         width: uX,                         height: uY                     });                 }             }         }     },50);     const handleMapLayerGraphic = (event, obj, prev)=>{         if('map' === obj.get('layer')){             switch(event){                 case 'add:graphic':                     _.defer(()=>resizePage((getObj('graphic',obj.id)||{get:()=>{}}).get('pageid')));                     break;                 case 'change:graphic':                     if(bKeys.reduce((m,k)=>m || obj.get(k)!=prev[k],false)){                         _.defer(()=>resizePage(obj.get('pageid')));                     }                     break;                 case 'destroy:graphic':                     _.defer(()=>resizePage(obj.get('pageid')));                     break;             }         }     };     ['add:graphic','change:graphic','destroy:graphic'].forEach((e)=>on(e,(o,p)=>handleMapLayerGraphic(e,o,p))); }); This will automatically adjust the size of the page whenever you change something on the map layer.  It will calculate the new bounds of the page based only on things on the map layer, then it will shift everything on the page to be at the right place.  This includes tokens, text, paths, and Dynamic Lighting paths. Some Caveats: I suggest not moving something on the map layer if you've lined up Dynamic Lighting Lines with it, as this script doesn't know which are associated with it, so they will get moved elsewhere. If you orphan any of the things on the objects, gm or walls layers by moving the map out from under them, they'll remain off the page and will only show up again when you move something on the map layer out underneath them.  This might be an interesting way to hide encounters, but just be aware of it. The minimum size it will set a page to is 10x10.  This is to prevent deleting the last object on the maps later and then not having anywhere to drop something.  If you remove the last item from the map layer, it won't resize. If you have a map you don't want shrinking, be sure to put some markers on the map layer in the corners. Changes that happen because of the API won't trigger.
1507499308
So I know absoutely nothing about what you showed me over than that it looks like code :P  How do I apply it? 
1507499830

Edited 1507500174
Vince
Pro
Sheet Author
Assuming the script is complete... just copy and paste the script into the API console and save.  To open the API console; from the roll20 top menu>Games>My Games>Click on your game's title, then from the Settings dropdown selector>choose API Scripts.  Choose "New Script", paste in the script and save.  Open game and test/check.  Most scripts require some commands that start with an " ! " ie "!startFoo"  It looks like Aaron's script automatically runs without any need for user input.  Hope this helps.  Cheers
1507503701
The Aaron
Pro
API Scripter
 That’s exactly correct, Vince. No commands required, it operates purely based on events occurring on the map layer. I did realize I need to account for rotations as well, so I’ll update with that code a little later. 
1507503910
Vince
Pro
Sheet Author
The Aaron said:  That’s exactly correct, Vince. No commands required, it operates purely based on events occurring on the map layer. I did realize I need to account for rotations as well, so I’ll update with that code a little later.  I was going to say something about the lack of rotation...  lol.  ;-P
1507507004
The Aaron
Pro
API Scripter
Hahaha, you get no points AFTER I’ve already said it!! =D There are a few edge cases where you might not want it to resize, which ice not accounted for and some where you might think it should, but doesn’t. In general though, you can just do the fine tuning with the script disabled. Or maybe I’ll add a way to turn it off...
1507516305
The Aaron
Pro
API Scripter
There, updated to respect rotations (even ones that aren't a perfect 90º!).  Will also update on rotation.
1507518155
The Aaron
Pro
API Scripter
Updated again with a little correction for rounding errors that could occasionally lead to having 1 more column of width or row of height than is needed.  
1507546123
ok so followed instruction. I have here an example using a map from the princes of the apocalypse, 5E, campaign  So the image should be inserted of the "Scarlet moon hall"  the dimensions for it is 3742 X 2591 I set the image accurately in the map layer and i also set the page units to be taller and wider than the image BEFORE setting it to the map layer. so i expected to see the page get smaller to wrap around the image once i moved it from token to map layer. That didn't quite happen. (I definately added the script, restarted the api . Just confirmation this image is sitting on the map layer. The page hasn't quite updated for it.
1507547040
I made a new page and dragged the same image out (before resizing)  Now this time, the page did update!  It didn't match the image's proportions but it did shrink abit and did drag it to top left corner.  So about to set same dimensions.  On the update, VERY POSITIVE STUFF! Just off alittle bit  Even then, this tool is pretty much spot on for what I need!  There is however one more step. I kind of expected/ presumed that the grid would automatically line up over this map when the page was perfectly in lined with the image, the grid would just lined up. I wouldn't expect this with a typical image but I recon the official maps would have an aligned grid.  On close inspection I do think the grid is the correct size for each box though.  Above you can see the drawn out square (shift and drag) and then I dragged it over the map without the constrains of the grid and as you can see, the grid is perfectly the same size as the map squares!  So now, trick would be to align this and i'm a happy bunny!!  If it in any way saves time, this is literally to use only on dungeons and dragons campaign maps and such!
1507556537
Oh!!!!! Could it be done so that the map is put into the very center of the page rather than the top left?  This might be the trick to always lining up the grid to the maps!! :D
1507557033
The Aaron
Pro
API Scripter
Ah, so the issue here is that the grid is a partial offset from the edge.  That's not something that can be accounted for by the script (it has no way of accessing the pixel data on the image to try and detect a grid, but that would be cool...). However, try putting that teal box in the upper leftmost corner, then grab the map and while holding alt, move it down and to the right until the grid is aligned.  Then set the box's color to transparent so it doesn't show up.  That should get it aligned to the grid and the now invisible box will keep page dimensions the right size.
1507557331
The Aaron said: Ah, so the issue here is that the grid is a partial offset from the edge.  That's not something that can be accounted for by the script (it has no way of accessing the pixel data on the image to try and detect a grid, but that would be cool...). However, try putting that teal box in the upper leftmost corner, then grab the map and while holding alt, move it down and to the right until the grid is aligned.  Then set the box's color to transparent so it doesn't show up.  That should get it aligned to the grid and the now invisible box will keep page dimensions the right size. Thanks dude :P Apparantly there is just no easy fix to get the grid and page lined up X_X I can have it to scale, it's just my ocd at this point >_<  This still stands as a neat little tool to clip the newly made page into the image I upload which is neat. That will do for me! 
1507557541
FYI, I tried it again with a new image, same campaign resource: Princes of the apocalypse,  Black geode image, dimensions of the image in the details, the dimensions of the map image set as shown in the image. A box drawn to follow the grid, copied and pasted again to try and align to the map box, in this case, shows that just matching the image proportions isn't enough to make it exact in roll20..... DAMN! how do the people at wizards do this?!?!!? What is the math and system to get this spot on?!?!?! x_x
1507559013
Ziechael
Pro
Sheet Author
API Scripter
The issue generally arises from the fact that they don't use a 1 pixel wide grid so by the time you've lined A1 up Z26 is WAY out. Your best bet is to get it as near as possible or turn the grid off and just use the hard coded one supplied in the image.
1507559348
The Aaron
Pro
API Scripter
Have you tried the Align to Grid Tool?  That might be what you're missing, really.  It should make getting the image to the right size easy.
1507603631
Vince
Pro
Sheet Author
This may help:  Map alignment and you: Or 101 ways to get your grids in a row.
1507647905
The align tool does do a pretty close job. i do like your script you gave me before, may you be able to update it so it gets rid of it's re-alignment to the top left so after aligning, I can line up the map to the grid but still have the page trimming tool? because it is handy! :D
1507649855
The Aaron
Pro
API Scripter
There isn't really a way for it to know, but you can put some transparent thing in the upper left corner to hold it's spot, then move the map with alt to put it where it should be.
1507650255
ah ok cool! :D
1507651348
The Aaron
Pro
API Scripter
That's a bit hard to describe, so if you want me to drop in and show you what I mean, just PM me an invite and GM me. =D
1507653669

Edited 1507653691
Actually yes please that could be cool.
1507653754
The Aaron
Pro
API Scripter
Can do, fire me a PM with the join link.