I have a script for this that I built a while back. I've just adjusted it to work with Doors and Windows. Once you have this installed, if you move something off the edge of the map layer, it will resize and reposition things as needed. If you then remove those things, it will again resize to the bounds of all the objects on the map layer. If you want, you can change the 4th line to false, and it will resize for any layer, but note that it can't distinguish between player and gm additions. 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, {
x: bounds.left,
y: -bounds.top,
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(!mapOnlyBounds || obj.get('layer')=='map'){
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)));
});