diff --git a/src/App.vue b/src/App.vue index bcd115565a..de67b825c1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -75,6 +75,7 @@ + @@ -105,6 +106,7 @@ import ActionCommandPromptDialog from '@/components/common/ActionCommandPromptDi import KeyboardShortcutsDialog from '@/components/common/KeyboardShortcutsDialog.vue' import { eventTargetIsContentEditable, keyboardEventToKeyboardShortcut } from '@/util/event-helpers' import MmuEditTtgMapDialog from './components/widgets/mmu/MmuEditTtgMapDialog.vue' +import AfcPrintStartDialog from './components/widgets/afc/dialogs/AfcPrintStartDialog.vue' @Component({ metaInfo () { @@ -120,7 +122,8 @@ import MmuEditTtgMapDialog from './components/widgets/mmu/MmuEditTtgMapDialog.vu FileSystemUploadDialog, ActionCommandPromptDialog, KeyboardShortcutsDialog, - MmuEditTtgMapDialog + MmuEditTtgMapDialog, + AfcPrintStartDialog } }) export default class App extends Mixins(StateMixin, FilesMixin, BrowserMixin) { diff --git a/src/components/widgets/afc/AfcCardUnitLaneBody.vue b/src/components/widgets/afc/AfcCardUnitLaneBody.vue index 9ae9deb754..8b5ac7ded4 100644 --- a/src/components/widgets/afc/AfcCardUnitLaneBody.vue +++ b/src/components/widgets/afc/AfcCardUnitLaneBody.vue @@ -122,7 +122,7 @@ import { Component, Mixins, Prop, Watch } from 'vue-property-decorator' import StateMixin from '@/mixins/state' import AfcMixin from '@/mixins/afc' -import type { Spool, SpoolSelectionDialogState } from '@/store/spoolman/types' +import type { SpoolSelectionDialogState } from '@/store/spoolman/types' import AfcUnitLaneInfiniteDialog from '@/components/widgets/afc/dialogs/AfcUnitLaneInfiniteDialog.vue' import AfcUnitLaneFilamentDialog from '@/components/widgets/afc/dialogs/AfcUnitLaneFilamentDialog.vue' import AfcFilamentReel from './AfcFilamentReel.vue' @@ -151,73 +151,60 @@ export default class AfcCardUnitLaneBody extends Mixins(StateMixin, AfcMixin) { return this.lane?.runout_lane ?? 'NONE' } - get spoolId (): number | undefined { - return this.lane?.spool_id ?? undefined + get laneInfo (): Klipper.AfcSpoolLaneInfo { + return this.getAfcLaneInfo(this.name) } - get spool (): Spool | null { - if (!this.spoolId) return null + get spoolId (): number | undefined { + return this.laneInfo.spoolId + } - return this.$typedGetters['spoolman/getSpoolById'](this.spoolId) ?? null + get spool () { + return this.laneInfo.spool } get spoolColor (): string { - if ( - this.afc?.td1_present && - this.lane?.td1_color && - this.afcShowTd1Color - ) { - return `#${this.lane.td1_color}` - } - - return this.lane?.color || '#000000' + return this.laneInfo.color } get spoolRemainingWeight (): number | undefined { - return this.spool?.remaining_weight ?? this.lane?.weight + return this.laneInfo.remainingWeight } get spoolFullWeight (): number | undefined { - return this.spool?.initial_weight ?? this.lane?.initial_weight + return this.laneInfo.fullWeight } get spoolPercent (): number { - if (this.spoolRemainingWeight == null || this.spoolFullWeight == null) return 100 - if (this.spoolFullWeight === 0) return 100 - - return Math.round((this.spoolRemainingWeight / this.spoolFullWeight) * 100) + return this.laneInfo.spoolPercent } get spoolMaterial (): string { - return this.spool?.filament?.material ?? this.lane?.material ?? '' + return this.laneInfo.material } get spoolFilamentVendor (): string | undefined { - return this.spool?.filament?.vendor?.name + return this.laneInfo.filamentVendor } get spoolFilamentName (): string | undefined { - return this.spool?.filament?.name || - this.lane?.filament_name || - undefined + return this.laneInfo.filamentName } get spoolUrl (): string | undefined { - const base: string | undefined = this.$typedGetters['spoolman/getSpoolmanUrl'] - if (!base || !this.spoolId) return undefined - return `${base.replace(/\/$/, '')}/spool/show/${this.spoolId}` + return this.laneInfo.spoolUrl } get spoolExtruderTemp (): number | undefined { - return this.spool?.filament?.settings_extruder_temp + return this.laneInfo.extruderTemp } get spoolBedTemp (): number | undefined { - return this.spool?.filament?.settings_bed_temp + return this.laneInfo.bedTemp } get spoolUsedWeight (): number | undefined { - return this.spool?.used_weight + return this.laneInfo.usedWeight } get tdPresent (): boolean { diff --git a/src/components/widgets/afc/dialogs/AfcPrintStartDialog.vue b/src/components/widgets/afc/dialogs/AfcPrintStartDialog.vue new file mode 100644 index 0000000000..fffdf9164b --- /dev/null +++ b/src/components/widgets/afc/dialogs/AfcPrintStartDialog.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/src/components/widgets/afc/dialogs/AfcPrintStartDialogTool.vue b/src/components/widgets/afc/dialogs/AfcPrintStartDialogTool.vue new file mode 100644 index 0000000000..839af7465d --- /dev/null +++ b/src/components/widgets/afc/dialogs/AfcPrintStartDialogTool.vue @@ -0,0 +1,468 @@ + + + diff --git a/src/components/widgets/filesystem/FileSystem.vue b/src/components/widgets/filesystem/FileSystem.vue index 5731025460..609c93d4f0 100644 --- a/src/components/widgets/filesystem/FileSystem.vue +++ b/src/components/widgets/filesystem/FileSystem.vue @@ -153,6 +153,7 @@ import FileEditorDialog from './FileEditorDialog.vue' import FileNameDialog from './FileNameDialog.vue' import FileSystemGoToFileDialog from './FileSystemGoToFileDialog.vue' import FilePreviewDialog from './FilePreviewDialog.vue' +import AfcMixin from '@/mixins/afc' import type { AppDataTableHeader, FileWithPath } from '@/types' import { getFilesFromDataTransfer, hasFilesInDataTransfer } from '@/util/file-system-entry' import { getFileDataTransferDataFromDataTransfer, hasFileDataTransferTypeInDataTransfer, setFileDataTransferDataInDataTransfer } from '@/util/file-data-transfer' @@ -178,7 +179,7 @@ import type { KlipperSaveAndRestartAction } from '@/store/config/types' FilePreviewDialog } }) -export default class FileSystem extends Mixins(StateMixin, FilesMixin, ServicesMixin) { +export default class FileSystem extends Mixins(StateMixin, FilesMixin, ServicesMixin, AfcMixin) { // Can be a list of roots, or a single root. @Prop({ type: [String, Array], required: true }) readonly roots!: string | string[] @@ -929,6 +930,20 @@ export default class FileSystem extends Mixins(StateMixin, FilesMixin, ServicesM const filename = file.path ? `${file.path}/${file.filename}` : file.filename + // AFC check — show lane mapping dialog when AFC is installed. If metadata + // is not yet loaded we open the dialog anyway (it will fetch on open); + // if metadata is loaded, this panel should show for both single-color + // and multi-color prints to allow the user to remap lanes(if needed) + // before starting their print. + if (this.shouldShowAfcDialog(file)) { + this.$typedCommit('afc/setDialogState', { + show: true, + filename + }) + + return + } + if (this.$typedState.printer.printer.mmu?.enabled === true) { if ('referenced_tools' in file) { const mmuPrint = (file.referenced_tools?.length ?? 1) > 1 || this.$typedState.printer.printer.mmu?.gate !== -2 diff --git a/src/components/widgets/status/PrinterStatusCard.vue b/src/components/widgets/status/PrinterStatusCard.vue index d6d691ee03..4f36019509 100644 --- a/src/components/widgets/status/PrinterStatusCard.vue +++ b/src/components/widgets/status/PrinterStatusCard.vue @@ -72,6 +72,7 @@ import { Component, Mixins, Watch } from 'vue-property-decorator' import { SocketActions } from '@/api/socketActions' import StateMixin from '@/mixins/state' +import AfcMixin from '@/mixins/afc' import StatusControls from './StatusControls.vue' import StatusTab from './StatusTab.vue' import ReprintTab from './ReprintTab.vue' @@ -85,7 +86,7 @@ import getFilePaths from '@/util/get-file-paths' ReprintTab } }) -export default class PrinterStatusCard extends Mixins(StateMixin) { +export default class PrinterStatusCard extends Mixins(StateMixin, AfcMixin) { tab = 0 // If the user has no history plugin, and there's no print running.. @@ -128,10 +129,24 @@ export default class PrinterStatusCard extends Mixins(StateMixin) { } handlePrint (filename: string) { - if (this.$typedState.printer.printer.mmu?.enabled === true) { - const { rootPath, filename: filenameOnly } = getFilePaths(filename, 'gcodes') - const fileWithMeta = this.$typedGetters['files/getFile'](rootPath, filenameOnly) + const { rootPath, filename: filenameOnly } = getFilePaths(filename, 'gcodes') + const fileWithMeta = this.$typedGetters['files/getFile'](rootPath, filenameOnly) + + // AFC check — show lane mapping dialog when AFC is installed. If metadata + // is not yet loaded we open the dialog anyway (it will fetch on open); + // if metadata is loaded, this panel should show for both single-color + // and multi-color prints to allow the user to remap lanes(if needed) + // before starting their print. + if (this.shouldShowAfcDialog(fileWithMeta)) { + this.$typedCommit('afc/setDialogState', { + show: true, + filename + }) + return + } + + if (this.$typedState.printer.printer.mmu?.enabled === true) { if (fileWithMeta != null && 'referenced_tools' in fileWithMeta) { const mmuPrint = (fileWithMeta.referenced_tools?.length ?? 1) > 1 || this.$typedState.printer.printer.mmu.gate !== -2 diff --git a/src/locales/en.yaml b/src/locales/en.yaml index e0a5d8f989..2f95abbf19 100644 --- a/src/locales/en.yaml +++ b/src/locales/en.yaml @@ -39,6 +39,19 @@ app: PreExtruderSensor: "Pre-Extruder Sensor" PrepDetected: "Filament detected but not loaded" Printing: "Printing" + PrintStartDialog: + StartJob: "Start Job" + Title: "AFC Lane Assignment" + NoLane: "No Lane" + NoLaneMapped: "No lane mapped to {tool}" + NoFilamentLoaded: "No filament loaded into {lane}" + FilamentTypeMismatch: "Filament material type mismatch: file requires {file}, lane has {lane}" + FilamentWeightNotEnough: "Not enough filament in lane {lane}: requires {required}, available {available}" + Question: "Do you want to start printing {filename}?" + QuestionWithSpool: "Do you want to start printing {filename} with the active spool?" + SpoolId: "ID #{id}" + SpoolWeightRemaining: "{remaining}g remaining ({used}g used)" + LaneWeightRemaining: "Weight remaining {weight}g" RammingSensor: "Ramming Sensor" Restoring: "Restoring" SetSpool: "Set Spool" diff --git a/src/mixins/afc.ts b/src/mixins/afc.ts index 797bd7e1a2..1232f41204 100644 --- a/src/mixins/afc.ts +++ b/src/mixins/afc.ts @@ -1,5 +1,7 @@ import Vue from 'vue' import Component from 'vue-class-component' +import type { Spool } from '@/store/spoolman/types' +import type { AppFile, AppFileWithMeta } from '@/store/files/types' @Component export default class AfcMixin extends Vue { @@ -137,4 +139,65 @@ export default class AfcMixin extends Vue { return printerSettings[`afc_hub ${hub.toLowerCase()}`] } + + // ─── Lane spool helpers ─────────────────────────────────────────────────── + // Single method returning all resolved lane + spool data so each component + // does not need to call individual accessors or implement its own look-ups. + + getAfcLaneInfo (lane: string): Klipper.AfcSpoolLaneInfo { + const laneObj = this.getAfcLaneObject(lane) + + const spoolId = laneObj?.spool_id ?? undefined + const spool: Spool | null = spoolId + ? (this.$typedGetters['spoolman/getSpoolById'](spoolId) ?? null) + : null + + // Color: td1_color (when enabled) → spoolman color_hex → lane color + let color: string + if (this.afc?.td1_present && laneObj?.td1_color && this.afcShowTd1Color) { + color = `#${laneObj.td1_color}` + } else if (spool?.filament?.color_hex) { + color = `#${spool.filament.color_hex.replace(/^#/, '')}` + } else { + color = laneObj?.color || '#000000' + } + + const material = spool?.filament?.material || laneObj?.material || '' + const remainingWeight = spool?.remaining_weight ?? laneObj?.weight + const fullWeight = spool?.initial_weight ?? laneObj?.initial_weight + + let spoolPercent = 100 + if (remainingWeight != null && fullWeight != null && fullWeight > 0) { + spoolPercent = Math.round((remainingWeight / fullWeight) * 100) + } + + const spoolmanBase: string | undefined = this.$typedGetters['spoolman/getSpoolmanUrl'] + const spoolUrl = spoolmanBase && spoolId + ? `${spoolmanBase.replace(/\/$/, '')}/spool/show/${spoolId}` + : undefined + + return { + spoolId, + spool, + color, + material, + filamentVendor: spool?.filament?.vendor?.name ?? undefined, + filamentName: spool?.filament?.name || laneObj?.filament_name || undefined, + remainingWeight, + fullWeight, + spoolPercent, + usedWeight: spool?.used_weight ?? undefined, + extruderTemp: spool?.filament?.settings_extruder_temp ?? laneObj?.extruder_temp ?? undefined, + bedTemp: spool?.filament?.settings_bed_temp ?? undefined, + spoolUrl, + filamentLoaded: laneObj ? (laneObj?.prep && laneObj?.load) : undefined + } + } + + shouldShowAfcDialog (fileWithMeta: AppFileWithMeta | AppFile | undefined): boolean { + if (this.afc == null) return false + const hasMetadata = fileWithMeta != null && 'filament_weights' in fileWithMeta + const usedTools = hasMetadata ? (fileWithMeta!.filament_weights ?? []).filter(w => w > 0) : [] + return !hasMetadata || usedTools.length > 0 + } } diff --git a/src/typings/klipper.d.ts b/src/typings/klipper.d.ts index 83c886c3ce..58b35dac14 100644 --- a/src/typings/klipper.d.ts +++ b/src/typings/klipper.d.ts @@ -1797,4 +1797,21 @@ declare namespace Klipper { pin: string; long_press_duration: number; } + + export interface AfcSpoolLaneInfo { + spoolId: number | undefined; + spool: import('@/store/spoolman/types').Spool | null; + color: string; + material: string; + filamentVendor: string | undefined; + filamentName: string | undefined; + remainingWeight: number | undefined; + fullWeight: number | undefined; + spoolPercent: number; + usedWeight: number | undefined; + extruderTemp: number | undefined; + bedTemp: number | undefined; + spoolUrl: string | undefined; + filamentLoaded: boolean | undefined; + } }