From 5459b686891c01334dee0cbb428a532cc6bef842 Mon Sep 17 00:00:00 2001 From: Guillaume Besson Date: Sun, 3 Apr 2022 19:32:05 +0200 Subject: [PATCH] Add option to export a png preview of the path --- package.json | 1 + src/common/util.js | 2 + src/features/exporter/Downloader.js | 54 +++++++++++--------------- src/features/exporter/exporterSlice.js | 1 + src/features/preview/PreviewLayer.js | 8 ++-- src/features/preview/PreviewWindow.js | 27 ++++++++++--- src/models/Exporter.js | 3 ++ 7 files changed, 56 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index 25cfc406..12dbfdee 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "d3": "^6.7.0", "d3-fisheye": "^2.0.1", "eslint-config-react-app": "^6.0.0", + "file-saver": "^2.0.5", "gcode-toolpath": "^2.2.0", "javascript-algorithms": "0.0.5", "jest-canvas-mock": "^2.3.1", diff --git a/src/common/util.js b/src/common/util.js index 39d27c1b..461c7351 100644 --- a/src/common/util.js +++ b/src/common/util.js @@ -35,3 +35,5 @@ const debug = false export const log = (message) => { if (debug) { console.log(message) } } + +export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/src/features/exporter/Downloader.js b/src/features/exporter/Downloader.js index d00f758b..74ef95fc 100644 --- a/src/features/exporter/Downloader.js +++ b/src/features/exporter/Downloader.js @@ -1,5 +1,6 @@ import React, { Component } from 'react' import { connect } from 'react-redux' +import { saveAs } from 'file-saver'; import { Button, Modal, Col, Row } from 'react-bootstrap' import DropdownOption from '../../components/DropdownOption' import InputOption from '../../components/InputOption' @@ -13,6 +14,7 @@ import ScaraGCodeExporter from './ScaraGCodeExporter' import SvgExporter from './SvgExporter' import ThetaRhoExporter from './ThetaRhoExporter' import { Exporter, GCODE, THETARHO, SVG, SCARA } from '../../models/Exporter' +import { exportCurrentPreviewWindow } from '../preview/PreviewWindow' const exporters = { [GCODE]: GCodeExporter, @@ -24,6 +26,7 @@ const exporters = { const mapStateToProps = (state, ownProps) => { return { reverse: state.exporter.reverse, + pngPreview: state.exporter.pngPreview, show: state.exporter.show, vertices: getAllComputedVertices(state), comments: getComments(state), @@ -75,7 +78,7 @@ class Downloader extends Component { }) } - download() { + async download() { let exporter = new exporters[this.props.fileType](this.props) let startTime = performance.now() let fileName = this.props.fileName @@ -87,7 +90,16 @@ class Downloader extends Component { } this.gaRecord(exporter.label) - this.downloadFile(fileName, exporter.lines.join("\n")) + saveAs(new Blob([exporter.lines.join("\n")], { + type: this.props.fileType === SVG ? 'image/svg+xml;charset=utf-8' : 'text/plain;charset=utf-8' + }), fileName); + + if (this.props.pngPreview) { + const preview = await exportCurrentPreviewWindow(); + if (preview) { + saveAs(preview, `${fileName.match(/(.*)\.\w+$/)[1]}.png`); + } + } this.props.close() let endTime = performance.now() @@ -98,35 +110,6 @@ class Downloader extends Component { }) } - // Helper function to take a string and make the user download a text file with that text as the - // content. I don't really understand this, but I took it from here, and it seems to work: - // https://stackoverflow.com/a/18197511 - downloadFile(fileName, text) { - let link = document.createElement('a') - link.download = fileName - - let fileType = 'text/plain;charset=utf-8' - if (this.props.fileType === SVG) { - fileType = 'image/svg+xml;charset=utf-8' - } - let blob = new Blob([text],{type: fileType}) - - // Windows Edge fix - if (window.navigator && window.navigator.msSaveOrOpenBlob) { - window.navigator.msSaveOrOpenBlob(blob, fileName) - } else { - link.href = URL.createObjectURL(blob) - if (document.createEvent) { - var event = document.createEvent('MouseEvents') - event.initEvent('click', true, true) - link.dispatchEvent(event) - } else { - link.click() - } - URL.revokeObjectURL(link.href) - } - } - render() { return (
@@ -212,6 +195,15 @@ class Downloader extends Component { index={5} model={this.props} />
+
+ +
diff --git a/src/features/exporter/exporterSlice.js b/src/features/exporter/exporterSlice.js index 04e2958b..75208459 100644 --- a/src/features/exporter/exporterSlice.js +++ b/src/features/exporter/exporterSlice.js @@ -30,6 +30,7 @@ const exporterSlice = createSlice({ pre: localStorage.getItem('export_pre') ? localStorage.getItem('export_pre') : '', post: localStorage.getItem('export_post') ? localStorage.getItem('export_post') : '', reverse: false, + pngPreview: localStorage.getItem('export_pngPreview') !== null ? Boolean(localStorage.getItem('png_preview')) : false, show: false, polarRhoMax: localStorage.getItem('export_polarRhoMax') ? localStorage.getItem('export_polarRhoMax') : 1.0, unitsPerCircle: localStorage.getItem('export_unitsPerCircle') ? localStorage.getItem('export_unitsPerCircle') : 6.0 diff --git a/src/features/preview/PreviewLayer.js b/src/features/preview/PreviewLayer.js index 0f952a61..5cb295f4 100644 --- a/src/features/preview/PreviewLayer.js +++ b/src/features/preview/PreviewLayer.js @@ -131,7 +131,7 @@ const PreviewLayer = (ownProps) => { drawLayerVertices(context, props.bounds) - if (props.start || props.end || isSelected) { + if (!ownProps.exportMode && (props.start || props.end || isSelected)) { drawStartAndEndPoints(context) } helper.drawSliderEndPoint(context) @@ -159,12 +159,12 @@ const PreviewLayer = (ownProps) => { const trRef = React.createRef() React.useEffect(() => { - if (props.layer.visible && isSelected && props.layer.canChangeSize) { + if (props.layer.visible && isSelected && props.layer.canChangeSize && !ownProps.exportMode) { // we need to attach transformer manually trRef.current.nodes([shapeRef.current]) trRef.current.getLayer().batchDraw() } - }, [isSelected, props.layer, props.currentLayer.canMove, shapeRef, trRef]) + }, [isSelected, props.layer, props.currentLayer.canMove, shapeRef, trRef, ownProps.exportMode]) return ( @@ -214,7 +214,7 @@ const PreviewLayer = (ownProps) => { }) }} />} - {props.layer.visible && isSelected && props.layer.canChangeSize && ( + {props.layer.visible && isSelected && props.layer.canChangeSize && !ownProps.exportMode && ( { + if (getPreview) { + return getPreview(); + } +} + const mapStateToProps = (state, ownProps) => { return { layers: state.layers, @@ -43,6 +50,8 @@ const mapDispatchToProps = (dispatch, ownProps) => { // Contains the preview window, and any parameters for the machine. class PreviewWindow extends Component { + state = {exportMode: false}; + componentDidMount() { const wrapper = document.getElementById('preview-wrapper') @@ -51,7 +60,15 @@ class PreviewWindow extends Component { setTimeout(() => { this.visible = true this.resize(wrapper) - }, 250) + }, 250); + + getPreview = async () => { + this.setState({exportMode: true}); + await sleep(0); // sleep to let react re-render the canvas before we export it + const preview = await new Promise((resolve) => this.stageRef.toCanvas().toBlob(resolve)); + this.setState({exportMode: false}); + return preview; + } } resize(wrapper) { @@ -87,7 +104,7 @@ class PreviewWindow extends Component { // which is not our usual React Component {({store}) => ( - this.stageRef = ref} className={visibilityClass} scaleX={scale * reduceScale} scaleY={scale * reduceScale} height={height * scale} @@ -123,7 +140,7 @@ class PreviewWindow extends Component { return ( [ nextId && , - + ].filter(e => e !== null) ) }).flat()} diff --git a/src/models/Exporter.js b/src/models/Exporter.js index 787c1765..7380b029 100644 --- a/src/models/Exporter.js +++ b/src/models/Exporter.js @@ -38,6 +38,9 @@ const exporterOptions = { reverse: { title: 'Reverse path in the code', }, + pngPreview: { + title: 'Export the preview image', + }, } export class Exporter {