441 lines
12 KiB
Vue
441 lines
12 KiB
Vue
<template>
|
|
<div class="board" @mouseenter="highlightBoard" @mouseleave="removeHighlightBoard">
|
|
<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)" @mouseenter="highlightLayer(index)" @mouseleave="removeHighlightLayer(index)">{{ index + 1 }}</div>
|
|
<select v-model="layer.wood" class="wood" @mouseenter="highlightLayer(index)" @mouseleave="removeHighlightLayer(index)">
|
|
<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)" @mouseenter="highlightLayer(index)" @mouseleave="removeHighlightLayer(index)"/>
|
|
|
|
<div class="remove" @mouseenter="highlightLayer(index)" @mouseleave="removeHighlightLayer(index)">
|
|
<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>
|
|
<div>
|
|
<input id="borders" type="checkbox" :checked="settings.borders" @change="$store.commit('updateSettings', { borders: $event.target.checked })" />
|
|
<label for="borders"> Show borders</label>
|
|
</div>
|
|
|
|
<div>
|
|
<input id="highlightBoard" type="checkbox" :checked="settings.highlightBoard" @change="$store.commit('updateSettings', { highlightBoard: $event.target.checked })" />
|
|
<label for="highlightBoard"> Highlight current board in end grain preview</label>
|
|
</div>
|
|
|
|
<div>
|
|
<input id="highlightLayer" type="checkbox" :checked="settings.highlightLayer" @change="$store.commit('updateSettings', { highlightLayer: $event.target.checked })" />
|
|
<label for="highlightLayer"> Highlight current layer in end grain preview</label>
|
|
</div>
|
|
</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>
|
|
import { units } from '../lib/units';
|
|
|
|
export default {
|
|
data()
|
|
{
|
|
return {
|
|
boardIndex: 0,
|
|
dragIndex: null,
|
|
dropTarget: null,
|
|
updateHighlightedBoard: false
|
|
}
|
|
},
|
|
|
|
|
|
computed: {
|
|
volatile() { return this.$store.state.volatile; },
|
|
settings() { return this.$store.state.settings; },
|
|
wood() { return this.$store.state.wood; },
|
|
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);
|
|
}
|
|
},
|
|
|
|
|
|
methods: {
|
|
parseFloatDef(value)
|
|
{
|
|
const parsedValue = parseFloat(value);
|
|
return Object.is(parsedValue, NaN) ? 0 : parsedValue;
|
|
},
|
|
|
|
|
|
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', this.boardIndex);
|
|
},
|
|
|
|
|
|
removeLayer(index)
|
|
{
|
|
this.$store.commit('removeLayer', { board: this.boardIndex, layer: index });
|
|
},
|
|
|
|
|
|
startDrag(index)
|
|
{
|
|
const self = this;
|
|
self.dragIndex = index;
|
|
self.dropTarget = index;
|
|
|
|
const dragMouseMove = (event) =>
|
|
{
|
|
self.dropTarget = self.getTargetLayer(event.pageY);
|
|
};
|
|
|
|
let dragMouseUp;
|
|
dragMouseUp = () =>
|
|
{
|
|
document.removeEventListener('mousemove', dragMouseMove);
|
|
document.removeEventListener('mouseup', dragMouseUp);
|
|
|
|
if (self.dragIndex !== self.dropTarget)
|
|
self.$store.commit('moveLayer', { board: this.boardIndex, from: self.dragIndex, to: self.dropTarget });
|
|
|
|
self.dropTarget = null;
|
|
self.dragIndex = null;
|
|
};
|
|
|
|
document.addEventListener('mousemove', dragMouseMove);
|
|
document.addEventListener('mouseup', dragMouseUp);
|
|
},
|
|
|
|
|
|
getTargetLayer(yPos)
|
|
{
|
|
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.currentBoard.layers.length - 1)]);
|
|
|
|
// On or above the first item
|
|
if (yPos <= firstLayer.bottom)
|
|
return 0;
|
|
|
|
// Below the last item
|
|
if (yPos >= lastLayer.bottom)
|
|
return this.currentBoard.layers.length;
|
|
|
|
// On the last item
|
|
if (yPos >= lastLayer.top)
|
|
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.currentBoard.layers.length - 1)
|
|
{
|
|
const currentTarget = this.getPageOffsetRect(this.$refs['layer' + this.dropTarget]);
|
|
if (yPos >= currentTarget.top && yPos < currentTarget.bottom)
|
|
return this.dropTarget;
|
|
}
|
|
|
|
// Just loop through all the layers, there shouldn't be enough to warrant anything more efficient
|
|
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)
|
|
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
|
|
};
|
|
},
|
|
|
|
|
|
setDirection(event, direction)
|
|
{
|
|
if (!event.target.checked)
|
|
return;
|
|
|
|
this.$store.commit('updateSettings', { direction: direction });
|
|
},
|
|
|
|
|
|
highlightBoard()
|
|
{
|
|
this.updateHighlightedBoard = true;
|
|
this.$store.commit('updateVolatile', { highlightedBoard: this.boardIndex });
|
|
},
|
|
|
|
|
|
removeHighlightBoard()
|
|
{
|
|
this.updateHighlightedBoard = false;
|
|
this.$store.commit('updateVolatile', { highlightedBoard: null });
|
|
},
|
|
|
|
|
|
highlightLayer(index)
|
|
{
|
|
this.$store.commit('updateVolatile', { highlightedBoard: this.boardIndex, highlightedLayer: index });
|
|
},
|
|
|
|
|
|
removeHighlightLayer(index)
|
|
{
|
|
if (this.volatile.highlightedLayer === index)
|
|
{
|
|
this.$store.commit('updateVolatile', { highlightedBoard: null, highlightedLayer: null });
|
|
}
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
boardIndex(newValue)
|
|
{
|
|
if (this.updateHighlightedBoard)
|
|
this.$store.commit('updateVolatile', { highlightedBoard: newValue });
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.layers
|
|
{
|
|
display: grid;
|
|
grid-template-columns: min-content auto 5em min-content;
|
|
grid-column-gap: 1em;
|
|
|
|
.hint
|
|
{
|
|
color: #808080;
|
|
text-align: center;
|
|
|
|
grid-column: 1 / 5;
|
|
margin-bottom: 1em;
|
|
}
|
|
|
|
.index
|
|
{
|
|
cursor: grab;
|
|
|
|
&.dropTargetAbove
|
|
{
|
|
border-top: solid 1px white;
|
|
cursor: grabbing;
|
|
}
|
|
|
|
&.dropTargetBelow
|
|
{
|
|
border-bottom: solid 1px white;
|
|
cursor: grabbing;
|
|
}
|
|
}
|
|
|
|
.add
|
|
{
|
|
grid-column: 2 / 5;
|
|
padding-top: 1em;
|
|
}
|
|
|
|
.widthwarning
|
|
{
|
|
font-size: 80%;
|
|
color: yellow;
|
|
grid-column: 2 / 5;
|
|
padding-top: .5em;
|
|
}
|
|
|
|
.header
|
|
{
|
|
font-weight: bold;
|
|
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> |