refactor: refactor heightmap page (#1759)

* refactor: refactor heightmap page

Signed-off-by: Stefan Dej <meteyou@gmail.com>

* refactor: remove unused import in printer/getters

Signed-off-by: Stefan Dej <meteyou@gmail.com>

---------

Signed-off-by: Stefan Dej <meteyou@gmail.com>
This commit is contained in:
Stefan Dej 2024-02-09 23:14:21 +01:00 committed by GitHub
parent 405bcac390
commit f9c59e1f26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1298 additions and 1267 deletions

View File

@ -0,0 +1,411 @@
<template>
<e-chart
ref="heightmap"
:option="chartOptions"
:init-options="{ renderer: 'canvas' }"
style="height: 600px; width: 100%; overflow: hidden" />
</template>
<script lang="ts">
import { Component, Mixins, Prop } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import BedmeshMixin from '@/components/mixins/bedmesh'
import { use } from 'echarts/core'
// import ECharts modules manually to reduce bundle size
import { CanvasRenderer } from 'echarts/renderers'
import { VisualMapComponent } from 'echarts/components'
// @ts-ignore
import { Grid3DComponent } from 'echarts-gl/components'
//type definitions for echarts-gl do not exist
// @ts-ignore
import { SurfaceChart } from 'echarts-gl/charts'
import type { ECharts } from 'echarts'
import ThemeMixin from '@/components/mixins/theme'
use([CanvasRenderer, VisualMapComponent, Grid3DComponent, SurfaceChart])
interface HeightmapSerie {
type: string
name: string
data: number[][]
dataShape?: number[]
itemStyle?: {
opacity?: number
color?: number[]
}
wireframe: {
show: boolean
}
}
@Component
export default class HeightmapChart extends Mixins(BaseMixin, BedmeshMixin, ThemeMixin) {
declare $refs: {
// eslint-disable-next-line
heightmap: any
}
@Prop({ type: Boolean, default: false }) showProbed!: boolean
@Prop({ type: Boolean, default: false }) showMesh!: boolean
@Prop({ type: Boolean, default: false }) showFlat!: boolean
@Prop({ type: Boolean, default: false }) wireframe!: boolean
@Prop({ type: Boolean, default: false }) scaleGradient!: boolean
@Prop({ type: Number, default: 1 }) scaleZMax!: number
get chart(): ECharts | null {
return this.$refs.heightmap?.chart ?? null
}
get chartOptions() {
return {
tooltip: {
backgroundColor: this.bgColor(0.9),
borderWidth: 0,
textStyle: {
color: this.fgColor(1),
fontSize: '14px',
},
padding: 15,
formatter: this.tooltipFormatter,
},
darkMode: this.$vuetify.theme.dark,
animation: false,
legend: {
show: false,
selected: this.selected,
},
visualMap: {
show: true,
min: this.visualMapRange[0],
max: this.visualMapRange[1],
calculable: true,
dimension: 2,
inRange: {
color: this.colorMap,
},
seriesIndex: this.visualMapSeriesIndex,
left: this.isMobile ? 10 : 30,
top: 20,
bottom: 0,
itemWidth: this.isMobile ? 10 : 30,
itemHeight: 550,
precision: 3,
textStyle: {
color: this.fgColorHi,
fontSize: 14,
},
},
xAxis3D: {
type: 'value',
nameTextStyle: {
color: this.fgColorMid,
},
min: this.rangeX[0],
max: this.rangeX[1],
minInterval: 1,
},
yAxis3D: {
type: 'value',
nameTextStyle: {
color: this.fgColorMid,
},
min: this.rangeY[0],
max: this.rangeY[1],
},
zAxis3D: {
type: 'value',
min: this.scaleZMax * -1,
max: this.scaleZMax,
nameTextStyle: {
color: this.fgColorMid,
},
axisPointer: {
label: {
formatter: function (value: any) {
value = parseFloat(value)
return value.toFixed(2)
},
},
},
},
grid3D: {
axisLabel: {
textStyle: { color: this.fgColorMid },
},
axisLine: {
lineStyle: { color: this.fgColorLow },
},
axisTick: {
lineStyle: { color: this.fgColorLow },
},
splitLine: {
lineStyle: { color: this.fgColorLow },
},
axisPointer: {
lineStyle: { color: this.fgColorHi },
label: {
textStyle: { color: this.fgColorHi },
},
},
boxWidth: 100 * this.scaleX,
boxDepth: 100 * this.scaleY,
viewControl: { distance: 150 },
},
series: this.series,
}
}
get selected(): { [key: string]: boolean } {
return {
probed: this.showProbed,
mesh: this.showMesh,
flat: this.showFlat,
}
}
get series(): HeightmapSerie[] {
const series = []
if (this.bed_mesh) {
series.push(this.seriesProbed)
series.push(this.seriesMesh)
series.push(this.seriesFlat)
}
return series
}
get seriesProbed(): HeightmapSerie {
const serie: HeightmapSerie = {
type: 'surface',
name: 'probed',
data: [],
itemStyle: { opacity: 1 },
wireframe: { show: this.wireframe },
}
if (this.bed_mesh) {
const xCount = this.bed_mesh.probed_matrix[0].length
const yCount = this.bed_mesh.probed_matrix.length
const xMin = this.bed_mesh.mesh_min[0]
const xMax = this.bed_mesh.mesh_max[0]
const yMin = this.bed_mesh.mesh_min[1]
const yMax = this.bed_mesh.mesh_max[1]
const xStep = (xMax - xMin) / (xCount - 1)
const yStep = (yMax - yMin) / (yCount - 1)
const data: any[] = []
let yPoint = 0
this.bed_mesh.probed_matrix.forEach((meshRow: number[]) => {
let xPoint = 0
meshRow.forEach((value: number) => {
data.push([xMin + xStep * xPoint, yMin + yStep * yPoint, value])
xPoint++
})
yPoint++
})
serie.data = data
serie.dataShape = [yCount, xCount]
}
return serie
}
get seriesMesh(): HeightmapSerie {
const serie: HeightmapSerie = {
type: 'surface',
name: 'mesh',
data: [],
itemStyle: { opacity: 1 },
wireframe: { show: this.wireframe },
}
if (this.bed_mesh) {
const xCount = this.bed_mesh.mesh_matrix[0].length
const yCount = this.bed_mesh.mesh_matrix.length
const xMin = this.bed_mesh.mesh_min[0]
const xMax = this.bed_mesh.mesh_max[0]
const yMin = this.bed_mesh.mesh_min[1]
const yMax = this.bed_mesh.mesh_max[1]
const xStep = (xMax - xMin) / (xCount - 1)
const yStep = (yMax - yMin) / (yCount - 1)
const data: any[] = []
let yPoint = 0
this.bed_mesh.mesh_matrix.forEach((meshRow: number[]) => {
let xPoint = 0
meshRow.forEach((value: number) => {
data.push([xMin + xStep * xPoint, yMin + yStep * yPoint, value])
xPoint++
})
yPoint++
})
serie.data = data
serie.dataShape = [yCount, xCount]
}
return serie
}
get seriesFlat(): HeightmapSerie {
const serie: HeightmapSerie = {
type: 'surface',
name: 'flat',
data: [],
itemStyle: {
color: [1, 1, 1, 1],
opacity: 0.5,
},
wireframe: { show: this.wireframe },
}
const config = this.$store.state.printer.configfile?.settings?.bed_mesh
if (config) {
let probe_count = [1, 1]
if (config.probe_count && typeof config.probe_count === 'string') {
probe_count = config.probe_count.split(',')
} else if (config.probe_count) {
probe_count =
config.probe_count.length < 2 ? [config.probe_count, config.probe_count] : config.probe_count
} else if (config.round_probe_count) {
probe_count = [config.round_probe_count, config.round_probe_count]
}
let mesh_min = config.mesh_min ?? [0, 0]
let mesh_max = config.mesh_max ?? [200, 200]
if ('mesh_radius' in config) {
// delta min/max
mesh_min = [config.mesh_radius * -1, config.mesh_radius * -1]
mesh_max = [config.mesh_radius, config.mesh_radius]
}
const xCount = probe_count[0]
const yCount = probe_count[1]
const xMin = parseFloat(mesh_min[0])
const xMax = parseFloat(mesh_max[0])
const yMin = parseFloat(mesh_min[1])
const yMax = parseFloat(mesh_max[1])
const xStep = (xMax - xMin) / (xCount - 1)
const yStep = (yMax - yMin) / (yCount - 1)
const data: number[][] = []
for (let y = 0; y < yCount; y++) {
for (let x = 0; x < xCount; x++) {
data.push([xMin + xStep * x, yMin + yStep * y, 0])
}
}
serie.data = data
serie.dataShape = [yCount, xCount]
}
return serie
}
get colorMap(): string[] {
return this.$store.getters['gui/heightmap/getActiveColorSchemeList']
}
get visualMapSeriesIndex(): number[] {
const output = []
if (this.showProbed) output.push(0)
else if (this.showMesh) output.push(1)
return output
}
get visualMapRange(): number[] {
if (!this.scaleGradient) return [-0.1, 0.1]
return this.heightmapLimit
}
get heightmapLimit(): number[] {
let min = 0
let max = 0
if (this.bed_mesh) {
const points = []
if (this.showProbed) {
for (const row of this.bed_mesh.probed_matrix) for (const col of row) points.push(col)
}
if (this.showMesh) {
for (const row of this.bed_mesh.mesh_matrix) for (const col of row) points.push(col)
}
min = Math.min(min, ...points)
max = Math.max(max, ...points)
}
return [min, max]
}
get rangeX(): number[] {
const axis_minimum = this.$store.state.printer.toolhead?.axis_minimum
const axis_maximum = this.$store.state.printer.toolhead?.axis_maximum
return [axis_minimum[0] ?? 0, axis_maximum[0] ?? 0]
}
get rangeY(): number[] {
const axis_minimum = this.$store.state.printer.toolhead?.axis_minimum ?? [0, 0]
const axis_maximum = this.$store.state.printer.toolhead?.axis_maximum ?? [0, 0]
return [axis_minimum[1] ?? 0, axis_maximum[1] ?? 0]
}
get absRangeX(): number {
return this.rangeX[1] - this.rangeX[0]
}
get absRangeY(): number {
return this.rangeY[1] - this.rangeY[0]
}
get minRangeXY(): number {
return Math.min(this.absRangeX, this.absRangeY)
}
get scaleX(): number {
if (this.minRangeXY === 0) return 1
return this.absRangeX / this.minRangeXY
}
get scaleY(): number {
if (this.minRangeXY === 0) return 1
return this.absRangeY / this.minRangeXY
}
tooltipFormatter(data: any): string {
const outputArray: string[] = []
outputArray.push(`<b>${data.seriesName}</b>`)
Object.keys(data.encode)
.sort()
.forEach((axisName: string) => {
const value = data.data[data.encode[axisName][0]].toFixed(axisName === 'z' ? 3 : 1)
outputArray.push(`<b>${axisName.toUpperCase()}</b>: ${value} mm`)
})
return outputArray.join('<br />')
}
beforeDestroy(): void {
if (typeof window === 'undefined') return
if (this.chart) this.chart.dispose()
}
}
</script>

View File

@ -0,0 +1,88 @@
<template>
<v-dialog :value="show" persistent :max-width="400" @keydown.esc="closeDialog">
<panel
:title="$t('Heightmap.BedMeshCalibrate')"
:icon="mdiGrid"
card-class="heightmap-calibrate-dialog"
:margin-bottom="false">
<template #buttons>
<v-btn icon tile @click="closeDialog">
<v-icon>{{ mdiCloseThick }}</v-icon>
</v-btn>
</template>
<v-card-text>
<v-text-field
ref="input"
v-model="name"
:label="$t('Heightmap.Name')"
required
:rules="rules"
@update:error="
(newVal) => {
isInvalidName = newVal
}
"
@keyup.enter="calibrateMesh" />
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text @click="closeDialog">{{ $t('Heightmap.Abort') }}</v-btn>
<v-btn :disabled="isInvalidName" color="primary" text @click="calibrateMesh">
{{ $t('Heightmap.Calibrate') }}
</v-btn>
</v-card-actions>
</panel>
</v-dialog>
</template>
<script lang="ts">
import { Component, Mixins, Prop, Watch } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import { mdiCloseThick, mdiGrid } from '@mdi/js'
@Component
export default class HeightmapRenameProfileDialog extends Mixins(BaseMixin) {
mdiCloseThick = mdiCloseThick
mdiGrid = mdiGrid
@Prop({ type: Boolean, required: true }) show!: boolean
$refs!: {
input: HTMLInputElement
}
isInvalidName = false
name = ''
rules = [
(value: string) => !!value || this.$t('Heightmap.InvalidNameEmpty'),
// eslint-disable-next-line no-control-regex
(value: string) => value === value.replace(/[^\x00-\x7F]/g, '') || this.$t('Heightmap.InvalidNameAscii'),
]
calibrateMesh(): void {
const gcode = `BED_MESH_CALIBRATE PROFILE="${this.name}"`
this.$store.dispatch('server/addEvent', { message: gcode, type: 'command' })
this.$socket.emit('printer.gcode.script', { script: gcode }, { loading: 'bedMeshCalibrate' })
this.closeDialog()
}
closeDialog() {
this.$emit('close')
}
@Watch('show')
showChanged() {
if (this.show) {
this.name = 'default'
this.$nextTick(() => {
setTimeout(() => {
this.$refs.input?.focus()
}, 100)
})
}
}
}
</script>

View File

@ -0,0 +1,50 @@
<template>
<v-dialog :value="show" persistent :max-width="400" @keydown.esc="closeDialog">
<panel
:title="$t('Heightmap.BedMeshRemove')"
:icon="mdiGrid"
card-class="heightmap-remove-dialog"
:margin-bottom="false">
<template #buttons>
<v-btn icon tile @click="closeDialog">
<v-icon>{{ mdiCloseThick }}</v-icon>
</v-btn>
</template>
<v-card-text>
<p class="mb-0">{{ $t('Heightmap.DoYouReallyWantToDelete', { name }) }}</p>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text @click="closeDialog">{{ $t('Heightmap.Abort') }}</v-btn>
<v-btn color="error" text @click="removeProfile">{{ $t('Heightmap.Remove') }}</v-btn>
</v-card-actions>
</panel>
</v-dialog>
</template>
<script lang="ts">
import { Component, Mixins, Prop } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import { mdiCloseThick, mdiGrid } from '@mdi/js'
@Component
export default class HeightmapRemoveProfileDialog extends Mixins(BaseMixin) {
mdiCloseThick = mdiCloseThick
mdiGrid = mdiGrid
@Prop({ type: Boolean, required: true }) show!: boolean
@Prop({ type: String, required: true }) name!: string
removeProfile() {
const gcode = `BED_MESH_PROFILE REMOVE="${this.name}"`
this.$store.dispatch('server/addEvent', { message: gcode, type: 'command' })
this.$socket.emit('printer.gcode.script', { script: gcode }, { loading: 'bedMeshRemove' })
this.closeDialog()
}
closeDialog() {
this.$emit('close')
}
}
</script>

View File

@ -0,0 +1,95 @@
<template>
<v-dialog :value="show" persistent :max-width="400" @keydown.esc="closeDialog">
<panel
:title="$t('Heightmap.RenameBedMeshProfile')"
:icon="mdiGrid"
card-class="heightmap-rename-dialog"
:margin-bottom="false">
<template #buttons>
<v-btn icon tile @click="closeDialog">
<v-icon>{{ mdiCloseThick }}</v-icon>
</v-btn>
</template>
<v-card-text>
<v-text-field
ref="input"
v-model="newName"
:label="$t('Heightmap.Name')"
required
:rules="rules"
@update:error="
(newVal) => {
isInvalidName = newVal
}
"
@keyup.enter="renameProfile" />
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text @click="closeDialog">{{ $t('Heightmap.Abort') }}</v-btn>
<v-btn :disabled="isInvalidName" color="primary" text @click="renameProfile">
{{ $t('Heightmap.Rename') }}
</v-btn>
</v-card-actions>
</panel>
</v-dialog>
</template>
<script lang="ts">
import { Component, Mixins, Prop, Watch } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import { mdiCloseThick, mdiGrid } from '@mdi/js'
@Component
export default class HeightmapRenameProfileDialog extends Mixins(BaseMixin) {
mdiCloseThick = mdiCloseThick
mdiGrid = mdiGrid
@Prop({ type: Boolean, required: true }) show!: boolean
@Prop({ type: String, required: true }) name!: string
$refs!: {
input: HTMLInputElement
}
isInvalidName = false
newName = ''
rules = [
(value: string) => !!value || this.$t('Heightmap.InvalidNameEmpty'),
(value: string) => value !== 'default' || this.$t('Heightmap.InvalidNameReserved'),
(value: string) => !this.profileNames.includes(value) || this.$t('Heightmap.InvalidNameAlreadyExists'),
// eslint-disable-next-line no-control-regex
(value: string) => value === value.replace(/[^\x00-\x7F]/g, '') || this.$t('Heightmap.InvalidNameAscii'),
]
get profileNames() {
return Object.keys(this.$store.state.printer.bed_mesh?.profiles ?? {})
}
renameProfile() {
const gcode = `BED_MESH_PROFILE SAVE="${this.newName}"\nBED_MESH_PROFILE REMOVE="${this.name}"`
this.$store.dispatch('server/addEvent', { message: gcode, type: 'command' })
this.$socket.emit('printer.gcode.script', { script: gcode }, { loading: 'bedMeshRename' })
this.closeDialog()
}
closeDialog() {
this.$emit('close')
}
@Watch('show')
showChanged() {
if (this.show) {
this.newName = this.name
this.$nextTick(() => {
setTimeout(() => {
this.$refs.input?.focus()
}, 100)
})
}
}
}
</script>

View File

@ -0,0 +1,62 @@
import Vue from 'vue'
import Component from 'vue-class-component'
@Component
export default class BedmeshMixin extends Vue {
get bed_mesh() {
return this.$store.state.printer.bed_mesh ?? {}
}
get profiles() {
return this.bed_mesh.profiles ?? {}
}
get mesh_min() {
return this.bed_mesh.mesh_min ?? [0, 0]
}
get mesh_max() {
return this.bed_mesh.mesh_max ?? [0, 0]
}
get min() {
return Math.min(...this.points)
}
get max() {
return Math.max(...this.points)
}
get variance() {
return Math.abs(this.min - this.max).toFixed(3)
}
get is_active() {
// if the current profile_mane is not empty, return true
if (this.bed_mesh.profile_name !== '') return true
return this.mesh_min[0] !== 0 || this.mesh_min[1] !== 0 || this.mesh_max[0] !== 0 || this.mesh_max[1] !== 0
}
get name() {
if (this.bed_mesh.profile_name !== '') return this.bed_mesh.profile_name
return 'Unknown'
}
get probed_matrix() {
return this.bed_mesh.probed_matrix ?? []
}
get points() {
const points: number[] = []
for (let i = 0; i < this.probed_matrix.length; i++) {
for (let j = 0; j < this.probed_matrix[i].length; j++) {
points.push(this.probed_matrix[i][j])
}
}
return points
}
}

View File

@ -0,0 +1,220 @@
<template>
<panel card-class="heightmap-map-panel" :title="$t('Heightmap.Heightmap')" :icon="mdiGrid">
<template #buttons>
<v-btn
icon
tile
class="d-none d-sm-flex"
:disabled="printerIsPrinting"
:color="homedAxes.includes('xyz') ? 'primary' : 'warning'"
:loading="loadings.includes('homeAll')"
:title="$t('Heightmap.TitleHomeAll')"
:ripple="true"
@click="homePrinter">
<v-icon>{{ mdiHome }}</v-icon>
</v-btn>
<v-btn
v-if="is_active"
text
tile
class="d-none d-sm-flex"
:loading="loadings.includes('bedMeshClear')"
:title="$t('Heightmap.TitleClear')"
@click="clearBedMesh">
{{ $t('Heightmap.Clear') }}
</v-btn>
<v-btn
text
tile
class="d-none d-sm-flex"
:loading="loadings.includes('bedMeshCalibrate')"
:disabled="printerIsPrinting"
:title="$t('Heightmap.TitleCalibrate')"
@click="calibrateDialog = true">
{{ $t('Heightmap.Calibrate') }}
</v-btn>
</template>
<v-card-text class="d-sm-none text-center pb-0">
<v-item-group tile class="v-btn-toggle" name="controllers">
<v-btn
text
small
class="px-2 minwidth-0"
:disabled="printerIsPrinting"
:color="homedAxes.includes('xyz') ? 'primary' : 'warning'"
:loading="loadings.includes('homeAll')"
:title="$t('Heightmap.TitleHomeAll')"
@click="homePrinter">
<v-icon :color="homedAxes.includes('xyz') ? 'primary' : 'warning'" small>
{{ mdiHome }}
</v-icon>
</v-btn>
<v-btn
v-if="bed_mesh"
text
small
class="px-2 minwidth-0"
color="primary"
:loading="loadings.includes('bedMeshClear')"
:title="$t('Heightmap.TitleClear')"
@click="clearBedMesh">
{{ $t('Heightmap.Clear') }}
</v-btn>
<v-btn
text
small
class="px-2 minwidth-0"
color="primary"
:loading="loadings.includes('bedMeshCalibrate')"
:disabled="printerIsPrinting"
:title="$t('Heightmap.TitleCalibrate')"
@click="calibrateDialog = true">
{{ $t('Heightmap.Calibrate') }}
</v-btn>
</v-item-group>
</v-card-text>
<template v-if="!is_active">
<v-card-text class="text-center py-3 font-italic">
{{ $t('Heightmap.NoBedMeshHasBeenLoadedYet') }}
</v-card-text>
</template>
<template v-else>
<v-card-text class="py-0 px-0">
<v-row>
<v-col class="">
<heightmap-chart
:show-probed="showProbed"
:show-mesh="showMesh"
:show-flat="showFlat"
:wireframe="wireframe"
:scale-gradient="scaleGradient"
:scale-z-max="scaleZMax" />
</v-col>
</v-row>
<v-row>
<v-col class="col-12 col-sm-auto pt-0 pb-0 pl-lg-6 d-flex justify-center justify-sm-start">
<v-switch v-model="scaleGradient" :label="$t('Heightmap.ScaleGradient')" class="mt-0 ml-5" />
</v-col>
<v-col class="d-flex justify-center pt-0 pb-6 pb-lg-3">
<v-checkbox
v-model="showProbed"
:label="$t('Heightmap.Probed')"
hide-details
class="mx-3 mt-0" />
<v-checkbox v-model="showMesh" :label="$t('Heightmap.Mesh')" hide-details class="mx-3 mt-0" />
<v-checkbox v-model="showFlat" :label="$t('Heightmap.Flat')" hide-details class="mx-3 mt-0" />
<v-checkbox
v-model="wireframe"
:label="$t('Heightmap.Wireframe')"
hide-details
class="mx-3 mt-0" />
</v-col>
</v-row>
</v-card-text>
<v-divider />
<v-card-text class="pt-0 pb-3">
<v-row>
<v-col>
<v-slider
v-model="scaleZMax"
:label="$t('Heightmap.ScaleZMax')"
:min="heightmapRangeLimit[0]"
:max="heightmapRangeLimit[1]"
:step="0.1"
ticks="always"
class="mt-4"
hide-details />
</v-col>
</v-row>
</v-card-text>
</template>
<heightmap-calibrate-mesh-dialog :show="calibrateDialog" @close="calibrateDialog = false" />
</panel>
</template>
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import { mdiGrid, mdiHome } from '@mdi/js'
import ControlMixin from '@/components/mixins/control'
import BedmeshMixin from '@/components/mixins/bedmesh'
import HeightmapCalibrateMeshDialog from '@/components/dialogs/HeightmapCalibrateMeshDialog.vue'
@Component({
components: { HeightmapCalibrateMeshDialog },
})
export default class HeightmapChartPanel extends Mixins(BaseMixin, ControlMixin, BedmeshMixin) {
mdiGrid = mdiGrid
mdiHome = mdiHome
calibrateDialog = false
get showProbed(): boolean {
return this.$store.state.gui.view.heightmap.probed ?? true
}
set showProbed(newVal) {
this.$store.dispatch('gui/saveSetting', { name: 'view.heightmap.probed', value: newVal })
}
get showMesh(): boolean {
return this.$store.state.gui.view.heightmap.mesh ?? true
}
set showMesh(newVal) {
this.$store.dispatch('gui/saveSetting', { name: 'view.heightmap.mesh', value: newVal })
}
get showFlat(): boolean {
return this.$store.state.gui.view.heightmap.flat ?? true
}
set showFlat(newVal) {
this.$store.dispatch('gui/saveSetting', { name: 'view.heightmap.flat', value: newVal })
}
get wireframe(): boolean {
return this.$store.state.gui.view.heightmap.wireframe ?? true
}
set wireframe(newVal) {
this.$store.dispatch('gui/saveSetting', { name: 'view.heightmap.wireframe', value: newVal })
}
get scaleGradient(): boolean {
return this.$store.state.gui.view.heightmap.scaleGradient ?? false
}
set scaleGradient(newVal) {
this.$store.dispatch('gui/saveSetting', { name: 'view.heightmap.scaleGradient', value: newVal })
}
get scaleZMax(): number {
return this.$store.state.gui.view.heightmap.scaleZMax ?? 0.5
}
set scaleZMax(newVal) {
this.$store.dispatch('gui/saveSetting', { name: 'view.heightmap.scaleZMax', value: newVal })
}
get heightmapRangeLimit(): number[] {
const minRange = Math.round(Math.max(Math.abs(this.min), Math.abs(this.max)) * 10) / 10
const maxRange = Math.max(minRange, 1)
return [minRange, maxRange]
}
homePrinter(): void {
const gcode = 'G28'
this.$store.dispatch('server/addEvent', { message: gcode, type: 'command' })
this.$socket.emit('printer.gcode.script', { script: gcode }, { loading: 'homeAll' })
}
clearBedMesh(): void {
const gcode = 'BED_MESH_CLEAR'
this.$store.dispatch('server/addEvent', { message: gcode, type: 'command' })
this.$socket.emit('printer.gcode.script', { script: gcode }, { loading: 'bedMeshClear' })
}
}
</script>

View File

@ -0,0 +1,142 @@
<template>
<panel
v-if="is_active"
:title="$t('Heightmap.CurrentMesh.Headline')"
card-class="heightmap-current-mesh-panel"
:icon="mdiInformation"
:collapsible="true"
class="mt-0">
<v-card-text class="py-3 px-0">
<v-row class="px-3">
<v-col>{{ $t('Heightmap.CurrentMesh.Name') }}</v-col>
<v-col class="text-right">
<span
v-if="!name.startsWith('adaptive-')"
class="currentMeshName cursor-pointer font-weight-bold"
@click="showRename = true">
<v-icon left small color="primary">{{ mdiPencil }}</v-icon>
{{ name }}
</span>
<span v-else class="font-weight-bold">{{ name }}</span>
</v-col>
</v-row>
<v-divider class="my-3" />
<v-row class="px-3">
<v-col>{{ $t('Heightmap.CurrentMesh.Size') }}</v-col>
<v-col class="text-right">{{ x_count }}x{{ y_count }}</v-col>
</v-row>
<v-divider class="my-3" />
<v-row v-if="index_max > -1" class="px-3">
<v-col>
{{ $t('Heightmap.CurrentMesh.Max') }} [{{ position_max_x.toFixed(1) }},
{{ position_max_y.toFixed(1) }}]
</v-col>
<v-col class="text-right">{{ max.toFixed(3) }} mm</v-col>
</v-row>
<v-divider class="my-3" />
<v-row class="px-3">
<v-col>
{{ $t('Heightmap.CurrentMesh.Min') }} [{{ position_min_x.toFixed(1) }},
{{ position_min_y.toFixed(1) }}]
</v-col>
<v-col class="text-right">{{ min.toFixed(3) }} mm</v-col>
</v-row>
<v-divider class="my-3" />
<v-row class="px-3">
<v-col>{{ $t('Heightmap.CurrentMesh.Range') }}</v-col>
<v-col class="text-right">{{ variance }} mm</v-col>
</v-row>
</v-card-text>
<heightmap-rename-profile-dialog :show="showRename" :name="name" @close="showRename = false" />
</panel>
</template>
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import { mdiInformation, mdiPencil } from '@mdi/js'
import BedmeshMixin from '@/components/mixins/bedmesh'
@Component({
components: {},
})
export default class HeightmapCurrentProfilePanel extends Mixins(BaseMixin, BedmeshMixin) {
mdiInformation = mdiInformation
mdiPencil = mdiPencil
showRename = false
get x_count() {
return this.bed_mesh.probed_matrix[0]?.length ?? 0
}
get y_count() {
return this.bed_mesh.probed_matrix?.length ?? 0
}
get x_step_size() {
if (this.x_count < 1) return 0
return (this.mesh_max[0] - this.mesh_min[0]) / (this.x_count - 1)
}
get y_step_size() {
if (this.y_count < 1) return 0
return (this.mesh_max[1] - this.mesh_min[1]) / (this.y_count - 1)
}
get index_max() {
return this.points.indexOf(this.max)
}
get index_max_y() {
return Math.trunc(this.index_max / this.x_count)
}
get index_max_x() {
return this.index_max % this.y_count
}
get position_max_x() {
return this.mesh_min[0] + this.index_max_x * this.x_step_size
}
get position_max_y() {
return this.mesh_min[1] + this.index_max_y * this.y_step_size
}
get index_min() {
return this.points.indexOf(this.min)
}
get index_min_y() {
return Math.trunc(this.index_min / this.x_count)
}
get index_min_x() {
return this.index_min % this.y_count
}
get position_min_x() {
return this.mesh_min[0] + this.index_min_x * this.x_step_size
}
get position_min_y() {
return this.mesh_min[1] + this.index_min_y * this.y_step_size
}
}
</script>
<style scoped lang="scss">
.currentMeshName {
color: var(--v-primary-base);
.v-icon {
opacity: 0;
}
&:hover .v-icon {
opacity: 1;
}
}
</style>

View File

@ -0,0 +1,35 @@
<template>
<panel
:title="$t('Heightmap.Profiles')"
card-class="heightmap-profiles-panel"
:icon="mdiStackOverflow"
:collapsible="true"
class="mt-6 mt-md-0">
<v-card-text v-if="Object.keys(profiles).length" class="px-0 py-3">
<template v-for="(profile, name, index) in profiles">
<v-divider v-if="index" :key="`deliver_${name}`" class="my-3" />
<heightmap-profiles-panel-row :key="`profile_${name}`" :profile="profile" :name="name" />
</template>
</v-card-text>
<v-card-text v-else>
<p class="mb-0">{{ $t('Heightmap.NoProfile') }}</p>
</v-card-text>
</panel>
</template>
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import { mdiStackOverflow } from '@mdi/js'
import HeightmapProfilesPanelRow from '@/components/panels/Heightmap/HeightmapProfilesPanelRow.vue'
@Component({
components: { HeightmapProfilesPanelRow },
})
export default class HeightmapProfilesPanel extends Mixins(BaseMixin) {
mdiStackOverflow = mdiStackOverflow
get profiles() {
return this.$store.state.printer.bed_mesh?.profiles ?? {}
}
}
</script>

View File

@ -0,0 +1,148 @@
<template>
<v-row class="rowProfile">
<v-col class="pl-6">
<span
:class="{ 'font-weight-bold': is_active, currentMeshName: is_active, 'cursor-pointer': true }"
@click="clickOnName">
{{ name }}
</span>
</v-col>
<v-col class="col-auto text-center d-flex align-center justify-center pr-6">
<v-tooltip top color="rgba(0,0,0,0.8)">
<template #activator="{ on, attrs }">
<small v-bind="attrs" v-on="on">{{ variance }}</small>
</template>
<span>
max: {{ max }}
<br />
min: {{ min }}
</span>
</v-tooltip>
</v-col>
<v-col class="col-auto py-0 d-flex flex-row align-center justify-end">
<v-btn
v-if="!is_active"
text
tile
class="px-2 minwidth-0"
:loading="isLoadingLoad"
style="height: 48px; width: 48px"
@click="loadProfile">
<v-icon>{{ mdiProgressUpload }}</v-icon>
</v-btn>
<v-btn
v-else
text
tile
class="px-2 minwidth-0"
:loading="isLoadingLoad"
style="height: 48px; width: 48px"
@click="showRename = true">
<v-icon>{{ mdiPencil }}</v-icon>
</v-btn>
<v-btn
text
tile
class="px-2 minwidth-0"
style="height: 48px; width: 48px"
:loading="isLoadingRemove"
:title="$t('Heightmap.DeleteBedMeshProfile')"
@click="showRemove = true">
<v-icon>{{ mdiDelete }}</v-icon>
</v-btn>
</v-col>
<heightmap-remove-profile-dialog :show="showRemove" :name="name" @close="showRemove = false" />
<heightmap-rename-profile-dialog :show="showRename" :name="name" @close="showRename = false" />
</v-row>
</template>
<script lang="ts">
import { Component, Mixins, Prop } from 'vue-property-decorator'
import { mdiDelete, mdiPencil, mdiProgressUpload } from '@mdi/js'
import BaseMixin from '@/components/mixins/base'
import { PrinterStateBedMeshProfile } from '@/store/printer/types'
import HeightmapRenameProfileDialog from '@/components/dialogs/HeightmapRenameProfileDialog.vue'
import HeightmapRemoveProfileDialog from '@/components/dialogs/HeightmapRemoveProfileDialog.vue'
@Component({
components: { HeightmapRenameProfileDialog, HeightmapRemoveProfileDialog },
})
export default class HeightmapProfilesPanelRow extends Mixins(BaseMixin) {
mdiDelete = mdiDelete
mdiPencil = mdiPencil
mdiProgressUpload = mdiProgressUpload
@Prop({ type: String, required: true }) name!: string
@Prop({ type: Object, required: true }) profile!: PrinterStateBedMeshProfile
showRemove = false
showRename = false
get points() {
const points: number[] = []
for (let i = 0; i < this.profile.points.length; i++) {
for (let j = 0; j < this.profile.points[i].length; j++) {
points.push(this.profile.points[i][j])
}
}
return points
}
get min() {
return Math.round(Math.min(...this.points) * 1000) / 1000
}
get max() {
return Math.round(Math.max(...this.points) * 1000) / 1000
}
get variance() {
return Math.abs(this.min - this.max).toFixed(3)
}
get is_active() {
const currentProfile = this.$store.state.printer.bed_mesh?.profile_name ?? ''
return currentProfile === this.name
}
get loadingNameLoad() {
return `bedMeshLoad_${this.name}`
}
get loadingNameRemove() {
return `bedMeshRemove_${this.name}`
}
get isLoadingLoad() {
return this.loadings.includes(this.loadingNameLoad)
}
get isLoadingRemove() {
return this.loadings.includes(this.loadingNameRemove)
}
clickOnName() {
if (this.is_active) {
this.showRename = true
return
}
this.loadProfile()
}
loadProfile(): void {
const gcode = `BED_MESH_PROFILE LOAD="${this.name}"`
this.$store.dispatch('server/addEvent', { message: gcode, type: 'command' })
this.$socket.emit('printer.gcode.script', { script: gcode }, { loading: this.loadingNameLoad })
}
}
</script>
<style scoped>
.currentMeshName {
color: var(--v-primary-base);
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,6 @@ import { checkKlipperConfigModules } from '@/store/variables'
import { GetterTree } from 'vuex'
import {
PrinterState,
PrinterStateBedMesh,
PrinterStateExtruder,
PrinterStateExtruderStepper,
PrinterStateFan,
@ -526,38 +525,6 @@ export const getters: GetterTree<PrinterState, RootState> = {
return output
},
getBedMeshProfiles: (state) => {
const profiles: PrinterStateBedMesh[] = []
let currentProfile = ''
if (state.bed_mesh) currentProfile = state.bed_mesh.profile_name
if (state.bed_mesh && 'profiles' in state.bed_mesh) {
Object.keys(state.bed_mesh?.profiles).forEach((key) => {
const value: any = state.bed_mesh.profiles[key]
let points: number[] = []
value.points.forEach((row: number[]) => {
points = points.concat(row)
})
const min = Math.min(...points)
const max = Math.max(...points)
profiles.push({
name: key,
data: { ...value.mesh_params, points: value.points },
points: points,
min: min,
max: max,
variance: Math.abs(min - max),
is_active: currentProfile === key,
})
})
}
return caseInsensitiveSort(profiles, 'name')
},
getExtruders: (state) => {
const extruders: PrinterStateExtruder[] = []
if (state.configfile?.settings) {

View File

@ -157,26 +157,30 @@ export interface PrinterStateFilamentSensors {
}
export interface PrinterStateBedMesh {
name: string
data: {
algo: string
max_x: number
max_y: number
mesh_x_pps: number
mesh_y_pps: number
profile_name: string
mesh_min: [number, number]
mesh_max: [number, number]
probed_matrix: number[][]
mesh_matrix: number[][]
profiles: {
[key: string]: PrinterStateBedMeshProfile
}
}
export interface PrinterStateBedMeshProfile {
points: number[][]
mesh_params: {
min_x: number
max_x: number
min_y: number
points: { [key: number]: number[] }
tension: number
version: number
max_y: number
x_count: number
y_count: number
mesh_x_pps: number
mesh_y_pps: number
algo: 'bicubic' | 'lagrange'
tension: number
}
points: number[]
min: number
max: number
variance: number
is_active: boolean
}
export interface PrinterStateMacroParam {