Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
33f0e44
robot visualistion working
paulocinco Mar 13, 2026
357465c
robot traj setup
paulocinco Mar 13, 2026
a90c73d
added opacity parameter to physical material
paulocinco Mar 13, 2026
0fd91b5
fix streaming data error when viewer.start(show=True)
paulocinco Mar 13, 2026
4c33bdc
--
paulocinco Mar 13, 2026
a915f3e
opacity added to Material object
paulocinco Mar 16, 2026
c8510c9
changed update_transform to transform. takes geom as input instead of…
paulocinco Mar 16, 2026
71c02bc
Merge branch 'main' of https://github.com/gramaziokohler/compas_three…
paulocinco Mar 16, 2026
a245b69
linematerial opacity added
paulocinco Mar 17, 2026
faae608
work object movment
paulocinco Mar 17, 2026
4854219
trajectory timeline added and works
paulocinco Mar 19, 2026
1a6a08c
Potential fix for pull request finding
paulocinco Mar 23, 2026
6ab36a2
Potential fix for pull request finding
paulocinco Mar 23, 2026
fcb475f
Update src/compas_threejs/ui/timeline.py
paulocinco Mar 23, 2026
05c6005
Update frontend/compas_threejs/src/viewer/transform_manager.ts
paulocinco Mar 23, 2026
b742d5e
camera options
paulocinco Mar 25, 2026
722185d
Merge branch 'fab_integration' of https://github.com/gramaziokohler/c…
paulocinco Mar 25, 2026
fa34691
clean up messages
paulocinco Mar 25, 2026
cbd66e4
robot traj example relocated
paulocinco Mar 27, 2026
6ff0cc3
Merge branch 'main' of https://github.com/gramaziokohler/compas_three…
paulocinco May 6, 2026
588445c
chore: automated build of frontend assets
paulocinco May 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
466 changes: 466 additions & 0 deletions examples/robot_trajectory_scrubber.py

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions frontend/compas_threejs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions frontend/compas_threejs/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
<div ref="threeContainer" class="three-container"></div>
<ObjectInfo />
</div>

<TrajectoryTimeline />
</template>

<script setup lang="ts">
// 1. Make sure to import `ref` and `onMounted` from 'vue'
import { ref, onMounted } from "vue";
import Toolbar from "./components/layout/Toolbar.vue";
import Openbar from "./components/layout/Openbar.vue";
import TrajectoryTimeline from './components/layout/TrajectoryTimeline.vue';
import ObjectInfo from "./components/layout/ObjectInfo.vue";
import { renderer } from "./viewer/scene_manager";
import { initializeWebSocketConnection } from "./communications/communication";
Expand Down
6 changes: 5 additions & 1 deletion frontend/compas_threejs/src/communications/messages.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { scene } from "../viewer/scene_builder";
import { unpackMessageToGeometry, unpackMessage } from "../protobuff/analyzers";
import type { AnyData } from "../protobuff/generated/compas_pb/data/message";
import { transformManager } from "../viewer/transform_manager";
import { Dictionary } from "../protobuff/messages";
import * as THREE from "three";
import { lightManager } from "../viewer/light_manager";
Expand Down Expand Up @@ -51,7 +52,10 @@ function analyzeDictionary(dictionary: Dictionary) {
case "remove_object":
removeObjectFromScene(data);
break;
default:
case "transform":
transformManager(data);
break;
default:
console.warn("Unknown dispatch value:", data.dispatch.value);
}
}
18 changes: 17 additions & 1 deletion frontend/compas_threejs/src/communications/sidebarStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { reactive } from "vue";
import { sendData } from "@/communications/communication";
import type { Dictionary } from "../protobuff/messages";
import { sideBarInfoState } from "../store/store";
import { trajectoryState } from "../store/store";

// Define a type for the components we want to add.
// This can be expanded later (e.g., to include different component types).
Expand Down Expand Up @@ -70,7 +71,10 @@ export function uiManager(data: { [key: string]: any }) {
addNumberField(data);
sideBarInfoState.isVisible = true;
break;
default:
case "timeline":
setupTimeline(data);
break;
default:
console.warn("Unknown component type:", type);
}
}
Expand Down Expand Up @@ -123,6 +127,18 @@ export function addNumberField(data: { [key: string]: any }) {
sidebarComponents.push(newNumberField);
}

// --- Function to add a Timeline ---
export function setupTimeline(data: { [key: string]: any }) {
trajectoryState.id = data.guid.value;
trajectoryState.totalTime = data.total_time.value;
trajectoryState.step = data.step.value;

if (data.value && data.value.value !== undefined) {
trajectoryState.currentTime = [data.value.value];
}
trajectoryState.isVisible = true;
console.log("⏱️ Timeline intercepted! Total time:", trajectoryState.totalTime);
}
// --- Updated Action Handler ---
// It now accepts a payload, which will be the slider's value.
export function handleAction(actionGuid: string, value?: any) {
Expand Down
1 change: 1 addition & 0 deletions frontend/compas_threejs/src/communications/websocket.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { updateObjectInfo, showObjectInfo, hideObjectInfo } from "./objectInfo";
import { trajectoryState } from "../store/store"; //

const WEBSOCKET_URL = "ws://localhost:8765"; // Default WebSocket server URL for Python backend

Expand Down
187 changes: 187 additions & 0 deletions frontend/compas_threejs/src/components/layout/TrajectoryTimeline.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<template>
<div
v-if="trajectoryState.isVisible"
class="fixed bottom-0 left-0 w-full px-8 py-4 bg-white/80 dark:bg-zinc-950/80 backdrop-blur-md border-t z-50 flex items-center gap-6 shadow-lg"
@wheel.prevent="handleWheel"
>
<div class="flex items-center gap-3">

<button
@click="cycleCameraMode"
class="p-2 rounded hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors flex items-center justify-center w-10 h-10"
:class="{ 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20': trajectoryState.cameraMode !== 'free', 'text-zinc-500': trajectoryState.cameraMode === 'free' }"
:title="`Camera: ${trajectoryState.cameraMode.toUpperCase()}`"
>
<svg v-if="trajectoryState.cameraMode === 'free'" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/>
<circle cx="12" cy="13" r="3"/>
</svg>

<svg v-if="trajectoryState.cameraMode === 'look'" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/>
<circle cx="12" cy="12" r="3"/>
</svg>

<svg v-if="trajectoryState.cameraMode === 'follow'" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="3 11 22 2 13 21 11 13 3 11"/><
</svg>
</button>

<button
@click="trajectoryState.isLooping = !trajectoryState.isLooping"
class="p-2 rounded hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors"
:class="{ 'text-blue-600 dark:text-blue-400': trajectoryState.isLooping, 'text-zinc-500': !trajectoryState.isLooping }"
title="Toggle Loop"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/></svg>
</button>

<button
@click="trajectoryState.isPlaying = !trajectoryState.isPlaying"
class="flex items-center justify-center w-10 h-10 bg-zinc-900 text-white dark:bg-white dark:text-zinc-900 rounded-full hover:scale-105 transition-transform"
>
<svg v-if="trajectoryState.isPlaying" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
</button>

<button
@click="cycleSpeed"
class="w-12 text-sm font-semibold text-zinc-600 dark:text-zinc-300 hover:text-zinc-900 dark:hover:text-white transition-colors"
title="Playback Speed"
>
{{ trajectoryState.speedMultiplier }}x
</button>

</div>

<div class="text-sm font-mono text-zinc-500 w-12 text-right">
{{ trajectoryState.currentTime[0].toFixed(2) }}
</div>

<Slider
class="flex-1 cursor-pointer"
v-model="trajectoryState.currentTime"
:max="trajectoryState.totalTime"
:step="trajectoryState.step"
@update:model-value="pauseOnDrag"
/>

<div class="text-sm font-mono text-zinc-500 w-12">
{{ trajectoryState.totalTime.toFixed(2) }}
</div>
</div>
</template>

<script setup lang="ts">
import { watch, onUnmounted } from 'vue';
import { Slider } from '@/components/ui/slider';
import { trajectoryState } from '../../store/store';
import { handleAction } from "@/communications/sidebarStore";

// --- UI Interaction Helpers ---
const handleWheel = (event: WheelEvent) => {
const stepDirection = Math.sign(event.deltaY);
let newTime = trajectoryState.currentTime[0] + (stepDirection * trajectoryState.step * 10);
newTime = Math.max(0.0, Math.min(newTime, trajectoryState.totalTime));
trajectoryState.currentTime = [newTime];
};

const pauseOnDrag = () => {
// If the user manually grabs the slider, auto-pause the playback!
trajectoryState.isPlaying = false;
};

const cycleSpeed = () => {
const speeds = [0.5, 1.0, 1.5, 2.0];
const currentIndex = speeds.indexOf(trajectoryState.speedMultiplier);
const nextIndex = (currentIndex + 1) % speeds.length;
trajectoryState.speedMultiplier = speeds[nextIndex];
};

const cycleCameraMode = () => {
const modes = ['free', 'look', 'follow'];
const currentIndex = modes.indexOf(trajectoryState.cameraMode);
trajectoryState.cameraMode = modes[(currentIndex + 1) % modes.length];
};

// --- THE ANIMATION ENGINE ---
let animationFrameId: number | null = null;
let lastTimestamp: number = 0;

const playLoop = (timestamp: number) => {
if (!trajectoryState.isPlaying) return;

if (!lastTimestamp) lastTimestamp = timestamp;
const deltaTimeSec = (timestamp - lastTimestamp) / 1000.0;
lastTimestamp = timestamp;

let newTime = trajectoryState.currentTime[0] + (deltaTimeSec * trajectoryState.speedMultiplier);

if (newTime >= trajectoryState.totalTime) {
if (trajectoryState.isLooping) {
newTime = 0.0; // Loop back to the beginning
} else {
newTime = trajectoryState.totalTime;
trajectoryState.isPlaying = false; // Auto-pause at the end
}
}

trajectoryState.currentTime = [newTime];
if (trajectoryState.isPlaying) {
animationFrameId = requestAnimationFrame(playLoop);
}
};

// Watch for Play/Pause toggles ---
watch(() => trajectoryState.isPlaying, (isPlaying) => {
if (isPlaying) {
if (trajectoryState.currentTime[0] >= trajectoryState.totalTime) {
trajectoryState.currentTime = [0.0];
}
lastTimestamp = performance.now(); // Reset the clock
animationFrameId = requestAnimationFrame(playLoop);
} else {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
}
});

// Prevent memory leaks if the component is closed ---
onUnmounted(() => {
if (animationFrameId) cancelAnimationFrame(animationFrameId);
});

// ==========================================
// --- SMART NETWORK THROTTLE ---
// ==========================================
let throttleTimeout: ReturnType<typeof setTimeout> | null = null;
let lastNetworkSend = 0;

// The maximum rate Python can handle (increase to 200 or 250 if it still lags)
const NETWORK_FRAMERATE_MS = 130;

watch(() => trajectoryState.currentTime[0], (newTime) => {
if (!trajectoryState.id) return;

const now = Date.now();
const timeSinceLastSend = now - lastNetworkSend;

// 1. If enough time has passed, send to Python immediately
if (timeSinceLastSend >= NETWORK_FRAMERATE_MS) {
lastNetworkSend = now;
handleAction(trajectoryState.id, [newTime]);
}
// 2. If we are moving too fast, queue up the final frame!
else {
if (throttleTimeout) clearTimeout(throttleTimeout);

// Schedule the final frame to send exactly when the throttle window opens
throttleTimeout = setTimeout(() => {
lastNetworkSend = Date.now();
handleAction(trajectoryState.id, [newTime]);
}, NETWORK_FRAMERATE_MS - timeSinceLastSend);
}
});
</script>
13 changes: 13 additions & 0 deletions frontend/compas_threejs/src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,16 @@ export const pickerEnabled = reactive({ value: true });
export const pickerMode = reactive({ value: "translate" });

export const showEdges = reactive({ value: false });

export const trajectoryState = reactive({
id: "",
isVisible: false,
currentTime: [0.0],
totalTime: 10.0,
step: 0.01,
data: null as any,
isPlaying: false,
isLooping: false,
speedMultiplier: 1.0,
cameraMode: 'free', // Options: 'free', 'look', 'follow'
});
31 changes: 16 additions & 15 deletions frontend/compas_threejs/src/viewer/geometry_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export function geometryManager(obj: any) {

// INITIAL CREATION
// Since it's already a Mesh, we can just add it
newMesh.name = guid;
scene.add(newMesh);
SCENE_GEOMETRIES[guid] = newMesh;

Expand Down Expand Up @@ -99,15 +100,15 @@ function abstractGeometryManager(obj: any) {
return;
}

// GEOMETRY
if (geometry instanceof THREE.Line || geometry instanceof THREE.Points) {
geometry.material = material;
} else if (
geometry instanceof THREE.ArrowHelper ||
geometry instanceof THREE.PlaneHelpers
) {
geometry.setColor(material.color);
}
// GEOMETRY
if (geometry instanceof THREE.Line || geometry instanceof THREE.Points) {
geometry.material = material;
} else if (
geometry instanceof THREE.ArrowHelper ||
geometry instanceof THREE.PlaneHelper
) {
geometry.setColor(material.color);
}

scene.add(geometry);
SCENE_GEOMETRIES[guid] = geometry;
Expand All @@ -128,10 +129,10 @@ export function updateMaterial(
return;
}

if (
object instanceof THREE.ArrowHelper ||
object instanceof THREE.PlaneHelpers
) {
object.setColor(material.color);
}
if (
object instanceof THREE.ArrowHelper ||
object instanceof THREE.PlaneHelper
) {
object.setColor(material.color);
}
}
9 changes: 9 additions & 0 deletions frontend/compas_threejs/src/viewer/material_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ function buildStandardMaterial(data: { [key: string]: any }) {
emissive: parseInt(emissive),
emissiveIntensity: data.emissive_intensity.value,
flatShading: data.flat_shading.value,
opacity: data.opacity.value,
transparent: data.opacity.value < 1.0,
depthWrite: data.opacity.value >= 1.0,
wireframe: data.wireframe.value,
side: THREE.DoubleSide,
});
Expand All @@ -58,6 +61,9 @@ function buildLineMaterial(data: {

const material = new THREE.LineBasicMaterial({
color: parseInt(color),
opacity: data.opacity.value,
transparent: data.opacity.value < 1.0,
depthWrite: data.opacity.value >= 1.0,
});
return material;
}
Expand Down Expand Up @@ -112,6 +118,9 @@ function buildPhysicalMaterial(data: {
data.iridescence_thickness_start.value,
data.iridescence_thickness_end.value,
],
opacity: data.opacity.value,
transparent: data.opacity.value < 1.0,
depthWrite: data.opacity.value >= 1.0,
reflectivity: data.reflectivity.value,
sheen: data.sheen.value,
sheenColor: parseInt(sheenColor),
Expand Down
Loading