From ce1e31432f6a7284aac211690e32fe799aa7d322 Mon Sep 17 00:00:00 2001
From: Mark van Renswoude
Date: Mon, 28 Dec 2020 21:20:51 +0100
Subject: [PATCH] Added units switching
Added layer reordering
Fixed a few issues related to non-millimeter units
Edge/End components were incorrectly named, whoops
---
TODO.md | 8 +-
src/App.vue | 10 +--
src/components/EdgeGrainPreview.vue | 72 +++++++---------
src/components/EndGrainPreview.vue | 71 +++++++++++++---
src/components/Layers.vue | 125 +++++++++++++++++++++++++++-
src/components/Settings.vue | 5 +-
src/lib/units.js | 37 +++++++-
src/store.js | 89 +++++++++++++++-----
8 files changed, 324 insertions(+), 93 deletions(-)
diff --git a/TODO.md b/TODO.md
index 6581d1f..30d8bb1 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,20 +1,16 @@
ToDo
====
-Must have
-----
-- Implement switching units
-- Re-ordering of the layers (preferably drag/drop)
-
Should have
----
-- Render width and height of the boards in the previews
- Material usage overview
- Generate cutting list
- Support for fractional inches (see, not all europeans look down on freedom units!)
+- Save/load via URL (MessagePack encoded Base64 in URL)
Nice to have
----
+- 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)
- 3D effect for previews emulating thickness / crosscut width
- Make it a tiny bit prettier overall
diff --git a/src/App.vue b/src/App.vue
index 6a6b76e..3b44116 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -17,7 +17,7 @@
-
+
Load
@@ -35,10 +35,10 @@
Edge grain
-
+
End grain
-
+
@@ -80,7 +80,7 @@ export default {
load()
{
- const loadFile = document.getElementById("loadFile").files[0];
+ const loadFile = this.$refs.loadFile.files[0];
if (!loadFile)
return;
@@ -139,7 +139,7 @@ h1
}
}
-.about, .todo
+.about
{
width: 30em;
}
diff --git a/src/components/EdgeGrainPreview.vue b/src/components/EdgeGrainPreview.vue
index 3f365a4..baab4dd 100644
--- a/src/components/EdgeGrainPreview.vue
+++ b/src/components/EdgeGrainPreview.vue
@@ -1,27 +1,19 @@
+
Dimensions: {{ display(boardWidth) }} x {{ display(boardHeight) }} x {{ display(settings.boardThickness) }}
+
-
-
-
-
-
-
+
@@ -40,20 +32,7 @@ export default {
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;
-
- return Math.floor((this.settings.boardLength + this.settings.bladeKerf) / stripAndKerf);
- },
-
- boardWidth()
- {
- const boardWidth = this.stripsPerBoard * this.settings.boardThickness;
- return this.toPixels(boardWidth);
- },
+ boardWidth() { return this.settings.boardLength; },
boardHeight()
{
@@ -62,9 +41,19 @@ export default {
.reduce((accumulator, currentValue) => accumulator + currentValue);
},
- viewportWidth() { return Math.floor(this.boardWidth * this.scale); },
- viewportHeight() { return Math.floor(this.boardHeight * this.scale); },
- viewBox() { return '0 0 ' + this.boardWidth + ' ' + this.boardHeight; }
+ boardPixelWidth()
+ {
+ return this.toPixels(this.boardWidth);
+ },
+
+ boardPixelHeight()
+ {
+ return this.toPixels(this.boardHeight);
+ },
+
+ viewportWidth() { return Math.floor(this.boardPixelWidth * this.scale); },
+ viewportHeight() { return Math.floor(this.boardPixelHeight * this.scale); },
+ viewBox() { return '0 0 ' + this.boardPixelWidth + ' ' + this.boardPixelHeight; }
},
@@ -74,6 +63,11 @@ export default {
return units.toPixels(value, this.settings.units);
},
+ display(value)
+ {
+ return units.display(value, this.settings.units);
+ },
+
getLayerOffset(index)
{
if (index < 0 || index >= this.layers.length)
@@ -84,7 +78,7 @@ export default {
for (let i = 0; i < index; i++)
offset += this.layers[i].width;
- return offset;
+ return this.toPixels(offset);
},
getLayerStyle(index)
@@ -101,14 +95,6 @@ export default {
: '';
return 'fill: ' + this.wood[woodIndex].color + borderStyle;
- },
-
- getLayerTransform(index)
- {
- if (!this.settings.alternateDirection || (index % 2) == 0)
- return '';
-
- return 'scale(1, -1) translate(0, -' + this.boardHeight + ')';
}
}
}
diff --git a/src/components/EndGrainPreview.vue b/src/components/EndGrainPreview.vue
index 4cbe808..a33704b 100644
--- a/src/components/EndGrainPreview.vue
+++ b/src/components/EndGrainPreview.vue
@@ -1,17 +1,29 @@
+
Dimensions: {{ display(boardWidth) }} x {{ display(boardHeight) }} x {{ display(settings.crosscutWidth) }}
+
+
+
+
+
+
-
+
@@ -30,7 +42,19 @@ export default {
wood() { return this.$store.state.wood; },
layers() { return this.$store.state.boards[0].layers; },
- boardWidth() { return this.toPixels(this.settings.boardLength); },
+ stripsPerBoard()
+ {
+ const stripAndKerf = this.settings.crosscutWidth + this.settings.bladeKerf;
+ if (stripAndKerf === 0)
+ return 0;
+
+ return Math.floor((this.settings.boardLength + this.settings.bladeKerf) / stripAndKerf);
+ },
+
+ boardWidth()
+ {
+ return this.stripsPerBoard * this.settings.boardThickness;
+ },
boardHeight()
{
@@ -39,9 +63,19 @@ export default {
.reduce((accumulator, currentValue) => accumulator + currentValue);
},
- viewportWidth() { return Math.floor(this.boardWidth * this.scale); },
- viewportHeight() { return Math.floor(this.boardHeight * this.scale); },
- viewBox() { return '0 0 ' + this.boardWidth + ' ' + this.boardHeight; }
+ boardPixelWidth()
+ {
+ return this.toPixels(this.boardWidth);
+ },
+
+ boardPixelHeight()
+ {
+ return this.toPixels(this.boardHeight);
+ },
+
+ viewportWidth() { return Math.floor(this.boardPixelWidth * this.scale); },
+ viewportHeight() { return Math.floor(this.boardPixelHeight * this.scale); },
+ viewBox() { return '0 0 ' + this.boardPixelWidth + ' ' + this.boardPixelHeight; }
},
@@ -51,6 +85,11 @@ export default {
return units.toPixels(value, this.settings.units);
},
+ display(value)
+ {
+ return units.display(value, this.settings.units);
+ },
+
getLayerOffset(index)
{
if (index < 0 || index >= this.layers.length)
@@ -61,7 +100,7 @@ export default {
for (let i = 0; i < index; i++)
offset += this.layers[i].width;
- return offset;
+ return this.toPixels(offset);
},
getLayerStyle(index)
@@ -78,6 +117,14 @@ export default {
: '';
return 'fill: ' + this.wood[woodIndex].color + borderStyle;
+ },
+
+ getLayerTransform(index)
+ {
+ if (!this.settings.alternateDirection || (index % 2) == 0)
+ return '';
+
+ return 'scale(1, -1) translate(0, -' + this.boardPixelHeight + ')';
}
}
}
diff --git a/src/components/Layers.vue b/src/components/Layers.vue
index 6e59f7f..902ba0a 100644
--- a/src/components/Layers.vue
+++ b/src/components/Layers.vue
@@ -4,13 +4,18 @@
Add layer
+
+ Tip: click and drag the layer number to move a layer
+
+
+
- {{ index + 1 }}
+ {{ index + 1 }}
{{ item.name }}
@@ -27,6 +32,15 @@
import { units } from '../lib/units';
export default {
+ data()
+ {
+ return {
+ dragIndex: null,
+ dropTarget: null
+ }
+ },
+
+
computed: {
settings() { return this.$store.state.settings; },
wood() { return this.$store.state.wood; },
@@ -51,6 +65,91 @@ export default {
removeLayer(index)
{
this.$store.commit('removeLayer', { board: 0, layer: index });
+ },
+
+
+ startDrag(index)
+ {
+ this.dragIndex = index;
+ this.dropTarget = index;
+
+ const dragMouseMove = (event) =>
+ {
+ this.dropTarget = this.getTargetLayer(event.pageY);
+ };
+
+ let dragMouseUp;
+ dragMouseUp = () =>
+ {
+ 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 });
+
+ this.dropTarget = null;
+ this.dragIndex = null;
+ };
+
+ document.addEventListener('mousemove', dragMouseMove);
+ document.addEventListener('mouseup', dragMouseUp);
+ },
+
+
+ getTargetLayer(yPos)
+ {
+ if (this.layers.length == 0)
+ return null;
+
+ const firstLayer = this.getPageOffsetRect(this.$refs.layer0);
+ const lastLayer = this.getPageOffsetRect(this.$refs['layer' + (this.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.layers.length;
+
+ // On the last item
+ if (yPos >= lastLayer.top)
+ return this.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)
+ {
+ 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.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
+ };
}
}
}
@@ -63,6 +162,30 @@ export default {
grid-template-columns: 3em 20em 5em 3em;
grid-column-gap: 1em;
+ .hint
+ {
+ color: #808080;
+ text-align: center;
+
+ grid-column: 1 / 5;
+ margin-bottom: 1em;
+ }
+
+ .index
+ {
+ cursor: pointer;
+
+ &.dropTargetAbove
+ {
+ border-top: solid 1px black;
+ }
+
+ &.dropTargetBelow
+ {
+ border-bottom: solid 1px black;
+ }
+ }
+
.add
{
grid-column: 2 / 5;
diff --git a/src/components/Settings.vue b/src/components/Settings.vue
index 17a1eb1..4428949 100644
--- a/src/components/Settings.vue
+++ b/src/components/Settings.vue
@@ -3,10 +3,11 @@
Designer
Units
-
+
Millimeters
Centimeters
- Inches (fractional)
+ Inches (decimal)
+
Show borders
diff --git a/src/lib/units.js b/src/lib/units.js
index 6564299..1aeaa20 100644
--- a/src/lib/units.js
+++ b/src/lib/units.js
@@ -5,9 +5,16 @@ const pixelsPerMillimeter = 1;
const units = {
+ convert(value, fromUnits, toUnits)
+ {
+ const millimeters = this.toMillimeters(value, fromUnits);
+ return this.fromMillimeters(millimeters, toUnits);
+ },
+
+
toPixels(value, units)
{
- return this.toMillimeters(value, units) * pixelsPerMillimeter;
+ return Math.ceil(this.toMillimeters(value, units) * pixelsPerMillimeter);
},
@@ -17,7 +24,7 @@ const units = {
{
case 'mm': return value;
case 'cm': return value * millimetersPerCentimeter;
- case 'inch': return value * millimetersPerInch;
+ case 'inchdecimal': return value * millimetersPerInch;
}
console.error('Invalid units type: ' + units);
@@ -31,11 +38,35 @@ const units = {
{
case 'mm': return value;
case 'cm': return value / millimetersPerCentimeter;
- case 'inch': return value / millimetersPerInch;
+ case 'inchdecimal': return value / millimetersPerInch;
}
console.error('Invalid units type: ' + units);
return 0;
+ },
+
+
+ display(value, units)
+ {
+ const displayValue = this.limitDecimals(value, 3);
+
+ switch (units)
+ {
+ case 'mm': return displayValue + ' mm';
+ case 'cm': return displayValue + ' cm';
+ case 'inchdecimal': return displayValue + ' inch';
+ }
+
+ console.error('Invalid units type: ' + units);
+ return displayValue;
+ },
+
+
+ limitDecimals(value, decimals)
+ {
+ // toFixed turns it into a string and pads it with zeroes
+ const power = Math.pow(10, decimals);
+ return Math.round(value * power) / power;
}
};
diff --git a/src/store.js b/src/store.js
index 8da2e50..b5e1056 100644
--- a/src/store.js
+++ b/src/store.js
@@ -25,12 +25,12 @@ function parseFloatDef(value)
export default createStore({
state: {
settings: {
- units: 'mm',
+ units: 'cm',
borders: false,
- boardThickness: 20,
- boardLength: 700,
- bladeKerf: 3.5,
- crosscutWidth: 30,
+ boardThickness: 2,
+ boardLength: 70,
+ bladeKerf: 0.35,
+ crosscutWidth: 3,
alternateDirection: true
},
@@ -50,20 +50,14 @@ export default createStore({
boards: [
{
layers: [
- { wood: 0, width: 20 },
- { wood: 1, width: 20 },
- { wood: 0, width: 20 },
- { wood: 1, width: 20 },
- { wood: 0, width: 20 },
- { wood: 1, width: 20 },
- { wood: 0, width: 20 },
- { wood: 1, width: 20 },
- { wood: 0, width: 20 },
- { wood: 1, width: 20 },
- { wood: 0, width: 20 },
- { wood: 1, width: 20 },
- { wood: 0, width: 20 },
- { wood: 1, width: 20 }
+ { 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 }
]
}
]
@@ -86,10 +80,43 @@ export default createStore({
if (payload.board < 0 || payload.board >= state.boards.length)
return;
- if (payload.layer < 0 || payload.layer >= state.boards[payload.board].length)
+ const board = state.boards[payload.board];
+
+ if (payload.layer < 0 || payload.layer >= board.layers.length)
return;
- state.boards[payload.board].layers.splice(payload.layer, 1);
+ 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];
+
+ 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);
+ }
},
@@ -124,7 +151,27 @@ 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
+ state.boards.forEach(board =>
+ {
+ board.layers.forEach(layer =>
+ {
+ layer.width = units.limitDecimals(units.convert(layer.width, oldUnits, state.settings.units), 3);
+ });
+ });
+ }
},