Files
StarPilot/openpilot/starpilot/galaxy/web/js/layout-snap.js
T
firestar5683 fab3ec9aeb Galaxy UI
Add Galaxy web assets and client settings interfaces with their bundled vendor notices.

Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>

Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 19:56:17 -05:00

76 lines
3.5 KiB
JavaScript

import { widgetSize } from "./onroad-layout-document.js"
export const SNAP_DISTANCE = { touch: 10, fine: 6 }
const NON_TARGET_KINDS = ["torque_bar"]
const PRIORITY = { center: 0, bounds: 1, widget: 2, zone: 3 }
const EPSILON = 1e-6
function lines(box, axis) {
const start = box[axis], size = box[axis === "x" ? "width" : "height"]
return { start, center: start + size / 2, end: start + size }
}
export function snapTargets(profile, layout, id, visible) {
if (!Object.hasOwn(profile.widgets, id)) return []
const targets = []
const add = (box, kind, label, names) => {
for (const axis of ["x", "y"]) {
const values = lines(box, axis)
// Keep the line role so widget centers cannot attract moving edges.
for (const line of names) targets.push({ axis, value: values[line], kind, label, line })
}
}
const bounds = profile.widgets[id].bounds || profile.bounds
add(bounds, "bounds", "the edge", ["start", "end"])
add(bounds, "center", "the center", ["center"])
for (const [key, widget] of Object.entries(profile.widgets)) {
const at = layout[key]
if (key === id || !at?.enabled || !Number.isFinite(at.x) || !Number.isFinite(at.y) ||
!visible(key) || NON_TARGET_KINDS.includes(widget.kind)) continue
const [width, height] = widgetSize(widget, at)
add({ x: at.x, y: at.y, width, height }, "widget", widget.label, ["start", "center", "end"])
}
for (const zone of profile.reservedZones || []) add(zone, "zone", "a reserved area", ["start", "end"])
return targets
}
function snapAxis(box, targets, threshold, axis, resize) {
if (!(threshold > 0)) return { delta: 0, guides: [] }
const moving = lines(box, axis), names = resize ? ["end"] : ["start", "center", "end"]
const matches = (name, target) => (name === "center") === (target.line === "center")
let best = null
for (const target of targets) {
if (target.axis !== axis) continue
for (const name of names) {
if (!matches(name, target)) continue
const delta = target.value - moving[name], distance = Math.abs(delta)
if (distance <= threshold && (!best || distance < best.distance ||
distance === best.distance && PRIORITY[target.kind] < PRIORITY[best.target.kind])) best = { delta, distance, target }
}
}
if (!best) return { delta: 0, guides: [] }
const guides = []
for (const target of targets) {
if (target.axis !== axis || !names.some(name => matches(name, target) && Math.abs(moving[name] + best.delta - target.value) <= EPSILON)) continue
const guide = { axis, value: target.value, kind: target.kind, label: target.label }
const index = guides.findIndex(item => Math.abs(item.value - guide.value) <= EPSILON)
if (index < 0) guides.push(guide)
else if (PRIORITY[guide.kind] < PRIORITY[guides[index].kind]) guides[index] = guide
}
return { delta: best.delta, guides }
}
export function snapMove(box, targets, threshold) {
const x = snapAxis(box, targets, threshold, "x", false), y = snapAxis(box, targets, threshold, "y", false)
return { x: box.x + x.delta, y: box.y + y.delta, guides: [...x.guides, ...y.guides] }
}
export function snapResize(box, targets, threshold) {
const x = snapAxis(box, targets, threshold, "x", true), y = snapAxis(box, targets, threshold, "y", true)
return { width: box.width + x.delta, height: box.height + y.delta, guides: [...x.guides, ...y.guides] }
}
export function snapThreshold(profile, rect, pointerType) {
return rect.width > 0 ? (pointerType === "touch" ? SNAP_DISTANCE.touch : SNAP_DISTANCE.fine) * profile.width / rect.width : 0
}