Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
- Deleted the gcode-preview package from package.json and pnpm-lock.yaml. - Removed references to gcode-preview in vite.config.js. - Introduced new GCodePreview component and related files for G-code visualization. - Added GCodeViewer components including Bed, HotEnd, and SegmentMesh for rendering. - Implemented color modes and feature types for enhanced G-code rendering. - Added GCodeLegend for displaying feature information and toggling visibility. - Established GCodeScene for managing the 3D rendering context and controls.
170 lines
5.3 KiB
JavaScript
170 lines
5.3 KiB
JavaScript
import { FEATURE } from './featureTypes.js'
|
|
|
|
// Simplified Klipper-style motion planner. For every segment it computes the
|
|
// trapezoid the firmware would run given the requested feedrate, the active
|
|
// acceleration and the junction limits between consecutive moves, which yields
|
|
// the speed the toolhead actually reaches and how long each segment takes.
|
|
//
|
|
// It ignores extruder-specific limits, minimum cruise ratio smoothing and
|
|
// input shaper, so times are approximate but relative values are faithful.
|
|
|
|
const SQRT2_MINUS_1 = Math.SQRT2 - 1
|
|
|
|
export function planMotion(store, options = {}) {
|
|
const {
|
|
maxVelocity = 500,
|
|
squareCornerVelocity = 5,
|
|
defaultAccel = 3000
|
|
} = options
|
|
|
|
const n = store.count
|
|
const positions = store.positions
|
|
const feed = store.feed
|
|
const accelField = store.accel
|
|
const type = store.type
|
|
|
|
const distance = new Float32Array(n)
|
|
const deltaV2 = new Float32Array(n)
|
|
const maxStartV2 = new Float32Array(n)
|
|
const maxCruiseV2 = new Float32Array(n)
|
|
const startV2 = new Float32Array(n)
|
|
const actualSpeed = new Float32Array(n)
|
|
const segmentTime = new Float32Array(n)
|
|
|
|
const scv2 = squareCornerVelocity * squareCornerVelocity
|
|
|
|
let prevValid = false
|
|
let prevDx = 0
|
|
let prevDy = 0
|
|
let prevDz = 0
|
|
let prevAccel = defaultAccel
|
|
let prevJunctionDeviation = 0
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
const p = i * 3
|
|
const dx = positions[p + 3] - positions[p]
|
|
const dy = positions[p + 4] - positions[p + 1]
|
|
const dz = positions[p + 5] - positions[p + 2]
|
|
const d = Math.sqrt(dx * dx + dy * dy + dz * dz)
|
|
distance[i] = d
|
|
|
|
if (type[i] === FEATURE.NONE || d <= 0) {
|
|
// Position jump: no motion, acts as a full stop.
|
|
deltaV2[i] = 0
|
|
maxStartV2[i] = 0
|
|
maxCruiseV2[i] = 0
|
|
prevValid = false
|
|
continue
|
|
}
|
|
|
|
let accel = accelField[i]
|
|
if (!(accel > 0)) accel = defaultAccel
|
|
let vReq = feed[i]
|
|
if (!(vReq > 0)) vReq = maxVelocity
|
|
if (vReq > maxVelocity) vReq = maxVelocity
|
|
const cruiseV2 = vReq * vReq
|
|
maxCruiseV2[i] = cruiseV2
|
|
deltaV2[i] = 2 * d * accel
|
|
const junctionDeviation = (scv2 * SQRT2_MINUS_1) / accel
|
|
|
|
const ux = dx / d
|
|
const uy = dy / d
|
|
const uz = dz / d
|
|
|
|
if (!prevValid) {
|
|
maxStartV2[i] = 0
|
|
} else {
|
|
let cosTheta = -(ux * prevDx + uy * prevDy + uz * prevDz)
|
|
if (cosTheta > 0.999999) {
|
|
maxStartV2[i] = 0
|
|
} else {
|
|
if (cosTheta < -0.999999) cosTheta = -0.999999
|
|
const sinThetaD2 = Math.sqrt(0.5 * (1 - cosTheta))
|
|
const rJd = sinThetaD2 / (1 - sinThetaD2)
|
|
let limit = rJd * junctionDeviation * accel
|
|
const prevLimit = rJd * prevJunctionDeviation * prevAccel
|
|
if (prevLimit < limit) limit = prevLimit
|
|
if (cruiseV2 < limit) limit = cruiseV2
|
|
const prevCruise = maxCruiseV2[i - 1]
|
|
if (prevCruise < limit) limit = prevCruise
|
|
const reachable = maxStartV2[i - 1] + deltaV2[i - 1]
|
|
if (reachable < limit) limit = reachable
|
|
maxStartV2[i] = limit
|
|
}
|
|
}
|
|
|
|
prevValid = true
|
|
prevDx = ux
|
|
prevDy = uy
|
|
prevDz = uz
|
|
prevAccel = accel
|
|
prevJunctionDeviation = junctionDeviation
|
|
}
|
|
|
|
// Backward pass: nothing may end faster than it can decelerate into the
|
|
// next junction (the file ends at rest).
|
|
let nextStartV2 = 0
|
|
for (let i = n - 1; i >= 0; i--) {
|
|
const reachable = nextStartV2 + deltaV2[i]
|
|
const v2 = maxStartV2[i] < reachable ? maxStartV2[i] : reachable
|
|
startV2[i] = v2
|
|
nextStartV2 = v2
|
|
}
|
|
|
|
// Forward pass and trapezoid generation.
|
|
let currentStartV2 = 0
|
|
for (let i = 0; i < n; i++) {
|
|
const d = distance[i]
|
|
if (type[i] === FEATURE.NONE || d <= 0) {
|
|
currentStartV2 = 0
|
|
continue
|
|
}
|
|
let sV2 = startV2[i] < currentStartV2 ? startV2[i] : currentStartV2
|
|
const nextS = i + 1 < n ? startV2[i + 1] : 0
|
|
const reachableEnd = sV2 + deltaV2[i]
|
|
let eV2 = nextS < reachableEnd ? nextS : reachableEnd
|
|
if (eV2 < 0) eV2 = 0
|
|
let cV2 = (sV2 + eV2 + deltaV2[i]) * 0.5
|
|
if (cV2 > maxCruiseV2[i]) cV2 = maxCruiseV2[i]
|
|
if (cV2 < sV2) cV2 = sV2
|
|
if (cV2 < eV2) cV2 = eV2
|
|
|
|
let accel = accelField[i]
|
|
if (!(accel > 0)) accel = defaultAccel
|
|
const sV = Math.sqrt(sV2)
|
|
const eV = Math.sqrt(eV2)
|
|
const cV = Math.sqrt(cV2)
|
|
const halfInvAccel = 0.5 / accel
|
|
const accelD = (cV2 - sV2) * halfInvAccel
|
|
const decelD = (cV2 - eV2) * halfInvAccel
|
|
let cruiseD = d - accelD - decelD
|
|
if (cruiseD < 0) cruiseD = 0
|
|
const t =
|
|
(cV - sV) / accel + (cV > 0 ? cruiseD / cV : 0) + (cV - eV) / accel
|
|
|
|
actualSpeed[i] = cV
|
|
segmentTime[i] = t
|
|
currentStartV2 = eV2
|
|
}
|
|
|
|
return { actualSpeed, segmentTime }
|
|
}
|
|
|
|
// Sums segment times per layer and expands them back to one value per
|
|
// segment so the shader can colour by layer time with a plain attribute.
|
|
export function computeLayerTimes(store, segmentTime, layers) {
|
|
const n = store.count
|
|
const layerTime = new Float32Array(n)
|
|
const perLayer = new Float32Array(layers.length)
|
|
let total = 0
|
|
for (let l = 0; l < layers.length; l++) {
|
|
const { start, end } = layers[l]
|
|
let sum = 0
|
|
for (let i = start; i < end && i < n; i++) sum += segmentTime[i]
|
|
perLayer[l] = sum
|
|
total += sum
|
|
for (let i = start; i < end && i < n; i++) layerTime[i] = sum
|
|
}
|
|
return { layerTime, perLayer, total }
|
|
}
|