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 }; }); }