diff --git a/src/components/widgets/filesystem/FileSystem.vue b/src/components/widgets/filesystem/FileSystem.vue index 7d20342e3d..f953a42b78 100644 --- a/src/components/widgets/filesystem/FileSystem.vue +++ b/src/components/widgets/filesystem/FileSystem.vue @@ -337,7 +337,7 @@ export default class FileSystem extends Mixins(StateMixin, FilesMixin, ServicesM { text: this.$tc('app.general.table.header.extruder_colors'), value: 'extruder_colors', - visible: false, + visible: isNotDashboard, cellClass: 'text-no-wrap' }, { diff --git a/src/components/widgets/gcode-preview/GcodePreview.vue b/src/components/widgets/gcode-preview/GcodePreview.vue index 26a561ed92..34b9662a84 100644 --- a/src/components/widgets/gcode-preview/GcodePreview.vue +++ b/src/components/widgets/gcode-preview/GcodePreview.vue @@ -61,7 +61,7 @@ /> @@ -161,10 +163,12 @@ class="layer" > @@ -172,13 +176,16 @@ id="currentLayer" class="layer" > - + @@ -229,9 +236,11 @@ class="layer" > @@ -252,61 +261,72 @@ @touchstart="panzoom?.pause()" @touchend="panzoom?.resume()" > - - - - - - - - - - - - - - - - - - {{ autoZoom ? '$magnifyMinus' : '$magnifyPlus' }} - +
+ + + + + + + + + + + + + + + + + + {{ autoZoom ? '$magnifyMinus' : '$magnifyPlus' }} + +
+
+ +
{ + return this.$typedGetters['gcodePreview/getToolColors'] + } + get bounds (): BBox { return this.$typedGetters['gcodePreview/getBounds'] } diff --git a/src/components/widgets/gcode-preview/GcodePreviewCard.vue b/src/components/widgets/gcode-preview/GcodePreviewCard.vue index c549ec9f57..5cb74baea2 100644 --- a/src/components/widgets/gcode-preview/GcodePreviewCard.vue +++ b/src/components/widgets/gcode-preview/GcodePreviewCard.vue @@ -261,7 +261,7 @@ export default class GcodePreviewCard extends Mixins(StateMixin, FilesMixin, Bro } } - get file (): AppFile | undefined { + get file (): AppFile | AppFileWithMeta | null { return this.$typedState.gcodePreview.file } @@ -278,7 +278,7 @@ export default class GcodePreviewCard extends Mixins(StateMixin, FilesMixin, Bro } get showParserProgressDialog (): boolean { - return this.file !== undefined && this.parserProgress !== this.file.size + return this.file != null && this.parserProgress !== this.file.size } get filePosition (): number { @@ -355,7 +355,7 @@ export default class GcodePreviewCard extends Mixins(StateMixin, FilesMixin, Bro } } - async loadFile (file: AppFile) { + async loadFile (file: AppFile | AppFileWithMeta) { try { const response = await this.getGcode(file) diff --git a/src/components/widgets/gcode-preview/GcodePreviewTool.vue b/src/components/widgets/gcode-preview/GcodePreviewTool.vue new file mode 100644 index 0000000000..4c081e4043 --- /dev/null +++ b/src/components/widgets/gcode-preview/GcodePreviewTool.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/src/store/gcodePreview/actions.ts b/src/store/gcodePreview/actions.ts index 442e19f16a..bdff223d53 100644 --- a/src/store/gcodePreview/actions.ts +++ b/src/store/gcodePreview/actions.ts @@ -1,7 +1,7 @@ import type { ActionTree } from 'vuex' import type { GcodePreviewState } from './types' import type { RootState } from '../types' -import type { AppFile } from '@/store/files/types' +import type { AppFile, AppFileWithMeta } from '@/store/files/types' import { EventBus } from '@/eventBus' import i18n from '@/plugins/i18n' import { consola } from 'consola' @@ -26,11 +26,11 @@ export const actions = { worker.terminate() - commit('clearFile') + commit('setFile', null) } }, - async loadGcode ({ commit, state, rootState, dispatch }, payload: { file: AppFile; gcode: ArrayBuffer }) { + async loadGcode ({ commit, state, rootState, dispatch }, payload: { file: AppFile | AppFileWithMeta; gcode: ArrayBuffer }) { const worker = new ParseGcodeWorker() commit('setParserWorker', worker) @@ -49,6 +49,7 @@ export const actions = { commit('setMoves', message.moves) commit('setLayers', message.layers) commit('setParts', message.parts) + commit('setTools', message.tools) commit('setParserProgress', payload.file.size ?? payload.gcode.byteLength) if (rootState.config.uiSettings.gcodePreview.hideSinglePartBoundingBox && message.parts.length <= 1) { @@ -71,7 +72,7 @@ export const actions = { } if (state.moves.length === 0) { - commit('clearFile') + commit('setFile', null) } break @@ -90,6 +91,8 @@ export const actions = { commit('setParserProgress', 0) commit('setMoves', []) commit('setLayers', []) + commit('setParts', []) + commit('setTools', []) commit('setFile', payload.file) diff --git a/src/store/gcodePreview/getters.ts b/src/store/gcodePreview/getters.ts index a60cea4016..1aaf77e8a7 100644 --- a/src/store/gcodePreview/getters.ts +++ b/src/store/gcodePreview/getters.ts @@ -1,5 +1,5 @@ import type { GetterTree } from 'vuex' -import type { BBox, GcodePreviewState, Layer, LayerNr, LayerPaths, Move, Part, Point3D } from './types' +import type { BBox, GcodePreviewState, Layer, LayerNr, LayerPaths, Move, Part, Point3D, Tool } from './types' import type { RootState } from '../types' import { binarySearch, moveToSVGPath } from '@/util/gcode-preview' import isKeyOf from '@/util/is-key-of' @@ -10,7 +10,7 @@ export const getters = { return state.layers } - const output = [] + const output: Layer[] = [] const moves = state.moves let z = NaN @@ -152,19 +152,75 @@ export const getters = { } }, - getPaths: (state, getters) => (startMove: number, endMove: number): LayerPaths => { + getFileFilamentColors: (state): string[] => { + const file = state.file + + if (file) { + if ( + 'extruder_colors' in file && + Array.isArray(file.extruder_colors) + ) { + return file.extruder_colors + } + + if ( + 'filament_colors' in file && + Array.isArray(file.filament_colors) + ) { + return file.filament_colors + } + } + + return [] + }, + + getDefaultColors: (state, getters, rootState) => { + const defaultColor = rootState.config.uiSettings.theme.isDark + ? '#FFF' + : '#000' + + return [defaultColor, '#1fb0ff', '#ff5252', '#D67600', '#830EE3', '#B366F2', '#E06573', '#E38819', '#795548', '#607D8B'] + }, + + getToolColors: (state, getters): Record => { + const colorsFromFileMetadata: string[] = getters.getFileFilamentColors + + const toolIndexes = state.tools.length === 0 + ? [0] + : state.tools + + const defaultColors: string[] = getters.getDefaultColors + + const tools = toolIndexes.reduce((tools, toolIndex, index) => { + const tool: Tool = `T${toolIndex}` + const color: string = ( + colorsFromFileMetadata[index] || + defaultColors[index - colorsFromFileMetadata.length] || + defaultColors[0] + ) + + tools[tool] = color + + return tools + }, {} as Record) + + return tools + }, + + getPaths: (state, getters) => (startMove: number, endMove: number, ignoreTools = false): LayerPaths => { const toolhead: Point3D = getters.getToolHeadPosition(startMove) const moves = state.moves const path: LayerPaths = { - extrusions: '', + extrusions: {}, moves: `M${toolhead.x},${toolhead.y}`, retractions: [], - extrusionStarts: [], + unretractions: [], toolhead: { x: 0, y: 0 - } + }, + tool: 'T0' } let traveling = true @@ -172,10 +228,14 @@ export const getters = { for (let index = startMove; index <= endMove && index < moves.length; index++) { const move = moves[index] + if (!ignoreTools) { + path.tool = `T${move.tool}` + } + if (move.e != null && move.e > 0) { if (traveling) { - path.extrusions += `M${toolhead.x},${toolhead.y}` - path.extrusionStarts.push({ + path.extrusions[path.tool] = `${path.extrusions[path.tool] || ''}M${toolhead.x},${toolhead.y}` + path.unretractions.push({ x: toolhead.x, y: toolhead.y }) @@ -183,7 +243,7 @@ export const getters = { traveling = false } - path.extrusions += moveToSVGPath(toolhead, move) + path.extrusions[path.tool] += moveToSVGPath(toolhead, move) Object.assign(toolhead, move) } else { if (!traveling) { @@ -214,7 +274,7 @@ export const getters = { getLayerPaths: (state, getters) => (layerNr: LayerNr): LayerPaths => { const layers: Layer[] = getters.getLayers - return getters.getPaths(layers[layerNr]?.move ?? 0, (layers[layerNr + 1]?.move ?? Infinity) - 1) + return getters.getPaths(layers[layerNr]?.move ?? 0, (layers[layerNr + 1]?.move ?? Infinity) - 1, true) }, getPartPaths: (state, getters): string[] => { diff --git a/src/store/gcodePreview/mutations.ts b/src/store/gcodePreview/mutations.ts index b8f4ee4f03..92d7e5515d 100644 --- a/src/store/gcodePreview/mutations.ts +++ b/src/store/gcodePreview/mutations.ts @@ -1,8 +1,8 @@ import type { MutationTree } from 'vuex' import { defaultState } from './state' -import type { GcodePreviewState } from './types' +import type { GcodePreviewState, Layer, Move, Part, Tool } from './types' import Vue from 'vue' -import type { AppFile } from '@/store/files/types' +import type { AppFile, AppFileWithMeta } from '@/store/files/types' export const mutations = { /** @@ -12,24 +12,24 @@ export const mutations = { Object.assign(state, defaultState()) }, - setMoves (state, payload) { + setMoves (state, payload: Move[]) { Vue.set(state, 'moves', Object.freeze(payload.map(Object.freeze))) }, - setLayers (state, payload) { + setLayers (state, payload: Layer[]) { Vue.set(state, 'layers', Object.freeze(payload.map(Object.freeze))) }, - setParts (state, payload) { + setParts (state, payload: Part[]) { Vue.set(state, 'parts', Object.freeze(payload.map(Object.freeze))) }, - setFile (state, file: AppFile) { - state.file = file + setTools (state, payload: Tool[]) { + Vue.set(state, 'tools', Object.freeze(payload)) }, - clearFile (state) { - state.file = undefined + setFile (state, file: AppFile | AppFileWithMeta | null) { + state.file = file }, setParserProgress (state, payload: number) { diff --git a/src/store/gcodePreview/state.ts b/src/store/gcodePreview/state.ts index f0c32c443d..330b3af0f2 100644 --- a/src/store/gcodePreview/state.ts +++ b/src/store/gcodePreview/state.ts @@ -5,7 +5,8 @@ export const defaultState = (): GcodePreviewState => { moves: [], layers: [], parts: [], - file: undefined, + tools: [], + file: null, parserProgress: 0, parserWorker: null } diff --git a/src/store/gcodePreview/types.ts b/src/store/gcodePreview/types.ts index 1984dc6b32..7c04770fad 100644 --- a/src/store/gcodePreview/types.ts +++ b/src/store/gcodePreview/types.ts @@ -1,12 +1,13 @@ -import type { AppFile } from '@/store/files/types' +import type { AppFile, AppFileWithMeta } from '@/store/files/types' export type LayerNr = number export interface GcodePreviewState { moves: Move[]; - layers: Layer[], - parts: Part[], - file?: AppFile; + layers: Layer[]; + parts: Part[]; + tools: number[]; + file: AppFile | AppFileWithMeta | null; parserProgress: number; parserWorker: Worker | null; } @@ -16,7 +17,7 @@ export interface LinearMove { y?: number; z?: number; e?: number; - + tool: number; filePosition: number; } @@ -25,19 +26,22 @@ export interface ArcMove extends LinearMove { j?: number; k?: number; r?: number; - direction: Rotation; + d: Rotation; } export type Move = LinearMove | ArcMove export type Rotation = 'clockwise' | 'counter-clockwise' +export type Tool = `T${number}` + export interface LayerPaths { moves: string; - extrusions: string; + extrusions: Record; retractions: Point[]; - extrusionStarts: Point[]; + unretractions: Point[]; toolhead: Point; + tool: Tool; } export interface Point { diff --git a/src/util/gcode-preview.ts b/src/util/gcode-preview.ts index 490fd6ec31..ea7857ea5b 100644 --- a/src/util/gcode-preview.ts +++ b/src/util/gcode-preview.ts @@ -59,7 +59,7 @@ function arcIJMoveToSVGPath (toolhead: Point, move: ArcMove): string { angle += 360 } - switch (move.direction) { + switch (move.d) { case 'clockwise': return 'A' + [ radius, radius, 0, +(angle < 0), 0, destination.x, destination.y @@ -129,7 +129,7 @@ export function arcMoveToSvgPath (toolhead: Point, move: ArcMove): string { // Assumes the path is pr export function moveToSVGPath (toolhead: Point, move: Move) { - if ('direction' in move) { + if ('d' in move) { return arcMoveToSvgPath(toolhead, move) } else { return `L${move.x ?? toolhead.x},${move.y ?? toolhead.y}` diff --git a/src/workers/parseGcode.ts b/src/workers/parseGcode.ts index 48b92a216e..35acc8eb15 100644 --- a/src/workers/parseGcode.ts +++ b/src/workers/parseGcode.ts @@ -33,7 +33,7 @@ const parseLine = (line: string) => { .split(';', 2)[0] const [, gcodeCommand, gcodeCommandArgs = ''] = clearedLine - .split(/^([gm]\d+)\s*/i) + .split(/^([gmt]\d+)\s*/i) if (gcodeCommand) { return { @@ -67,6 +67,7 @@ const parseGcode = (gcode: string, sendProgress: (filePosition: number) => void) const moves: Move[] = [] const layers: Layer[] = [] const parts: Part[] = [] + const tools = new Set() const lines = gcode.split('\n') let newLayerForNextMove = false @@ -77,8 +78,9 @@ const parseGcode = (gcode: string, sendProgress: (filePosition: number) => void) y: 0, z: 0, e: 0, - filePosition: 0 } + let tool = 0 + let filePosition = 0 // todo get from firmware // store path: printer.printer.configFile.settings.firmware_retraction @@ -136,7 +138,8 @@ const parseGcode = (gcode: string, sendProgress: (filePosition: number) => void) if (params.some(param => param in args)) { move = { ...pick(args, params), - filePosition: toolhead.filePosition + tool, + filePosition } satisfies LinearMove } break @@ -151,10 +154,11 @@ const parseGcode = (gcode: string, sendProgress: (filePosition: number) => void) if (params.some(param => param in args)) { move = { ...pick(args, params), - direction: command === 'G2' + d: command === 'G2' ? 'clockwise' : 'counter-clockwise', - filePosition: toolhead.filePosition + tool, + filePosition } satisfies ArcMove } break @@ -162,7 +166,8 @@ const parseGcode = (gcode: string, sendProgress: (filePosition: number) => void) case 'G10': move = { e: -fwretraction.length, - filePosition: toolhead.filePosition + tool, + filePosition } satisfies LinearMove if (fwretraction.z !== 0) { @@ -172,7 +177,8 @@ const parseGcode = (gcode: string, sendProgress: (filePosition: number) => void) case 'G11': move = { e: decimalRound(fwretraction.length + fwretraction.extrudeExtra), - filePosition: toolhead.filePosition + tool, + filePosition } satisfies LinearMove if (fwretraction.z !== 0) { @@ -186,7 +192,8 @@ const parseGcode = (gcode: string, sendProgress: (filePosition: number) => void) const noXYZ = !hasX && !hasY && !hasZ move = { - filePosition: toolhead.filePosition + tool, + filePosition } satisfies LinearMove if (hasX || noXYZ) { @@ -226,6 +233,17 @@ const parseGcode = (gcode: string, sendProgress: (filePosition: number) => void) fwretraction.length = args.s ?? fwretraction.length fwretraction.z = args.z ?? fwretraction.z break + case 'M600': + tools.add(0) + tool = (tool + 1) % 10 + tools.add(tool) + break + default: + if (command.startsWith('T')) { + tool = +command.substring(1) + tools.add(tool) + } + break } if (move) { @@ -256,7 +274,7 @@ const parseGcode = (gcode: string, sendProgress: (filePosition: number) => void) const layer: Layer = { z: toolhead.z, move: moves.length - 1, - filePosition: toolhead.filePosition + filePosition } layers.push(Object.freeze(layer)) @@ -274,15 +292,21 @@ const parseGcode = (gcode: string, sendProgress: (filePosition: number) => void) } if (i % Math.floor(lines.length / 100) === 0) { - sendProgress(toolhead.filePosition) + sendProgress(filePosition) } - toolhead.filePosition += lines[i].length + 1 // + 1 for newline + filePosition += lines[i].length + 1 // + 1 for newline } - sendProgress(toolhead.filePosition) + sendProgress(filePosition) - return { moves, layers, parts } + return { + moves, + layers, + parts, + tools: [...tools] + .sort((a, b) => a - b) + } } export default parseGcode diff --git a/src/workers/parseGcode.worker.ts b/src/workers/parseGcode.worker.ts index 92056b3a2a..f28127e8b1 100644 --- a/src/workers/parseGcode.worker.ts +++ b/src/workers/parseGcode.worker.ts @@ -9,7 +9,8 @@ export type ParseGcodeWorkerClientMessage = { action: 'result', moves: Move[], layers: Layer[], - parts: Part[] + parts: Part[], + tools: number[] } | { action: 'error', error?: unknown @@ -29,12 +30,13 @@ const sendProgress = (filePosition: number) => { self.postMessage(message) } -const sendResult = (moves: Move[], layers: Layer[], parts: Part[]) => { +const sendResult = (moves: Move[], layers: Layer[], parts: Part[], tools: number[]) => { const message : ParseGcodeWorkerClientMessage = { action: 'result', moves, layers, - parts + parts, + tools } self.postMessage(message) @@ -57,9 +59,9 @@ self.onmessage = (event: MessageEvent) => { case 'parse': { const gcode = new TextDecoder().decode(message.gcode) - const { moves, layers, parts } = parseGcode(gcode, sendProgress) + const { moves, layers, parts, tools } = parseGcode(gcode, sendProgress) - sendResult(moves, layers, parts) + sendResult(moves, layers, parts, tools) break }