summaryrefslogtreecommitdiff
path: root/electron/src/renderer/assets/js/simulator.js
diff options
context:
space:
mode:
authorLex Neva <lexelby@users.noreply.github.com>2020-04-28 12:34:05 -0400
committerGitHub <noreply@github.com>2020-04-28 18:34:05 +0200
commitcb2b4e3522cb7f426ba5ad3e68deb9e6ccc581ec (patch)
tree2a2f38823c3c9f0b5439ce2aa7c9040299109292 /electron/src/renderer/assets/js/simulator.js
parenteb526927e16954390d06929535d6f5c3766b5f6c (diff)
electron simulator (#531)
Diffstat (limited to 'electron/src/renderer/assets/js/simulator.js')
-rw-r--r--electron/src/renderer/assets/js/simulator.js583
1 files changed, 583 insertions, 0 deletions
diff --git a/electron/src/renderer/assets/js/simulator.js b/electron/src/renderer/assets/js/simulator.js
new file mode 100644
index 00000000..638f0ca2
--- /dev/null
+++ b/electron/src/renderer/assets/js/simulator.js
@@ -0,0 +1,583 @@
+const inkStitch = require("../../../lib/api")
+const Mousetrap = require("mousetrap")
+import { SVG } from '@svgdotjs/svg.js'
+require('@svgdotjs/svg.panzoom.js/src/svg.panzoom.js')
+require('@svgdotjs/svg.filter.js')
+const svgpath = require('svgpath')
+import Loading from 'vue-loading-overlay';
+import 'vue-loading-overlay/dist/vue-loading.css';
+import VueSlider from 'vue-slider-component'
+import 'vue-slider-component/theme/default.css'
+
+const throttle = require('lodash.throttle')
+
+function SliderMark(command, icon) {
+ this.label = ""
+ this.command = command
+ this.icon = icon
+}
+
+export default {
+ name: 'simulator',
+ components: {
+ Loading,
+ VueSlider
+ },
+ data: function () {
+ return {
+ loading: false,
+ controlsExpanded: true,
+ infoExpanded: false,
+ infoMaxHeight: 0,
+ speed: 16,
+ currentStitch: 1,
+ currentStitchDisplay: 1,
+ direction: 1,
+ numStitches: 1,
+ animating: false,
+ sliderProcess: dotPos => this.sliderColorSections,
+ showTrims: false,
+ showJumps: false,
+ showColorChanges: false,
+ showStops: false,
+ showNeedlePenetrationPoints: false,
+ showRealisticPreview: false,
+ showCursor: true
+ }
+ },
+ watch: {
+ currentStitch: throttle(function () {
+ this.currentStitchDisplay = Math.floor(this.currentStitch)
+ }, 100, {leading: true, trailing: true}),
+ showNeedlePenetrationPoints: function () {
+ if (this.needlePenetrationPoints === null) {
+ return;
+ }
+
+ this.needlePenetrationPoints.forEach(npp => {
+ if (this.showNeedlePenetrationPoints) {
+ npp.show()
+ } else {
+ npp.hide()
+ }
+ })
+ },
+ showRealisticPreview() {
+ let animating = this.animating
+ this.stop()
+
+ if (this.showRealisticPreview) {
+ if (this.realisticPreview === null) {
+ // This workflow should be improved and might be a bit unconventional.
+ // We don't want to make the user wait for it too long.
+ // It would be best, if the realistic preview could load before it is actually requested.
+ this.$nextTick(() => {this.loading=true})
+ setImmediate(()=> {this.generateRealisticPaths()})
+ setImmediate(()=> {this.loading = false})
+ }
+
+ setImmediate(()=> {
+ for (let i = 1; i < this.stitches.length; i++) {
+ if (i < this.currentStitch) {
+ this.realisticPaths[i].show()
+ } else {
+ this.realisticPaths[i].hide()
+ }
+ }
+
+ this.simulation.hide()
+ this.realisticPreview.show()
+ })
+
+ } else {
+
+ for (let i = 1; i < this.stitches.length; i++) {
+ if (i < this.currentStitch) {
+ this.stitchPaths[i].show()
+ } else {
+ this.stitchPaths[i].hide()
+ }
+ }
+
+ this.simulation.show()
+ this.realisticPreview.hide()
+
+ }
+ if (animating) {
+ this.start()
+ }
+ },
+ showCursor: function () {
+ if (this.showCursor) {
+ this.cursor.show()
+ } else {
+ this.cursor.hide()
+ }
+ }
+ },
+ computed: {
+ speedDisplay() {
+ return this.speed * this.direction
+ },
+ currentCommand() {
+ let stitch = this.stitches[Math.floor(this.currentStitch)]
+
+ if (stitch === undefined || stitch === null) {
+ return ""
+ }
+
+ let label = "STITCH"
+ switch (true) {
+ case stitch.jump:
+ label = this.$gettext("JUMP")
+ break
+ case stitch.trim:
+ label = this.$gettext("TRIM")
+ break
+ case stitch.stop:
+ label = this.$gettext("STOP")
+ break
+ case stitch.color_change:
+ label = this.$gettext("COLOR CHANGE")
+ break
+ }
+
+ return label
+ },
+ paused() {
+ return !this.animating
+ },
+ forward() {
+ return this.direction > 0
+ },
+ reverse() {
+ return this.direction < 0
+ },
+ sliderMarks() {
+ var marks = {}
+
+ if (this.showTrims)
+ Object.assign(marks, this.trimMarks);
+
+ if (this.showJumps)
+ Object.assign(marks, this.jumpMarks);
+
+ if (this.showColorChanges)
+ Object.assign(marks, this.colorChangeMarks);
+
+ if (this.showStops)
+ Object.assign(marks, this.stopMarks);
+
+ return marks
+ }
+ },
+ methods: {
+ toggleInfo() {
+ this.infoExpanded = !this.infoExpanded;
+ this.infoMaxHeight = this.$refs.controlInfoButton.getBoundingClientRect().top;
+ },
+ toggleControls() {
+ this.controlsExpanded = !this.controlsExpanded;
+ },
+ animationSpeedUp() {
+ this.speed *= 2.0
+ },
+ animationSlowDown() {
+ this.speed = Math.max(this.speed / 2.0, 1)
+ },
+ animationReverse() {
+ this.direction = -1
+ this.start()
+ },
+ animationForward() {
+ this.direction = 1
+ this.start()
+ },
+ toggleAnimation(e) {
+ if (this.animating) {
+ this.stop()
+ } else {
+ this.start()
+ }
+
+ e.preventDefault();
+ },
+ animationForwardOneStitch() {
+ this.setCurrentStitch(this.currentStitch + 1)
+ },
+ animationBackwardOneStitch() {
+ this.setCurrentStitch(this.currentStitch - 1)
+ },
+ animationNextCommand() {
+ let nextCommandIndex = this.getNextCommandIndex()
+ if (nextCommandIndex === -1) {
+ this.setCurrentStitch(this.stitches.length)
+ } else {
+ this.setCurrentStitch(this.commandList[nextCommandIndex])
+ }
+ },
+ animationPreviousCommand() {
+ let nextCommandIndex = this.getNextCommandIndex()
+ let prevCommandIndex = 0
+ if (nextCommandIndex === -1) {
+ prevCommandIndex = this.commandList.length - 2
+ } else {
+ prevCommandIndex = nextCommandIndex - 2
+ }
+ let previousCommand = this.commandList[prevCommandIndex]
+ if (previousCommand === undefined) {
+ previousCommand = 1
+ }
+ this.setCurrentStitch(previousCommand)
+ },
+ getNextCommandIndex() {
+ let currentStitch = this.currentStitchDisplay
+ let nextCommand = this.commandList.findIndex(function (command) {
+ return command > currentStitch
+ })
+ return nextCommand
+ },
+ onCurrentStitchEntered() {
+ let newCurrentStitch = parseInt(this.$refs.currentStitchInput.value)
+
+ if (isNaN(newCurrentStitch)) {
+ this.$refs.currentStitchInput.value = Math.floor(this.currentStitch)
+ } else {
+ this.setCurrentStitch(parseInt(newCurrentStitch))
+ }
+ },
+ setCurrentStitch(newCurrentStitch) {
+ this.stop()
+ this.currentStitch = newCurrentStitch
+ this.clampCurrentStitch()
+ this.renderFrame()
+ },
+ clampCurrentStitch() {
+ this.currentStitch = Math.max(Math.min(this.currentStitch, this.numStitches), 0)
+ },
+ animate() {
+ let frameStart = performance.now()
+ let frameTime = null
+
+ if (this.lastFrameStart !== null) {
+ frameTime = frameStart - this.lastFrameStart
+ } else {
+ frameTime = this.targetFramePeriod
+ }
+
+ this.lastFrameStart = frameStart
+
+ let numStitches = this.speed * Math.max(frameTime, this.targetFramePeriod) / 1000.0;
+ this.currentStitch = this.currentStitch + numStitches * this.direction
+ this.clampCurrentStitch()
+
+ this.renderFrame()
+
+ if (this.animating && this.shouldAnimate()) {
+ this.timer = setTimeout(this.animate, Math.max(0, this.targetFramePeriod - frameTime))
+ } else {
+ this.timer = null;
+ this.stop()
+ }
+ },
+ renderFrame() {
+ while (this.renderedStitch < this.currentStitch) {
+ this.renderedStitch += 1
+ if (this.showRealisticPreview) {
+ this.realisticPaths[this.renderedStitch].show()
+ } else {
+ this.stitchPaths[this.renderedStitch].show();
+ }
+ }
+
+ while (this.renderedStitch > this.currentStitch) {
+ if (this.showRealisticPreview) {
+ this.realisticPaths[this.renderedStitch].hide()
+ } else {
+ this.stitchPaths[this.renderedStitch].hide();
+ }
+ this.renderedStitch -= 1
+ }
+
+ this.moveCursor()
+ },
+ shouldAnimate() {
+ if (this.direction == 1 && this.currentStitch < this.numStitches) {
+ return true;
+ } else if (this.direction == -1 && this.currentStitch > 0) {
+ return true;
+ } else {
+ return false;
+ }
+ },
+ start() {
+ if (!this.animating && this.shouldAnimate()) {
+ this.animating = true
+ this.timer = setTimeout(this.animate, 0);
+ }
+ },
+ stop() {
+ if (this.animating) {
+ if (this.timer) {
+ clearTimeout(this.timer)
+ this.timer = null
+ }
+ this.animating = false
+ this.lastFrameStart = null
+ }
+ },
+ resizeCursor() {
+ // This makes the cursor stay the same size when zooming in or out.
+ // I'm not exactly sure how it works, but it does.
+ this.cursor.size(25 / this.svg.zoom())
+ this.cursor.stroke({width: 2 / this.svg.zoom()})
+
+ // SVG.js seems to move the cursor when we resize it, so we need to put
+ // it back where it goes.
+ this.moveCursor()
+
+ this.adjustScale()
+ },
+ moveCursor() {
+ let stitch = this.stitches[Math.floor(this.currentStitch)]
+ if (stitch === null || stitch === undefined) {
+ this.cursor.hide()
+ } else if (this.showCursor) {
+ this.cursor.show()
+ this.cursor.center(stitch.x, stitch.y)
+ }
+ },
+ adjustScale: throttle(function () {
+ let one_mm = 96 / 25.4 * this.svg.zoom();
+ let scaleWidth = one_mm
+ let simulatorWidth = this.$refs.simulator.getBoundingClientRect().width
+ let maxWidth = Math.min(simulatorWidth / 2, 300)
+
+ while (scaleWidth > maxWidth) {
+ scaleWidth /= 2.0
+ }
+
+ while (scaleWidth < 100) {
+ scaleWidth += one_mm
+ }
+
+ let scaleMM = scaleWidth / one_mm
+
+ this.scale.plot(`M0,0 v10 h${scaleWidth / 2} v-5 v5 h${scaleWidth / 2} v-10`)
+
+ // round and strip trailing zeros, source: https://stackoverflow.com/a/53397618
+ let mm = scaleMM.toFixed(8).replace(/([0-9]+(\.[0-9]+[1-9])?)(\.?0+$)/, '$1')
+ this.scaleLabel.text(`${mm} mm`)
+ }, 100, {leading: true, trailing: true}
+ ),
+ generateMarks() {
+ this.commandList = Array()
+ for (let i = 1; i < this.stitches.length; i++) {
+ if (this.stitches[i].trim) {
+ this.trimMarks[i] = new SliderMark("trim", "cut")
+ this.commandList.push(i)
+ } else if (this.stitches[i].stop) {
+ this.stopMarks[i] = new SliderMark("stop", "pause")
+ this.commandList.push(i)
+ } else if (this.stitches[i].jump) {
+ this.jumpMarks[i] = new SliderMark("jump", "frog")
+ this.commandList.push(i)
+ } else if (this.stitches[i].color_change) {
+ this.colorChangeMarks[i] = new SliderMark("color-change", "exchange-alt")
+ this.commandList.push(i)
+ }
+ }
+ },
+ generateColorSections() {
+ var currentStitch = 0
+ this.stitchPlan.color_blocks.forEach(color_block => {
+ this.sliderColorSections.push([
+ (currentStitch + 1) / this.numStitches * 100,
+ (currentStitch + color_block.stitches.length) / this.numStitches * 100,
+ {backgroundColor: color_block.color.visible_on_white.hex}
+ ])
+ currentStitch += color_block.stitches.length
+ })
+ },
+ generateMarker(color) {
+ return this.svg.marker(3, 3, add => {
+ let needlePenetrationPoint = add.circle(3).fill(color).hide()
+ this.needlePenetrationPoints.push(needlePenetrationPoint)
+ })
+ },
+ generateScale() {
+ let svg = SVG().addTo(this.$refs.simulator)
+ svg.node.classList.add("simulation-scale")
+ this.scale = svg.path("M0,0").stroke({color: "black", width: "1px"}).fill("none")
+ this.scaleLabel = svg.text("0 mm").move(0, 12)
+ this.scaleLabel.node.classList.add("simulation-scale-label")
+ },
+ generateCursor() {
+ this.cursor =
+ this.svg.path("M0,0 v2.8 h1.2 v-2.8 h2.8 v-1.2 h-2.8 v-2.8 h-1.2 v2.8 h-2.8 v1.2 h2.8")
+ .stroke({
+ width: 0.1,
+ color: '#FFFFFF',
+ })
+ .fill('#000000')
+ this.cursor.node.classList.add("cursor")
+ },
+ generateRealisticPaths() {
+
+ // Create Realistic Filter
+ this.filter = this.svg.defs().filter()
+
+ this.filter.attr({id: "realistic-stitch-filter", x: "-0.1", y: "-0.1", height: "1.2", width: "1.2", style: "color-interpolation-filters:sRGB"})
+ this.filter.gaussianBlur({id: "gaussianBlur1", stdDeviation: "1.5", in: "SourceAlpha"})
+ this.filter.componentTransfer(function (add) {
+ add.funcR({ type: "identity" }),
+ add.funcG({ type: "identity" }),
+ add.funcB({ type: "identity", slope: "4.53" }),
+ add.funcA({ type: "gamma", slope: "0.149", intercept: "0", amplitude: "3.13", offset: "-0.33" })
+ }).attr({id: "componentTransfer1", in: "gaussianBlur1"})
+ this.filter.composite({id: "composite1", in: "componentTransfer1", in2: "SourceAlpha", operator: "in"})
+ this.filter.gaussianBlur({id: "gaussianBlur2", in: "composite1", stdDeviation: 0.09})
+ this.filter.morphology({id: "morphology1", in: "gaussianBlur2", operator: "dilate", radius: 0.1})
+ this.filter.specularLighting({id: "specularLighting1", in: "morphology1", specularConstant: 0.709, surfaceScale: 30}).pointLight({z: 10})
+ this.filter.gaussianBlur({id: "gaussianBlur3", in: "specularLighting1", stdDeviation: 0.04})
+ this.filter.composite({id: "composite2", in: "gaussianBlur3", in2: "SourceGraphic", operator: "arithmetic", k2: 1, k3: 1, k1: 0, k4: 0})
+ this.filter.composite({in: "composite2", in2: "SourceAlpha", operator: "in"})
+
+ // Create realistic paths in it's own group and move it behind the cursor
+ this.realisticPreview = this.svg.group({id: 'realistic'}).backward()
+
+ this.stitchPlan.color_blocks.forEach(color_block => {
+ let color = `${color_block.color.visible_on_white.hex}`
+ let realistic_path_attrs = {fill: color, stroke: "none", filter: this.filter}
+
+ let stitching = false
+ let prevStitch = null
+ color_block.stitches.forEach(stitch => {
+
+ let realisticPath = null
+ if (stitching && prevStitch) {
+
+ // Position
+ let stitch_center = []
+ stitch_center.x = (prevStitch.x + stitch.x) / 2.0
+ stitch_center.y = (prevStitch.y + stitch.y) / 2.0
+
+ // Angle
+ var stitch_angle = Math.atan2(stitch.y - prevStitch.y, stitch.x - prevStitch.x) * (180 / Math.PI)
+
+ // Length
+ let path_length = Math.hypot(stitch.x - prevStitch.x, stitch.y - prevStitch.y)
+
+ var path = `M0,0 c 0.4,0,0.4,0.3,0.4,0.6 c 0,0.3,-0.1,0.6,-0.4,0.6 v 0.2,-0.2 h -${path_length} c -0.4,0,-0.4,-0.3,-0.4,-0.6 c 0,-0.3,0.1,-0.6,0.4,-0.6 v -0.2,0.2 z`
+ path = svgpath(path).rotate(stitch_angle).toString()
+
+ realisticPath = this.realisticPreview.path(path).attr(realistic_path_attrs).center(stitch_center.x, stitch_center.y).hide()
+
+ } else {
+ realisticPath = this.realisticPreview.rect(0, 1).attr(realistic_path_attrs).center(stitch.x, stitch.y).hide()
+ }
+
+ this.realisticPaths.push(realisticPath)
+
+ if (stitch.trim || stitch.color_change) {
+ stitching = false
+ } else if (!stitch.jump) {
+ stitching = true
+ }
+ prevStitch = stitch
+ })
+ })
+ }
+ },
+ created: function () {
+ // non-reactive properties
+ this.targetFPS = 30
+ this.targetFramePeriod = 1000.0 / this.targetFPS
+ this.renderedStitch = 0
+ this.lastFrameStart = null
+ this.stitchPaths = [null] // 1-indexed to match up with stitch number display
+ this.realisticPaths = [null]
+ this.stitches = [null]
+ this.svg = null
+ this.simulation = null
+ this.realisticPreview = null
+ this.timer = null
+ this.sliderColorSections = []
+ this.trimMarks = {}
+ this.stopMarks = {}
+ this.colorChangeMarks = {}
+ this.jumpMarks = {}
+ this.needlePenetrationPoints = []
+ this.cursor = null
+ },
+ mounted: function () {
+ this.svg = SVG().addTo(this.$refs.simulator).size('100%', '100%').panZoom({zoomMin: 0.1})
+ this.svg.node.classList.add('simulation')
+ this.simulation = this.svg.group({id: 'line'})
+
+ this.loading = true
+
+ inkStitch.get('stitch_plan').then(response => {
+ this.stitchPlan = response.data
+ let [minx, miny, maxx, maxy] = this.stitchPlan.bounding_box
+ let width = maxx - minx
+ let height = maxy - miny
+ this.svg.viewbox(0, 0, width, height);
+
+ this.stitchPlan.color_blocks.forEach(color_block => {
+ let color = `${color_block.color.visible_on_white.hex}`
+ let path_attrs = {fill: "none", stroke: color, "stroke-width": 0.3}
+ let marker = this.generateMarker(color)
+
+ let stitching = false
+ let prevStitch = null
+ color_block.stitches.forEach(stitch => {
+ stitch.x -= minx
+ stitch.y -= miny
+
+ let path = null
+ if (stitching && prevStitch) {
+ path = this.simulation.path(`M${prevStitch.x},${prevStitch.y} ${stitch.x},${stitch.y}`).attr(path_attrs).hide()
+ } else {
+ path = this.simulation.path(`M${stitch.x},${stitch.y} ${stitch.x},${stitch.y}`).attr(path_attrs).hide()
+ }
+ path.marker('end', marker)
+ this.stitchPaths.push(path)
+ this.stitches.push(stitch)
+
+ if (stitch.trim || stitch.color_change) {
+ stitching = false
+ } else if (!stitch.jump) {
+ stitching = true
+ }
+
+ prevStitch = stitch
+ })
+ })
+
+ this.numStitches = this.stitches.length - 1
+ this.generateMarks()
+ this.generateColorSections()
+ this.generateScale()
+ this.generateCursor()
+
+ this.loading = false
+
+ // v-on:keydown doesn't seem to work, maybe an Electron issue?
+ Mousetrap.bind("up", this.animationSpeedUp)
+ Mousetrap.bind("down", this.animationSlowDown)
+ Mousetrap.bind("left", this.animationReverse)
+ Mousetrap.bind("right", this.animationForward)
+ Mousetrap.bind("pagedown", this.animationPreviousCommand)
+ Mousetrap.bind("pageup", this.animationNextCommand)
+ Mousetrap.bind("space", this.toggleAnimation)
+ Mousetrap.bind("+", this.animationForwardOneStitch)
+ Mousetrap.bind("-", this.animationBackwardOneStitch)
+
+ this.svg.on('zoom', this.resizeCursor)
+ this.resizeCursor()
+
+ this.start()
+ })
+ }
+}