Spaces:
Running
Running
<script lang="ts"> | |
import { T } from "@threlte/core"; | |
import type { IntersectionEvent } from "@threlte/extras"; | |
import { interactivity } from "@threlte/extras"; | |
import type { Snippet } from "svelte"; | |
import { Spring, Tween } from "svelte/motion"; | |
import { useCursor } from "@threlte/extras"; | |
import { onMount, onDestroy } from "svelte"; | |
import { Group, Box3, Vector3 } from "three"; | |
import type { Robot } from "$lib/elements/robot/Robot.svelte"; | |
interface Props { | |
content: Snippet<[{ isHovered: boolean; isSelected: boolean; offset: number }]>; // renderable | |
onClickObject?: () => void; | |
robot?: Robot; // Optional robot for height calculation | |
rescale?: boolean; | |
} | |
let { content, onClickObject, robot, rescale = true }: Props = $props(); | |
const scale = new Spring(1); | |
// Height calculation state | |
let groupRef = $state<Group | undefined>(undefined); | |
let updateInterval: number; | |
let lastJointSnapshot: string = ""; | |
let lastCalculatedHeight: number = 0; | |
const CONSTANT_OFFSET = 0.03; | |
const HEIGHT_THRESHOLD = 0.015; // Only update if height changes by more than 1.5cm | |
const HEIGHT_QUANTIZATION = 0.01; // Quantize to 1cm steps | |
// Hover state | |
let isHovered = $state(false); | |
let isSelected = $state(false); | |
let isHighlighted = $derived(isHovered || isSelected); | |
let offsetTween = new Tween(0.26, { | |
duration: 500, | |
easing: (t) => t * (2 - t) | |
}); | |
$effect(() => { | |
if (isHighlighted) { | |
if (rescale) { | |
scale.target = 1.05; | |
} | |
} else { | |
if (rescale) { | |
scale.target = 1; | |
} | |
} | |
}); | |
function getJointSnapshot(): string { | |
if (!robot?.urdfRobotState.urdfRobot.joints) return ""; | |
return robot.urdfRobotState.urdfRobot.robot.joints | |
.filter((joint) => joint.type === "revolute" || joint.type === "continuous") | |
.map((joint) => { | |
const rotation = joint.rotation || [0, 0, 0]; | |
return `${joint.name}:${rotation.map((r) => Math.round(r * 100) / 100).join(",")}`; | |
}) | |
.join("|"); | |
} | |
function quantizeHeight(height: number): number { | |
return Math.round(height / HEIGHT_QUANTIZATION) * HEIGHT_QUANTIZATION; | |
} | |
function calculateRobotHeight() { | |
if (!groupRef || !robot) return; | |
const currentJointSnapshot = getJointSnapshot(); | |
if (currentJointSnapshot === lastJointSnapshot) { | |
return; // No significant joint changes | |
} | |
try { | |
groupRef.updateMatrixWorld(true); | |
const box = new Box3().setFromObject(groupRef); | |
const size = new Vector3(); | |
box.getSize(size); | |
const height = size.y; | |
const actualHeight = height / (10 * scale.current); | |
const quantizedHeight = quantizeHeight(actualHeight); | |
const heightDiff = Math.abs(quantizedHeight - lastCalculatedHeight); | |
if (heightDiff < HEIGHT_THRESHOLD) { | |
return; // Change too small, ignore | |
} | |
const newTarget = Math.max(quantizedHeight + CONSTANT_OFFSET, 0.08); | |
offsetTween.target = newTarget; | |
lastCalculatedHeight = quantizedHeight; | |
lastJointSnapshot = currentJointSnapshot; | |
} catch (error) { | |
console.warn("Error calculating robot height:", error); | |
offsetTween.target = 0.28; | |
} | |
} | |
const handleKeyDown = (event: KeyboardEvent) => { | |
if (event.key === "Escape" && isSelected) { | |
isSelected = false; | |
} | |
}; | |
onMount(() => { | |
if (!robot) return; | |
// Only run the height‐update check every 200 ms | |
// updateInterval = setInterval(calculateRobotHeight, 500); | |
// setTimeout(calculateRobotHeight, 100); | |
document.addEventListener("keydown", handleKeyDown); | |
}); | |
onDestroy(() => { | |
// if (updateInterval) { | |
// clearInterval(updateInterval); | |
// } | |
document.removeEventListener("keydown", handleKeyDown); | |
}); | |
const { onPointerEnter, onPointerLeave } = useCursor(); | |
interactivity(); | |
</script> | |
<T.Group | |
bind:ref={groupRef} | |
onpointerdown={(event: IntersectionEvent<MouseEvent>) => { | |
event.stopPropagation(); | |
isSelected = true; | |
onClickObject?.(); | |
}} | |
onpointerenter={(event: IntersectionEvent<PointerEvent>) => { | |
event.stopPropagation(); | |
onPointerEnter(); | |
isHovered = true; | |
}} | |
onpointerleave={(event: IntersectionEvent<PointerEvent>) => { | |
event.stopPropagation(); | |
onPointerLeave(); | |
isHovered = false; | |
}} | |
scale={scale.current} | |
> | |
{#snippet children({ ref })} | |
{@render content({ isHovered, isSelected, offset: offsetTween.current })} | |
{/snippet} | |
</T.Group> | |