Working proof of concept
This commit is contained in:
parent
0f97197438
commit
64c066ffed
3
.browserslistrc
Normal file
3
.browserslistrc
Normal file
@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
18
README.md
Normal file
18
README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# CuttingBoard
|
||||
|
||||
A web-based cutting board designer.
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
21
TODO.md
Normal file
21
TODO.md
Normal file
@ -0,0 +1,21 @@
|
||||
ToDo
|
||||
====
|
||||
|
||||
Must have
|
||||
----
|
||||
- Implement switching units
|
||||
- Re-ordering of the layers (preferably drag/drop)
|
||||
- Save / load designs (clipboard, or preferably file download/upload - maybe Cloud storage integration later?)
|
||||
|
||||
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!)
|
||||
|
||||
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
|
5
babel.config.js
Normal file
5
babel.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
1
docs/css/app.037fc7fb.css
Normal file
1
docs/css/app.037fc7fb.css
Normal file
@ -0,0 +1 @@
|
||||
.settings[data-v-660f17f9]{display:inline-grid;grid-template-columns:auto auto;grid-column-gap:1em;grid-row-gap:.25em}.settings h2[data-v-660f17f9]{font-size:80%;margin-top:1em;margin-bottom:0;grid-column:1/3}.layers[data-v-535527d4]{display:inline-grid;grid-template-columns:3em 20em 5em 3em;grid-column-gap:1em}.layers .add[data-v-535527d4]{grid-column:2/5;padding-bottom:1em}.layers .header[data-v-535527d4]{font-weight:700;margin-bottom:.25em}.wood[data-v-55181d8c]{display:inline-grid;grid-template-columns:3em 20em 5em 3em;grid-column-gap:1em}.wood .add[data-v-55181d8c]{grid-column:2/5;padding-bottom:1em}.wood .header[data-v-55181d8c]{font-weight:700;margin-bottom:.25em}#app{background-color:#fff;color:#000;font-family:Verdana,Arial,sans-serif;font-size:10pt;display:flex;flex-direction:horizontal}h1{background-color:#f0f0f0;border-bottom:1px solid silver;font-size:100%;margin-top:0;margin-bottom:.5em;padding:.25em}.app-settings{margin-right:1em}.app-settings .block{margin-bottom:2em}.app-preview .preview{margin-bottom:1em}.about,.todo{width:30em}
|
1
docs/index.html
Normal file
1
docs/index.html
Normal file
@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>CuttingBoard</title><link href="/css/app.037fc7fb.css" rel="preload" as="style"><link href="/js/app.f2508f9a.js" rel="preload" as="script"><link href="/js/chunk-vendors.d9b83edc.js" rel="preload" as="script"><link href="/css/app.037fc7fb.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but CuttingBoard doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/js/chunk-vendors.d9b83edc.js"></script><script src="/js/app.f2508f9a.js"></script></body></html>
|
2
docs/js/app.f2508f9a.js
Normal file
2
docs/js/app.f2508f9a.js
Normal file
File diff suppressed because one or more lines are too long
1
docs/js/app.f2508f9a.js.map
Normal file
1
docs/js/app.f2508f9a.js.map
Normal file
File diff suppressed because one or more lines are too long
7
docs/js/chunk-vendors.d9b83edc.js
Normal file
7
docs/js/chunk-vendors.d9b83edc.js
Normal file
File diff suppressed because one or more lines are too long
1
docs/js/chunk-vendors.d9b83edc.js.map
Normal file
1
docs/js/chunk-vendors.d9b83edc.js.map
Normal file
File diff suppressed because one or more lines are too long
11295
package-lock.json
generated
Normal file
11295
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Normal file
22
package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "cuttingboard",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.5",
|
||||
"vue": "^3.0.0",
|
||||
"vuex": "^4.0.0-0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.0.0",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2"
|
||||
}
|
||||
}
|
17
public/index.html
Normal file
17
public/index.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
95
src/App.vue
Normal file
95
src/App.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="app-settings">
|
||||
<h1>Settings</h1>
|
||||
<Settings class="block" />
|
||||
|
||||
<h1>Layers</h1>
|
||||
<Layers class="block" />
|
||||
|
||||
<h1>Wood types</h1>
|
||||
<Wood class="block" />
|
||||
|
||||
<h1>About / feedback</h1>
|
||||
<div class="about">
|
||||
<p>
|
||||
Created by Mark van Renswoude. Open-source and available under the Unlicense to the public domain on <a href="https://github.com/MvRens/CuttingBoard" target="_blank">Github</a>, where feedback is welcome under Issues.
|
||||
</p>
|
||||
<p>
|
||||
Heavily inspired by <a href="http://www.lastalias.com/cbdesigner/">CBdesigner</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-preview">
|
||||
<h1>Edge grain</h1>
|
||||
<EndGrainPreview :scale="1" />
|
||||
|
||||
<h1>End grain</h1>
|
||||
<EdgeGrainPreview :scale="1" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Settings from './components/Settings.vue'
|
||||
import Layers from './components/Layers.vue'
|
||||
import Wood from './components/Wood.vue'
|
||||
import EndGrainPreview from './components/EndGrainPreview.vue'
|
||||
import EdgeGrainPreview from './components/EdgeGrainPreview.vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
EndGrainPreview,
|
||||
EdgeGrainPreview,
|
||||
Settings,
|
||||
Layers,
|
||||
Wood
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#app {
|
||||
background-color: white;
|
||||
color: black;
|
||||
font-family: 'Verdana', 'Arial', sans-serif;
|
||||
font-size: 10pt;
|
||||
|
||||
display: flex;
|
||||
flex-direction: horizontal;
|
||||
}
|
||||
|
||||
|
||||
h1
|
||||
{
|
||||
background-color: #f0f0f0;
|
||||
border-bottom: solid 1px #c0c0c0;
|
||||
font-size: 100%;
|
||||
margin-top: 0;
|
||||
margin-bottom: .5em;
|
||||
padding: .25em;
|
||||
}
|
||||
|
||||
.app-settings
|
||||
{
|
||||
margin-right: 1em;
|
||||
|
||||
.block
|
||||
{
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.app-preview
|
||||
{
|
||||
.preview
|
||||
{
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.about, .todo
|
||||
{
|
||||
width: 30em;
|
||||
}
|
||||
</style>
|
115
src/components/EdgeGrainPreview.vue
Normal file
115
src/components/EdgeGrainPreview.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="preview">
|
||||
<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)" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { units } from '../lib/units';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
scale: Number
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
settings() { return this.$store.state.settings; },
|
||||
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);
|
||||
},
|
||||
|
||||
boardHeight()
|
||||
{
|
||||
return this.layers
|
||||
.map(currentValue => currentValue.width)
|
||||
.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; }
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
toPixels(value)
|
||||
{
|
||||
return units.toPixels(value, this.settings.units);
|
||||
},
|
||||
|
||||
getLayerOffset(index)
|
||||
{
|
||||
if (index < 0 || index >= this.layers.length)
|
||||
return 0;
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (let i = 0; i < index; i++)
|
||||
offset += this.layers[i].width;
|
||||
|
||||
return offset;
|
||||
},
|
||||
|
||||
getLayerStyle(index)
|
||||
{
|
||||
if (index < 0 || index >= this.layers.length)
|
||||
return 'fill: fuchsia';
|
||||
|
||||
const woodIndex = this.layers[index].wood;
|
||||
if (woodIndex === null)
|
||||
return '';
|
||||
|
||||
const borderStyle = this.settings.borders
|
||||
? '; stroke-width: 1; stroke: black'
|
||||
: '';
|
||||
|
||||
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 + ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
84
src/components/EndGrainPreview.vue
Normal file
84
src/components/EndGrainPreview.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="preview">
|
||||
<svg
|
||||
:width="viewportWidth"
|
||||
:height="viewportHeight"
|
||||
:viewBox="viewBox">
|
||||
|
||||
<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>
|
||||
|
||||
<script>
|
||||
import { units } from '../lib/units';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
scale: Number
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
settings() { return this.$store.state.settings; },
|
||||
wood() { return this.$store.state.wood; },
|
||||
layers() { return this.$store.state.boards[0].layers; },
|
||||
|
||||
boardWidth() { return this.toPixels(this.settings.boardLength); },
|
||||
|
||||
boardHeight()
|
||||
{
|
||||
return this.layers
|
||||
.map(currentValue => currentValue.width)
|
||||
.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; }
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
toPixels(value)
|
||||
{
|
||||
return units.toPixels(value, this.settings.units);
|
||||
},
|
||||
|
||||
getLayerOffset(index)
|
||||
{
|
||||
if (index < 0 || index >= this.layers.length)
|
||||
return 0;
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (let i = 0; i < index; i++)
|
||||
offset += this.layers[i].width;
|
||||
|
||||
return offset;
|
||||
},
|
||||
|
||||
getLayerStyle(index)
|
||||
{
|
||||
if (index < 0 || index >= this.layers.length)
|
||||
return 'fill: fuchsia';
|
||||
|
||||
const woodIndex = this.layers[index].wood;
|
||||
if (woodIndex === null)
|
||||
return '';
|
||||
|
||||
const borderStyle = this.settings.borders
|
||||
? '; stroke-width: 1; stroke: black'
|
||||
: '';
|
||||
|
||||
return 'fill: ' + this.wood[woodIndex].color + borderStyle;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
78
src/components/Layers.vue
Normal file
78
src/components/Layers.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="layers">
|
||||
<div class="add">
|
||||
<button @click="addLayer()">Add layer</button>
|
||||
</div>
|
||||
|
||||
<span class="header"> </span>
|
||||
<span class="header">Wood type</span>
|
||||
<span class="header">Width</span>
|
||||
<span class="header"> </span>
|
||||
|
||||
<template v-for="(layer, index) in layers">
|
||||
<div class="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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { units } from '../lib/units';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
settings() { return this.$store.state.settings; },
|
||||
wood() { return this.$store.state.wood; },
|
||||
layers() { return this.$store.state.boards[0].layers; },
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
parseFloatDef(value)
|
||||
{
|
||||
const parsedValue = parseFloat(value);
|
||||
return Object.is(parsedValue, NaN) ? 0 : parsedValue;
|
||||
},
|
||||
|
||||
|
||||
addLayer()
|
||||
{
|
||||
this.$store.commit('addLayer', 0);
|
||||
},
|
||||
|
||||
|
||||
removeLayer(index)
|
||||
{
|
||||
this.$store.commit('removeLayer', { board: 0, layer: index });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layers
|
||||
{
|
||||
display: inline-grid;
|
||||
grid-template-columns: 3em 20em 5em 3em;
|
||||
grid-column-gap: 1em;
|
||||
|
||||
.add
|
||||
{
|
||||
grid-column: 2 / 5;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.header
|
||||
{
|
||||
font-weight: bold;
|
||||
margin-bottom: .25em;
|
||||
}
|
||||
}
|
||||
</style>
|
69
src/components/Settings.vue
Normal file
69
src/components/Settings.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="settings">
|
||||
<h2>Designer</h2>
|
||||
|
||||
<label for="units">Units</label>
|
||||
<select id="units" disabled>
|
||||
<option value="mm">Millimeters</option>
|
||||
<option value="cm">Centimeters</option>
|
||||
<option value="inch">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) })" />
|
||||
|
||||
<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>
|
||||
|
||||
<script>
|
||||
import { units } from '../lib/units';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
settings() { return this.$store.state.settings; }
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
parseFloatDef(value)
|
||||
{
|
||||
const parsedValue = parseFloat(value);
|
||||
return Object.is(parsedValue, NaN) ? 0 : parsedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings
|
||||
{
|
||||
display: inline-grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-column-gap: 1em;
|
||||
grid-row-gap: .25em;
|
||||
|
||||
h2
|
||||
{
|
||||
font-size: 80%;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0;
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
}
|
||||
</style>
|
66
src/components/Wood.vue
Normal file
66
src/components/Wood.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="wood">
|
||||
<div class="add">
|
||||
<button @click="addWood()">Add wood type</button>
|
||||
</div>
|
||||
|
||||
<span class="header"> </span>
|
||||
<span class="header">Name</span>
|
||||
<span class="header">Colour</span>
|
||||
<span class="header"> </span>
|
||||
|
||||
<template v-for="(item, index) in wood">
|
||||
<span> </span>
|
||||
<input type="text" class="name" v-model="item.name" />
|
||||
<input type="color" class="color" v-model="item.color" />
|
||||
|
||||
<div class="remove">
|
||||
<button @click="removeWood(index)">X</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
settings() { return this.$store.state.settings; },
|
||||
wood() { return this.$store.state.wood; }
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
addWood()
|
||||
{
|
||||
this.$store.commit('addWood');
|
||||
},
|
||||
|
||||
|
||||
removeWood(index)
|
||||
{
|
||||
this.$store.commit('removeWood', index);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wood
|
||||
{
|
||||
display: inline-grid;
|
||||
grid-template-columns: 3em 20em 5em 3em;
|
||||
grid-column-gap: 1em;
|
||||
|
||||
.add
|
||||
{
|
||||
grid-column: 2 / 5;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.header
|
||||
{
|
||||
font-weight: bold;
|
||||
margin-bottom: .25em;
|
||||
}
|
||||
}
|
||||
</style>
|
43
src/lib/units.js
Normal file
43
src/lib/units.js
Normal file
@ -0,0 +1,43 @@
|
||||
const millimetersPerInch = 25.4;
|
||||
const millimetersPerCentimeter = 10;
|
||||
const pixelsPerMillimeter = 1;
|
||||
|
||||
|
||||
|
||||
const units = {
|
||||
toPixels(value, units)
|
||||
{
|
||||
return this.toMillimeters(value, units) * pixelsPerMillimeter;
|
||||
},
|
||||
|
||||
|
||||
toMillimeters(value, units)
|
||||
{
|
||||
switch (units)
|
||||
{
|
||||
case 'mm': return value;
|
||||
case 'cm': return value * millimetersPerCentimeter;
|
||||
case 'inch': return value * millimetersPerInch;
|
||||
}
|
||||
|
||||
console.error('Invalid units type: ' + units);
|
||||
return 0;
|
||||
},
|
||||
|
||||
|
||||
fromMillimeters(value, units)
|
||||
{
|
||||
switch (units)
|
||||
{
|
||||
case 'mm': return value;
|
||||
case 'cm': return value / millimetersPerCentimeter;
|
||||
case 'inch': return value / millimetersPerInch;
|
||||
}
|
||||
|
||||
console.error('Invalid units type: ' + units);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export { units }
|
5
src/main.js
Normal file
5
src/main.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import store from './store'
|
||||
|
||||
createApp(App).use(store).mount('#app')
|
118
src/store.js
Normal file
118
src/store.js
Normal file
@ -0,0 +1,118 @@
|
||||
import { createStore } from 'vuex';
|
||||
import { units } from './lib/units';
|
||||
|
||||
export default createStore({
|
||||
state: {
|
||||
settings: {
|
||||
units: 'mm',
|
||||
borders: false,
|
||||
boardThickness: 20,
|
||||
boardLength: 700,
|
||||
bladeKerf: 3.5,
|
||||
crosscutWidth: 30,
|
||||
|
||||
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: 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 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
mutations: {
|
||||
addLayer(state, board)
|
||||
{
|
||||
if (board < 0 || board >= state.boards.length)
|
||||
return;
|
||||
|
||||
state.boards[board].layers.push({
|
||||
wood: 0,
|
||||
width: units.fromMillimeters(20, state.settings.units)
|
||||
});
|
||||
},
|
||||
|
||||
removeLayer(state, payload)
|
||||
{
|
||||
if (payload.board < 0 || payload.board >= state.boards.length)
|
||||
return;
|
||||
|
||||
if (payload.layer < 0 || payload.layer >= state.boards[payload.board].length)
|
||||
return;
|
||||
|
||||
state.boards[payload.board].layers.splice(payload.layer, 1);
|
||||
},
|
||||
|
||||
|
||||
addWood(state)
|
||||
{
|
||||
state.wood.push({
|
||||
name: 'Wood #' + (state.wood.length + 1),
|
||||
color: '#f2e0aa'
|
||||
});
|
||||
},
|
||||
|
||||
removeWood(state, index)
|
||||
{
|
||||
if (index < 0 || index >= state.wood.length)
|
||||
return;
|
||||
|
||||
// Update all layers
|
||||
state.boards.forEach(board =>
|
||||
{
|
||||
board.layers.forEach(layer =>
|
||||
{
|
||||
if (layer.wood === index)
|
||||
layer.wood = null
|
||||
else if (layer.wood > index)
|
||||
layer.wood--;
|
||||
});
|
||||
});
|
||||
|
||||
state.wood.splice(index, 1);
|
||||
},
|
||||
|
||||
|
||||
updateSettings(state, payload)
|
||||
{
|
||||
for (const property in payload)
|
||||
{
|
||||
if (!payload.hasOwnProperty(property) || !state.settings.hasOwnProperty(property))
|
||||
continue;
|
||||
|
||||
state.settings[property] = payload[property];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
}
|
||||
})
|
11
vue.config.js
Normal file
11
vue.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
outputDir: 'docs',
|
||||
chainWebpack: config => {
|
||||
config
|
||||
.plugin('html')
|
||||
.tap(args => {
|
||||
args[0].title = 'CuttingBoard';
|
||||
return args;
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user