CuttingBoard/src/store.js

714 lines
16 KiB
JavaScript

import { createStore } from 'vuex';
import { units } from './lib/units';
import { UnitsEnum, DirectionEnum } from './lib/enums';
import { serialize, deserialize } from '@ygoe/msgpack';
function DefaultSettings()
{
return {
units: UnitsEnum.cm,
borders: false,
bladeKerf: 0.35,
crosscutWidth: 3,
direction: DirectionEnum.alternate,
highlightBoard: true,
highlightLayer: true
};
}
function DefaultWood()
{
return [
{ name: 'Walnut', color: '#58443f' },
{ name: 'Maple', color: '#f2e0aa' },
{ name: 'Cherry', color: '#bb8359' },
{ name: 'Mahogany', color: '#98473f' },
{ name: 'Yellowheart', color: '#ffff84' },
{ name: 'White oak', color: '#fdf4b9' },
{ name: 'Bubinga', color: '#7e3c34' },
{ name: 'Jatoba', color: '#9b281c' },
{ name: 'Padouk', color: '#933350' }
];
}
function DefaultBoards()
{
return [
{
thickness: 2,
length: 70,
layers: [
{ wood: 8, width: 1 },
{ wood: 1, width: 1.5 },
{ wood: 8, width: 2 },
{ wood: 1, width: 2 },
{ wood: 8, width: 15 },
{ wood: 1, width: 2 },
{ wood: 8, width: 1.5 },
{ wood: 1, width: 1 }
]
}
];
}
function DefaultEndGrain()
{
const settings = DefaultSettings();
const boards = DefaultBoards();
const result = [];
updateEndGrain(result, settings, boards);
return result;
}
export default createStore({
state: {
// These are not stored persistently
volatile: {
highlightedBoard: null,
highlightedLayer: null
},
// When adding any persistent settings, remember to update the
// serializeState and deserializeState functions below
settings: DefaultSettings(),
wood: DefaultWood(),
boards: DefaultBoards(),
endGrain: DefaultEndGrain()
},
mutations: {
addLayer(state, board)
{
if (board < 0 || board >= state.boards.length)
return;
state.boards[board].layers.push({
wood: 0,
width: units.fromMillimeters(20, state.settings.units)
});
},
removeLayer(state, payload)
{
if (payload.board < 0 || payload.board >= state.boards.length)
return;
const board = state.boards[payload.board];
if (payload.layer < 0 || payload.layer >= board.layers.length)
return;
board.layers.splice(payload.layer, 1);
},
moveLayer(state, payload)
{
if (payload.board < 0 || payload.board >= state.boards.length)
return;
const board = state.boards[payload.board];
moveArrayItem(board.layers, payload.from, payload.to);
},
addWood(state)
{
state.wood.push({
name: 'Wood #' + (state.wood.length + 1),
color: '#f2e0aa'
});
},
removeWood(state, index)
{
if (index < 0 || index >= state.wood.length)
return;
// Update all layers
state.boards.forEach(board =>
{
board.layers.forEach(layer =>
{
if (layer.wood === index)
layer.wood = -1
else if (layer.wood > index)
layer.wood--;
});
});
state.wood.splice(index, 1);
},
updateVolatile(state, payload)
{
mergeObject(payload, state.volatile);
},
updateSettings(state, payload)
{
const oldUnits = state.settings.units;
mergeObject(payload, state.settings);
if (oldUnits !== state.settings.units)
{
// Convert the settings
state.settings.bladeKerf = units.limitDecimals(units.convert(state.settings.bladeKerf, oldUnits, state.settings.units), 3);
state.settings.crosscutWidth = units.limitDecimals(units.convert(state.settings.crosscutWidth, oldUnits, state.settings.units), 3);
// Convert the boards
state.boards.forEach(board =>
{
board.thickness = units.limitDecimals(units.convert(board.thickness, oldUnits, state.settings.units), 3);
board.length = units.limitDecimals(units.convert(board.length, oldUnits, state.settings.units), 3);
board.layers.forEach(layer =>
{
layer.width = units.limitDecimals(units.convert(layer.width, oldUnits, state.settings.units), 3);
});
});
}
},
addBoard(state, copyFrom)
{
if (copyFrom < 0 || copyFrom >= state.boards.length)
{
state.boards.push({
thickness: 2,
length: 70,
layers: []
});
return;
}
const source = state.boards[copyFrom];
state.boards.push({
thickness: source.thickness,
length: source.length,
layers: source.layers.map(layer =>
{
return {
wood: layer.wood,
width: layer.width
};
})
});
updateEndGrain(state.endGrain, state.settings, state.boards);
},
removeBoard(state, index)
{
if (index < 0 || index >= state.boards.length)
return;
state.boards.splice(index, 1);
// Update all references to the boards coming after
state.endGrain.forEach(layer =>
{
if (layer.board == index)
layer.board = -1;
else if (layer.board > index)
layer.board--;
});
updateEndGrain(state.endGrain, state.settings, state.boards);
},
updateBoard(state, payload)
{
if (payload.board < 0 || payload.board >= state.boards.length)
return;
const board = state.boards[payload.board];
const oldLength = board.length;
mergeObject(payload.values, board);
if (oldLength !== board.length)
updateEndGrain(state.endGrain, state.settings, state.boards);
},
moveEndgrain(state, payload)
{
moveArrayItem(state.endGrain, payload.from, payload.to);
},
// TODO some updates, like Wood properties and Layer width, don't go through the store yet. this works,
// but is kinda defeating the rules of Vuex.
load(state, payload)
{
const parsedPayload = JSON.parse(payload);
loadObject(state, parsedPayload);
},
loadMsgPack(state, payload)
{
try
{
const parsedPayload = deserialize(payload);
if (parsedPayload)
loadObject(state, parsedPayload);
}
catch(e)
{
console.error(e);
}
}
},
getters: {
save(state)
{
const serialized = serializeState(state, false);
return JSON.stringify(serialized);
},
saveMsgPack(state)
{
const serialized = serializeState(state, true);
return serialize(serialized);
}
}
});
function moveArrayItem(array, from, to)
{
if (from < 0 || from >= array.length)
return;
if (to < 0 || to > array.length)
return;
if (to == array.length)
{
// Move to end
array.push(array[from]);
array.splice(from, 1);
}
else
{
const item = array[from];
array.splice(from, 1);
if (to > from)
to--;
array.splice(to, 0, item);
}
}
function stripsPerBoard(settings, board)
{
const stripAndKerf = settings.crosscutWidth + settings.bladeKerf;
if (stripAndKerf === 0)
return 0;
let stripsPerBoard = (board.length + settings.bladeKerf) / stripAndKerf;
// Try to account for rounding errors
stripsPerBoard = units.limitDecimals(stripsPerBoard, 3);
return Math.floor(stripsPerBoard);
}
function updateEndGrain(result, settings, boards)
{
const boardTally = [];
for (let i = 0; i < boards.length; i++)
boardTally[i] = boards[i].length + settings.bladeKerf;
const cutWidth = settings.crosscutWidth + settings.bladeKerf;
const remove = [];
// Check the current configuration and see if we still have enough
result.forEach((layer, index) =>
{
let boardAvailable = false;
if (layer.board >= 0 && layer.board < boards.length)
{
boardAvailable = boardTally[layer.board] >= cutWidth;
if (boardAvailable)
boardTally[layer.board] -= cutWidth;
}
if (!boardAvailable)
remove.push(index);
});
for (let i = remove.length - 1; i >= 0; i--)
result.splice(remove[i], 1);
// If we have sufficient board length left, add the layers for it
boardTally.forEach((remaining, index) =>
{
while (remaining >= cutWidth)
{
result.push({ board: index, reversed: false });
remaining -= cutWidth;
// Try to account for rounding errors
remaining = units.limitDecimals(remaining, 3);
}
});
}
function mergeObject(source, target)
{
for (const property in source)
{
if (!source.hasOwnProperty(property) || !target.hasOwnProperty(property))
continue;
target[property] = source[property];
}
}
function parseFloatDef(value)
{
const parsedValue = parseFloat(value);
return isNaN(parsedValue) ? 0 : parsedValue;
}
function parseIntDef(value)
{
const parsedValue = parseInt(value);
return isNaN(parsedValue) ? 0 : parsedValue;
}
function loadObject(state, parsedPayload)
{
const deserialized = deserializeState(parsedPayload);
mergeObject(deserialized.settings, state.settings);
state.wood = deserialized.wood;
state.boards = deserialized.boards;
state.endGrain = deserialized.endGrain;
updateEndGrain(state.endGrain, state.settings, state.boards);
}
// For the MessagePack version that is used in the URL, we minimize the
// property names as well to significantly reduce the size. Do not do
// this for the downloaded versions to make them more readable.
// Note that this map is also used to convert older MsgPack encoded payloads,
// as they used the same layout as the JSON data.
const SettingsNameMapJSON = {
settings: {
self: 'settings',
units: 'units',
borders: 'borders',
bladeKerf: 'bladeKerf',
crosscutWidth: 'crosscutWidth',
direction: 'direction',
highlightBoard: 'highlightBoard',
highlightLayer: 'highlightLayer'
},
wood: {
self: 'wood',
name: 'name',
color: 'color'
},
boards: {
self: 'boards',
thickness: 'thickness',
length: 'length',
layers: {
self: 'layers',
wood: 'wood',
width: 'width'
}
},
endGrain: {
self: 'endGrain',
board: 'board',
reversed: 'reversed'
}
};
const SettingsNameMapMsgPack = {
settings: {
self: 's',
units: 'u',
borders: 'b',
bladeKerf: 'k',
crosscutWidth: 'c',
direction: 'd',
highlightBoard: 'h',
highlightLayer: 'l'
},
wood: {
self: 'w',
name: 'n',
color: 'c'
},
boards: {
self: 'b',
thickness: 't',
length: 'x',
layers: {
self: 'l',
wood: 'w',
width: 'x'
}
},
endGrain: {
self: 'e',
board: 'b',
reversed: 'r'
}
};
// All external input and output is done through these functions to provide backwards
// compatibility and perform sanity checking. This means the rest of the application
// trust that the contents of the store is as expected.
function serializeState(state, messagePack)
{
const map = messagePack ? SettingsNameMapMsgPack : SettingsNameMapJSON;
const result = {};
result[map.settings.self] = serializeSettings(state.settings, map);
result[map.wood.self] = serializeWood(state.wood, map);
result[map.boards.self] = serializeBoards(state.boards, map);
result[map.endGrain.self] = serializeEndgrain(state.endGrain, map);
return result;
}
function serializeSettings(settings, map)
{
const result = {};
result[map.settings.units] = settings.units;
result[map.settings.borders] = settings.borders;
result[map.settings.bladeKerf] = settings.bladeKerf;
result[map.settings.crosscutWidth] = settings.crosscutWidth;
result[map.settings.direction] = settings.direction;
result[map.settings.highlightBoard] = settings.highlightBoard;
result[map.settings.highlightLayer] = settings.highlightLayer;
return result;
}
function serializeWood(wood, map)
{
if (wood.length === 0)
return [];
return wood.map(item =>
{
const result = {};
result[map.wood.name] = item.name;
result[map.wood.color] = item.color;
return result;
});
}
function serializeBoards(boards, map)
{
return boards.map(board =>
{
const boardResult = {};
boardResult[map.boards.length] = board.length;
boardResult[map.boards.thickness] = board.thickness;
boardResult[map.boards.layers.self] = board.layers.map(layer =>
{
const layerResult = {};
layerResult[map.boards.layers.wood] = layer.wood;
layerResult[map.boards.layers.width] = layer.width;
return layerResult;
});
return boardResult;
});
}
function serializeEndgrain(endGrain, map)
{
return endGrain.map(layer =>
{
const layerResult = {};
layerResult[map.endGrain.board] = layer.board;
layerResult[map.endGrain.reversed] = layer.reversed;
return layerResult;
});
}
function deserializeState(parsedPayload)
{
const map = parsedPayload.hasOwnProperty(SettingsNameMapMsgPack.settings.self) ? SettingsNameMapMsgPack : SettingsNameMapJSON;
const result = {
settings: deserializeSettings(parsedPayload, map),
wood: deserializeWood(parsedPayload, map),
boards: deserializeBoards(parsedPayload, map),
endGrain: deserializeEndgrain(parsedPayload, map)
};
// Backwards compatibility
if (parsedPayload.hasOwnProperty('settings'))
{
if (parsedPayload.settings.hasOwnProperty('alternateDirection'))
result.settings.direction = parsedPayload.settings.alternateDirection ? DirectionEnum.alternate : DirectionEnum.uniform;
if (parsedPayload.settings.hasOwnProperty('boardLength'))
result.boards[0].length = parseFloatDef(parsedPayload.settings.boardLength);
if (parsedPayload.settings.hasOwnProperty('boardThickness'))
result.boards[0].thickness = parseFloatDef(parsedPayload.settings.boardThickness);
}
if (parsedPayload.hasOwnProperty(map.settings.self))
{
if (!parsedPayload[map.settings.self].hasOwnProperty(map.settings.highlightBoard))
result.settings.highlightBoard = true;
if (!parsedPayload[map.settings.self].hasOwnProperty(map.settings.highlightLayer))
result.settings.highlightLayer = true;
}
if (result.endGrain.length === 0)
updateEndGrain(result.endGrain, result.settings, result.boards);
return result;
}
function deserializeSettings(parsedPayload, map)
{
if (!parsedPayload.hasOwnProperty(map.settings.self))
return DefaultSettings();
const settings = parsedPayload[map.settings.self];
return {
units: UnitsEnum.isValid(settings[map.settings.units]) ? settings[map.settings.units] : UnitsEnum.cm,
borders: settings[map.settings.borders] === true,
bladeKerf: parseFloatDef(settings[map.settings.bladeKerf]),
crosscutWidth: parseFloatDef(settings[map.settings.crosscutWidth]),
direction: DirectionEnum.isValid(settings[map.settings.direction]) ? settings[map.settings.direction] : DirectionEnum.uniform,
highlightBoard: settings[map.settings.highlightBoard] === true,
highlightLayer: settings[map.settings.highlightLayer] === true
};
}
function deserializeWood(parsedPayload, map)
{
if (!parsedPayload.hasOwnProperty(map.wood.self) || !Array.isArray(parsedPayload[map.wood.self]))
return [];
return parsedPayload[map.wood.self].map(item =>
{
return {
name: item[map.wood.name],
color: /^#[0-9A-F]{6}$/i.test(item[map.wood.color] || '') ? item[map.wood.color] : '#000000'
};
});
}
function deserializeBoards(parsedPayload, map)
{
if (!parsedPayload.hasOwnProperty(map.boards.self) || !Array.isArray(parsedPayload[map.boards.self]))
return [];
const result = parsedPayload[map.boards.self].map(board =>
{
const boardResult = {
thickness: parseFloatDef(board[map.boards.thickness]),
length: parseFloatDef(board[map.boards.length]),
layers: []
};
if (board.hasOwnProperty(map.boards.layers.self) && Array.isArray(board[map.boards.layers.self]))
{
boardResult.layers = board[map.boards.layers.self].map(layer =>
{
return {
wood: parseIntDef(layer[map.boards.layers.wood]),
width: parseFloatDef(layer[map.boards.layers.width])
}
});
}
return boardResult;
});
return result.length > 0
? result
: { length: 0, thickness: 0, layers: [] };
}
function deserializeEndgrain(parsedPayload, map)
{
if (!parsedPayload.hasOwnProperty(map.endGrain.self) || !Array.isArray(parsedPayload[map.endGrain.self]))
return [];
return parsedPayload[map.endGrain.self].map(layer =>
{
return {
board: parseIntDef(layer[map.endGrain.board]),
reversed: layer[map.endGrain.reversed] === true
};
});
}