Model multi-monitor layout geometry
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
function logicalSize(record) {
|
||||
if (!record)
|
||||
return { width: 0, height: 0 };
|
||||
|
||||
const width = Number(record.width);
|
||||
const height = Number(record.height);
|
||||
const scale = Number(record.scale);
|
||||
const transform = Number(record.transform);
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height)
|
||||
|| !Number.isFinite(scale) || scale <= 0)
|
||||
return { width: 0, height: 0 };
|
||||
|
||||
const rotated = transform === 1 || transform === 3;
|
||||
return {
|
||||
width: (rotated ? height : width) / scale,
|
||||
height: (rotated ? width : height) / scale
|
||||
};
|
||||
}
|
||||
|
||||
function validCoordinate(value) {
|
||||
return Number.isFinite(value) && Number.isInteger(value)
|
||||
&& value >= -100000 && value <= 100000;
|
||||
}
|
||||
|
||||
function validate(layout) {
|
||||
if (!Array.isArray(layout) || layout.length === 0)
|
||||
return false;
|
||||
|
||||
const names = {};
|
||||
let primaryCount = 0;
|
||||
for (const record of layout) {
|
||||
if (!record || typeof record.name !== "string"
|
||||
|| !/^[A-Za-z0-9_.-]+$/.test(record.name)
|
||||
|| names[record.name])
|
||||
return false;
|
||||
names[record.name] = true;
|
||||
|
||||
if (!Number.isFinite(record.width) || record.width <= 0
|
||||
|| !Number.isFinite(record.height) || record.height <= 0
|
||||
|| !Number.isFinite(record.scale) || record.scale <= 0
|
||||
|| !Number.isInteger(record.transform)
|
||||
|| record.transform < 0 || record.transform > 3
|
||||
|| !validCoordinate(record.x) || !validCoordinate(record.y)
|
||||
|| typeof record.primary !== "boolean")
|
||||
return false;
|
||||
|
||||
const size = logicalSize(record);
|
||||
if (!Number.isFinite(size.width) || size.width <= 0
|
||||
|| !Number.isFinite(size.height) || size.height <= 0)
|
||||
return false;
|
||||
if (record.primary)
|
||||
primaryCount += 1;
|
||||
}
|
||||
return primaryCount === 1;
|
||||
}
|
||||
|
||||
function cloneLayout(layout) {
|
||||
return (layout || []).map(record => Object.assign({}, record));
|
||||
}
|
||||
|
||||
function normalize(layout) {
|
||||
const result = cloneLayout(layout);
|
||||
const primary = result.find(record => record.primary === true);
|
||||
if (!primary)
|
||||
return result;
|
||||
const anchorX = primary.x;
|
||||
const anchorY = primary.y;
|
||||
for (const record of result) {
|
||||
record.x -= anchorX;
|
||||
record.y -= anchorY;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function bounds(layout) {
|
||||
if (!Array.isArray(layout) || layout.length === 0)
|
||||
return { x: 0, y: 0, width: 0, height: 0 };
|
||||
|
||||
let left = Infinity;
|
||||
let top = Infinity;
|
||||
let right = -Infinity;
|
||||
let bottom = -Infinity;
|
||||
for (const record of layout) {
|
||||
const size = logicalSize(record);
|
||||
left = Math.min(left, record.x);
|
||||
top = Math.min(top, record.y);
|
||||
right = Math.max(right, record.x + size.width);
|
||||
bottom = Math.max(bottom, record.y + size.height);
|
||||
}
|
||||
return { x: left, y: top, width: right - left, height: bottom - top };
|
||||
}
|
||||
|
||||
function snap(layout, movingName, threshold) {
|
||||
const result = cloneLayout(layout);
|
||||
const moving = result.find(record => record.name === movingName);
|
||||
if (!moving)
|
||||
return result;
|
||||
|
||||
const limit = Number.isFinite(threshold) && threshold >= 0 ? threshold : 16;
|
||||
const movingSize = logicalSize(moving);
|
||||
const movingXEdges = [moving.x, moving.x + movingSize.width];
|
||||
const movingYEdges = [moving.y, moving.y + movingSize.height];
|
||||
const stationary = result
|
||||
.filter(record => record.name !== movingName)
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
|
||||
let bestX = null;
|
||||
let bestY = null;
|
||||
for (const record of stationary) {
|
||||
const size = logicalSize(record);
|
||||
const xEdges = [record.x, record.x + size.width];
|
||||
const yEdges = [record.y, record.y + size.height];
|
||||
for (const source of movingXEdges) {
|
||||
for (const target of xEdges) {
|
||||
const delta = target - source;
|
||||
const distance = Math.abs(delta);
|
||||
if (distance <= limit && (bestX === null || distance < bestX.distance))
|
||||
bestX = { delta, distance };
|
||||
}
|
||||
}
|
||||
for (const source of movingYEdges) {
|
||||
for (const target of yEdges) {
|
||||
const delta = target - source;
|
||||
const distance = Math.abs(delta);
|
||||
if (distance <= limit && (bestY === null || distance < bestY.distance))
|
||||
bestY = { delta, distance };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestX !== null)
|
||||
moving.x += bestX.delta;
|
||||
if (bestY !== null)
|
||||
moving.y += bestY.delta;
|
||||
return result;
|
||||
}
|
||||
|
||||
function canvasRects(layout, canvasWidth, canvasHeight, padding) {
|
||||
const desktopBounds = bounds(layout);
|
||||
const inset = Math.max(0, Number(padding) || 0);
|
||||
const availableWidth = Math.max(0, Number(canvasWidth) - inset * 2);
|
||||
const availableHeight = Math.max(0, Number(canvasHeight) - inset * 2);
|
||||
const scale = desktopBounds.width > 0 && desktopBounds.height > 0
|
||||
? Math.min(availableWidth / desktopBounds.width,
|
||||
availableHeight / desktopBounds.height)
|
||||
: 0;
|
||||
const contentWidth = desktopBounds.width * scale;
|
||||
const contentHeight = desktopBounds.height * scale;
|
||||
const originX = inset + (availableWidth - contentWidth) / 2;
|
||||
const originY = inset + (availableHeight - contentHeight) / 2;
|
||||
return {
|
||||
bounds: desktopBounds,
|
||||
scale,
|
||||
rects: (layout || []).map(record => {
|
||||
const size = logicalSize(record);
|
||||
return {
|
||||
name: record.name,
|
||||
x: originX + (record.x - desktopBounds.x) * scale,
|
||||
y: originY + (record.y - desktopBounds.y) * scale,
|
||||
width: size.width * scale,
|
||||
height: size.height * scale,
|
||||
primary: record.primary === true
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user