Tom Butcher 0e9b6392c2
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
Enhance GCodePreview and Tracking Logic for Improved Motion Handling
- Refactored GCodePreview component to integrate new tracking logic, allowing for smoother motion representation based on live printer state.
- Introduced sampleTime and updateIntervalMs props to manage motion updates more effectively.
- Updated tracking.js to include new functions for handling sample intervals and refining segment positions.
- Enhanced segmentStore and parser.worker.js to accommodate new segmentTime data for better performance tracking.
- Improved PrinterGCodeFilePanel to utilize the new tracking state, ensuring accurate representation of gcode and live positions during printing.
2026-11-09 01:30:58 +00:00

182 lines
5.0 KiB
JavaScript

// Structure-of-arrays storage for parsed G-code segments.
//
// Segment i runs from positions[i] to positions[i + 1], so the path is a
// single shared polyline and every other field is one entry per segment. The
// same class is used by the parser worker (filling it) and the main thread
// (receiving transferred chunks and feeding GPU buffers).
export const SEGMENT_FIELDS = [
['type', Uint8Array],
['tool', Uint8Array],
['fan', Uint8Array],
['layer', Uint16Array],
['temp', Uint16Array],
['width', Float32Array],
['height', Float32Array],
['feed', Float32Array],
['accel', Float32Array],
['offset', Uint32Array]
]
// Filled after the planner pass, not part of streamed chunks.
export const PLANNER_FIELDS = [
['actualSpeed', Float32Array],
['segmentTime', Float32Array],
['layerTime', Float32Array]
]
const ALL_FIELDS = [...SEGMENT_FIELDS, ...PLANNER_FIELDS]
export default class SegmentStore {
constructor(capacity = 1024) {
this.count = 0
this.capacity = 0
// Bumped whenever the backing arrays are reallocated so GPU-side
// consumers know to rebuild their attribute bindings.
this.generation = 0
this.positions = null
this.plannerReady = false
this.allocate(Math.max(16, capacity | 0))
}
allocate(capacity) {
const oldCount = this.count
const oldPositions = this.positions
const nextPositions = new Float32Array((capacity + 1) * 3)
if (oldPositions) {
nextPositions.set(oldPositions.subarray(0, (oldCount + 1) * 3))
}
this.positions = nextPositions
for (const [name, Type] of ALL_FIELDS) {
const next = new Type(capacity)
if (this[name]) next.set(this[name].subarray(0, oldCount))
this[name] = next
}
this.capacity = capacity
this.generation++
}
ensureCapacity(required) {
if (required <= this.capacity) return
let next = this.capacity
while (next < required) next = Math.ceil(next * 1.5) + 1024
this.allocate(next)
}
// Worker side: append one segment. The start point is written here and the
// end point is whatever the next push (or setEnd) writes.
push(
x,
y,
z,
type,
tool,
fan,
layer,
temp,
width,
height,
feed,
accel,
offset
) {
const i = this.count
if (i + 1 > this.capacity) this.ensureCapacity(i + 1)
const p = i * 3
this.positions[p] = x
this.positions[p + 1] = y
this.positions[p + 2] = z
this.type[i] = type
this.tool[i] = tool
this.fan[i] = fan
this.layer[i] = layer > 65535 ? 65535 : layer
this.temp[i] = temp < 0 ? 0 : temp > 65535 ? 65535 : temp
this.width[i] = width
this.height[i] = height
this.feed[i] = feed
this.accel[i] = accel
this.offset[i] = offset
this.count = i + 1
return i
}
setEnd(x, y, z) {
const p = this.count * 3
this.positions[p] = x
this.positions[p + 1] = y
this.positions[p + 2] = z
}
// Worker side: copy segments [from, to) into transferable buffers.
sliceChunk(from, to) {
const count = to - from
const chunk = { start: from, count }
const transfer = []
// Start points for every segment in the range plus the end of the last
// one, which is just the next start (or the final end point).
const positions = this.positions.slice(from * 3, (to + 1) * 3)
chunk.positions = positions
transfer.push(positions.buffer)
for (const [name] of SEGMENT_FIELDS) {
const copy = this[name].slice(from, to)
chunk[name] = copy
transfer.push(copy.buffer)
}
return { chunk, transfer }
}
// Main thread side: append a chunk produced by sliceChunk.
appendChunk(chunk) {
const { start, count } = chunk
if (start !== this.count) {
throw new Error(
`SegmentStore chunk out of order (expected ${this.count}, got ${start})`
)
}
this.ensureCapacity(start + count)
this.positions.set(chunk.positions, start * 3)
for (const [name] of SEGMENT_FIELDS) {
this[name].set(chunk[name], start)
}
this.count = start + count
}
applyPlanner({ actualSpeed, segmentTime, layerTime }) {
if (actualSpeed) this.actualSpeed.set(actualSpeed.subarray(0, this.count))
if (segmentTime) this.segmentTime.set(segmentTime.subarray(0, this.count))
if (layerTime) this.layerTime.set(layerTime.subarray(0, this.count))
this.plannerReady = true
}
getStart(i, out) {
const p = i * 3
out[0] = this.positions[p]
out[1] = this.positions[p + 1]
out[2] = this.positions[p + 2]
return out
}
getEnd(i, out) {
return this.getStart(i + 1, out)
}
// Index of the last segment whose source line starts at or before the given
// byte offset, or -1 when the offset precedes every segment.
segmentAtByteOffset(byteOffset) {
const offsets = this.offset
let lo = 0
let hi = this.count - 1
let result = -1
while (lo <= hi) {
const mid = (lo + hi) >>> 1
if (offsets[mid] <= byteOffset) {
result = mid
lo = mid + 1
} else {
hi = mid - 1
}
}
return result
}
}