diff --git a/src/components/widgets/exclude-objects/ExcludeObjects.vue b/src/components/widgets/exclude-objects/ExcludeObjects.vue new file mode 100644 index 0000000000..32801c5cb2 --- /dev/null +++ b/src/components/widgets/exclude-objects/ExcludeObjects.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/src/components/widgets/exclude-objects/ExcludeObjectsDialog.vue b/src/components/widgets/exclude-objects/ExcludeObjectsDialog.vue new file mode 100644 index 0000000000..b1580cccbb --- /dev/null +++ b/src/components/widgets/exclude-objects/ExcludeObjectsDialog.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/src/components/widgets/gcode-preview/GcodePreview.vue b/src/components/widgets/gcode-preview/GcodePreview.vue index 6e91d1b34f..2c7b3c028b 100644 --- a/src/components/widgets/gcode-preview/GcodePreview.vue +++ b/src/components/widgets/gcode-preview/GcodePreview.vue @@ -193,6 +193,10 @@ :stroke-width="extrusionLineWidth" /> + @@ -204,8 +208,13 @@ import StateMixin from '@/mixins/state' import panzoom, { PanZoom } from 'panzoom' import { BBox, LayerNr, LayerPaths } from '@/store/gcodePreview/types' import { GcodePreviewConfig } from '@/store/config/types' +import ExcludeObjects from '@/components/widgets/exclude-objects/ExcludeObjects.vue' -@Component({}) +@Component({ + components: { + ExcludeObjects + } +}) export default class GcodePreview extends Mixins(StateMixin) { @Prop({ type: Boolean, default: true }) public disabled!: boolean @@ -276,6 +285,21 @@ export default class GcodePreview extends Mixins(StateMixin) { return this.panning ? 'optimizeSpeed' : 'geometricPrecision' } + get showExcludeObjects () { + if (!(this.printerPrinting || this.printerPaused)) return false + + const file = this.$store.getters['gcodePreview/getFile'] + if (!file) { + return true + } + const printerFile = this.$store.state.printer.printer.current_file + + if (printerFile.filename) { + return (file.path + '/' + file.filename) === (printerFile.path + '/' + printerFile.filename) + } + return false + } + get flipX (): boolean { return this.$store.state.config.uiSettings.gcodePreview.flip.horizontal } diff --git a/src/components/widgets/gcode-preview/GcodePreviewCard.vue b/src/components/widgets/gcode-preview/GcodePreviewCard.vue index 8dd9ed9797..84a33d75d2 100644 --- a/src/components/widgets/gcode-preview/GcodePreviewCard.vue +++ b/src/components/widgets/gcode-preview/GcodePreviewCard.vue @@ -16,6 +16,18 @@ + + $cancelled + + + + @@ -139,13 +159,15 @@ import GcodePreviewParserProgressDialog from '@/components/widgets/gcode-preview import { MinMax } from '@/store/gcodePreview/types' import AppBtnCollapseGroup from '@/components/ui/AppBtnCollapseGroup.vue' import { AxiosResponse } from 'axios' +import ExcludeObjectsDialog from '@/components/widgets/exclude-objects/ExcludeObjectsDialog.vue' @Component({ components: { AppBtnCollapseGroup, GcodePreviewParserProgressDialog, GcodePreview, - GcodePreviewControls + GcodePreviewControls, + ExcludeObjectsDialog } }) export default class GcodePreviewCard extends Mixins(StateMixin, FilesMixin) { @@ -157,6 +179,7 @@ export default class GcodePreviewCard extends Mixins(StateMixin, FilesMixin) { currentLayer = 0 moveProgress = 0 + excludeObjectDialog = false get visibleLayer () { return this.currentLayer + 1 @@ -226,6 +249,15 @@ export default class GcodePreviewCard extends Mixins(StateMixin, FilesMixin) { } } + @Watch('fileLoaded') + onFileLoaded () { + if (this.fileLoaded && + this.$store.state.config?.uiSettings.gcodePreview.autoFollowOnFileLoad && + this.printerFileLoaded) { + this.$store.commit('gcodePreview/setViewerState', { followProgress: true }, { root: true }) + } + } + get file (): AppFile | undefined { return this.$store.getters['gcodePreview/getFile'] } @@ -348,5 +380,22 @@ export default class GcodePreviewCard extends Mixins(StateMixin, FilesMixin) { get autoLoadOnPrintStart () { return this.$store.state.config.uiSettings.gcodePreview.autoLoadOnPrintStart } + + async cancelObject (id: string) { + const reqId = id.toUpperCase().replace(/\s/g, '_') + + const res = await this.$confirm( + this.$tc('app.general.simple_form.msg.confirm_exclude_object'), + { title: this.$tc('app.general.label.confirm'), color: 'card-heading', icon: '$error' } + ) + + if (res) { + this.sendGcode(`EXCLUDE_OBJECT NAME=${reqId}`) + } + } + + get parts () { + return Object.values(this.$store.getters['parts/getParts']) + } } diff --git a/src/locales/de.yaml b/src/locales/de.yaml index de2ca882f6..93e02a50df 100644 --- a/src/locales/de.yaml +++ b/src/locales/de.yaml @@ -110,6 +110,7 @@ app: load_current_file: Aktuelle Datei laden label: current_layer_height: Aktuelle Schichthöhe + exclude_object: Objekt ausschließen follow_progress: Fortschritt folgen layer: Schicht layers: Schichten @@ -292,6 +293,7 @@ app: Probe/Bltouch Z-Offset wird aus dem aktuellen Z-Offset berechnet und aktualisiert. confirm: Sind Sie sicher? + confirm_exclude_object: Sind Sie sicher, dass Sie dieses Objekt vom Druck ausschließen wollen? confirm_power_device_toggle: Sind Sie sicher? Dies wird das Gerät ein- bzw. ausschalten. confirm_reboot_host: Sind Sie sicher? Das Host-System wird neu gestartet. confirm_service_restart: Möchten Sie den Dienst %{name} wirklich neu starten? diff --git a/src/locales/en.yaml b/src/locales/en.yaml index 2a9078c58f..c1cb914a14 100644 --- a/src/locales/en.yaml +++ b/src/locales/en.yaml @@ -109,6 +109,7 @@ app: load_current_file: Load Current File label: current_layer_height: Current Layer Height + exclude_object: Exclude Object follow_progress: Follow progress layer: Layer layers: Layers @@ -296,6 +297,7 @@ app: required: Required msg: confirm: Are you sure? + confirm_exclude_object: Are you sure you want to exclude this object from printing? confirm_reboot_host: Are you sure? This will reboot your host system. confirm_shutdown_host: Are you sure? This will shutdown your host system. confirm_service_restart: Are you sure you want to restart the %{name} service? diff --git a/src/store/config/index.ts b/src/store/config/index.ts index 0898e5a264..ef0f3245a3 100644 --- a/src/store/config/index.ts +++ b/src/store/config/index.ts @@ -122,7 +122,7 @@ export const defaultState = (): ConfigState => { drawBackground: true, showAnimations: true, groupLowerLayers: false, - autoLoadOnPrintStart: true, + autoLoadOnPrintStart: false, autoFollowOnFileLoad: true, flip: { horizontal: false, diff --git a/src/store/gcodePreview/actions.ts b/src/store/gcodePreview/actions.ts index 9490ddd0b5..92b2c5e616 100644 --- a/src/store/gcodePreview/actions.ts +++ b/src/store/gcodePreview/actions.ts @@ -23,7 +23,7 @@ export const actions: ActionTree = { } }, - async loadGcode ({ commit, getters, state, rootState }, payload: { file: AppFile; gcode: string }) { + async loadGcode ({ commit, getters, state }, payload: { file: AppFile; gcode: string }) { const worker = await spawn(new Worker(new URL('@/workers/parseGcode.worker.ts', import.meta.url) as any)) commit('setParserWorker', worker) @@ -46,10 +46,6 @@ export const actions: ActionTree = { commit('setMoves', []) commit('setFile', payload.file) - if (rootState.config?.uiSettings.gcodePreview.autoFollowOnFileLoad) { - // check if loaded file equals printed file is handled downstream - commit('setViewerState', { followProgress: true }) - } try { commit('setMoves', await Promise.race([abort, worker.parse(payload.gcode)])) diff --git a/src/store/helpers.ts b/src/store/helpers.ts index 501a6b014a..c73668bc95 100644 --- a/src/store/helpers.ts +++ b/src/store/helpers.ts @@ -49,7 +49,23 @@ export const getThumb = (thumbnails: Thumbnail[], path: string, large = true) => } } -export const handlePrintStateChange = (payload: any) => { +export const handleExcludeObjectChange = (payload: any, state: any, dispatch: any) => { + // For every notify - if print_stats.state changes from standby -> printing, + // then record an entry in our print history. + // If the state changes from printing -> complete, then record the finish time. + if ('exclude_object' in payload) { + dispatch('parts/onPartUpdate', payload.exclude_object, { root: true }) + } + + if ( + 'print_stats' in payload && + ('state' in payload.print_stats || 'filename' in payload.print_stats) + ) { + dispatch('parts/onPrintStatsUpdate', payload.print_stats, { root: true }) + } +} + +export const handlePrintStateChange = (payload: any, state: any, dispatch: any) => { // For every notify - if print_stats.state changes from standby -> printing, // then record an entry in our print history. // If the state changes from printing -> complete, then record the finish time. @@ -58,23 +74,23 @@ export const handlePrintStateChange = (payload: any) => { 'state' in payload.print_stats ) { if ( - store.state.printer?.printer.print_stats.state === 'standby' && + state.printer?.printer.print_stats.state !== 'printing' && payload.print_stats.state === 'printing' ) { // This is a new print starting... - store.dispatch('printer/onPrintStart', payload) + dispatch('printer/onPrintStart', payload, { root: true }) } else if ( - store.state.printer?.printer.print_stats.state === 'printing' && + state.printer?.printer.print_stats.state === 'printing' && payload.print_stats.state === 'complete' ) { // This is a completed print... - store.dispatch('printer/onPrintEnd', payload) + dispatch('printer/onPrintEnd', payload, { root: true }) } else if ( - store.state.printer?.printer.print_stats.state === 'printing' && + state.printer?.printer.print_stats.state === 'printing' && payload.print_stats.state === 'standby' ) { // This is a cancelled print... - store.dispatch('printer/onPrintEnd', payload) + dispatch('printer/onPrintEnd', payload, { root: true }) } } } diff --git a/src/store/index.ts b/src/store/index.ts index d4df048f5b..0fad07d9e1 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -25,6 +25,7 @@ import { announcements } from './announcements' import { wait } from './wait' import { gcodePreview } from './gcodePreview' import { timelapse } from './timelapse' +import { parts } from './parts' Vue.use(Vuex) @@ -51,7 +52,8 @@ export default new Vuex.Store({ announcements, wait, gcodePreview, - timelapse + timelapse, + parts }, mutations: {}, actions: { diff --git a/src/store/parts/actions.ts b/src/store/parts/actions.ts new file mode 100644 index 0000000000..236328daf9 --- /dev/null +++ b/src/store/parts/actions.ts @@ -0,0 +1,20 @@ +import { ActionTree } from 'vuex' +import { PartsState } from './types' +import { RootState } from '../types' + +export const actions: ActionTree = { + /** + * Reset our store + */ + async reset ({ commit }) { + commit('setReset') + }, + + async onPartUpdate ({ commit }, payload) { + commit('partUpdate', payload) + }, + + async onPrintStatsUpdate ({ commit }, payload) { + commit('printStatsUpdate', payload) + } +} diff --git a/src/store/parts/getters.ts b/src/store/parts/getters.ts new file mode 100644 index 0000000000..814ad0fba6 --- /dev/null +++ b/src/store/parts/getters.ts @@ -0,0 +1,42 @@ +import { GetterTree } from 'vuex' +import { Point, Part, PartsState } from './types' +import { RootState } from '../types' + +export const getters: GetterTree = { + getParts: (state): {[key: string]: Part} => { + return state.parts + }, + + getIsPartCurrent: (state) => (partName: string): boolean => { + return state.currentPart === partName + }, + + getIsPartExcluded: (state) => (partName: string): boolean => { + return state.excludedParts.includes(partName) + }, + + getPartPos: (state, getters) => (partName: string): Point => { + const p = getters.getParts[partName] + return p.target + }, + + getPartSVG: (state, getters) => (partName: string): string => { + const part = getters.getParts[partName] + + let svg = '' + let op = 'M' + + part.outline.forEach((p: Point) => { + svg += `${op}${p.x},${p.y}` + op = 'L' + }) + + svg += 'z' + + return svg + }, + + getPrintState: (state): string => { + return state.printState + } +} diff --git a/src/store/parts/index.ts b/src/store/parts/index.ts new file mode 100644 index 0000000000..c92bf9b112 --- /dev/null +++ b/src/store/parts/index.ts @@ -0,0 +1,29 @@ +import { Module } from 'vuex' +import { getters } from './getters' +import { actions } from './actions' +import { mutations } from './mutations' +import { PartsState } from './types' +import { RootState } from '../types' + +/** + * Maintains the state of the console + */ +export const defaultState = (): PartsState => { + return { + parts: {}, + excludedParts: [], + printState: 'unknown' + } +} + +export const state = defaultState() + +const namespaced = true + +export const parts: Module = { + namespaced, + state, + getters, + actions, + mutations +} diff --git a/src/store/parts/mutations.ts b/src/store/parts/mutations.ts new file mode 100644 index 0000000000..f8956c7a48 --- /dev/null +++ b/src/store/parts/mutations.ts @@ -0,0 +1,54 @@ +import { MutationTree } from 'vuex' +import { defaultState } from '.' +import { Part, PartObject, PartsState } from './types' +import Vue from 'vue' + +export const mutations: MutationTree = { + /** + * Reset state + */ + setReset (state) { + Object.assign(state, defaultState()) + }, + + partUpdate (state, payload) { + if ('current_object' in payload) { + Vue.set(state, 'currentPart', payload.current_object) + } + + if ('excluded_objects' in payload) { + Vue.set(state, 'excludedParts', payload.excluded_objects) + } + + if ('objects' in payload) { + const partMap: { [key: string]: Part} = {} + payload.objects.forEach((obj: PartObject) => { + const name = obj.name + const part: Part = { + name, + outline: [], + target: null + } + + if ('center' in obj && obj.center.length === 2) { + part.target = { x: obj.center[0], y: obj.center[1] } + } + + if ('polygon' in obj) { + part.outline = obj.polygon.map(p => { + return { x: p[0], y: p[1] } + }) + } + + partMap[name] = part + }) + Vue.set(state, 'parts', Object.freeze(partMap)) + } + }, + + printStatsUpdate (state, payload) { + if ('state' in payload) { + Vue.set(state, 'printState', payload.state) + } + } +} diff --git a/src/store/parts/types.ts b/src/store/parts/types.ts new file mode 100644 index 0000000000..2eb9dc4766 --- /dev/null +++ b/src/store/parts/types.ts @@ -0,0 +1,23 @@ +export interface PartsState { + parts: { [key: string]: Part}; + excludedParts: string[]; + printState: string; + currentPart?: string; +} + +export interface Point { + x: number; + y: number; +} + +export interface Part { + name: string; + outline: Point[]; + target: Point | null; +} + +export interface PartObject { + name: string; + polygon: [number, number][]; + center: [number, number] | [number, number, number]; +} diff --git a/src/store/printer/actions.ts b/src/store/printer/actions.ts index 2f42a542f4..ce5b61d7a1 100644 --- a/src/store/printer/actions.ts +++ b/src/store/printer/actions.ts @@ -1,7 +1,7 @@ import { ActionTree } from 'vuex' import { PrinterState } from './types' import { RootState } from '../types' -import { handlePrintStateChange, handleCurrentFileChange } from '../helpers' +import { handlePrintStateChange, handleCurrentFileChange, handleExcludeObjectChange } from '../helpers' import { handleAddChartEntry, handleSystemStatsChange, handleMcuStatsChange } from '../chart_helpers' import { SocketActions } from '@/api/socketActions' import { Globals } from '@/globals' @@ -115,7 +115,7 @@ export const actions: ActionTree = { */ /** Automated notify events via socket */ - async onNotifyStatusUpdate ({ rootState, commit, getters }, payload) { + async onNotifyStatusUpdate ({ rootState, commit, getters, dispatch }, payload) { // TODO: We potentially get many updates here. // Consider caching the updates and sending them every . // We don't want to miss an update - but also don't need all of them @@ -129,7 +129,8 @@ export const actions: ActionTree = { // We do this prior to commiting the notify so we can // compare the before and after. handleCurrentFileChange(payload) - handlePrintStateChange(payload) + handlePrintStateChange(payload, rootState, dispatch) + handleExcludeObjectChange(payload, rootState, dispatch) handleSystemStatsChange(payload) handleMcuStatsChange(payload) diff --git a/src/store/types.ts b/src/store/types.ts index 4e86c76eb0..bbd50539c0 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -10,6 +10,7 @@ import { DevicePowerState } from './power/types' import { HistoryState } from './history/types' import { VersionState } from './version/types' import { GcodePreviewState } from './gcodePreview/types' +import { PartsState } from './parts/types' import { LayoutState } from './layout/types' import { MeshState } from './mesh/types' import { NotificationsState } from './notifications/types' @@ -33,6 +34,7 @@ export interface RootState { history?: HistoryState; version?: VersionState; gcodePreview?: GcodePreviewState; + parts?: PartsState; notifications?: NotificationsState; announcements?: AnnouncementsState; timelapse?: TimelapseState;