Spaces:
Running
Running
import log from '../lib/log'; | |
const ADD_MONITOR_RECT = 'scratch-gui/monitors/ADD_MONITOR_RECT'; | |
const MOVE_MONITOR_RECT = 'scratch-gui/monitors/MOVE_MONITOR_RECT'; | |
const RESIZE_MONITOR_RECT = 'scratch-gui/monitors/RESIZE_MONITOR_RECT'; | |
const REMOVE_MONITOR_RECT = 'scratch-gui/monitors/REMOVE_MONITOR_RECT'; | |
const RESET_MONITOR_LAYOUT = 'scratch-gui/monitors/RESET_MONITOR_LAYOUT'; | |
const initialState = { | |
monitors: {}, | |
savedMonitorPositions: {} | |
}; | |
// Verify that the rectangle formed by the 2 points is well-formed | |
const _verifyRect = function (upperStart, lowerEnd) { | |
if (isNaN(upperStart.x) || isNaN(upperStart.y) || isNaN(lowerEnd.x) || isNaN(lowerEnd.y)) { | |
return false; | |
} | |
if (!(upperStart.x < lowerEnd.x)) { | |
return false; | |
} | |
if (!(upperStart.y < lowerEnd.y)) { | |
return false; | |
} | |
return true; | |
}; | |
const _addMonitorRect = function (state, action) { | |
if (state.monitors.hasOwnProperty(action.monitorId)) { | |
log.error(`Can't add monitor, monitor with id ${action.monitorId} already exists.`); | |
return state; | |
} | |
if (!_verifyRect(action.upperStart, action.lowerEnd)) { | |
log.error(`Monitor rectangle not formatted correctly`); | |
return state; | |
} | |
return { | |
monitors: Object.assign({}, state.monitors, { | |
[action.monitorId]: { | |
upperStart: action.upperStart, | |
lowerEnd: action.lowerEnd | |
} | |
}), | |
savedMonitorPositions: action.savePosition ? | |
Object.assign({}, state.savedMonitorPositions, { | |
[action.monitorId]: {x: action.upperStart.x, y: action.upperStart.y} | |
}) : | |
state.savedMonitorPositions | |
}; | |
}; | |
const _moveMonitorRect = function (state, action) { | |
if (!state.monitors.hasOwnProperty(action.monitorId)) { | |
log.error(`Can't move monitor, monitor with id ${action.monitorId} does not exist.`); | |
return state; | |
} | |
if (isNaN(action.newX) || isNaN(action.newY)) { | |
log.error(`Monitor rectangle not formatted correctly`); | |
return state; | |
} | |
const oldMonitor = state.monitors[action.monitorId]; | |
if (oldMonitor.upperStart.x === action.newX && | |
oldMonitor.upperStart.y === action.newY) { | |
// Hasn't moved | |
return state; | |
} | |
const monitorWidth = oldMonitor.lowerEnd.x - oldMonitor.upperStart.x; | |
const monitorHeight = oldMonitor.lowerEnd.y - oldMonitor.upperStart.y; | |
return { | |
monitors: Object.assign({}, state.monitors, { | |
[action.monitorId]: { | |
upperStart: {x: action.newX, y: action.newY}, | |
lowerEnd: {x: action.newX + monitorWidth, y: action.newY + monitorHeight} | |
} | |
}), | |
// User generated position is saved | |
savedMonitorPositions: Object.assign({}, state.savedMonitorPositions, { | |
[action.monitorId]: {x: action.newX, y: action.newY} | |
}) | |
}; | |
}; | |
const _resizeMonitorRect = function (state, action) { | |
if (!state.monitors.hasOwnProperty(action.monitorId)) { | |
log.error(`Can't resize monitor, monitor with id ${action.monitorId} does not exist.`); | |
return state; | |
} | |
if (isNaN(action.newWidth) || isNaN(action.newHeight) || | |
action.newWidth <= 0 || action.newHeight <= 0) { | |
log.error(`Monitor rectangle not formatted correctly`); | |
return state; | |
} | |
const oldMonitor = state.monitors[action.monitorId]; | |
const newMonitor = { | |
upperStart: oldMonitor.upperStart, | |
lowerEnd: { | |
x: oldMonitor.upperStart.x + action.newWidth, | |
y: oldMonitor.upperStart.y + action.newHeight | |
} | |
}; | |
if (newMonitor.lowerEnd.x === oldMonitor.lowerEnd.x && | |
newMonitor.lowerEnd.y === oldMonitor.lowerEnd.y) { | |
// no change | |
return state; | |
} | |
return { | |
monitors: Object.assign({}, state.monitors, {[action.monitorId]: newMonitor}), | |
savedMonitorPositions: state.savedMonitorPositions | |
}; | |
}; | |
const _removeMonitorRect = function (state, action) { | |
if (!state.monitors.hasOwnProperty(action.monitorId)) { | |
log.error(`Can't remove monitor, monitor with id ${action.monitorId} does not exist.`); | |
return state; | |
} | |
const newMonitors = Object.assign({}, state.monitors); | |
delete newMonitors[action.monitorId]; | |
return { | |
monitors: newMonitors, | |
savedMonitorPositions: state.savedMonitorPositions | |
}; | |
}; | |
const reducer = function (state, action) { | |
if (typeof state === 'undefined') state = initialState; | |
switch (action.type) { | |
case ADD_MONITOR_RECT: | |
return _addMonitorRect(state, action); | |
case MOVE_MONITOR_RECT: | |
return _moveMonitorRect(state, action); | |
case RESIZE_MONITOR_RECT: | |
return _resizeMonitorRect(state, action); | |
case REMOVE_MONITOR_RECT: | |
return _removeMonitorRect(state, action); | |
case RESET_MONITOR_LAYOUT: | |
return initialState; | |
default: | |
return state; | |
} | |
}; | |
// Init position -------------------------- | |
const PADDING = 5; | |
// @todo fix these numbers when we fix https://github.com/LLK/scratch-gui/issues/980 | |
const SCREEN_WIDTH = 400; | |
const SCREEN_HEIGHT = 300; | |
const SCREEN_EDGE_BUFFER = 40; | |
const _rectsIntersect = function (rect1, rect2) { | |
// If one rectangle is on left side of other | |
if (rect1.upperStart.x >= rect2.lowerEnd.x || rect2.upperStart.x >= rect1.lowerEnd.x) return false; | |
// If one rectangle is above other | |
if (rect1.upperStart.y >= rect2.lowerEnd.y || rect2.upperStart.y >= rect1.lowerEnd.y) return false; | |
return true; | |
}; | |
// We need to place a monitor with the given width and height. Return a rect defining where it should be placed. | |
const getInitialPosition = function (state, monitorId, eltWidth, eltHeight) { | |
// If this monitor was purposefully moved to a certain position before, put it back in that position | |
if (state.savedMonitorPositions.hasOwnProperty(monitorId)) { | |
const saved = state.savedMonitorPositions[monitorId]; | |
return { | |
upperStart: saved, | |
lowerEnd: {x: saved.x + eltWidth, y: saved.y + eltHeight} | |
}; | |
} | |
// Try all starting positions for the new monitor to find one that doesn't intersect others | |
const endXs = [0]; | |
const endYs = [0]; | |
let lastX = null; | |
let lastY = null; | |
for (const monitor in state.monitors) { | |
let x = state.monitors[monitor].lowerEnd.x; | |
x = Math.ceil(x / 50) * 50; // Try to choose a sensible "tab width" so more monitors line up | |
endXs.push(x); | |
endYs.push(Math.ceil(state.monitors[monitor].lowerEnd.y)); | |
} | |
endXs.sort((a, b) => a - b); | |
endYs.sort((a, b) => a - b); | |
// We'll use plan B if the monitor doesn't fit anywhere (too long or tall) | |
let planB = null; | |
for (const x of endXs) { | |
if (x === lastX) { | |
continue; | |
} | |
lastX = x; | |
outer: | |
for (const y of endYs) { | |
if (y === lastY) { | |
continue; | |
} | |
lastY = y; | |
const monitorRect = { | |
upperStart: {x: x + PADDING, y: y + PADDING}, | |
lowerEnd: {x: x + PADDING + eltWidth, y: y + PADDING + eltHeight} | |
}; | |
// Intersection testing rect that includes padding | |
const rect = { | |
upperStart: {x, y}, | |
lowerEnd: {x: x + eltWidth + (2 * PADDING), y: y + eltHeight + (2 * PADDING)} | |
}; | |
for (const monitor in state.monitors) { | |
if (_rectsIntersect(state.monitors[monitor], rect)) { | |
continue outer; | |
} | |
} | |
// If the rect overlaps the ends of the screen | |
if (rect.lowerEnd.x > SCREEN_WIDTH || rect.lowerEnd.y > SCREEN_HEIGHT) { | |
// If rect is not too close to completely off screen, set it as plan B | |
if (!planB && | |
!(rect.upperStart.x + SCREEN_EDGE_BUFFER > SCREEN_WIDTH || | |
rect.upperStart.y + SCREEN_EDGE_BUFFER > SCREEN_HEIGHT)) { | |
planB = monitorRect; | |
} | |
continue; | |
} | |
return monitorRect; | |
} | |
} | |
// If the monitor is too long to fit anywhere, put it in the leftmost spot available | |
// that intersects the right or bottom edge and isn't too close to the edge. | |
if (planB) { | |
return planB; | |
} | |
// If plan B fails and there's nowhere reasonable to put it, plan C is to place the monitor randomly | |
const randX = Math.ceil(Math.random() * (SCREEN_WIDTH / 2)); | |
const randY = Math.ceil(Math.random() * (SCREEN_HEIGHT - SCREEN_EDGE_BUFFER)); | |
return { | |
upperStart: { | |
x: randX, | |
y: randY | |
}, | |
lowerEnd: { | |
x: randX + eltWidth, | |
y: randY + eltHeight | |
} | |
}; | |
}; | |
// Action creators ------------------------ | |
/** | |
* @param {!string} monitorId Id to add | |
* @param {!object} upperStart upper point defining the rectangle | |
* @param {!number} upperStart.x X of top point that defines the monitor location | |
* @param {!number} upperStart.y Y of top point that defines the monitor location | |
* @param {!object} lowerEnd lower point defining the rectangle | |
* @param {!number} lowerEnd.x X of bottom point that defines the monitor location | |
* @param {!number} lowerEnd.y Y of bottom point that defines the monitor location | |
* @param {?boolean} savePosition True if the placement should be saved when adding the monitor | |
* @returns {object} action to add a new monitor at the location | |
*/ | |
const addMonitorRect = function (monitorId, upperStart, lowerEnd, savePosition) { | |
return { | |
type: ADD_MONITOR_RECT, | |
monitorId: monitorId, | |
upperStart: upperStart, | |
lowerEnd: lowerEnd, | |
savePosition: savePosition | |
}; | |
}; | |
/** | |
* @param {!string} monitorId Id for monitor to move | |
* @param {!number} newX X of top point that defines the monitor location | |
* @param {!number} newY Y of top point that defines the monitor location | |
* @returns {object} action to move an existing monitor to the location | |
*/ | |
const moveMonitorRect = function (monitorId, newX, newY) { | |
return { | |
type: MOVE_MONITOR_RECT, | |
monitorId: monitorId, | |
newX: newX, | |
newY: newY | |
}; | |
}; | |
/** | |
* @param {!string} monitorId Id for monitor to resize | |
* @param {!number} newWidth Width to set monitor to | |
* @param {!number} newHeight Height to set monitor to | |
* @returns {object} action to resize an existing monitor to the given dimensions | |
*/ | |
const resizeMonitorRect = function (monitorId, newWidth, newHeight) { | |
return { | |
type: RESIZE_MONITOR_RECT, | |
monitorId: monitorId, | |
newWidth: newWidth, | |
newHeight: newHeight | |
}; | |
}; | |
/** | |
* @param {!string} monitorId Id for monitor to remove | |
* @returns {object} action to remove an existing monitor | |
*/ | |
const removeMonitorRect = function (monitorId) { | |
return { | |
type: REMOVE_MONITOR_RECT, | |
monitorId: monitorId | |
}; | |
}; | |
const resetMonitorLayout = function () { | |
return { | |
type: RESET_MONITOR_LAYOUT | |
}; | |
}; | |
export { | |
reducer as default, | |
initialState as monitorLayoutInitialState, | |
addMonitorRect, | |
getInitialPosition, | |
moveMonitorRect, | |
resizeMonitorRect, | |
removeMonitorRect, | |
resetMonitorLayout, | |
PADDING, | |
SCREEN_HEIGHT, | |
SCREEN_WIDTH | |
}; | |