It has to do with the way rectangles are constructed as SVG objects by Fabric JS, the drawing backend for Roll20, and how those objects are persisted. It's something there might be a fix for in the future. In the interim, if you can get access to the API (Pro Subscriber Perk) in your game, here's a script that will fix it for all your rectangles by calling: !fix-rect-close And also fix any new rectangles you draw. Here's the code: on('ready',()=>{
const pathType= (obj) => {
try {
let pd = JSON.parse(obj.get('path'));
let key = pd.map(a=>a[0]).join('');
switch(key) {
case 'MCCCC':
return 'circle';
case 'MLLLLZ':
return 'rectangle:closed';
case 'MLLLLL': // hack for fixing DL corners
if( pd[0][1]===pd[4][1] && pd[0][2]===pd[4][2] && // begin and end same
pd[1][1]===pd[2][1] && pd[0][2]===pd[1][2] && // top right corner
pd[2][1]===pd[1][1] && pd[2][2]===pd[3][2] && // bottom right corner
pd[3][1]===pd[4][1] && pd[3][2]===pd[2][2] && // bottom left corner
pd[4][1]===pd[5][1] && pd[4][2]===pd[3][2] // top right corner (again, for DL fix)
) {
return 'rectangle:dlclosed';
} else {
return 'polyline';
}
case 'MLLLL':
if( pd[0][1]===pd[4][1] && pd[0][2]===pd[4][2] && // begin and end same
pd[1][1]===pd[2][1] && pd[0][2]===pd[1][2] && // top right corner
pd[2][1]===pd[1][1] && pd[2][2]===pd[3][2] && // bottom right corner
pd[3][1]===pd[4][1] && pd[3][2]===pd[2][2] // bottom left corner
) {
return 'rectangle';
} else {
return 'polyline';
}
default:
if( /^MQ+L/.test(key)){
return 'pen';
} else if( /^ML+/.test(key)){
return 'polyline';
} else {
return `custom:${key}`;
}
}
} catch(e) {
return 'invalid';
}
};
const closeRectangle = (path) => {
let fixedPath = JSON.parse(path.get('path'));
if('walls'===path.get('layer')){
fixedPath.push([...fixedPath[1]]);
} else {
fixedPath.push(['Z']);
}
let props = JSON.parse(JSON.stringify(path));
delete props._type;
delete props._id;
props.pageid=props._pageid;
delete props._pageid;
delete props._path;
createObj('path',{
...props,
path: JSON.stringify(fixedPath)
});
path.remove();
};
on('add:path',(path)=>{
if('rectangle'===pathType(path)){
closeRectangle(path);
}
});
on('chat:message',msg=>{
if('api'===msg.type && /^!fix-rect-close(\b\s|$)/i.test(msg.content) && playerIsGM(msg.playerid)){
let who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname');
let paths = findObjs({type:'path'});
let closedRects = 0;
sendChat('',`/w "${who}" <div style="border:1px solid black;padding:.5em;background-color:white;">Considering <code>${paths.length}<code> path${1!==paths.length ? 's' : ''}.</div>`);
const burndown = ()=>{
let p = paths.shift();
if(p){
if('rectangle'===pathType(p)){
++closedRects;
closeRectangle(p);
}
setTimeout(burndown,0);
} else {
sendChat('',`/w "${who}" <div style="border:1px solid black;padding:.5em;background-color:white;">Cloesed <code>${closedRects}<code> rectangle${1!==closedRects ? 's' : ''}.</div>`);
}
};
burndown();
}
});
});