Added units switching

Added layer reordering
Fixed a few issues related to non-millimeter units
Edge/End components were incorrectly named, whoops
This commit is contained in:
Mark van Renswoude 2020-12-28 21:20:51 +01:00
parent ef79e6891b
commit ce1e31432f
8 changed files with 324 additions and 93 deletions

View File

@ -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

View File

@ -17,7 +17,7 @@
</p>
<p>
<input type="file" id="loadFile" accept=".json" />
<input type="file" ref="loadFile" accept=".json" />
<button @click="load()">Load</button>
</p>
</div>
@ -35,10 +35,10 @@
<div class="app-preview">
<h1>Edge grain</h1>
<EndGrainPreview :scale="1" />
<EdgeGrainPreview :scale="1" />
<h1>End grain</h1>
<EdgeGrainPreview :scale="1" />
<EndGrainPreview :scale="1" />
</div>
</template>
@ -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;
}

View File

@ -1,27 +1,19 @@
<template>
<div class="preview">
<div class="dimensions">Dimensions: {{ display(boardWidth) }} x {{ display(boardHeight) }} x {{ display(settings.boardThickness) }}</div>
<svg
:width="viewportWidth"
:height="viewportHeight"
:viewBox="viewBox">
<defs>
<g id="strip">
<rect
v-for="(layer, index) in layers"
:width="toPixels(settings.boardThickness)"
:height="toPixels(layer.width)"
x="0"
:y="getLayerOffset(index)"
:style="getLayerStyle(index)" />
</g>
</defs>
<use
v-for="(strip, index) in stripsPerBoard"
xlink:href="#strip"
:x="index * settings.boardThickness"
y="0"
:transform="getLayerTransform(index)" />
<rect
v-for="(layer, index) in layers"
:width="toPixels(settings.boardLength)"
:height="toPixels(layer.width)"
x="0"
:y="getLayerOffset(index)"
:style="getLayerStyle(index)" />
</svg>
</div>
</template>
@ -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 + ')';
}
}
}

View File

@ -1,17 +1,29 @@
<template>
<div class="preview">
<div class="dimensions">Dimensions: {{ display(boardWidth) }} x {{ display(boardHeight) }} x {{ display(settings.crosscutWidth) }}</div>
<svg
:width="viewportWidth"
:height="viewportHeight"
:viewBox="viewBox">
<defs>
<g id="strip">
<rect
v-for="(layer, index) in layers"
:width="toPixels(settings.boardThickness)"
:height="toPixels(layer.width)"
x="0"
:y="getLayerOffset(index)"
:style="getLayerStyle(index)" />
</g>
</defs>
<rect
v-for="(layer, index) in layers"
:width="toPixels(settings.boardLength)"
:height="toPixels(layer.width)"
x="0"
:y="getLayerOffset(index)"
:style="getLayerStyle(index)" />
<use
v-for="(strip, index) in stripsPerBoard"
xlink:href="#strip"
:x="toPixels(index * settings.boardThickness)"
y="0"
:transform="getLayerTransform(index)" />
</svg>
</div>
</template>
@ -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 + ')';
}
}
}

View File

@ -4,13 +4,18 @@
<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">&nbsp;</span>
<span class="header">Wood type</span>
<span class="header">Width</span>
<span class="header">&nbsp;</span>
<template v-for="(layer, index) in layers">
<div class="index">{{ index + 1 }}</div>
<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>
@ -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;

View File

@ -3,10 +3,11 @@
<h2>Designer</h2>
<label for="units">Units</label>
<select id="units" disabled>
<select id="units" :value="settings.units" @change="$store.commit('updateSettings', { units: $event.target.value })">
<option value="mm">Millimeters</option>
<option value="cm">Centimeters</option>
<option value="inch">Inches (fractional)</option>
<option value="inchdecimal">Inches (decimal)</option>
<!--<option value="inchfractional">Inches (fractional)</option>-->
</select>
<label for="borders">Show borders</label>

View File

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

View File

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