Added support for multiple boards in a single end grain result
...and probably loads of little things changed in the process
This commit is contained in:
parent
f65a7f37e3
commit
fe4d17c7f2
5
TODO.md
5
TODO.md
@ -3,6 +3,8 @@ ToDo
|
||||
|
||||
Should have
|
||||
----
|
||||
- Highlight strips for the current board, highlight layer for the focused/hovered layer
|
||||
- Show list of end grain board numbers and reversed in a "Glue list" for printing
|
||||
- Support for fractional inches (see, not all europeans look down on freedom units!)
|
||||
|
||||
Nice to have
|
||||
@ -10,5 +12,6 @@ Nice to have
|
||||
- Show remaining material
|
||||
- Theme selection for the preview background
|
||||
- Render width and height of the boards in the previews (simplified version implemented, moved to Nice to have)
|
||||
- More advanced options, like custom direction per strip and mixing multiple edge grain boards with different layers for the end grain board (the code is half prepared for this by having the boards array encapsulating the layers, though it's all hardcoded to board[0] now)
|
||||
- Support for mixing multiple boards in the end grain version
|
||||
- 3D effect for previews emulating thickness / crosscut width
|
||||
- Actual wood textures
|
31
src/App.vue
31
src/App.vue
@ -27,14 +27,14 @@
|
||||
Use your browser's built-in print functionality (for example, Ctrl+P on Windows) or click the button below to get a printable version of your board and cutting list.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<input type="checkbox" v-model="printEdgeGrain" id="printEdgeGrain" />
|
||||
<label for="printEdgeGrain"> Edge grain preview</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" v-model="printEndGrain" id="printEndGrain" />
|
||||
<label for="printEndGrain"> End grain preview</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" v-model="printEdgeGrain" id="printEdgeGrain" />
|
||||
<label for="printEdgeGrain"> Edge grain preview</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" v-model="printCuttingList" id="printCuttingList" />
|
||||
<label for="printCuttingList"> Cutting list and bill of materials</label>
|
||||
@ -72,16 +72,19 @@
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div :class="{ hideOnPrint: !printEdgeGrain }">
|
||||
<h1>Edge grain</h1>
|
||||
<EdgeGrainPreview :scale="1" />
|
||||
</div>
|
||||
|
||||
<div :class="{ hideOnPrint: !printEndGrain }">
|
||||
<h1>End grain</h1>
|
||||
<EndGrainPreview :scale="1" />
|
||||
</div>
|
||||
|
||||
<div :class="{ hideOnPrint: !printEdgeGrain }">
|
||||
<h1>Edge grain</h1>
|
||||
<template v-for="(board, boardIndex) in boards">
|
||||
<h2 v-if="boards.length > 1">Board {{ boardIndex + 1}}</h2>
|
||||
<EdgeGrainPreview :board="board" :scale="1" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div :class="{ hideOnPrint: !printCuttingList }">
|
||||
<h1>Cutting list</h1>
|
||||
<CuttingList />
|
||||
@ -153,6 +156,8 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
boards() { return this.$store.state.boards; },
|
||||
|
||||
hash()
|
||||
{
|
||||
return bytesToBase64(this.$store.getters.saveMsgPack);
|
||||
@ -234,6 +239,12 @@ html, body
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
h2
|
||||
{
|
||||
color: #808080;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
a
|
||||
{
|
||||
color: #99ccff;
|
||||
@ -266,7 +277,7 @@ button
|
||||
padding-top: .3em;
|
||||
padding-bottom: .3em;
|
||||
|
||||
&:hover
|
||||
&:hover:not([disabled])
|
||||
{
|
||||
background-color: #808080;
|
||||
}
|
||||
|
@ -5,27 +5,39 @@
|
||||
<th>Wood species</th>
|
||||
<th class="dimension">Width</th>
|
||||
</tr>
|
||||
<tr v-for="(layer, index) in layers">
|
||||
<td>{{ index + 1 }}</td>
|
||||
<td>{{ getLayerWood(index) }}</td>
|
||||
<td class="dimension">{{ getLayerWidth(index) }}</td>
|
||||
</tr>
|
||||
<template v-for="(board, boardIndex) in boards">
|
||||
<tr class="board" v-if="boards.length > 1">
|
||||
<td colspan="3">Board {{ boardIndex + 1 }}</td>
|
||||
</tr>
|
||||
|
||||
<tr v-for="(layer, index) in board.layers">
|
||||
<td>{{ index + 1 }}</td>
|
||||
<td>{{ getLayerWood(board, index) }}</td>
|
||||
<td class="dimension">{{ getLayerWidth(board, index) }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
|
||||
<h2>Bill of materials</h2>
|
||||
<table class="list">
|
||||
<tr>
|
||||
<th>Wood species</th>
|
||||
<th class="dimension">Thickness</th>
|
||||
<th class="dimension">Length</th>
|
||||
<th class="dimension">Width</th>
|
||||
<th class="dimension">Thickness</th>
|
||||
</tr>
|
||||
<tr v-for="stock in bom">
|
||||
<td>{{ stock.woodName }}</td>
|
||||
<td class="dimension">{{ display(settings.boardThickness) }}</td>
|
||||
<td class="dimension">{{ display(settings.boardLength) }}</td>
|
||||
<td class="dimension">{{ display(stock.width) }}</td>
|
||||
</tr>
|
||||
<template v-for="(board, boardIndex) in bom">
|
||||
<tr class="board" v-if="bom.length > 1">
|
||||
<td colspan="3">Board {{ boardIndex + 1 }}</td>
|
||||
</tr>
|
||||
|
||||
<tr v-for="stock in board">
|
||||
<td>{{ stock.woodName }}</td>
|
||||
<td class="dimension">{{ display(stock.length) }}</td>
|
||||
<td class="dimension">{{ display(stock.width) }}</td>
|
||||
<td class="dimension">{{ display(stock.thickness) }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
@ -35,59 +47,66 @@ import { units } from '../lib/units';
|
||||
export default {
|
||||
computed: {
|
||||
settings() { return this.$store.state.settings; },
|
||||
layers() { return this.$store.state.boards[0].layers; },
|
||||
boards() { return this.$store.state.boards; },
|
||||
wood() { return this.$store.state.wood; },
|
||||
|
||||
bom()
|
||||
{
|
||||
const woodTally = {};
|
||||
const self = this;
|
||||
|
||||
this.layers.forEach(layer =>
|
||||
return self.boards.map((board, boardIndex) =>
|
||||
{
|
||||
if (woodTally.hasOwnProperty(layer.wood))
|
||||
woodTally[layer.wood] += layer.width + this.settings.bladeKerf;
|
||||
else
|
||||
woodTally[layer.wood] = layer.width;
|
||||
});
|
||||
const bom = [];
|
||||
const woodTally = {};
|
||||
|
||||
const bom = [];
|
||||
|
||||
for (let wood in woodTally)
|
||||
{
|
||||
if (!woodTally.hasOwnProperty(wood))
|
||||
continue;
|
||||
|
||||
bom.push({
|
||||
woodName: wood !== null && wood >= 0 && wood < this.wood.length ? this.wood[wood].name : '',
|
||||
width: woodTally[wood]
|
||||
board.layers.forEach(layer =>
|
||||
{
|
||||
if (woodTally.hasOwnProperty(layer.wood))
|
||||
woodTally[layer.wood] += layer.width + self.settings.bladeKerf;
|
||||
else
|
||||
woodTally[layer.wood] = layer.width;
|
||||
});
|
||||
}
|
||||
|
||||
return bom;
|
||||
for (let wood in woodTally)
|
||||
{
|
||||
if (!woodTally.hasOwnProperty(wood))
|
||||
continue;
|
||||
|
||||
bom.push({
|
||||
board: boardIndex,
|
||||
woodName: wood >= 0 && wood < self.wood.length ? self.wood[wood].name : '',
|
||||
length: board.length,
|
||||
width: woodTally[wood],
|
||||
thickness: board.thickness
|
||||
});
|
||||
}
|
||||
|
||||
return bom;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
getLayerWood(index)
|
||||
getLayerWood(board, index)
|
||||
{
|
||||
if (index < 0 || index >= this.layers.length)
|
||||
if (index < 0 || index >= board.layers.length)
|
||||
return '';
|
||||
|
||||
const woodIndex = this.layers[index].wood;
|
||||
if (woodIndex === null || woodIndex < 0 || woodIndex >= this.wood.length)
|
||||
const woodIndex = board.layers[index].wood;
|
||||
if (woodIndex < 0 || woodIndex >= this.wood.length)
|
||||
return '';
|
||||
|
||||
return this.wood[woodIndex].name;
|
||||
},
|
||||
|
||||
|
||||
getLayerWidth(index)
|
||||
getLayerWidth(board, index)
|
||||
{
|
||||
if (index < 0 || index >= this.layers.length)
|
||||
if (index < 0 || index >= board.layers.length)
|
||||
return '';
|
||||
|
||||
return this.display(this.layers[index].width);
|
||||
return this.display(board.layers[index].width);
|
||||
},
|
||||
|
||||
|
||||
@ -123,7 +142,12 @@ h2
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
tr:nth-child(even) td
|
||||
tr.board td
|
||||
{
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
tr:nth-child(even):not(.board) td
|
||||
{
|
||||
background-color: #555555;
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="preview">
|
||||
<div class="dimensions">Dimensions: {{ display(boardWidth) }} x {{ display(boardHeight) }} x {{ display(settings.boardThickness) }}</div>
|
||||
<div class="dimensions">Dimensions: {{ display(boardWidth) }} x {{ display(boardHeight) }} x {{ display(board.thickness) }}</div>
|
||||
|
||||
<svg
|
||||
:width="viewportWidth"
|
||||
@ -9,7 +9,7 @@
|
||||
|
||||
<rect
|
||||
v-for="(layer, index) in layers"
|
||||
:width="toPixels(settings.boardLength)"
|
||||
:width="toPixels(board.length)"
|
||||
:height="toPixels(layer.width)"
|
||||
x="0"
|
||||
:y="getLayerOffset(index)"
|
||||
@ -23,16 +23,17 @@ import { units } from '../lib/units';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
scale: Number
|
||||
scale: Number,
|
||||
board: Object
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
settings() { return this.$store.state.settings; },
|
||||
wood() { return this.$store.state.wood; },
|
||||
layers() { return this.$store.state.boards[0].layers; },
|
||||
layers() { return this.board.layers; },
|
||||
|
||||
boardWidth() { return this.settings.boardLength; },
|
||||
boardWidth() { return this.board.length; },
|
||||
|
||||
boardHeight()
|
||||
{
|
||||
@ -41,7 +42,7 @@ export default {
|
||||
|
||||
return this.layers
|
||||
.map(currentValue => currentValue.width)
|
||||
.reduce((accumulator, currentValue) => accumulator + currentValue);
|
||||
.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
|
||||
},
|
||||
|
||||
boardPixelWidth()
|
||||
@ -90,7 +91,7 @@ export default {
|
||||
return 'fill: fuchsia';
|
||||
|
||||
const woodIndex = this.layers[index].wood;
|
||||
if (woodIndex === null)
|
||||
if (woodIndex < 0 || woodIndex >= this.wood.length)
|
||||
return '';
|
||||
|
||||
const borderStyle = this.settings.borders
|
||||
|
@ -2,28 +2,41 @@
|
||||
<div class="preview">
|
||||
<div class="dimensions">Dimensions: {{ display(boardWidth) }} x {{ display(boardHeight) }} x {{ display(settings.crosscutWidth) }}</div>
|
||||
|
||||
<div v-if="boards.length > 1" class="draghint hideOnPrint">Click and drag strips to reorder them. Click once to reverse the direction.</div>
|
||||
|
||||
<svg
|
||||
:width="viewportWidth"
|
||||
:height="viewportHeight"
|
||||
:viewBox="viewBox">
|
||||
:viewBox="viewBox"
|
||||
:class="{ dragging: dropTarget !== null }">
|
||||
<defs>
|
||||
<g id="strip">
|
||||
<g v-for="(board, boardIndex) in boards" :id="'strip' + boardIndex">
|
||||
<rect
|
||||
v-for="(layer, index) in layers"
|
||||
:width="toPixels(settings.boardThickness)"
|
||||
v-for="(layer, index) in board.layers"
|
||||
:width="toPixels(board.thickness)"
|
||||
:height="toPixels(layer.width)"
|
||||
x="0"
|
||||
:y="getLayerOffset(index)"
|
||||
:style="getLayerStyle(index)" />
|
||||
:y="getBoardLayerOffset(board, index)"
|
||||
:style="getBoardLayerStyle(board, index)" />
|
||||
</g>
|
||||
<g id="dropTarget">
|
||||
<line x1="0" y1="0" x2="0" :y2="boardPixelHeight" style="stroke: white; stroke-width: 2" />
|
||||
</g>
|
||||
</defs>
|
||||
|
||||
<use
|
||||
v-for="(strip, index) in stripsPerBoard"
|
||||
xlink:href="#strip"
|
||||
:x="toPixels(index * settings.boardThickness)"
|
||||
v-for="(layer, index) in endGrain"
|
||||
:ref="'strip' + index"
|
||||
:href="'#strip' + layer.board"
|
||||
:x="getLayerOffset(index)"
|
||||
y="0"
|
||||
:transform="getLayerTransform(index)" />
|
||||
:transform="getLayerTransform(index)"
|
||||
@mousedown.prevent="mouseDown(index, $event)" />
|
||||
|
||||
<use
|
||||
v-if="dropTarget !== null"
|
||||
href="#dropTarget"
|
||||
:x="getLayerOffset(dropTarget)" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@ -37,38 +50,39 @@ export default {
|
||||
},
|
||||
|
||||
|
||||
data()
|
||||
{
|
||||
return {
|
||||
dragIndex: null,
|
||||
dropTarget: null
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
settings() { return this.$store.state.settings; },
|
||||
boards() { return this.$store.state.boards; },
|
||||
wood() { return this.$store.state.wood; },
|
||||
layers() { return this.$store.state.boards[0].layers; },
|
||||
|
||||
stripsPerBoard()
|
||||
{
|
||||
const stripAndKerf = this.settings.crosscutWidth + this.settings.bladeKerf;
|
||||
if (stripAndKerf === 0)
|
||||
return 0;
|
||||
|
||||
let stripsPerBoard = (this.settings.boardLength + this.settings.bladeKerf) / stripAndKerf;
|
||||
|
||||
// Try to account for rounding errors
|
||||
stripsPerBoard = units.limitDecimals(stripsPerBoard, 3);
|
||||
|
||||
return Math.floor(stripsPerBoard);
|
||||
},
|
||||
endGrain() { return this.$store.state.endGrain },
|
||||
|
||||
boardWidth()
|
||||
{
|
||||
return this.stripsPerBoard * this.settings.boardThickness;
|
||||
const self = this;
|
||||
|
||||
return this.endGrain
|
||||
.map(layer => layer.board >= 0 && layer.board < self.boards.length ? self.boards[layer.board].thickness : 0)
|
||||
.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
|
||||
},
|
||||
|
||||
boardHeight()
|
||||
{
|
||||
if (this.layers.length == 0)
|
||||
return 0;
|
||||
|
||||
return this.layers
|
||||
.map(currentValue => currentValue.width)
|
||||
.reduce((accumulator, currentValue) => accumulator + currentValue);
|
||||
// Calculate the total width of each board (adding all the layers, inner map/reduce),
|
||||
// then use the maximum value (outer map/reduce)
|
||||
return this.boards
|
||||
.map(board => board.layers
|
||||
.map(layer => layer.width)
|
||||
.reduce((accumulator, currentValue) => accumulator + currentValue, 0))
|
||||
.reduce((accumulator, currentValue) => currentValue > accumulator ? currentValue : accumulator, 0);
|
||||
},
|
||||
|
||||
boardPixelWidth()
|
||||
@ -98,26 +112,26 @@ export default {
|
||||
return units.display(value, this.settings.units);
|
||||
},
|
||||
|
||||
getLayerOffset(index)
|
||||
getBoardLayerOffset(board, index)
|
||||
{
|
||||
if (index < 0 || index >= this.layers.length)
|
||||
if (index < 0 || index >= board.layers.length)
|
||||
return 0;
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (let i = 0; i < index; i++)
|
||||
offset += this.layers[i].width;
|
||||
offset += board.layers[i].width;
|
||||
|
||||
return this.toPixels(offset);
|
||||
},
|
||||
|
||||
getLayerStyle(index)
|
||||
getBoardLayerStyle(board, index)
|
||||
{
|
||||
if (index < 0 || index >= this.layers.length)
|
||||
if (index < 0 || index >= board.layers.length)
|
||||
return 'fill: fuchsia';
|
||||
|
||||
const woodIndex = this.layers[index].wood;
|
||||
if (woodIndex === null)
|
||||
const woodIndex = board.layers[index].wood;
|
||||
if (woodIndex < 0 || woodIndex >= this.wood.length)
|
||||
return '';
|
||||
|
||||
const borderStyle = this.settings.borders
|
||||
@ -127,12 +141,151 @@ export default {
|
||||
return 'fill: ' + this.wood[woodIndex].color + borderStyle;
|
||||
},
|
||||
|
||||
getLayerOffset(index)
|
||||
{
|
||||
if (index < 0 || index > this.endGrain.length)
|
||||
return 0;
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (let i = 0; i < index; i++)
|
||||
{
|
||||
const boardIndex = this.endGrain[i].board;
|
||||
if (boardIndex >= 0 && boardIndex < this.boards.length)
|
||||
offset += this.boards[boardIndex].thickness;
|
||||
}
|
||||
|
||||
return this.toPixels(offset);
|
||||
},
|
||||
|
||||
getLayerTransform(index)
|
||||
{
|
||||
if (!this.settings.alternateDirection || (index % 2) == 0)
|
||||
return '';
|
||||
let reversed = false;
|
||||
|
||||
return 'scale(1, -1) translate(0, -' + this.boardPixelHeight + ')';
|
||||
switch (this.settings.direction)
|
||||
{
|
||||
case 'alternate':
|
||||
reversed = (index % 2) == 0;
|
||||
break;
|
||||
|
||||
case 'custom':
|
||||
reversed = index >= 0 && index < this.endGrain.length && this.endGrain[index].reversed;
|
||||
break;
|
||||
}
|
||||
|
||||
return reversed ? 'scale(1, -1) translate(0, -' + this.boardPixelHeight + ')' : '';
|
||||
},
|
||||
|
||||
reverseLayer(index)
|
||||
{
|
||||
if (this.settings.direction !== 'custom')
|
||||
return;
|
||||
|
||||
if (index < 0 || index >= this.endGrain.length)
|
||||
return;
|
||||
|
||||
this.endGrain[index].reversed = !this.endGrain[index].reversed;
|
||||
},
|
||||
|
||||
|
||||
mouseDown(index, event)
|
||||
{
|
||||
const self = this;
|
||||
const startX = event.pageX;
|
||||
let dragging = false;
|
||||
|
||||
const dragMouseMove = (moveEvent) =>
|
||||
{
|
||||
if (!dragging)
|
||||
{
|
||||
if (Math.abs(moveEvent.pageX - startX) >= 5)
|
||||
{
|
||||
self.dragIndex = index;
|
||||
dragging = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (dragging)
|
||||
self.dropTarget = self.getTargetStrip(moveEvent.pageX);
|
||||
};
|
||||
|
||||
let dragMouseUp;
|
||||
dragMouseUp = () =>
|
||||
{
|
||||
document.removeEventListener('mousemove', dragMouseMove);
|
||||
document.removeEventListener('mouseup', dragMouseUp);
|
||||
|
||||
if (dragging)
|
||||
{
|
||||
if (self.dragIndex !== self.dropTarget)
|
||||
self.$store.commit('moveEndgrain', { from: self.dragIndex, to: self.dropTarget });
|
||||
|
||||
self.dropTarget = null;
|
||||
self.dragIndex = null;
|
||||
}
|
||||
else
|
||||
self.reverseLayer(index);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', dragMouseMove);
|
||||
document.addEventListener('mouseup', dragMouseUp);
|
||||
},
|
||||
|
||||
|
||||
getTargetStrip(xPos)
|
||||
{
|
||||
if (this.endGrain.length == 0)
|
||||
return null;
|
||||
|
||||
const firstStrip = this.getPageOffsetRect(this.$refs.strip0);
|
||||
const lastStrip = this.getPageOffsetRect(this.$refs['strip' + (this.endGrain.length - 1)]);
|
||||
|
||||
// On or above the first item
|
||||
if (xPos <= firstStrip.right)
|
||||
return 0;
|
||||
|
||||
// Below the last item
|
||||
if (xPos >= lastStrip.right)
|
||||
return this.endGrain.length;
|
||||
|
||||
// On the last item
|
||||
if (xPos >= lastStrip.left)
|
||||
return this.endGrain.length - 1;
|
||||
|
||||
// Check the previous target first, as it is most likely unchanged due to how
|
||||
// often mouseMove events occur
|
||||
if (this.dropTarget !== null && this.dropTarget > 0 && this.dropTarget < this.endGrain.length - 1)
|
||||
{
|
||||
const currentTarget = this.getPageOffsetRect(this.$refs['strip' + this.dropTarget]);
|
||||
if (xPos >= currentTarget.left && xPos < currentTarget.right)
|
||||
return this.dropTarget;
|
||||
}
|
||||
|
||||
// Just loop through all the strips, there shouldn't be enough to warrant anything more efficient
|
||||
for (let i = 1; i < this.endGrain.length - 1; i++)
|
||||
{
|
||||
const testTarget = this.getPageOffsetRect(this.$refs['strip' + i]);
|
||||
if (xPos >= testTarget.left && xPos < testTarget.right)
|
||||
return i;
|
||||
}
|
||||
|
||||
// This should never occur, so it probably will!
|
||||
return null;
|
||||
},
|
||||
|
||||
|
||||
getPageOffsetRect(element)
|
||||
{
|
||||
const clientRect = element.getBoundingClientRect();
|
||||
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
||||
return {
|
||||
top: clientRect.top + scrollTop,
|
||||
left: clientRect.left + scrollLeft,
|
||||
right: clientRect.right + scrollLeft,
|
||||
bottom: clientRect.bottom + scrollTop
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -145,8 +298,16 @@ export default {
|
||||
}
|
||||
|
||||
|
||||
.draghint
|
||||
{
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
|
||||
svg
|
||||
{
|
||||
user-select: none;
|
||||
|
||||
@media screen
|
||||
{
|
||||
box-shadow: 0 0 3em black;
|
||||
@ -156,5 +317,10 @@ svg
|
||||
{
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&.dragging
|
||||
{
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,31 +1,79 @@
|
||||
<template>
|
||||
<div class="layers">
|
||||
<div class="add">
|
||||
<button @click="addLayer()">Add layer</button>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
Tip: click and drag the layer number to move a layer
|
||||
</div>
|
||||
|
||||
|
||||
<span class="header"> </span>
|
||||
<span class="header">Wood species</span>
|
||||
<span class="header">Width</span>
|
||||
<span class="header"> </span>
|
||||
|
||||
<template v-for="(layer, index) in layers">
|
||||
<div class="index" :class="{ dropTargetAbove: dropTarget === index, dropTargetBelow: dropTarget === layers.length && index === layers.length - 1 }" :ref="'layer' + index" @mousedown.prevent="startDrag(index)">{{ index + 1 }}</div>
|
||||
<select v-model="layer.wood" class="wood">
|
||||
<option v-for="(item, index) in wood" :value="index">{{ item.name }}</option>
|
||||
</select>
|
||||
<input type="number" class="width" :value="layer.width" @input="layer.width = parseFloatDef($event.target.value)" />
|
||||
|
||||
<div class="remove">
|
||||
<button @click="removeLayer(index)">X</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="board">
|
||||
<button @click="previousBoard" :disabled="boardIndex == 0"><</button>
|
||||
<div class="name">Board {{ boardIndex + 1 }} of {{ boards.length }}</div>
|
||||
<button @click="removeBoard" v-if="boards.length > 1">Remove</button>
|
||||
<button @click="addBoard">Add</button>
|
||||
<button @click="nextBoard" :disabled="boardIndex == boards.length - 1">></button>
|
||||
</div>
|
||||
|
||||
<template v-if="currentBoard !== null">
|
||||
<div class="boardsettings">
|
||||
<label for="boardLength">Board length</label>
|
||||
<input id="boardLength" type="number" :value="currentBoard.length" @change="$store.commit('updateBoard', { board: boardIndex, values: { length: parseFloatDef($event.target.value) }})" />
|
||||
|
||||
<label for="boardThickness">Board thickness</label>
|
||||
<input id="boardThickness" type="number" :value="currentBoard.thickness" @change="$store.commit('updateBoard', { board: boardIndex, values: { thickness: parseFloatDef($event.target.value) }})" />
|
||||
</div>
|
||||
|
||||
<div class="layers">
|
||||
<div class="hint">
|
||||
Tip: click and drag the layer number to move a layer
|
||||
</div>
|
||||
|
||||
<span class="header"> </span>
|
||||
<span class="header">Wood species</span>
|
||||
<span class="header">Width</span>
|
||||
<span class="header"> </span>
|
||||
|
||||
<template v-for="(layer, index) in currentBoard.layers">
|
||||
<div class="index" :class="{ dropTargetAbove: dropTarget === index, dropTargetBelow: dropTarget === currentBoard.layers.length && index === currentBoard.layers.length - 1 }" :ref="'layer' + index" @mousedown.prevent="startDrag(index)">{{ index + 1 }}</div>
|
||||
<select v-model="layer.wood" class="wood">
|
||||
<option v-for="(item, index) in wood" :value="index">{{ item.name }}</option>
|
||||
</select>
|
||||
<input type="number" class="width" :value="layer.width" @input="layer.width = parseFloatDef($event.target.value)" />
|
||||
|
||||
<div class="remove">
|
||||
<button @click="removeLayer(index)">X</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="widthwarning" v-if="widthWarning !== null">
|
||||
{{ widthWarning }}
|
||||
</div>
|
||||
|
||||
<div class="add">
|
||||
<button @click="addLayer()">Add layer</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Preview settings</h2>
|
||||
<input id="borders" type="checkbox" :checked="settings.borders" @change="$store.commit('updateSettings', { borders: $event.target.checked })" />
|
||||
<label for="borders"> Show borders</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>End grain layer direction</h2>
|
||||
<div>
|
||||
<input id="directionUniform" type="radio" :checked="settings.direction === 'uniform'" @change="setDirection($event, 'uniform')" />
|
||||
<label for="directionUniform"> Uniform</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input id="directionAlternate" type="radio" :checked="settings.direction === 'alternate'" @change="setDirection($event, 'alternate')" />
|
||||
<label for="directionAlternate"> Alternate</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input id="directionCustom" type="radio" :checked="settings.direction === 'custom'" @change="setDirection($event, 'custom')" />
|
||||
<label for="directionCustom"> Custom</label>
|
||||
<p v-if="settings.direction === 'custom'">
|
||||
Click the strips in the preview to reverse their direction. <span v-if="!settings.borders">This may be easier if you <a href="#" @click.prevent="$store.commit('updateSettings', { borders: true })">turn on</a> the 'Show borders' setting.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -35,6 +83,7 @@ export default {
|
||||
data()
|
||||
{
|
||||
return {
|
||||
boardIndex: 0,
|
||||
dragIndex: null,
|
||||
dropTarget: null
|
||||
}
|
||||
@ -44,7 +93,66 @@ export default {
|
||||
computed: {
|
||||
settings() { return this.$store.state.settings; },
|
||||
wood() { return this.$store.state.wood; },
|
||||
layers() { return this.$store.state.boards[0].layers; },
|
||||
boards() { return this.$store.state.boards; },
|
||||
|
||||
currentBoard()
|
||||
{
|
||||
return this.boardIndex >= 0 && this.boardIndex < this.boards.length
|
||||
? this.boards[this.boardIndex]
|
||||
: null;
|
||||
},
|
||||
|
||||
widthWarning()
|
||||
{
|
||||
const self = this;
|
||||
if (self.currentBoard === null || self.boards.length == 1)
|
||||
return null;
|
||||
|
||||
let minWidth = null;
|
||||
let currentWidth = null;
|
||||
let maxWidth = null;
|
||||
|
||||
self.boards.forEach((board, index) =>
|
||||
{
|
||||
const boardWidth = units.limitDecimals(
|
||||
board.layers
|
||||
.map(layer => layer.width)
|
||||
.reduce((accumulator, currentValue) => accumulator + currentValue, 0),
|
||||
3);
|
||||
|
||||
if (index == self.boardIndex)
|
||||
currentWidth = boardWidth;
|
||||
|
||||
if (minWidth === null || boardWidth < minWidth)
|
||||
minWidth = boardWidth;
|
||||
|
||||
if (maxWidth === null || boardWidth > maxWidth)
|
||||
maxWidth = boardWidth;
|
||||
});
|
||||
|
||||
if (minWidth == maxWidth)
|
||||
return null;
|
||||
|
||||
let message = "Your board are not of equal width. The current board is " + units.display(currentWidth, this.settings.units) + " ";
|
||||
|
||||
if (currentWidth < maxWidth)
|
||||
message += "while the widest is " + units.display(maxWidth, this.settings.units) + ". ";
|
||||
else
|
||||
message += "while the smallest is " + units.display(minWidth, this.settings.units) + ". ";
|
||||
|
||||
message += "The end grain board will not align.";
|
||||
return message;
|
||||
},
|
||||
|
||||
maxBoardWidth()
|
||||
{
|
||||
// This is a copy from EndGrainPreview.vue, deduplicate maybe?
|
||||
return this.boards
|
||||
.map(board => board.layers
|
||||
.map(layer => layer.width)
|
||||
.reduce((accumulator, currentValue) => accumulator + currentValue, 0))
|
||||
.reduce((accumulator, currentValue) => currentValue > accumulator ? currentValue : accumulator, 0);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -56,26 +164,60 @@ export default {
|
||||
},
|
||||
|
||||
|
||||
previousBoard()
|
||||
{
|
||||
if (this.boardIndex > 0)
|
||||
this.boardIndex--;
|
||||
},
|
||||
|
||||
|
||||
nextBoard()
|
||||
{
|
||||
if (this.boardIndex < this.boards.length - 1)
|
||||
this.boardIndex++;
|
||||
},
|
||||
|
||||
|
||||
addBoard()
|
||||
{
|
||||
this.$store.commit('addBoard', this.boardIndex);
|
||||
this.boardIndex = this.boards.length - 1;
|
||||
},
|
||||
|
||||
|
||||
removeBoard()
|
||||
{
|
||||
if (this.boards.length <= 1)
|
||||
return;
|
||||
|
||||
this.$store.commit('removeBoard', this.boardIndex);
|
||||
|
||||
if (this.boardIndex >= this.boards.length)
|
||||
this.boardIndex = this.boards.length - 1;
|
||||
},
|
||||
|
||||
|
||||
addLayer()
|
||||
{
|
||||
this.$store.commit('addLayer', 0);
|
||||
this.$store.commit('addLayer', this.boardIndex);
|
||||
},
|
||||
|
||||
|
||||
removeLayer(index)
|
||||
{
|
||||
this.$store.commit('removeLayer', { board: 0, layer: index });
|
||||
this.$store.commit('removeLayer', { board: this.boardIndex, layer: index });
|
||||
},
|
||||
|
||||
|
||||
startDrag(index)
|
||||
{
|
||||
this.dragIndex = index;
|
||||
this.dropTarget = index;
|
||||
const self = this;
|
||||
self.dragIndex = index;
|
||||
self.dropTarget = index;
|
||||
|
||||
const dragMouseMove = (event) =>
|
||||
{
|
||||
this.dropTarget = this.getTargetLayer(event.pageY);
|
||||
self.dropTarget = self.getTargetLayer(event.pageY);
|
||||
};
|
||||
|
||||
let dragMouseUp;
|
||||
@ -84,11 +226,11 @@ export default {
|
||||
document.removeEventListener('mousemove', dragMouseMove);
|
||||
document.removeEventListener('mouseup', dragMouseUp);
|
||||
|
||||
if (this.dragIndex !== this.dropTarget)
|
||||
this.$store.commit('moveLayer', { board: 0, from: this.dragIndex, to: this.dropTarget });
|
||||
if (self.dragIndex !== self.dropTarget)
|
||||
self.$store.commit('moveLayer', { board: this.boardIndex, from: self.dragIndex, to: self.dropTarget });
|
||||
|
||||
this.dropTarget = null;
|
||||
this.dragIndex = null;
|
||||
self.dropTarget = null;
|
||||
self.dragIndex = null;
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', dragMouseMove);
|
||||
@ -98,11 +240,11 @@ export default {
|
||||
|
||||
getTargetLayer(yPos)
|
||||
{
|
||||
if (this.layers.length == 0)
|
||||
if (this.currentBoard === null || this.currentBoard.layers.length == 0)
|
||||
return null;
|
||||
|
||||
const firstLayer = this.getPageOffsetRect(this.$refs.layer0);
|
||||
const lastLayer = this.getPageOffsetRect(this.$refs['layer' + (this.layers.length - 1)]);
|
||||
const lastLayer = this.getPageOffsetRect(this.$refs['layer' + (this.currentBoard.layers.length - 1)]);
|
||||
|
||||
// On or above the first item
|
||||
if (yPos <= firstLayer.bottom)
|
||||
@ -110,15 +252,15 @@ export default {
|
||||
|
||||
// Below the last item
|
||||
if (yPos >= lastLayer.bottom)
|
||||
return this.layers.length;
|
||||
return this.currentBoard.layers.length;
|
||||
|
||||
// On the last item
|
||||
if (yPos >= lastLayer.top)
|
||||
return this.layers.length - 1;
|
||||
return this.currentBoard.layers.length - 1;
|
||||
|
||||
// Check the previous target first, as it is most likely unchanged due to how
|
||||
// often mouseMove events occur
|
||||
if (this.dropTarget !== null && this.dropTarget > 0 && this.dropTarget < this.layers.length - 1)
|
||||
if (this.dropTarget !== null && this.dropTarget > 0 && this.dropTarget < this.currentBoard.layers.length - 1)
|
||||
{
|
||||
const currentTarget = this.getPageOffsetRect(this.$refs['layer' + this.dropTarget]);
|
||||
if (yPos >= currentTarget.top && yPos < currentTarget.bottom)
|
||||
@ -126,7 +268,7 @@ export default {
|
||||
}
|
||||
|
||||
// Just loop through all the layers, there shouldn't be enough to warrant anything more efficient
|
||||
for (let i = 1; i < this.layers.length - 1; i++)
|
||||
for (let i = 1; i < this.currentBoard.layers.length - 1; i++)
|
||||
{
|
||||
const testTarget = this.getPageOffsetRect(this.$refs['layer' + i]);
|
||||
if (yPos >= testTarget.top && yPos < testTarget.bottom)
|
||||
@ -150,6 +292,15 @@ export default {
|
||||
right: clientRect.right + scrollLeft,
|
||||
bottom: clientRect.bottom + scrollTop
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
setDirection(event, direction)
|
||||
{
|
||||
if (!event.target.checked)
|
||||
return;
|
||||
|
||||
this.$store.commit('updateSettings', { direction: direction });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -158,8 +309,8 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
.layers
|
||||
{
|
||||
display: inline-grid;
|
||||
grid-template-columns: 3em 20em 5em 3em;
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto 5em min-content;
|
||||
grid-column-gap: 1em;
|
||||
|
||||
.hint
|
||||
@ -173,23 +324,33 @@ export default {
|
||||
|
||||
.index
|
||||
{
|
||||
cursor: pointer;
|
||||
cursor: grab;
|
||||
|
||||
&.dropTargetAbove
|
||||
{
|
||||
border-top: solid 1px white;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&.dropTargetBelow
|
||||
{
|
||||
border-bottom: solid 1px white;
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.add
|
||||
{
|
||||
grid-column: 2 / 5;
|
||||
padding-bottom: 1em;
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.widthwarning
|
||||
{
|
||||
font-size: 80%;
|
||||
color: yellow;
|
||||
grid-column: 2 / 5;
|
||||
padding-top: .5em;
|
||||
}
|
||||
|
||||
.header
|
||||
@ -198,4 +359,32 @@ export default {
|
||||
margin-bottom: .25em;
|
||||
}
|
||||
}
|
||||
|
||||
.board
|
||||
{
|
||||
display: flex;
|
||||
margin-bottom: 2em;
|
||||
|
||||
.name
|
||||
{
|
||||
flex-grow: 1;
|
||||
padding: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.boardsettings
|
||||
{
|
||||
display: inline-grid;
|
||||
grid-template-columns: max-content 5em;
|
||||
grid-column-gap: 1em;
|
||||
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
|
||||
h2
|
||||
{
|
||||
margin-top: 2em;
|
||||
}
|
||||
</style>
|
@ -10,25 +10,13 @@
|
||||
<!--<option value="inchfractional">Inches (fractional)</option>-->
|
||||
</select>
|
||||
|
||||
<label for="borders">Show borders</label>
|
||||
<input id="borders" type="checkbox" :checked="settings.borders" @change="$store.commit('updateSettings', { borders: $event.target.checked })" />
|
||||
|
||||
<h2>Material</h2>
|
||||
<label for="boardThickness">Board thickness</label>
|
||||
<input id="boardThickness" type="number" :value="settings.boardThickness" @change="$store.commit('updateSettings', { boardThickness: parseFloatDef($event.target.value) })" />
|
||||
|
||||
<label for="boardLength">Board length</label>
|
||||
<input id="boardLength" type="number" :value="settings.boardLength" @change="$store.commit('updateSettings', { boardLength: parseFloatDef($event.target.value) })" />
|
||||
|
||||
<h2>Tools</h2>
|
||||
<label for="bladeKerf">Blade kerf</label>
|
||||
<input id="bladeKerf" type="number" :value="settings.bladeKerf" @change="$store.commit('updateSettings', { bladeKerf: parseFloatDef($event.target.value) })" />
|
||||
|
||||
<h2>End grain</h2>
|
||||
<label for="crosscutWidth">Crosscut width</label>
|
||||
<input id="crosscutWidth" type="number" :value="settings.crosscutWidth" @change="$store.commit('updateSettings', { crosscutWidth: parseFloatDef($event.target.value) })" />
|
||||
|
||||
<label for="alternateDirection">Alternate direction</label>
|
||||
<input id="alternateDirection" type="checkbox" :checked="settings.alternateDirection" @change="$store.commit('updateSettings', { alternateDirection: $event.target.checked })" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -61,8 +49,6 @@ export default {
|
||||
|
||||
h2
|
||||
{
|
||||
color: #808080;
|
||||
font-size: 80%;
|
||||
margin-top: 1em;
|
||||
margin-bottom: .25em;
|
||||
grid-column: 1 / 3;
|
||||
|
@ -1,9 +1,5 @@
|
||||
<template>
|
||||
<div class="wood">
|
||||
<div class="add">
|
||||
<button @click="addWood()">Add wood species</button>
|
||||
</div>
|
||||
|
||||
<span class="header">Name</span>
|
||||
<span class="header">Colour</span>
|
||||
<span class="header"> </span>
|
||||
@ -16,6 +12,10 @@
|
||||
<button @click="removeWood(index)">X</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="add">
|
||||
<button @click="addWood()">Add wood species</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -45,14 +45,14 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
.wood
|
||||
{
|
||||
display: inline-grid;
|
||||
grid-template-columns: 23em 5em 3em;
|
||||
display: grid;
|
||||
grid-template-columns: auto min-content min-content;
|
||||
grid-column-gap: 1em;
|
||||
|
||||
.add
|
||||
{
|
||||
grid-column: 1 / 4;
|
||||
padding-bottom: 1em;
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.header
|
||||
@ -60,5 +60,10 @@ export default {
|
||||
font-weight: bold;
|
||||
margin-bottom: .25em;
|
||||
}
|
||||
|
||||
button
|
||||
{
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
50
src/lib/enums.js
Normal file
50
src/lib/enums.js
Normal file
@ -0,0 +1,50 @@
|
||||
const UnitsEnum = {
|
||||
mm: 'mm',
|
||||
cm: 'cm',
|
||||
inchdecimal: 'inchdecimal',
|
||||
//inchfractional: 'inchfractional'
|
||||
};
|
||||
|
||||
|
||||
UnitsEnum.isValid = (value) =>
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case UnitsEnum.mm:
|
||||
case UnitsEnum.cm:
|
||||
case UnitsEnum.inchdecimal:
|
||||
//case UnitsEnum.inchfractional:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const DirectionEnum = {
|
||||
uniform: 'uniform',
|
||||
alternate: 'alternate',
|
||||
custom: 'custom'
|
||||
};
|
||||
|
||||
|
||||
DirectionEnum.isValid = (value) =>
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case DirectionEnum.uniform:
|
||||
case DirectionEnum.alternate:
|
||||
case DirectionEnum.custom:
|
||||
return value;
|
||||
|
||||
default:
|
||||
return DirectionEnum.uniform;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export {
|
||||
UnitsEnum,
|
||||
DirectionEnum
|
||||
}
|
667
src/store.js
667
src/store.js
@ -1,118 +1,79 @@
|
||||
import { createStore } from 'vuex';
|
||||
import { units } from './lib/units';
|
||||
import { UnitsEnum, DirectionEnum } from './lib/enums';
|
||||
|
||||
import { serialize, deserialize } from '@ygoe/msgpack';
|
||||
|
||||
|
||||
function mergeObject(source, target)
|
||||
{
|
||||
for (const property in source)
|
||||
{
|
||||
if (!source.hasOwnProperty(property) || !target.hasOwnProperty(property))
|
||||
continue;
|
||||
|
||||
target[property] = source[property];
|
||||
}
|
||||
function DefaultSettings()
|
||||
{
|
||||
return {
|
||||
units: UnitsEnum.cm,
|
||||
borders: false,
|
||||
bladeKerf: 0.35,
|
||||
crosscutWidth: 3,
|
||||
direction: DirectionEnum.uniform
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function parseFloatDef(value)
|
||||
function DefaultWood()
|
||||
{
|
||||
const parsedValue = parseFloat(value);
|
||||
return Object.is(parsedValue, NaN) ? 0 : parsedValue;
|
||||
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 loadObject(state, parsedPayload)
|
||||
function DefaultBoards()
|
||||
{
|
||||
if (parsedPayload.hasOwnProperty('settings'))
|
||||
mergeObject(parsedPayload.settings, state.settings);
|
||||
|
||||
if (parsedPayload.hasOwnProperty('boards'))
|
||||
{
|
||||
|
||||
const newBoards = parsedPayload.boards.map(board =>
|
||||
return [
|
||||
{
|
||||
if (!board.hasOwnProperty('layers'))
|
||||
{
|
||||
return {
|
||||
layers: []
|
||||
};
|
||||
}
|
||||
thickness: 2,
|
||||
length: 70,
|
||||
|
||||
return {
|
||||
layers: board.layers.map(layer =>
|
||||
{
|
||||
return {
|
||||
wood: parseFloatDef(layer.wood),
|
||||
width: parseFloatDef(layer.width)
|
||||
}
|
||||
})
|
||||
};
|
||||
})
|
||||
|
||||
if (newBoards.length === 0)
|
||||
newBoards.push({ layers: [] });
|
||||
|
||||
state.boards = newBoards;
|
||||
}
|
||||
|
||||
if (parsedPayload.hasOwnProperty('wood'))
|
||||
{
|
||||
const newWood = parsedPayload.wood.map(item =>
|
||||
{
|
||||
return {
|
||||
name: item.name,
|
||||
color: item.color
|
||||
};
|
||||
});
|
||||
|
||||
state.wood = newWood;
|
||||
}
|
||||
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: {
|
||||
settings: {
|
||||
units: 'cm',
|
||||
borders: false,
|
||||
boardThickness: 2,
|
||||
boardLength: 70,
|
||||
bladeKerf: 0.35,
|
||||
crosscutWidth: 3,
|
||||
|
||||
alternateDirection: true
|
||||
},
|
||||
|
||||
wood: [
|
||||
{ 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' }
|
||||
],
|
||||
|
||||
boards: [
|
||||
{
|
||||
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 }
|
||||
]
|
||||
}
|
||||
]
|
||||
settings: DefaultSettings(),
|
||||
wood: DefaultWood(),
|
||||
boards: DefaultBoards(),
|
||||
endGrain: DefaultEndGrain()
|
||||
},
|
||||
|
||||
mutations: {
|
||||
@ -146,29 +107,7 @@ export default createStore({
|
||||
return;
|
||||
|
||||
const board = state.boards[payload.board];
|
||||
|
||||
if (payload.from < 0 || payload.from >= board.layers.length)
|
||||
return;
|
||||
|
||||
if (payload.to < 0 || payload.to > board.layers.length)
|
||||
return;
|
||||
|
||||
if (payload.to == board.layers.length)
|
||||
{
|
||||
// Move to end
|
||||
board.layers.push(board.layers[payload.from]);
|
||||
board.layers.splice(payload.from, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
const item = board.layers[payload.from];
|
||||
board.layers.splice(payload.from, 1);
|
||||
|
||||
if (payload.to > payload.from)
|
||||
payload.to--;
|
||||
|
||||
board.layers.splice(payload.to, 0, item);
|
||||
}
|
||||
moveArrayItem(board.layers, payload.from, payload.to);
|
||||
},
|
||||
|
||||
|
||||
@ -191,7 +130,7 @@ export default createStore({
|
||||
board.layers.forEach(layer =>
|
||||
{
|
||||
if (layer.wood === index)
|
||||
layer.wood = null
|
||||
layer.wood = -1
|
||||
else if (layer.wood > index)
|
||||
layer.wood--;
|
||||
});
|
||||
@ -204,20 +143,20 @@ export default createStore({
|
||||
updateSettings(state, payload)
|
||||
{
|
||||
const oldUnits = state.settings.units;
|
||||
|
||||
mergeObject(payload, state.settings);
|
||||
|
||||
if (oldUnits !== state.settings.units)
|
||||
{
|
||||
// Convert the settings
|
||||
state.settings.boardThickness = units.limitDecimals(units.convert(state.settings.boardThickness, oldUnits, state.settings.units), 3);
|
||||
state.settings.boardLength = units.limitDecimals(units.convert(state.settings.boardLength, oldUnits, state.settings.units), 3);
|
||||
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 layers
|
||||
// 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);
|
||||
@ -227,6 +166,81 @@ export default createStore({
|
||||
},
|
||||
|
||||
|
||||
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);
|
||||
@ -238,7 +252,8 @@ export default createStore({
|
||||
try
|
||||
{
|
||||
const parsedPayload = deserialize(payload);
|
||||
loadObject(state, parsedPayload);
|
||||
if (parsedPayload)
|
||||
loadObject(state, parsedPayload);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
@ -250,12 +265,416 @@ export default createStore({
|
||||
getters: {
|
||||
save(state)
|
||||
{
|
||||
return JSON.stringify(state);
|
||||
const serialized = serializeState(state, false);
|
||||
return JSON.stringify(serialized);
|
||||
},
|
||||
|
||||
saveMsgPack(state)
|
||||
{
|
||||
return serialize(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'
|
||||
},
|
||||
|
||||
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'
|
||||
},
|
||||
|
||||
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;
|
||||
|
||||
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 (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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user