From fe4d17c7f2f3d62f26b408793269d6c0259b4368 Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Wed, 30 Dec 2020 23:00:03 +0100 Subject: [PATCH] Added support for multiple boards in a single end grain result ...and probably loads of little things changed in the process --- TODO.md | 5 +- src/App.vue | 31 +- src/components/CuttingList.vue | 104 +++-- src/components/EdgeGrainPreview.vue | 15 +- src/components/EndGrainPreview.vue | 250 +++++++++-- src/components/Layers.vue | 281 ++++++++++-- src/components/Settings.vue | 16 +- src/components/Wood.vue | 19 +- src/lib/enums.js | 50 +++ src/store.js | 667 ++++++++++++++++++++++------ 10 files changed, 1146 insertions(+), 292 deletions(-) create mode 100644 src/lib/enums.js diff --git a/TODO.md b/TODO.md index f71c6dc..403ccc2 100644 --- a/TODO.md +++ b/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 \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 745fb4c..15d619c 100644 --- a/src/App.vue +++ b/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.

-
- - -
+
+ + +
@@ -72,16 +72,19 @@
-
-

Edge grain

- -
-

End grain

+
+

Edge grain

+ +
+

Cutting list

@@ -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; } diff --git a/src/components/CuttingList.vue b/src/components/CuttingList.vue index 0caf06a..4897255 100644 --- a/src/components/CuttingList.vue +++ b/src/components/CuttingList.vue @@ -5,27 +5,39 @@ Wood species Width - - {{ index + 1 }} - {{ getLayerWood(index) }} - {{ getLayerWidth(index) }} - +

Bill of materials

- + - - - - - - +
Wood speciesThickness Length WidthThickness
{{ stock.woodName }}{{ display(settings.boardThickness) }}{{ display(settings.boardLength) }}{{ display(stock.width) }}
@@ -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; diff --git a/src/components/EdgeGrainPreview.vue b/src/components/EdgeGrainPreview.vue index 2868250..fbc4991 100644 --- a/src/components/EdgeGrainPreview.vue +++ b/src/components/EdgeGrainPreview.vue @@ -1,6 +1,6 @@ @@ -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; + } } \ No newline at end of file diff --git a/src/components/Layers.vue b/src/components/Layers.vue index b9fa986..6872067 100644 --- a/src/components/Layers.vue +++ b/src/components/Layers.vue @@ -1,31 +1,79 @@